diff --git a/.file-size-exceptions.json b/.file-size-exceptions.json index b1ea532..4e95afe 100644 --- a/.file-size-exceptions.json +++ b/.file-size-exceptions.json @@ -3,16 +3,8 @@ "max_lines": 7726, "reason": "Generated dependency lockfile; preserve resolver output and integrity hashes verbatim." }, - "scripts/patch-lumencast-protocol.mjs": { - "max_lines": 1932, - "reason": "Ordered, idempotent compatibility patches to external generated Lumencast artifacts; preserve patch ordering and exact upstream text assertions." - }, - "vendor/lumencast-runtime/index-DYoYgjoZ.js": { - "max_lines": 1610, - "reason": "Temporary generated Lumencast 0.18.4 chunk pinned by SHA-256; splitting or rewriting it would invalidate the reviewed runtime bundle. Remove with the npm pin." - }, - "vendor/lumencast-runtime/tree-AMqu7A5c.js": { - "max_lines": 3979, - "reason": "Temporary generated Lumencast 0.18.4 render chunk pinned by SHA-256; splitting or rewriting it would invalidate the reviewed runtime bundle. Remove with the npm pin." + "public/vision/pkg/lumencast_vision_web.js": { + "max_lines": 3118, + "reason": "Generated wasm-bindgen 0.2.129 glue including persistent engine/font preload and boolean live-texture clear; pinned by exact bytes and SHA-256, never manually split. The 29-line increase is generated wasm-bindgen glue for the tested direct scene-parts method; SHA-pinned with the rebuilt WASM, not handwritten application growth." } } diff --git a/.github/workflows/file-sizes.yml b/.github/workflows/file-sizes.yml index 4a8d996..5fe3ac5 100644 --- a/.github/workflows/file-sizes.yml +++ b/.github/workflows/file-sizes.yml @@ -15,5 +15,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.11" + - uses: actions/setup-node@v4 + with: + node-version: "22" + - run: npm ci - run: python scripts/check_file_sizes.py --self-test - - run: python scripts/check_file_sizes.py + - run: npm run check:architecture + - run: npm run test:architecture diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0399dcb..7f0d3a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,10 +25,8 @@ permissions: jobs: build-and-release: name: Build and publish bundle tarball - # Org Actions billing is frozen → the whole org runs on the self-hosted - # vps-ovh pool, not ubuntu-latest (see zab-ci-self-hosted-runner-substrate). - # Runners are EPHEMERAL/JIT (spawned per job), so setup-node's npm cache - # never hits — omitted on purpose (same rationale as Orion ci.yml). + # The release uses the configured vps-ovh self-hosted runner pool. + # Native binary paths below must be provisioned on that runner. runs-on: [self-hosted, vps-ovh] timeout-minutes: 15 steps: @@ -39,19 +37,24 @@ jobs: - run: npm ci - run: npm run lint - run: npm run typecheck + - run: npm run check:architecture + - run: npm run test:architecture + - run: npm run check:native-client - run: npm test - run: npm run build - run: npm run check:bundle - - - name: Install Playwright Chromium - # Browser binary only, NO --with-deps: the JIT job container mounts its - # rootfs `nosuid`, so sudo cannot elevate at job time ("effective uid is - # not 0 … nosuid") — --with-deps' internal `sudo apt-get` fails there even - # though NOPASSWD sudo is baked. Chromium's OS libs (libnspr4/libnss3/ - # libasound2 …) are instead pre-baked into the org runner image - # zab-org-runner:pg — see Orion docs/runbooks/ci-runner-pg-image.md. - run: npx playwright install chromium chromium-headless-shell - - run: npm run test:e2e + # Platform builds are an input to Solar's release, never compiled on the + # operator's machine at application startup. Missing binaries fail closed. + - name: Package unchanged native receiver binaries + env: + LSDP_WINDOWS_BINARY: ${{ vars.SOLAR_LSDP_WINDOWS_BINARY }} + LSDP_LINUX_BINARY: ${{ vars.SOLAR_LSDP_LINUX_BINARY }} + run: | + test -f "$LSDP_WINDOWS_BINARY" + test -f "$LSDP_LINUX_BINARY" + npm run package:native -- --binary "$LSDP_WINDOWS_BINARY" --platform win32-x64 --revision e695e14f9f664f430ffc2e0e75d083fab88abc7f + npm run package:native -- --binary "$LSDP_LINUX_BINARY" --platform linux-x64 --revision e695e14f9f664f430ffc2e0e75d083fab88abc7f + npm run check:native-package -- win32-x64 linux-x64 - name: Resolve version from tag id: version @@ -75,6 +78,7 @@ jobs: # are a contract with Orion deploy.yml — see header). run: | mkdir -p release + npm run check:layout tar -czf "release/solar-${GITHUB_REF_NAME}.tgz" -C dist . node scripts/create-runtime-manifest.mjs \ "release/solar-${GITHUB_REF_NAME}.tgz" \ diff --git a/.gitignore b/.gitignore index ccd909d..46451d4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,10 @@ Thumbs.db .idea # Build caches +/build/ *.tsbuildinfo +__pycache__/ +*.py[cod] # Test artefacts coverage diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c3f2f9d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,19 @@ +# Generated output, installed dependencies and independent worktrees. +node_modules/ +dist/ +build/ +coverage/ +test-results/ +playwright-report/ +.worktrees/ +.claude/ + +# Historical proofs are immutable; derived maps have their own generator. +evidence/ +docs/development/code-map.md +package-lock.json + +# These upstream runtime bytes are qualified and SHA-256 pinned. +public/vision/pkg/ +public/vision/ui/ +vendor/lsdp-native-browser/ diff --git a/CLAUDE.md b/CLAUDE.md index 4200bf0..1b92bcc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,147 +1,116 @@ # Solar -@../../docs/rules/git.md -@../../docs/rules/security.md -@../../docs/rules/agents.md -@../agents/_shared/architecture.md -@../agents/_shared/conventions.md -@../agents/_shared/deploy.md -@../agents/_shared/projects.md - -## Description - -Solar is the canonical scene-runtime bundle for the Zablab broadcast -platform. The same bundle runs in three hosts (Pulsar CEF browser -source, Prism live-control webview, editor preview iframe) and -turns Orion's WS state stream + a content-hashed render bundle into -the broadcast-quality DOM the operator authored in Canvas/Logic. - -Solar holds no logic of its own. Logic lives in Orion's graph -artifact ; Canvas declares the layout ; Solar receives leaf-addressed -deltas and projects them onto a React tree of primitives. New widget -kinds are *user components* authored in Canvas and inlined by Orion -at push time — Solar releases are reserved for new rendering -capabilities (a chart, a WebGL surface), never new visual designs. - -## Stack - -- **Runtime** : Node ≥ 20 (build-time only — Solar runs in browsers) -- **Language** : TypeScript 5.7 strict -- **UI** : React 19 + Tailwind 4 -- **Reactivity** : `@preact/signals-react` (one signal per leaf path) -- **Animation** : Framer Motion 12 (tween + spring) + thin `` -- **Build** : Vite 6 library mode → ESM bundle + CSS + types -- **Test** : Vitest (unit) + Playwright (E2E against mock-orion) -- **Distribution** : `@zablab/solar` private package ; vendored by - Prism into `resources/solar/v{N.N.N}/`, statically served by Orion - at `/orion/static/solar/v{N.N.N}/`. No public npm. - -## Setup local +## Current runtime + +Solar is the host-side LSDP client for the Zablab broadcast platform. Native LSDP +provides authoritative scene identity and mutable leaf state. Solar requests +the exact scene revision by identifier from ZabCanvas and presents its LSMLZ +archive with Lumencast Vision. +Received mutations are applied by Solar and rendered by Vision independently +of their producer. No Orion query or producer feedback is required, and local +mutation observations contain no producer projection. Explicit scene-selection +coordination has its own contract. +The required `nativeLSDP: { url, resource, selector? }` reads a complete LSML resource +from the actual Lumencast Rust server through its draft2 WebSocket binding. +Solar keeps the original source/assets and Blue manifest pinned, applies flat +default changes in Vision, and rebuilds structural variants only in memory. +The local `native:start` harness writes neither config nor mutated LSML. +`src/server/` is the product reception host, exported as `@zablab/solar/server`. +A Node/Electron main host launches one instance for declared Solar/Orion resources, +waits for capability/resource reads, and owns shutdown/session reset. Prism must +wire this lifecycle in its integration. The local +harness uses that same host. Full LSML uses Rust's existing state-diff route. + +There is one renderer for broadcast, control and test subscriptions. Solar does +not compile Blue programs, fetch or render a compiled scene bundle, or retain a +React scene tree. A scene switch is an LSDP state transition: Solar loads the +version pinned by the incoming snapshot, applies queued mutations, then presents +the new Vision scene. + +The public API is `mount()` plus types. `MountOptions.sceneSourceProvider` is +required. `createCanvasSceneSourceProvider()` is the online ZabCanvas provider; +createCachedSceneSourceProvider() wraps that boundary with a verified local store. Synchronization at application startup remains owned by the host. + +## Blue and local scene preparation + +Each ZabCanvas delivery includes the Blue declarations, validation record, +binding closure and readiness result pinned to the exact source revision. Solar +retains this manifest beside the active source and in verified immutable +scene/Blue cache capsules. Blue execution remains server-owned; its scene changes arrive +through LSDP mutations. Solar does not run Blue locally. + +The online provider fetches scenes from ZabCanvas. The optional cached provider retains only immutable exact revisions and complete offline Blue manifests; FileSceneSourceStore publishes capsules once and refuses conflicting bytes. The served browser host uses direct acquisition and does not synchronize the catalog or use IndexedDB. Local source synchronization belongs to the application launcher. Node consumers supply an account-specific directory and retention policy. See docs/development/source-cache.md. +Do not remove the Blue manifest from `SceneSourceDelivery`: LSML alone does not +describe every validated Blue dependency needed for offline preparation. + +## Cameras and live media + +Vision reports live media descriptors while loading LSMLZ. Solar resolves local +camera, screen, window and application references through the host capture map, +and joins remote camera rooms through the receive-only peer viewer. Solar fits +each video frame to Vision's declared texture and uploads it as an `ImageBitmap`. +Capture availability is not restricted by Solar mode; missing or denied sources +remain transparent and report recoverable errors. + +`@lumencast/runtime` is retained only for its receive-only WebRTC viewer entry. +The compiled scene renderer is excluded from both Solar builds. + +## Source ownership + +- `src/engine/vision-presenter.ts` owns Vision canvas lifecycle, patches, + resizing and live texture calls. +- `src/engine/native-lsdp-runtime.ts` owns native resource snapshots, + hash-pinned subscriptions, exact generation/session projection, source/Blue + pins, atomic state reception and separate presentation receipts after frame submission. +- `src/engine/frame-patches.ts` owns scalar coalescing and covering presentation + receipts; commands and structure keep their ordered execution. +- `src/engine/vision-frames.ts` owns one combined state/media submission in flight + through MessageChannel. `vision-media-bridge.ts` captures the supported WASM + engine dependency for upload before that same submission. Fixed-window cadence + and encoded pixels are qualified in `docs/development/render-cadence.md`. +- `src/internal/native-tree.ts` owns draft2 Merkle hashing, checked against the + byte-pinned upstream vectors and the real native server. +- `src/internal/native-state.ts` owns immutable atomic operation application and + incremental integrity for browser and reception subscriptions. +- `src/engine/live-media.ts` owns local/remote stream subscriptions, camera + capture pools, frame upload, texture clearing and stream cleanup. +- `src/scenes/` owns the scene source provider contract, ZabCanvas acquisition, + content/version checks and Blue manifest integrity. +- `src/sources/` owns host capture resolution and remote peer stream access. +- `src/host-entry.ts` owns standalone-host URL/config parsing. It reads no scene + identity from configuration; the LSDP snapshot is authoritative. +- `scripts/native-lsdp-local.mjs` and `scripts/native-lsdp-tools.mjs` own the + ephemeral local Rust process, read-only archive serving and mutation endpoint; + `docs/development/native-lsdp.md` documents their use and limits. +- `src/server/` owns native binary verification, receiver process lifecycle, + declared resources, native state-diff delivery, operation delivery and probes. + `scripts/package-native-server.mjs` and `scripts/check-native-package.mjs` + assemble and verify platform binaries in the release archive. + +The former React renderer, compiled bundle path, atlas/overlay, and editable +preview sideband are removed. ADR files and historical changelog entries record +the previous design and are not current runtime instructions. + +## Validation ```bash -# Installation -npm ci - -# Dev server (HMR while iterating on src/) -npm run dev - -# Lint + typecheck + tests + build npm run lint npm run typecheck +npm run check:architecture +npm run check:native-client npm test npm run build +npm run check:bundle +``` -# E2E (lands once mock-orion is wired) -npm run test:e2e - -# Format -npm run format +These checks prove local code and artifact construction. They do not prove +authenticated access to production ZabCanvas, physical camera permission, +Pulsar CEF compositing or deployment. -# Variables d'environnement -# Solar n'a pas de variable d'environnement runtime ; toute la -# configuration arrive via les paramètres de mount(). Si un -# développement futur introduit une variable build-time, elle est -# documentée ici et le secret correspondant vit à l'étage 1 -# (`../.env.solar`) — jamais dans le repo. -``` +## Discovery -## Conventions spécifiques - -- **Public surface = `mount()` + types** — toute extension de l'API - publique est une décision contractuelle (Prism / Pulsar dépendent). - Les changements breaking bumpent le major du paquet. -- **Render bundles content-hashed** — Solar fetch `?v={hash}` et le - cache forever. Aucune logique de "rafraîchir si vieux" : l'identité - du bundle est son hash. -- **Reactivity = leaf-grain** — un patch ADR 002 = un signal mis à - jour = re-render uniquement des composants qui lisent ce signal. - Pas de réconciliation custom dans Solar ; Preact Signals + React - font le travail. -- **GPU-only animations** — les primitives ne permettent pas - d'animer `width` / `height` / `top` / `left`. Seuls `transform`, - `opacity` et `filter` sont animables. Le composant qui voudrait - contredire échoue à la review. -- **`broadcast` mode est tree-shakable** — le code de l'overlay - n'apparaît pas dans le bundle quand le mode est `broadcast`. CI - vérifie le budget de taille. -- **Pas de logs en `broadcast`** — Solar en mode broadcast ne doit - jamais afficher de chrome de plateforme (status pill, erreur en - bandeau, etc.). Les erreurs remontent par `onError` au host. -- **State management** : `@preact/signals-react` (pas de Redux, pas - de Zustand). La store Zustand-style éventuelle pour l'overlay - test mode est local au composant, pas une dépendance Solar. -- **Error handling** : toute erreur traverse `onError` avec un type - `SolarError` ; aucune erreur silencieuse, aucune erreur en console - visible côté broadcast. - -## Test Coverage - -| Type | Seuil minimum | Mesure | -|---|---|---| -| Public API surface (`mount`, types) | 100 % de l'option-validation | `vitest --coverage` | -| Transport (codec, sequence, reconnect) | 90 % | `vitest --coverage` | -| State (apply-snapshot, apply-delta) | 90 % | `vitest --coverage` | -| Render primitives | 70 % (DOM smoke) | Vitest + happy-dom | -| Animation engine (composition rule) | empirique via Playwright | `npm run test:e2e` | - -## Performance - -| Métrique | Budget | Mesure | -|---|---|---| -| `mount()` → first paint avec snapshot prêt | < 100 ms | Playwright `performance.mark` | -| Delta → DOM update | ≤ 50 ms | Playwright `performance.mark` | -| Bundle ESM `broadcast` (gzipped) | ≤ 200 KiB | `scripts/check-bundle-size.mjs` (CI) | -| Bundle ESM `control` (gzipped) | ≤ 280 KiB | `scripts/check-bundle-size.mjs` (CI) | -| Animation hot path | 0 layout events | DevTools perf trace en E2E | - -Critères de résolution complets : `../docs/roadmap/chantier-solar.md`. - -## CI/CD - -- **Push sur branche** : `npm ci && npm run lint && npm run typecheck && npm test && npm run build`. -- **PR vers main** : ci.yml + bundle-size budget + Playwright E2E - (mock-orion). Security audit hérité de la CI workspace. -- **Merge sur main** : pas de deploy automatique (Solar est un - artefact à vendor / static-serve). -- **Tag semver** : déclenche un build de release qui publie - `dist/` versionné, consommable par Orion (static) et Prism - (vendor). - -## Decisions - -- **2026-05-02** — bundle JSON (pas CBOR). Empreinte texte - privilégiée à l'efficience binaire à ce stade. Décision tracée - dans `../docs/adr/002-runtime-ws-protocol.md` § 1. -- **2026-05-02** — primitives fermées + user components ouverts. - Solar versionne le runtime ; les nouveaux widgets sont des - compositions Canvas-side, pas des releases Solar. Décision tracée - dans `../docs/adr/003-solar-scene-runtime.md` § 6. -- **2026-05-02** — `@preact/signals-react` retenu pour la - reactivity. Alternative considérée : Zustand fine-grained ; le - fan-out implicite des signals colle mieux au modèle leaf-grain - des deltas Orion. Décision tracée dans - `../docs/adr/003-solar-scene-runtime.md` § 1. -- **2026-05-02** — Framer Motion 12 retenu pour tween/spring, - malgré ~70 KiB. Réécriture custom envisagée seulement si le - budget bundle se serre. Décision tracée dans - `../docs/adr/003-solar-scene-runtime.md` § 5. +Use `npm run code:read -- ` and `npm run code:find -- `. +The generated index is [docs/development/code-map.md](docs/development/code-map.md); +active maintenance/qualification tools and retained history have explicit ownership +in [scripts/README.md](scripts/README.md). `npm run check:architecture` checks +module/export/dependency reachability, the derived index, local documentation and file sizes. diff --git a/README.md b/README.md index b9ce62a..8bbe17c 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,126 @@ # Solar -[![Release](https://img.shields.io/github/v/release/ZabLaboratory/Solar?logo=github)](https://github.com/ZabLaboratory/Solar/releases/latest) -[![CI](https://github.com/ZabLaboratory/Solar/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/ZabLaboratory/Solar/actions/workflows/ci.yml) -[![Prism runtime release](https://img.shields.io/badge/Prism-local%20bundle-0f766e)](https://github.com/ZabLaboratory/Solar/releases/latest) - -Scene runtime bundle for the Zablab broadcast platform. - -Solar takes a render bundle (HTTP) plus a live state stream -(WebSocket from Orion v2) and produces the broadcast-quality DOM that -Pulsar's CEF browser source captures, that Prism's live control -panel displays, and that the editor preview iframe renders for -authoring. - -## Hosts - -| Host | Mode | What's visible | -| ------------------------- | ----------- | ---------------------------------------- | -| Pulsar CEF browser source | `broadcast` | The composed scene only — no UI | -| Prism live control panel | `control` | Scene + operator overlay | -| Editor preview iframe | `test` | Scene + adapter mocker + state inspector | +Solar is the LSDP scene client and Vision renderer for Zab broadcast hosts. +The native LSDP resource supplies the authoritative scene id, `scene_version` and live leaf state; +Solar fetches that exact published LSMLZ revision from ZabCanvas and mounts it +in Vision. Scene changes use the next authoritative LSDP snapshot. Solar does +not request or render compiled scene bundles. + +Once a mutation arrives over LSDP, Solar verifies and applies it, then Vision +renders it. This mutation contract is independent of its producer: no Orion +query, producer acknowledgement or `x-orion` projection is required. Solar's +reception and presentation events identify the native transaction and state; +they do not carry producer projections. Scene-selection coordination is a +separate contract. + +Vision owns LSML/LSMLZ parsing, retained scene state, assets and GPU rendering. +Solar owns the LSDP connection, pinned source acquisition, local capture and +receive-only peer streams, and camera texture updates. Blue declaration and +validation pins arrive with the ZabCanvas source manifest; Blue execution stays +on its validated server path and its LSML mutations arrive through LSDP. ## Public API ```ts -import { mount } from "@zablab/solar"; +import { createCanvasSceneSourceProvider, mount } from "@zablab/solar"; const handle = mount({ target: document.getElementById("scene")!, - orionUrl: "wss:///orion/api/v1/show/stream", + nativeLSDP: { url: "ws://127.0.0.1:4007/lsdp", resource: "solar/program" }, token: showToken, mode: "broadcast", + sceneSourceProvider: createCanvasSceneSourceProvider({ + apiUrl: "https://zabgate.cyell.dev/canvas/api/v1", + token: canvasToken, + }), }); -// later -handle.setToken(rotatedToken); +handle.setToken(rotatedShowToken); handle.disconnect(); ``` -The complete typed surface lives in `src/types/index.ts`. - -## Status - -Solar v2 is the versioned scene runtime used by the local Prism preview, -on-air surface and Pulsar CEF browser source. `mount()` owns the transport, -state, render and overlay layers for those hosts; the same host bundle is -also available to Orion's static serving path. - -## Stack - -- TypeScript 5.7 strict -- React 19 -- Tailwind 4 -- Vite 6 (library mode → ESM bundle + CSS + types) -- `@preact/signals-react` for fine-grained reactivity -- Framer Motion 12 for tween / spring transitions -- Vitest (unit) + Playwright (E2E against mock-orion) - -## Setup local +The source provider requests by scene id, pins every fetch to the LSDP +`scene_version`, verifies response identity and hashes, and carries the +revision's Blue manifest beside the LSMLZ bytes. `createCachedSceneSourceProvider` +and `FileSceneSourceStore` now preserve verified immutable sources/assets/Blue +closure through that same boundary when supplied by the application host. +The standalone Solar host acquires the selected scene directly and performs no +catalog synchronization or inactive-scene preloading. Launcher synchronization +and account-scoped local source storage remain an integration task. +See [source cache](docs/development/source-cache.md). + +Native Lumencast LSDP supplies the complete live LSML document. The mount +requires `nativeLSDP: { url, resource, selector? }`; the standalone host accepts +`/host.html?lsdp=&resource=scene`. `selector` pins one entry in +`solar/generations` or `solar/sessions`; unrelated entries never retarget the +renderer. Solar uses the byte-pinned Lumencast browser client and +`LSDP-TCP/2.0-draft2`; it reads a fragmented resource snapshot, subscribes at its +Merkle hash, and applies atomic `lsdp.apply/1` notifications. See +[native local development](docs/development/native-lsdp.md) for the Rust server +launcher and mutation endpoint. + +The `@zablab/solar/server` entry runs in a Node 22+/Electron-main host. It owns +the packaged Rust receiver, injects declared Solar/Orion resources and waits for +real protocol readiness. The browser entry never creates a process. Full LSML +is delivered to the existing native `state` route, where Rust computes the diff; +explicit operations use atomic application transactions. The application host owns one shared instance and supplies TCP to Orion and +WebSocket to Solar; wiring the installed Prism lifecycle remains its integration task. + +## Host configuration + +The served host gets its scene identity only from LSDP. A trusted embedding host +sets `globalThis.__SOLAR_CONFIG__ = { nativeLSDP, canvasApiUrl, canvasToken }` before +loading Solar. The default Canvas API is ZabGate's `/canvas/api/v1`; if no +Canvas-specific token is configured, Solar reuses the show token. Every mode +uses the same Vision scene path. Test sessions subscribe to `solar/sessions` +with the actual Orion API session ID as selector; closing that entry clears +the renderer. Program and Preview use separate role-stable resources. + +Vision's presenter module and WASM engine are served from `public/vision/` and +copied into `dist/host/vision/` only by the host build. The library build disables +Vite's public-directory copy. `npm run check:layout` rejects duplicate Vision +assets, missing or changed copies, and every collision when the host is promoted +to the installed runtime root. It runs at build completion, in `check:bundle`, +before npm packing and immediately before release archive creation. +The four engine/host files are SHA-256 pinned in public/vision/vision-assets.json. +Regenerate those checked-in +assets from the configured `lumencast-vision` source after changing its Rust or +presenter code, then run `npm run check:bundle`. + +## Development and checks ```bash npm ci -npm run dev # Vite dev server (HMR while iterating on src/) -npm run lint # ESLint, --max-warnings 0 -npm run typecheck # tsc --noEmit -npm test # Vitest unit tests -npm run build # Dual build: dist/solar.{js,css}+types (library, for - # Prism) AND dist/host/** (self-contained, for the - # CEF / Orion static serve). See ADR 001. -npm run test:e2e # Playwright served-bundle smoke test (no import map) +npm run dev +npm run lint +npm run typecheck +npm run check:architecture +npm test +npm run build +npm run check:bundle ``` -## Distribution - -Solar publishes as `@zablab/solar` to a private registry once one -exists. In the interim, consumers vendor a built artefact keyed by -version. The two consumers have opposite bundling needs, so the build -emits **two** artefacts (ADR 001) : - -- **Orion static serve → Pulsar CEF.** Serves `dist/host/**` at - `/orion/static/solar/v{N.N.N}/...` — a **self-contained** bundle with - every runtime dep inlined (no bare specifiers; the CEF has no bundler - and no import map). This is the `index.html` the browser source loads. -- **Prism webview.** Vendors the **library** entry (`dist/solar.js`, - externals) into `Prism/resources/solar/v{N.N.N}/...`; Prism re-bundles - and supplies React / Framer from its own tree. - -A breaking change to the render-bundle format bumps the major and -ships a migration helper in Orion's compiler. Compatibility within -a major is contractual. - -## Release and local cache - -Every `vX.Y.Z` tag publishes `solar-runtime-manifest.json` alongside the -versioned `solar-vX.Y.Z.tgz` bundle. Prism checks that manifest after login, -downloads and verifies the digest when needed, then stores the immutable -bundle in its local Solar cache. A suffix tag such as `v2.0.0b` is accepted -for a proof release without changing the base package version. The render -path does not download Solar during a scene switch. - -## License - -Proprietary — Zablab platform. See workspace governance under -`../docs/rules/`. +`npm run build` emits the public ESM API and a self-contained host page for +Orion static serving and Pulsar CEF. The peer-viewer compatibility dependency +is bundled from its WebRTC-only entry; its compiled-bundle renderer is not used. + +Unit checks cover native snapshots/mutations, exact scene revision acquisition, +Vision activation, Blue manifest identity and camera/peer texture updates. +They do not prove authenticated production access, physical camera behavior, +Pulsar composition or deployment. Cache storage and exact offline reads are +available through exported adapters with bounded storage; startup synchronization +is not wired into the standalone host. +Prism integration remains outside this scope. + +- [Qualification native, CEF et paquet installé](docs/runbooks/qualification.md) +- [Maturité locale et preuves](docs/development/maturity.md) + +- [Architecture and current code index](docs/development/architecture.md) +- [Code, symbols and consumers](docs/development/code-map.md) +- [Maintenance and qualification tools](scripts/README.md) + +`npm run code:map` refreshes the derived index; `npm run code:check` rejects drift. +`npm run code:read -- ` and `npm run code:find -- ` inspect +the current source graph. `npm run check:architecture` also verifies active local +documentation and named commands. Historical ADRs and evidence describe their own +candidate, not the current runtime. diff --git a/docs/development/architecture.md b/docs/development/architecture.md new file mode 100644 index 0000000..02eaa1a --- /dev/null +++ b/docs/development/architecture.md @@ -0,0 +1,63 @@ +# Solar responsibilities and extension boundaries + +Solar is one renderer with three entry boundaries: the ESM mount API, standalone browser +host, and Node reception host. Program and Preview subscribe to different native resources; +they have independent renderer instances and may share immutable source/cache/capture adapters. +Scene identity comes from the native document, never from an obsolete URL scene selector. + +```mermaid +flowchart LR + Canvas[ZabCanvas source + assets + Blue manifest] --> Scenes[scenes: exact immutable source/cache] + Host[Node/Electron application host] --> Server[server: one native receiver process] + Server --> Native[internal: atomic state + native worker] + Scenes --> Runtime[engine: lifecycle + coordinated presentation] + Native --> Runtime + Captures[sources + peer-viewer: local/remote capture] --> Runtime + Runtime --> Vision[Vision: LSML + GPU] + Vision --> Front[complete front canvas for CEF] +``` + +| Responsibility | Owner / extension rule | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Public mount/options and host configuration | Root README, `src/index.ts`, `src/types/index.ts`, `src/mount.ts`, HTML entries. Test selection uses `nativeLSDP.selector`; removed `scene`/`testSession` options had no consumer. | +| Immutable source, revision/asset/Blue pins and bounded cache | [scenes](../../src/scenes/README.md). Extend `SceneSourceProvider`/`SceneSourceStore`; live variants never enter the immutable cache. | +| Native checksums, Merkle integrity and atomic operations | [internal](../../src/internal/README.md). Share normative primitives between browser and Node; keep transport bytes and integrity checks intact. | +| Renderer state machine, coalescing, media, animations and compensation | [engine](../../src/engine/README.md). Add presentation behavior at its existing owner; ordered commands drain scalar frames. | +| Capture mapping and receive-only rooms/slots | [sources](../../src/sources/README.md), [peer viewer](../../src/peer-viewer/README.md). Add a host adapter, preserve stream/bitmap ownership and explicit empty slots. | +| Receiver process, resources, readiness and child recovery | [server](../../src/server/README.md). The application owns startup/shutdown; browser code never creates a daemon. | +| Installation-only upstream compatibility | [peer installer](../../scripts/peer-viewer/README.md). Only consumed WebRTC modules may be patched; no legacy renderer corrections. | +| Build/discovery/qualification | [tools](../../scripts/README.md). Current tools have owners; phase-specific generators remain with historical evidence. | + +The lifecycle roots keep related state and callback ownership together: splitting them by +line count would obscure generation, cancellation and compensation. Every handwritten file +remains within the 1,000-line policy; generated lock/WASM glue have individual exceptions. +No generic utility drawer or alternate renderer is retained. +Formatting excludes generated maps, lockfiles, historical evidence and byte-pinned +upstream runtime files through `.prettierignore`; their dedicated generators/pin checks +remain authoritative. + +Blue validation belongs to ZabCanvas and execution to Orion. Solar checks source identity, +manifest integrity, transport state and execution/presentation receipts; it does not decide +whether a Blue's business result is true. Application startup, capability provisioning, +credential renewal and durable per-account storage policy remain host responsibilities. + +Mutation application and rendering start at the native LSDP reception boundary. +Solar verifies/applies received state and Vision presents it without reading +producer control state or awaiting Orion. Mutation events carry native identity +and presentation progress, without producer projections. Explicit scene-selection +coordination is separate from this contract. + +[The generated code map](code-map.md) covers each source file, symbols, imports/dependencies, +product consumers, nearest owner and direct tests. Query the live graph with +`npm run code:read -- ` / `npm run code:find -- `; regenerate with +`npm run code:map`. Static usage is checked separately from emitted source maps, byte pins, +unit contracts, installed packaging and actual CEF evidence. + +Validation: `npm run check:architecture`, `npm run test:architecture`, lint/types/unit tests, +build/layout/bundle/native pins and the relevant installed/CEF driver. The historical ADR +records a prior implementation; it is not the current operational contract. + +The [5 October 2026 maintenance audit](../../evidence/local-20261005-solar-audit/forge/20261005T184644Z-audit.md) +records the removals, retained public contracts, installation compatibility hashes and +current documentation checks. Its scope is local Solar; the CEF evidence remains attached +to its qualified renderer bytes. diff --git a/docs/development/code-map.md b/docs/development/code-map.md new file mode 100644 index 0000000..c37736d --- /dev/null +++ b/docs/development/code-map.md @@ -0,0 +1,52 @@ +# Solar code map + +Generated by `npm run code:map` from TypeScript ASTs. Do not edit derived rows. `npm run code:check` rejects drift. + +Entries: `src/index.ts`, `src/server/index.ts`, `src/dev-entry.ts`, `src/host-entry.ts`. Worker edges are followed from their actual `new URL` dependency. Public re-exports are integration contracts; tests are separate consumers. AST module/export usage including public re-exports, literal dynamic imports and Worker URL edges. Types and local live helper calls count. Tests cannot retain unused product code. Symbol liveness inside methods uses tsc/eslint; opaque runtime behavior and upstream internals require their separate tests/pins. + +| File | Responsibility owner | Symbols | Dependencies/imports | Product consumers | Direct tests | +|---|---|---|---|---|---| +| [src/dev-entry.ts](../../src/dev-entry.ts) | [README.md](../../README.md) | entry | src/mount.ts, src/scenes/canvas.ts, src/types/index.ts | declared entry | via consuming boundary | +| [src/engine/animations.ts](../../src/engine/animations.ts) | [src/engine/README.md](../../src/engine/README.md) | `RecordValue`, `AnimationAsset`, `AnimationFrame`, `values`, `animationAssets`, `eased`, `sampleAnimation`, `visit`, `prepareAnimationBindings`, `hasGeometry`, `animationDocument`, `VisionAnimations`, `VisionAnimations.update`, `VisionAnimations.schedule`, `VisionAnimations.current`, `VisionAnimations.reset`, `VisionAnimations.dispose`, `VisionAnimations.clear` | src/scenes/native-document.ts | src/engine/native-lsdp-runtime.ts, src/scenes/native-document.ts | tests/unit/animations.test.ts | +| [src/engine/canvas-presentation.ts](../../src/engine/canvas-presentation.ts) | [src/engine/README.md](../../src/engine/README.md) | `CanvasPresentation`, `CanvasPresentation.submit` | none | src/engine/vision-presenter.ts | tests/unit/canvas-presentation.test.ts | +| [src/engine/control-feedback.ts](../../src/engine/control-feedback.ts) | [src/engine/README.md](../../src/engine/README.md) | `presentationFeedback` | src/internal/native-tree.ts | src/engine/native-lsdp-runtime.ts | tests/unit/control-feedback.test.ts | +| [src/engine/frame-patches.ts](../../src/engine/frame-patches.ts) | [src/engine/README.md](../../src/engine/README.md) | `FramePatches`, `FramePatches.add`, `FramePatches.schedule`, `FramePatches.flush`, `FramePatches.close` | none | src/engine/native-lsdp-runtime.ts | tests/unit/frame-patches.test.ts | +| [src/engine/live-media.ts](../../src/engine/live-media.ts) | [src/engine/README.md](../../src/engine/README.md) | `LocalStreamEntry`, `CaptureStreamPool`, `CaptureStreamPool.acquire`, `MediaSession`, `LiveMediaOptions`, `LiveMediaController`, `LiveMediaController.refresh`, `LiveMediaController.pause`, `LiveMediaController.resume`, `LiveMediaController.update`, `LiveMediaController.dispose`, `LiveMediaController.createSession`, `LiveMediaController.connectPeer`, `LiveMediaController.connectCapture`, `LiveMediaController.attachStream`, `LiveMediaController.markReady`, `LiveMediaController.watchVideo`, `LiveMediaController.schedulePump`, `LiveMediaController.uploadFrames`, `LiveMediaController.clear`, `LiveMediaController.clearTextures`, `LiveMediaController.disposeSession`, `signature`, `boundedDimension`, `drawVideo` | src/engine/vision-presenter.ts, src/sources/peers.ts, src/types/index.ts, @lumencast/protocol | src/engine/native-lsdp-runtime.ts | tests/unit/live-media.test.ts | +| [src/engine/native-lsdp-runtime.ts](../../src/engine/native-lsdp-runtime.ts) | [src/engine/README.md](../../src/engine/README.md) | `Change`, `NativeLsdpRuntime`, `NativeLsdpRuntime.visual`, `NativeLsdpRuntime.measure`, `NativeLsdpRuntime.start`, `NativeLsdpRuntime.disconnect`, `NativeLsdpRuntime.setToken`, `NativeLsdpRuntime.closePeer`, `NativeLsdpRuntime.reconnect`, `NativeLsdpRuntime.connect`, `NativeLsdpRuntime.readSubscribe`, `NativeLsdpRuntime.fail`, `NativeLsdpRuntime.receive`, `NativeLsdpRuntime.selectedDocument`, `NativeLsdpRuntime.transition`, `NativeLsdpRuntime.render`, `NativeLsdpRuntime.reserved`, `NativeLsdpRuntime.report` | src/engine/animations.ts, src/engine/control-feedback.ts, src/engine/frame-patches.ts, src/engine/live-media.ts, src/engine/native-scene-frames.ts, src/engine/native-worker-client.ts, src/engine/vision-presenter.ts, src/internal/native-state.ts, src/internal/native-stream.ts, src/internal/native-tree.ts, src/scenes/native-document.ts, src/sources/capture.ts, src/sources/peers.ts, src/types/index.ts, @lumencast/runtime, fast-json-patch | src/mount.ts | tests/unit/native-lsdp-runtime.test.ts | +| [src/engine/native-scene-frames.ts](../../src/engine/native-scene-frames.ts) | [src/engine/README.md](../../src/engine/README.md) | `NativeSceneFrames`, `NativeSceneFrames.retire`, `NativeSceneFrames.prepare` | src/engine/vision-presenter.ts, src/scenes/local-authoring.ts, src/scenes/native-document.ts, src/types/index.ts | src/engine/native-lsdp-runtime.ts | via consuming boundary | +| [src/engine/native-worker-client.ts](../../src/engine/native-worker-client.ts) | [src/engine/README.md](../../src/engine/README.md) | `Options`, `VerifiedWorkerState`, `NativeWorkerClient`, `NativeWorkerClient.transaction`, `NativeWorkerClient.close` | src/engine/native-worker.ts | src/engine/native-lsdp-runtime.ts | tests/unit/native-worker-client.test.ts | +| [src/engine/native-worker.ts](../../src/engine/native-worker.ts) | [src/engine/README.md](../../src/engine/README.md) | entry | src/internal/native-digest.ts, src/internal/native-state.ts, src/internal/native-stream.ts, src/internal/native-tree.ts | src/engine/native-worker-client.ts | via consuming boundary | +| [src/engine/vision-frames.ts](../../src/engine/vision-frames.ts) | [src/engine/README.md](../../src/engine/README.md) | `VisionFrame`, `VisionFrames`, `VisionFrames.addPatch`, `VisionFrames.addMedia`, `VisionFrames.pending`, `VisionFrames.schedule`, `VisionFrames.flush`, `VisionFrames.close` | none | src/engine/vision-presenter.ts | tests/unit/vision-frames.test.ts | +| [src/engine/vision-media-bridge.ts](../../src/engine/vision-media-bridge.ts) | [src/engine/README.md](../../src/engine/README.md) | `MediaEngine`, `EngineFactory`, `VisionMediaBridge`, `VisionMediaBridge.module`, `VisionMediaBridge.upload` | none | src/engine/vision-presenter.ts | tests/unit/vision-media-bridge.test.ts | +| [src/engine/vision-presenter.ts](../../src/engine/vision-presenter.ts) | [src/engine/README.md](../../src/engine/README.md) | `VisionLiveMediaSource`, `VisionPresenterMessage`, `loadHostFonts`, `PresenterEvent`, `VisionPresenter`, `VisionSceneHandle`, `PresenterModule`, `VisionRenderPackage`, `PersistentVisionSession`, `createSessionPresenter`, `listenToSession`, `disconnectVisionSession`, `mountVisionScene`, `mountVisionSceneNow`, `activateVisionScene`, `visibleState` | src/engine/canvas-presentation.ts, src/engine/vision-frames.ts, src/engine/vision-media-bridge.ts, src/scenes/native-document.ts, src/scenes/types.ts, src/scenes/verified-font.ts | src/engine/live-media.ts, src/engine/native-lsdp-runtime.ts, src/engine/native-scene-frames.ts | tests/unit/live-media.test.ts | +| [src/host-entry.ts](../../src/host-entry.ts) | [README.md](../../README.md) | `SolarHostConfig`, `localFonts` | src/mount.ts, src/scenes/canvas.ts, src/scenes/installation-fonts.ts, src/scenes/local-images.ts, src/scenes/verified-font.ts, src/types/index.ts | declared entry | via consuming boundary | +| [src/index.ts](../../src/index.ts) | [README.md](../../README.md) | entry | src/mount.ts, src/scenes/browser-store.ts, src/scenes/cache.ts, src/scenes/canvas.ts, src/scenes/startup.ts, src/scenes/types.ts, src/types/index.ts | declared entry | tests/unit/mount.test.ts, tests/unit/scene-source.test.ts | +| [src/internal/native-digest.ts](../../src/internal/native-digest.ts) | [src/internal/README.md](../../src/internal/README.md) | `workerDigest` | @noble/hashes/sha256 | src/engine/native-worker.ts | tests/unit/native-digest.test.ts | +| [src/internal/native-state.ts](../../src/internal/native-state.ts) | [src/internal/README.md](../../src/internal/README.md) | `Container`, `freeze`, `portable`, `NativeState`, `NativeState.from`, `NativeState.patch` | src/internal/native-tree.ts, fast-json-patch | src/engine/native-lsdp-runtime.ts, src/engine/native-worker.ts, src/internal/native-stream.ts, src/server/native-server.ts | tests/unit/native-document.test.ts, tests/unit/native-lsdp-runtime.test.ts, tests/unit/native-state.test.ts, tests/unit/native-stream.test.ts | +| [src/internal/native-stream.ts](../../src/internal/native-stream.ts) | [src/internal/README.md](../../src/internal/README.md) | `NativeChange`, `nativeChange`, `scalarOperations` | src/internal/native-state.ts, fast-json-patch | src/engine/native-lsdp-runtime.ts, src/engine/native-worker.ts | tests/unit/native-stream.test.ts | +| [src/internal/native-tree.ts](../../src/internal/native-tree.ts) | [src/internal/README.md](../../src/internal/README.md) | `Entry`, `TreeDigest`, `NativeTreeHasher`, `NativeTreeHasher.copied`, `NativeTreeHasher.hash`, `NativeTreeHasher.digest`, `NativeTreeHasher.mapDigest`, `NativeTreeHasher.scalar`, `NativeTreeHasher.budget`, `NativeTreeHasher.remember`, `nativeTreeHash`, `validateNativeJSON` | @noble/hashes/sha256, @noble/hashes/utils | src/engine/control-feedback.ts, src/engine/native-lsdp-runtime.ts, src/engine/native-worker.ts, src/internal/native-state.ts, src/server/native-server.ts | tests/unit/native-document.test.ts, tests/unit/native-lsdp-runtime.test.ts, tests/unit/native-state.test.ts | +| [src/internal/validate-options.ts](../../src/internal/validate-options.ts) | [src/internal/README.md](../../src/internal/README.md) | `validateOptions` | src/types/index.ts | src/mount.ts | tests/unit/mount.test.ts | +| [src/mount.ts](../../src/mount.ts) | [README.md](../../README.md) | `mount` | src/engine/native-lsdp-runtime.ts, src/internal/validate-options.ts, src/types/index.ts | src/dev-entry.ts, src/host-entry.ts, src/index.ts | via consuming boundary | +| [src/peer-viewer/antenne-controller.ts](../../src/peer-viewer/antenne-controller.ts) | [src/peer-viewer/README.md](../../src/peer-viewer/README.md) | `AntenneController`, `AntenneControllerDeps`, `BufferedSub`, `createAntenneController` | src/peer-viewer/injection.ts, src/peer-viewer/slot-binding.ts, @lumencast/runtime | src/sources/peers.ts | tests/unit/antenne-controller.test.ts | +| [src/peer-viewer/injection.ts](../../src/peer-viewer/injection.ts) | [src/peer-viewer/README.md](../../src/peer-viewer/README.md) | `tracePublisherOfferPolicy`, `PeerViewerRoom`, `ViewerInjectionShape`, `ResolvedInjection`, `PeerViewerChannel`, `readPeerViewerChannel`, `publisherOfferViewerInjection`, `roomsOf`, `slotsOf`, `viewerInjectionFromLeaf`, `readPeerViewerInjection` | @lumencast/runtime | src/peer-viewer/antenne-controller.ts, src/sources/peers.ts | tests/unit/peer-viewer-injection.test.ts, tests/unit/publisher-offer-policy.test.ts | +| [src/peer-viewer/slot-binding.ts](../../src/peer-viewer/slot-binding.ts) | [src/peer-viewer/README.md](../../src/peer-viewer/README.md) | `SlotBindings`, `SlotBindingRegistry`, `createSlotBindingRegistry` | @lumencast/runtime | src/peer-viewer/antenne-controller.ts, src/sources/peers.ts | tests/unit/slot-binding.test.ts | +| [src/scenes/browser-store.ts](../../src/scenes/browser-store.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `sceneCacheNamespace`, `RecordEntry`, `BrowserSceneSourceStore`, `BrowserSceneSourceStore.open`, `BrowserSceneSourceStore.key`, `BrowserSceneSourceStore.read`, `BrowserSceneSourceStore.write`, `BrowserSceneSourceStore.close` | src/scenes/cache.ts, @noble/hashes/sha256, @noble/hashes/utils | src/index.ts | tests/unit/startup-cache.test.ts | +| [src/scenes/cache.ts](../../src/scenes/cache.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `SceneSourceStore`, `sceneSourceKey`, `unpack`, `verifySceneSource`, `encodeSceneSource`, `decodeSceneSource`, `createCachedSceneSourceProvider` | src/scenes/types.ts, @lumencast/canonical, @noble/hashes/sha256, @noble/hashes/utils, fflate | src/index.ts, src/scenes/browser-store.ts, src/scenes/startup.ts, src/server/index.ts, src/server/scene-store.ts | tests/unit/scene-cache.test.ts, tests/unit/scene-cache.test.ts, tests/unit/startup-cache.test.ts | +| [src/scenes/canonical-render.ts](../../src/scenes/canonical-render.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `canonicalRender` | @lumencast/canonical | src/scenes/native-document.ts | tests/unit/render-preparation.test.ts | +| [src/scenes/canvas.ts](../../src/scenes/canvas.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `CanvasSceneSourceOptions`, `Descriptor`, `invalid`, `descriptor`, `validateBlueManifest`, `bytes`, `digest`, `createCanvasSceneSourceProvider` | src/scenes/types.ts, src/types/index.ts, @lumencast/canonical | src/dev-entry.ts, src/host-entry.ts, src/index.ts, src/scenes/startup.ts | via consuming boundary | +| [src/scenes/editable-bindings.ts](../../src/scenes/editable-bindings.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `prepareNativeComposition`, `prepareEditableBindings` | src/scenes/native-document.ts | src/scenes/native-document.ts | tests/unit/editable-bindings.test.ts | +| [src/scenes/installation-fonts.ts](../../src/scenes/installation-fonts.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `installationFontBatches` | none | src/host-entry.ts | tests/unit/installation-fonts.test.ts | +| [src/scenes/local-authoring.ts](../../src/scenes/local-authoring.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `readNativeAuthoringSource` | src/scenes/native-document.ts, src/scenes/types.ts, @lumencast/canonical, @noble/hashes/sha256, @noble/hashes/utils | src/engine/native-scene-frames.ts | tests/unit/local-authoring.test.ts | +| [src/scenes/local-images.ts](../../src/scenes/local-images.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `LocalRenderAssetEndpoint`, `createLocalImageAssetsProvider` | src/scenes/types.ts | src/host-entry.ts | tests/unit/local-images.test.ts | +| [src/scenes/native-document.ts](../../src/scenes/native-document.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `LSMLDocument`, `requireLSML`, `defaultsPatch`, `textValue`, `visionImageValue`, `visionTextPatch`, `prepareTextBindings`, `NativeSceneAssets`, `NativeSceneAssets.imagePath`, `NativeSceneAssets.prepareImageBindings`, `NativeSceneAssets.prepareImagePatch`, `NativeSceneAssets.renderPackage` | src/engine/animations.ts, src/scenes/canonical-render.ts, src/scenes/editable-bindings.ts, src/scenes/types.ts, src/scenes/verified-font.ts, @noble/hashes/sha256, @noble/hashes/utils, fast-json-patch, fflate | src/engine/animations.ts, src/engine/native-lsdp-runtime.ts, src/engine/native-scene-frames.ts, src/engine/vision-presenter.ts, src/scenes/editable-bindings.ts, src/scenes/local-authoring.ts, src/server/native-server.ts | tests/unit/animations.test.ts, tests/unit/local-authoring.test.ts, tests/unit/native-document.test.ts, tests/unit/native-document.test.ts | +| [src/scenes/startup.ts](../../src/scenes/startup.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `SceneCacheSync`, `synchronizeSceneSources`, `createStartupSceneSourceProvider` | src/scenes/cache.ts, src/scenes/canvas.ts, src/scenes/types.ts | src/index.ts | tests/unit/startup-cache.test.ts | +| [src/scenes/types.ts](../../src/scenes/types.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `SceneSourceProvider`, `SceneSourceRequest`, `SceneBlueManifest`, `SceneSourceDelivery`, `LocalSceneSourceDelivery`, `SceneRenderDelivery`, `SceneImageAssetsProvider`, `SceneSourceErrorCode`, `SceneSourceError` | none | src/engine/vision-presenter.ts, src/index.ts, src/scenes/cache.ts, src/scenes/canvas.ts, src/scenes/local-authoring.ts, src/scenes/local-images.ts, src/scenes/native-document.ts, src/scenes/startup.ts, src/types/index.ts | tests/helpers/cache-source.ts, tests/helpers/native-source.ts, tests/unit/native-document.test.ts, tests/unit/native-lsdp-runtime.test.ts | +| [src/scenes/verified-font.ts](../../src/scenes/verified-font.ts) | [src/scenes/README.md](../../src/scenes/README.md) | `VerifiedFont`, `VerifiedFont.admit`, `VerifiedFont.isVerified`, `VerifiedFont.copy` | none | src/engine/vision-presenter.ts, src/host-entry.ts, src/scenes/native-document.ts, src/types/index.ts | tests/unit/render-preparation.test.ts | +| [src/server/index.ts](../../src/server/index.ts) | [src/server/README.md](../../src/server/README.md) | entry | src/scenes/cache.ts, src/server/native-server.ts, src/server/scene-store.ts | declared entry | via consuming boundary | +| [src/server/installation-fonts.ts](../../src/server/installation-fonts.ts) | [src/server/README.md](../../src/server/README.md) | `InstallationFontEndpoint`, `FontEntry`, `SourceEntry`, `InstallationFonts`, `InstallationFonts.prepare`, `InstallationFonts.start`, `InstallationFonts.stop` | node:crypto, node:fs/promises, node:http, node:path | src/server/native-server.ts | tests/unit/installation-fonts.test.ts | +| [src/server/native-server.ts](../../src/server/native-server.ts) | [src/server/README.md](../../src/server/README.md) | `ReceptionResource`, `ReceptionOptions`, `ReceptionConnection`, `NativeSnapshot`, `receptionSettings`, `packagedNativeBinary`, `SolarReceptionServer`, `SolarReceptionServer.start`, `SolarReceptionServer.launch`, `SolarReceptionServer.failed`, `SolarReceptionServer.exchange`, `SolarReceptionServer.check`, `SolarReceptionServer.resource`, `SolarReceptionServer.read`, `SolarReceptionServer.apply`, `SolarReceptionServer.replace`, `SolarReceptionServer.status`, `SolarReceptionServer.watch`, `SolarReceptionServer.watchOnce`, `SolarReceptionServer.stop`, `SolarReceptionServer.halt` | src/internal/native-state.ts, src/internal/native-tree.ts, src/scenes/native-document.ts, src/server/installation-fonts.ts, src/server/windows-fonts.ts, fast-json-patch, node:child_process, node:crypto, node:fs/promises, node:os, node:path, node:url | src/server/index.ts | tests/unit/native-reception.test.ts | +| [src/server/scene-store.ts](../../src/server/scene-store.ts) | [src/server/README.md](../../src/server/README.md) | `FileSceneSourceStore`, `FileSceneSourceStore.path`, `FileSceneSourceStore.read`, `FileSceneSourceStore.write` | src/scenes/cache.ts, node:crypto, node:fs/promises, node:path | src/server/index.ts | tests/unit/scene-store.test.ts | +| [src/server/windows-fonts.ts](../../src/server/windows-fonts.ts) | [src/server/README.md](../../src/server/README.md) | `windowsRegisteredFonts` | node:child_process, node:path, node:util | src/server/native-server.ts | via consuming boundary | +| [src/sources/capture.ts](../../src/sources/capture.ts) | [src/sources/README.md](../../src/sources/README.md) | `ZabCaptureEntry`, `originLabelMap`, `createCaptureResolver` | src/types/index.ts | src/engine/native-lsdp-runtime.ts | tests/unit/capture-resolution.test.ts | +| [src/sources/peers.ts](../../src/sources/peers.ts) | [src/sources/README.md](../../src/sources/README.md) | `PeerSources`, `isLocalOrionUrl`, `createPeerSources` | src/peer-viewer/antenne-controller.ts, src/peer-viewer/injection.ts, src/peer-viewer/slot-binding.ts, src/types/index.ts, @lumencast/runtime | src/engine/live-media.ts, src/engine/native-lsdp-runtime.ts | tests/unit/live-media.test.ts | +| [src/types/index.ts](../../src/types/index.ts) | [README.md](../../README.md) | `SolarMode`, `SolarStatus`, `SolarTokenProvider`, `SolarToken`, `SolarErrorCode`, `SolarError`, `MountOptions`, `NativeLSDPOptions`, `ResolveCaptureDevice`, `SolarHandle` | src/scenes/types.ts, src/scenes/verified-font.ts | src/dev-entry.ts, src/engine/live-media.ts, src/engine/native-lsdp-runtime.ts, src/engine/native-scene-frames.ts, src/host-entry.ts, src/index.ts, src/internal/validate-options.ts, src/mount.ts, src/scenes/canvas.ts, src/sources/capture.ts, src/sources/peers.ts | via consuming boundary | diff --git a/docs/development/file-size-policy.md b/docs/development/file-size-policy.md index b774dc2..8c3ee7f 100644 --- a/docs/development/file-size-policy.md +++ b/docs/development/file-size-policy.md @@ -9,9 +9,10 @@ raising a ceiling requires an explicit review of that file's justification. Hist design snapshots, generated lockfiles, exhaustive contract catalogues, coherent regression scenarios and stateful lifecycle roots are distinguished from new modules. -Prism's broadcast engine is an explicit exception: preserve atomic lane transactions. -Stateful UI closures retained in this pass must not change callback/hook ownership simply -to reduce file length. Their exception is not permission to add unrelated features there. +Solar's renderer has one owner per native, presentation, media, animation and source +boundary. Do not change callback/track ownership merely to reduce file length. +Build output and the isolated proof Python environment are generated dependencies, +ignored under build/ rather than admitted as product source exceptions. Run `python scripts/check_file_sizes.py`; CI runs the same guard and its self-tests. The guard examines tracked and unignored new files. Binary assets and generated E2E/evidence diff --git a/docs/development/maturity.md b/docs/development/maturity.md new file mode 100644 index 0000000..543d43b --- /dev/null +++ b/docs/development/maturity.md @@ -0,0 +1,165 @@ +# Maturité locale — intégration fonctionnelle et cadence du rendu + +ZabCanvas valide la scène et ses Blues. Orion charge les artefacts admis, exécute +les commandes/awaits/stream-rules et rapporte dispatch, effets et erreurs. +Solar acquiert le LSMLZ ou le LSML avec ses assets et présente Vision. +Orion ne compare aucun résultat métier attendu pour décider si une Blue est vraie. + +Le prochain propriétaire de changements applicatifs est **Prism**. +Le verdict fonctionnel est **PASS_LOCAL_READY_FOR_PRISM_INTEGRATION**. Les adaptations +Solar, Orion et ZabCanvas de cette campagne restent locales, sans publication ni +déploiement. Les optimisations Vision ont été publiées et fusionnées dans +[Vision PR #1](https://github.com/Lumencast/lumencast-vision/pull/1) ; Solar référence +leur commit et leur merge dans son manifeste, avec les mêmes hashes d'assets qualifiés. +Le checkout canonique Vision, Prism et le serveur LSDP natif sont préservés. + +[Certificat, hashes et matrice de preuves](../../evidence/local-20261005-final-certification/forge/20261005T123800Z-final-chain-certificate.json). + +L'[audit de maintenance Solar du 5 octobre](../../evidence/local-20261005-solar-audit/forge/20261005T184644Z-audit.md) +complète ce certificat : reliquats supprimés, 35 modules rattachés à leurs propriétaires, +documentation courante contrôlée, 191 tests unitaires et 5 tests des gardes, build et +installation réelle du paquet vérifiés. Il ne remplace pas les preuves CEF du rendu. + +## Qualification du compteur et de la rafale + +La qualification actuelle utilise 512 transactions distinctes cadencées sur +2500 ms, la scène complète et la caméra physique dans Pulsar CEF. Solar reçoit +les 512 mutations séquentiellement, vérifie tous les hashes intermédiaires et les +couvre dans l'ordre sans resynchronisation. Vision termine **137 puis 144 états +distincts** dans les deux fenêtres complètes fixes de 1000 ms. La passe contient +361 présentations distinctes ; les valeurs scalaires peuvent partager une image. +Les premiers 100 ms contiennent 20 ACK natifs, 18 mutations LSML reçues et 9 états +distincts terminés. La cadence n'est pas uniforme dans chaque intervalle de 100 ms. + +L'extrait prédéclaré 1–2 s contient exactement **1 s, 60 images et 60 valeurs +différentes du compteur**, lisibles et strictement croissantes, vérifiées par une +lecture indépendante de chaque image exportée. Aucun compteur ajouté ni image +interpolée. Toutes les surfaces grises sont conservées ; le compteur 512 apparaît +vers 2520 ms avec une incertitude d'alignement de ±26,2 ms. Le LSML et le LSMLZ +immutables restent inchangés ; le document muté reste en RAM. + +Solar combine média et état sur un seul travail MessageChannel sans plafond RAF +à 60 Hz. Vision conserve les maillages de glyphes, réutilise les uniforms inchangés +et recompose au plus huit zones disjointes dans l'ordre des blends. La caméra et +le texte simultanés utilisent deux zones : 3 à 7 contributions de composition +par image, au lieu d'une retombée à 177 sur cette scène. Les limites de mémoire, +invalidations et chemins complets restent actifs. 40 sorties GPU exactes sont +comparées au compositeur avec le nouveau cache désactivé. + +La rafale immédiate historique, avant ces optimisations, atteignait 512 ACK natifs +en 507 ms mais livrait 108 événements intermédiaires avant resynchronisation. +Ce chemin n'est pas requalifié par le test cadencé. Le tampon d'abonnement natif +reste inchangé ; sa livraison exhaustive en rafale immédiate et le temps interne +d'application du serveur ne sont pas déduits de cette preuve. Le critère 120/s +porte sur la soumission Vision suivie de la copie complète du front canvas ; le +scanout physique n'est pas mesuré. CEF est demandé à 120 Hz, la vidéo reste à 60 fps. + +[Qualification actuelle, vidéo, matrice et hashes](../../evidence/local-20261005-render-cadence/forge/20261005T165339Z-qualification.md). +[Mécanismes et reproduction](render-cadence.md). +Les [échecs historiques](../../evidence/local-20261005-burst-consolidation/forge/20261005T144638Z-qualification.md) +restent conservés ; leur compteur à 23 valeurs n'est plus le résultat courant. + +## Critères et preuves + +Cette matrice décrit le certificat fonctionnel antérieur. Les résultats de maintenance +courants et leurs limites figurent dans l'audit lié ci-dessus. + +| Critère | Risque vérifié | Commande / preuve | +| ------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Admission Vision sans bundle de rendu | projection ou fermeture Blue incomplète | Canvas `ZabCanvas/scripts/prove-vision-source.py` : vraie identité ZabAuth, vraie Blue publiée via ZabGate, publication/projection/validation HTTP et SQL locales, verdict persisté ; 7 assets ; aucun verdict semé | +| Source et capsule Blue indissociables | mauvaise révision, digest, assets ou déclaration | LSML/LSMLZ servis par Canvas ; manifeste auteur + définition + fermeture validée ; vérifications Solar/Orion et reprise des mêmes capsules | +| Arrêt complet puis reprise | conservation accidentelle du LSML live ou cache incomplet | Solar `scripts/prove-cold-start-cef.py --capsule ...` : deux phases, processus neufs, 4 captures CEF 1920×1080, cache/selection inchangés, zéro lecture amont hors ligne | +| Exécution générique | couverture limitée à LEC/LCK | découverte de toutes les commandes du programme réel sur les deux lanes, reçus et erreurs ; tests génériques de tous les scopes et awaits dans Orion | +| Caméras peer | signaling, départ/retour, slot réaffecté ou libéré | `scripts/prove-peer-camera-cef.py` : serveur Meet réel, caméra physique publiée dans un Pulsar séparé, deux viewers CEF receive-only, 10 captures, arrivée/départ/retour/réaffectation/libération | +| Réception native et récupération | état périmé, transaction incertaine, duplication | 4 opt-in Go avec Rust release réel ; remplacement LSML et mutation structurelle ; reprise au même port et reçu perdu | +| Installation | scripts manquants, dépendances hoistées, binaire absent | tarball npm installé dans un dossier neuf ; postinstall exécuté ; 161 fichiers dist identiques ; adaptateur Prism existant chargé en mémoire, client Orion réel et enfant récolé | +| Concurrence et maintenance | course, référence morte, drift des assets | Solar 192 tests/26 fichiers, lint/types/reachability/pins ; Vision 144 tests Rust, 13 tests JavaScript, fmt/clippy/build WASM ; preuves fonctionnelles précédentes Orion race 1812 tests/sous-tests/22 packages et Canvas 1402 tests, ruff/mypy | +| Rendu des mutations | ACK ou images caméra comptés comme états affichés | fenêtres fixes : 137 puis 144 états/s ; 60 valeurs dans 60 images CEF exportées ; tous les hashes et reçus vérifiés | +| Préservation | modification hors autorité | baselines Git Prism, LSDP natif et checkout canonique Vision identiques ; source/binaire du candidat Vision explicitement pinés | + +Les tests Canvas annoncent 85 skips et un avertissement OpenAPI GET/HEAD préexistant. +Les harnais PostgreSQL, intégrations externes et santé CI ne sont pas comptés comme +des preuves réussies ; les logs de frontière et leurs raisons sont dans le certificat. +Aucune migration de schéma n'est introduite. + +## Contrats stabilisés + +- La source LSML conserve son adresse `scene_version`. Le programme Blue, le + `artifact_set_digest` et la revision signée ont leurs propres identités ; + `x-orion-artifact-set` transporte l'ensemble admis dans le document natif. +- La validation `presentation=vision` de Canvas conserve résolution/compilation + Blue, fermeture et digests, vérifie source et assets, puis produit une admission + sans bundle de rendu compilé. Le défaut historique `compiled` conserve uniquement + la compatibilité du Prism actuel. Solar utilise un seul moteur Vision. +- Toute source projetée doit déclarer l'inventaire complet des assets, polices + comprises, par leurs chemins `assets/.`, notamment dans + `assets.files`. Les liens implicites hors LSML ne peuvent pas être devinés. +- Le serveur commun est fourni par `@zablab/solar/server`. Le navigateur Solar + et Orion sont consommateurs du même processus Rust ; l'hôte applicatif le possède. +- Les opérations LSDP live, résultats Blue et observations restent en RAM. + Seuls source/assets/déclarations/programmes immuables et sélection désirée sont + conservés. Après perte du parent, les derniers résultats dynamiques sont abandonnés. +- Un snapshot devenu périmé pendant la préparation du rendu est relu, avec quatre + tentatives bornées et vérifications de hash intactes ; les autres erreurs restent + visibles. Une transaction d'issue incertaine n'est pas réémise sous une nouvelle identité. +- Une libération explicite d'un slot reste vide pendant les changements de roster. + Les feuilles caméra appartiennent à Solar et restent hors des bindings de Vision. +- Une erreur avant finalisation restaure l'ancien host/bridge/frame exact. + Une compensation n'annule pas un effet externe déjà émis ; un reçu de dispatch + ne prétend pas qu'un effet asynchrone est fini. + +## Ce que Prism doit intégrer + +1. Projeter l'inventaire complet des assets, notamment les polices, dans le LSML + immuable, appeler la validation Canvas en mode Vision et récupérer les capsules admises. +2. Posséder le serveur natif packagé commun : origins, readiness, propagation TCP/WS, + reprise, logout et arrêt de l'application. +3. Synchroniser source/assets/programme/déclarations au lancement, dans un stockage + stable par compte ; réutiliser les exports Solar et le catalogue Orion existants. +4. Obtenir et renouveler les références via le flux broker autorisé existant. + Expiration et renouvellement restent des décisions d'intégration applicative. +5. Muter la sélection souhaitée du LSML `orion.scene-control.v1` pour Program/Preview, + puis suivre `observed` et l'acquittement du renderer. +6. Consommer les commandes, états stream-rules/Marker et erreurs Orion ; + distribuer les capacités de capture et les rooms viewer. +7. Prouver le cycle complet de l'application installée et sa mise à jour. + +## Frontières externes + +La route source du Canvas **déployé** répond encore 404 : il faut publier/déployer +le candidat Canvas avant une acquisition distante par Prism. Ce n'est pas un +changement de conception supplémentaire de Solar/Orion. + +Le 404 obtenu avec le bearer opérateur sur un locator **broker-only** est le refus +attendu. Il ne prouve pas une absence d'artefacts. Le flux mTLS/delegation broker +n'a pas été exercé dans cette phase et reste à vérifier dans l'intégration Prism. +La preuve actuelle emploie une clé de signature locale au producteur Canvas, +la vraie identité et la vraie compilation Blue distantes, avec HTTP/SQL locaux. + +Les références réelles gardent leur durée de cinq minutes et la readiness sa minute. +La reprise démontrée est dans cette validité ; aucune autorité Blue hors ligne +indéfinie n'est ajoutée. Les six commandes découvertes sont dispatchées hors ligne +et leurs six erreurs DB_QUERY_FAILED remontent ; aucun succès métier distant +n'est revendiqué dans cette situation. + +Le cache disque immuable est qualifié entre processus. Le cache IndexedDB a ses +tests de transactions/quota/éviction, mais cette preuve à froid n'est pas une preuve +de sa persistance interprocessus. Prism devra choisir le cache commun par compte. +La caméra est qualifiée en WebRTC local ; WAN/TURN et release Linux restent hors +de ces preuves Windows. La stabilité longue durée Pulsar est acceptée par l'utilisateur. + +Les contrôles de workspace sont bornés aux preuves nouvelles dans leurs dépôts +et aux continuations préservées ; aucun nettoyage d'écarts historiques globaux. +Les preuves historiques restent conservées sous leurs phases d'origine. Les +anciennes mentions « gcc absent », « debug seulement », « pas de pair réel » et +« locator opérateur 404 donc artefacts manquants » sont remplacées par les résultats +et la correction de frontière ci-dessus. + +## Reproduction de la consolidation + +Le [générateur historique du certificat](../../evidence/local-20261005-solar-audit/forge/20261005T183100Z-historical-certify-chain.py) +est archivé avec les preuves de sa phase. Ses baselines et sa synthèse appartiennent +à ce candidat antérieur ; il ne constitue plus une commande courante et ne doit pas +réécrire ces documents. Les harnais actifs et leurs limites sont décrits dans +[le catalogue des outils](../../scripts/README.md) et +[le runbook de qualification](../runbooks/qualification.md). diff --git a/docs/development/native-lsdp.md b/docs/development/native-lsdp.md new file mode 100644 index 0000000..50940f9 --- /dev/null +++ b/docs/development/native-lsdp.md @@ -0,0 +1,199 @@ +# Native LSDP in Solar + +The native Rust server remains unchanged. Solar owns its reception host in +`src/server/`, exported as `@zablab/solar/server`; the browser is its subscriber. +The application main host must start one shared instance and supply its TCP +address to Orion and WebSocket address to Solar. Resources `solar/program`, `solar/preview`, +`solar/generations`, `solar/sessions` and `orion/state` are independent. +Initially null means no scene selected, not a +renderable demo. Orion performs a native handshake/read at startup and readiness. +The host must invalidate readiness on receiver failure, reset it on logout, and +await the owned receiver at shutdown. While its parent survives, automatic child +recovery uses the same ports but starts scene resources empty and control resources +from declared seeds. The host neither subscribes automatically nor mirrors accepted +state. Producers republish the active selection after receiver loss. Explicit +stop/start uses the original seeds; parent loss drops live state. + +Orion's ordered producer actor publishes original LSML, validated Blue output +mutations, editable inputs, cameras and overlay state to this node. Solar has +one native subscription path. Collections use a selector for an exact scene +generation or API test-session ID; unrelated entries cannot retarget Vision. +Scene-intent, editable and camera responses await native application ACK. + +## Mutation application contract + +After sending, the producer is outside the mutation application/rendering contract. +Solar receives and verifies native LSDP state, applies it, and Vision renders it. +Neither scalar nor structural mutations query `orion/state`, request Orion +permission, or await producer feedback. Native transport receipts and local +`solar:lsdp-received` / `solar:lsdp-applied` observations retain transaction, +sequence and state identity; they expose no `x-orion` projection. Explicit +scene-selection transition phases have separate coordination receipts. + +Mutation latency is measured at Solar/Vision, with its boundary stated explicitly: +verified reception occurs after the transport worker's integrity checks, while +presentation means GPU submission and completed front-canvas copy. A producer's +HTTP command, preliminary state read or acknowledgement is not this duration. + +## Run the current scene without saving mutations + +Use an existing native platform binary. Package it after the Solar build: + +```powershell +npm.cmd run build +npm.cmd run package:native -- --binary --platform win32-x64 --revision e695e14f9f664f430ffc2e0e75d083fab88abc7f +npm.cmd run check:native-package +npm.cmd run native:start -- --scene --port 8099 +``` + +Open `http://127.0.0.1:8099/` as a Pulsar CEF browser source or browser page. +The launcher serves Solar and the immutable original archive, and starts Rust +with `--settings -`. Its configuration and original LSML are injected by stdin; +there is no bootstrap file, journal, outbox, asset store or LSML export. +The development `--lsdp-bin` override is optional; installed startup resolves the +shipped binary, verifies SHA-256 and needs no source checkout/build tools. +`--camera ""` optionally maps the existing +`obs-virtual-camera` authored reference in this test scene to a host camera. + +Apply ordinary add/remove/replace/test JSON Pointer operations: + +```powershell +$operations = '[{"op":"replace","path":"/layout/children/1/background","value":"#203248"}]' +Invoke-RestMethod http://127.0.0.1:8099/mutations -Method Post -ContentType application/json -Body $operations +``` + +`POST /lsml` accepts the complete desired LSML JSON object. The product host +sends it to Rust's existing typed `state` route. Rust reads B and computes the +Merkle diff to A, skipping unchanged subtrees. The host does not turn it into +a whole-root replacement. Stable route IDs are supported by the product API +for retries. `/mutations` uses explicit operations and a baseline precondition. +Both paths modify receiver RAM only. + +The existing embedding adapter contract uses a session-token-gated `/solar/native` +route to report the common endpoints. Its installed application lifecycle is a separate +Prism integration proof; these are not endpoints implemented by the Solar browser. +`POST /solar/native/{program|preview}/lsml` receives `{document,id?}` and +`POST /solar/native/{program|preview}/mutations` receives +`{operations,id?,beforeHash?}`. The embedding host may translate `native=program`/`native=preview` to trusted +`__SOLAR_CONFIG__.nativeLSDP`; Solar itself reads that config or explicit `lsdp`, +`resource` and `selector` query parameters. Source +archives/assets and Blue provenance still come from the exact source provider. + +The release archive includes `server/index.mjs`, declarations and +`native/manifest.json` with platform binaries. Release assembly requires Windows +and Linux inputs (`SOLAR_LSDP_WINDOWS_BINARY`, `SOLAR_LSDP_LINUX_BINARY` runner +variables); absence blocks publication. This local work verifies Windows only, +and does not claim those release inputs are provisioned on CI. + +The endpoint reads the native resource, prepares `lsdp.apply/1` with its Merkle +`beforeHash` and a fresh transaction ID, then sends it to Rust. It returns the +native authoritative receipt, which proves server application. Solar separately +emits `solar:lsdp-applied` on its mount target after Vision submits the changed +frame. Scalar defaults use a distinct native `received` receipt after integrity +and atomic LSML application, plus `solar:lsdp-received`. The frame queue combines +the latest scalar values through the actual Vision submission barrier, using a +MessageChannel task rather than a 60 Hz animation-frame cap; every original +transaction gets a presentation event after its covering submission, carrying +`presentation.throughSequence`, final state hash and covered mutation count. +This permits intermediate values to be overwritten without discarding mutations +or claiming every intermediate state was visible. Structural edits, commands +and lane transitions drain that queue and retain synchronous applied receipts. +Camera images and state can share a submission. Camera-only work allows at most +8.33 ms for a patch to arrive; state and explicit barriers bypass that grace. +`scripts/proof/analyse-render-cadence.py` counts native ACKs, sequential LSML +reception and completed distinct mutation frames in fixed 100/1000 ms windows. +The latter requires the Vision submission and completed front-canvas copy, +and excludes camera-only frames. Physical scan-out and encoded video pixels +remain separate measurements; `analyse-native-counter.py` reads actual pixels. +The local harness records these observations in `/health`, alongside +camera acquisition and errors; `/state` reads current RAM state. All diagnostic +history is bounded and in memory. No request writes the edited document. + +Ctrl+C stops the owned HTTP/native processes. Restarting seeds the original +LSML again and drops all mutations. Source archives are opened read-only. + +## API and source provenance + +```ts +mount({ + target, + nativeLSDP: { url: "ws://127.0.0.1:4520/lsdp", resource: "scene" }, + token: "", // local native server without configured authentication + mode: "test", + sceneSourceProvider, +}); +``` + +The native resource contains full LSML, including scene identity, layout and +defaults. Its `scene_id` and `scene_version` identify the published source +origin used to fetch its assets and Blue manifest. That origin remains pinned +during edits. Changing origin fields triggers an exact new source request. +LSML's JCS address and LSDP's `tree-sha256:` hash are different contracts. + +Flat `/defaults/` add/replace updates go through Vision's retained +patch API, including `__lit` values. A new remote image URL is fetched from the +scene's allowed hosts and added to a temporary archive in memory before staging +the updated presenter; previously packaged URLs use the retained patch path. +The `solar:lsdp-applied` observation reports the actual `patch` or `document` +render path. Numeric/null text bindings use private string aliases without +changing the raw source variables. A removal, nested/default collection edit +or structural/layout mutation prepares a new LSMLZ **only in RAM**, preserving +the original binary assets. It recomputes the temporary variant's content +address for Vision and swaps after its first submitted frame. Vision currently +offers binding updates, not a complete source-document mutation API; this +bounded rebuild keeps arbitrary source edits correct. Adding new binary assets +still requires a source delivery containing them. + +The temporary render package does not alter the source delivery, original +LSML bytes, Blue manifest or published revision. It does not assert a new Blue +validation for edited content. The local launcher marks its fixture manifest +`local-source-only`, with readiness false, rather than pretending to have a +ZabCanvas validation record. Production source providers retain their checks. + +Snapshot subscriptions close the read/subscribe race with the native server's +baseline hash check. Explicit resync obtains a new snapshot on the same connection; +invalid operations or failed renders close the session and recover from a fresh +native snapshot. An in-flight read/subscription race retries at most four times. +Late work is aborted on disconnect or credential rotation. Rebuilding a +document shares the capture pool across the old and new Vision layers. +Before staging another Vision canvas, Solar pauses camera uploads and drains +the old presenter's GPU queue. It resumes uploads after old-engine teardown, +while keeping the capture tracks acquired. This prevents overlapping WebGL +work from erasing static scene layers during an otherwise accepted mutation. + +Scalar state is received and verified in a dedicated Solar worker. The main +consumer emits `solar:lsdp-received` after ordered LSML application; a frame queue +merges replaceable values while preserving every transaction ID. Following a +successful Vision submission, `solar:lsdp-applied` includes the original state +hash and `presentation` with the covering sequence/hash/count. A covering frame +does not claim that each overwritten intermediate value appeared on screen. +Commands, camera/animation effects and structure drain this queue before execution. + +The pinned Rust subscription has a bounded event buffer and no historical replay. +A producer faster than that subscriber can trigger `SUBSCRIBER_LAGGED`. Solar +resynchronizes the final LSML; that run cannot certify receipt/rendering of every +intermediate transaction. Mutation throughput, covering submissions and distinct +encoded counter values must be reported separately. `scripts/proof/native-burst-producer.mjs` +supports immediate queuing and an explicit paced duration. `analyse-native-counter.py` +reads the real counter pixels and recorded HUD clock, exports exactly 60 source +frames over one second, and retains its frame-by-frame values/alignment uncertainty. + +## Validation and protocol pin + +```powershell +npm.cmd run check:native-client +npm.cmd run test:native -- --lsdp-bin --scene +npm.cmd run typecheck +npm.cmd run lint +npm.cmd test +npm.cmd run build +npm.cmd run check:bundle +``` + +Native interoperability checks use the real Rust process: fragmented snapshot +and mutations, hash agreement, add/remove/replace/test, atomic failed batches, +stale baselines, idempotency, unchanged source bytes and restart reset. +Runtime tests also cover ACK timing, renderer failure and lifecycle aborts. +The exact upstream browser files and vectors are byte-pinned in +`vendor/lsdp-native-browser`; their owner/provenance are documented there. +CEF screenshots remain a separate host-level proof. diff --git a/docs/development/render-cadence.md b/docs/development/render-cadence.md new file mode 100644 index 0000000..d93682a --- /dev/null +++ b/docs/development/render-cadence.md @@ -0,0 +1,102 @@ +# Native mutation render cadence + +Solar measures three separate boundaries: the native server's authoritative +transaction ACK, sequential hash-verified LSML reception in the worker, and a +covering presentation after an actual Vision submission and front-canvas copy. +The encoded Pulsar CEF video is checked independently. Neither a native ACK nor +a browser RAF heartbeat counts as a new rendered mutation state. + +## Scheduling and retained rendering + +`src/engine/frame-patches.ts` preserves every transaction identity while merging +scalar values through a microtask. `src/engine/vision-frames.ts` owns one actual +submission in flight, scheduled by MessageChannel rather than a 60 Hz RAF. +Camera-only work waits at most 8.33 ms for state to join; a state patch or lifecycle +barrier bypasses that grace. `vision-media-bridge.ts` uploads the latest owned +camera image before the same state submission. Errors reject the covered callers; +scene transitions and ordered commands drain pending presentation first. + +Vision retains glyph tessellation (1024 entries, 8 MiB), byte-identical shader +uniforms per live pass occurrence, and raw pre-effect composition. Up to eight +disjoint old/new damage regions replay the original blend operations in their +original order. Overlapping regions merge transitively. A camera and a distant +score do not force replay of the space between them. Raw/prefix textures share +a 64 MiB budget. Topology, output-size and authoring-size changes invalidate the +cache; large damage, backdrop effects, non-normal blends and single-sample direct +paint keep the full path. The native GPU oracle compares 40 incremental outputs +with a fresh compositor whose new composition cache is disabled. + +The exact local engine bytes and source provenance are recorded in +`public/vision/vision-assets.json`. The qualified source is commit `f86fa436d3065402d23bbf06de8e309e8e954ea3`, merged +in Vision PR #1 as `4feaca8822400de4a201a8768334578918d9d949`. The manifest +resolves the source diff relative to that immutable repository tree; the completed +Vision worktree has been removed. The preserved archive is recorded beside it. +The WASM/glue bytes in Solar remain identical to the CEF-qualified candidate; +checks compare their hashes with the emitted host. The native LSDP source and binary stay unchanged. + +## Proof contract + +`native-burst-producer.mjs` prepares 512 different native transactions with unique +identities, preconditions and expected intermediate hashes. Paced mode uses +producer-anchored deadlines. It records actual send and ACK times; preparing or +queuing 512 transactions is separate from their sequential application. + +`analyse-render-cadence.py` counts native ACKs, sequential LSML receipts, actual +Vision patch submissions and distinct completed covering frames in fixed 100 ms +and 1000 ms windows starting at the producer's epoch. Camera-only frames do not +count. Every complete 1000 ms window must contain at least 120 completed mutation +states. This boundary is GPU submission plus the complete front-canvas copy, +not physical monitor scanout or an internal native-core apply-time trace. + +`analyse-native-counter.py` decodes the actual 1920x1080, 60 fps CEF recording. The +scene's `texte bloc` is the counter rendered by Vision; the diagnostic HUD does +not replace it. Every frame checks the unchanged gray surface interiors against +the complete reference scene, with a codec tolerance. The clock in the recorded +CEF HUD establishes the producer-relative alignment and reports its uncertainty. +The one-second export uses 60 consecutive source frames from the **predeclared +1–2 second window**, without interpolation or counter replacement. Startup and +the first 100 ms remain measured in the complete report. + +`verify-counter-clip.py` then independently OCRs all 60 frames of the exported +file, without reusing a previous reading. It requires 60 readable, strictly +increasing values matching the original consecutive frames, an exact one-second +duration and 60/1 frame rate. The aggregate verdict must require both the fixed +cadence and the encoded-pixel criteria; passing either alone is insufficient. + +The real scene, assets, physical camera, native receiver and Pulsar CEF process +are used. All intermediate hashes and receipt order must match, with no resync. +The source LSML/LSMLZ hashes must remain unchanged; the mutated document is +ephemeral RAM state and is discarded when the owned host stops. Only the proof's +own processes are reaped. No broadcast or Prism edit is part of this proof. + +## Reproduction + +From the Solar continuation worktree, with the real immutable source and the +local CEF-enabled Pulsar executable available: + +```powershell +$env:PULSAR_BROWSER_GPU = '1' +$env:SOLAR_PROOF_CEF_FPS = '120' +$env:SOLAR_PROOF_CAPTURE_FPS = '60' +$env:SOLAR_BURST_MODE = 'paced' +$env:SOLAR_PACED_DURATION_MS = '2500' +$prefix = Join-Path (Get-Location).Path ('evidence/local-20261005-render-cadence/forge/' + [DateTime]::UtcNow.ToString('yyyyMMddTHHmmssZ') + '-proof') +python evidence/local-20261005-burst-consolidation/forge/20261005T133902Z-reproduce.py --solar (Get-Location).Path --source '' --pulsar '' --output $prefix +python scripts/proof/analyse-render-cadence.py --prefix $prefix +python scripts/proof/analyse-native-counter.py --prefix $prefix --final-deadline-ms 2800 --video-window-offset-ms 1000 --minimum-distinct-values 60 +python scripts/proof/verify-counter-clip.py --prefix $prefix +``` + +CEF source cadence and recording cadence are separate. The host must request +120 Hz for the browser source; the stream/recording can remain 60 fps. The observed +backend here is WebGL/ANGLE, not an available WebGPU adapter. A controlled input +rate (512 / 2.5 s) is a load profile, not the native server's throughput ceiling. +Scalar state updates can share one rendered frame: all transactions are applied +and covered, while the video samples only the states actually visible at 60 fps. + +The historical immediate 512-transaction burst saturated the native subscription +and needed resync before these rendering optimizations. That uncontrolled burst +has not been requalified by this paced test. The native binary has no internal +apply-time traces, so ACK transport timing must not be labelled native-core +mutation execution time. Installed-application orchestration remains a Prism +integration responsibility; this work does not modify Prism. diff --git a/docs/development/scene-feature-map.md b/docs/development/scene-feature-map.md index 63f22fe..adc958b 100644 --- a/docs/development/scene-feature-map.md +++ b/docs/development/scene-feature-map.md @@ -1,13 +1,63 @@ -# Editable Preview feature map +# Scene runtime feature map -`src/internal/editable-preview-fast-path.ts` is the stable import facade. The public `mount()` API is unchanged. +Solar has one scene path: LSDP state plus an exact ZabCanvas LSMLZ revision, +rendered by Vision. Scene source identity comes from the server snapshot; Solar +does not resolve or compile a RenderBundle. -| Feature | File under `src/internal/editable-preview/` | -| --- | --- | -| Patch types, decoding and value validation | `patch.ts` | -| LSDP snapshot/delta sequencing gate | `delta-gate.ts` | -| Applying accepted values to rendered DOM | `dom.ts` | -| Local sideband URL, accepted messages, reconnect and bounded convergence | `sideband.ts` | -| Preview WebSocket adapter | `websocket.ts` | +| Capability | Owner | Consumers / checks | +| --- | --- | --- | +| Producer-independent mutation reception, application and presentation; no producer projection in events | `src/engine/native-lsdp-runtime.ts`, `src/engine/frame-patches.ts` | `tests/unit/native-lsdp-runtime.test.ts`: no producer feedback/read, separate reception and completed-frame observations | +| Renderer phase acknowledgement and retained compensation frame | `src/engine/control-feedback.ts`, `src/engine/native-lsdp-runtime.ts` | phase lifecycle and fragmented read tests; real Orion/two-CEF proof | +| Startup catalog synchronization and partitioned bounded browser cache | `src/scenes/startup.ts`, `src/scenes/browser-store.ts`, `src/host-entry.ts` | `tests/unit/startup-cache.test.ts`; offline/pagination/conflict/eviction | +| Same-port child recovery with empty scenes, producer republication and explicit watcher reconnection | `src/server/native-server.ts` | `scripts/test-reception-recovery.mjs`; no automatic mirror, forced child death and immutable archive | +| Public mount lifecycle and required source provider | `src/mount.ts`, `src/types/index.ts` | `tests/unit/mount.test.ts` | +| Canvas scene lookup, version pinning, source/assets and Blue manifest verification | `src/scenes/canvas.ts`, `src/scenes/types.ts` | `tests/unit/scene-source.test.ts`; `scripts/prove-authenticated-canvas.mjs` | +| Native binary WebSocket snapshots, exact selectors, source fetch and distinct reception/presentation receipts | `src/engine/native-lsdp-runtime.ts`, `vendor/lsdp-native-browser/` | `tests/unit/native-lsdp-runtime.test.ts`; `scripts/test-native-lsdp.mjs` | +| Scalar coalescing with every mutation retained in covering presentation receipts | `src/engine/frame-patches.ts` | `tests/unit/frame-patches.test.ts`; blocked-frame native subscriber proof | +| Combined state/camera submission without a RAF cap, bounded media grace and owned bitmap lifecycle | `src/engine/vision-frames.ts`, `src/engine/vision-media-bridge.ts` | `tests/unit/vision-frames.test.ts`, `tests/unit/vision-media-bridge.test.ts`; fixed-window CEF cadence and encoded counter | +| Atomic LSML operations, text-value projection and temporary render packages with pinned assets/provenance | `src/scenes/native-document.ts` | `tests/unit/native-document.test.ts` | +| Native draft2 Merkle hashes and portable JSON conformance | `src/internal/native-tree.ts` | upstream vectors in `tests/unit/native-document.test.ts`; Rust interoperability | +| Atomic path copies and incremental native integrity without full-scene rehash per leaf | `src/internal/native-state.ts`, `src/internal/native-tree.ts` | `tests/unit/native-state.test.ts`; runtime and reception consumers | +| Shared native reception lifecycle, resources and real readiness | `src/server/` | `scripts/test-reception-server.mjs`; `scripts/test-reception-install.mjs`; Prism startup and Orion readiness | +| Ephemeral local source/mutation/full-LSML endpoints using the product host | `scripts/native-lsdp-local.mjs`, `scripts/native-lsdp-tools.mjs` | `npm run native:start`; `docs/development/native-lsdp.md` | +| Packaged platform binary verification | `scripts/package-native-server.mjs`, `scripts/check-native-package.mjs` | installed `src/server/native-server.ts` startup; release manifest | +| LSMLZ loading, snapshot/delta updates and visible frame swap | `src/engine/vision-presenter.ts` | `tests/unit/native-lsdp-runtime.test.ts`; `scripts/check-host-bundle.mjs` | +| Blue declaration and validated binding closure attached to each revision | `SceneBlueManifest` in `src/scenes/types.ts` | source provider and runtime integrity checks | +| Immutable source cache and atomic Node storage | `src/scenes/cache.ts`, `src/server/scene-store.ts` | cache/store suites; offline revision, assets, Blue closure, corruption and live-variant refusal | +| Authenticated Canvas acquisition and real full-process cold restart | `scripts/prove-authenticated-canvas.mjs`, `scripts/prove-cold-start-cef.py`, `scripts/proof/cold-start-host.mjs` | actual local Canvas capsule or explicit fixture mode; Orion/Rust/CEF, both lanes and all discovered commands; remote producer probe separate | +| First-party module reachability | `scripts/check-source-usage.mjs` | `npm run check:source-usage`; tsc/eslint for unused symbols | +| Isolated native reception, immutable hashing and frame-paced scalar receipts | `src/engine/native-worker.ts`, `native-worker-client.ts`, `frame-patches.ts`, `src/internal/native-state.ts`, `native-tree.ts`, `native-stream.ts`, `native-digest.ts` | native-state/stream/worker-client/frame-patches/digest suites; real native/CEF counter acquisition | +| Atomic completed-frame projection for CEF | `src/engine/canvas-presentation.ts`, `vision-presenter.ts` | canvas-presentation suite; recorded unchanged gray panels checked on every encoded frame | +| Local camera/screen/window/app resolution and Vision texture updates | `src/sources/capture.ts`, `src/engine/live-media.ts` | `tests/unit/capture-resolution.test.ts`, `tests/unit/live-media.test.ts` | +| Receive-only remote camera rooms, slot mapping and stream lifecycle | `src/sources/peers.ts`, `src/peer-viewer/` | peer injection, Antenne and slot-binding suites; `scripts/prove-peer-camera-cef.py` with actual Meet, publisher and two CEF viewers | +| Served host configuration and Vision static assets | `src/host-entry.ts`, `host.html`, `public/vision/` | host build and `npm run check:bundle` | +| Single Vision asset owner and strict combined release layout | `vite.config.ts`, `vite.config.host.ts`, `scripts/check-runtime-layout.mjs` | `tests/unit/runtime-layout.test.ts`; build, bundle and packing gates | -Dependencies flow from transport to gate/parser and DOM application. Timings, accepted properties, sequence rules and teardown remain the existing implementation. The existing fast-path test suite imports the facade to retain compatibility coverage. +The same Vision renderer handles broadcast, control and test subscriptions. Mode +changes host audio policy; resource/selector declares the subscription. Source loads +are pinned to the incoming `(scene_id, scene_version)`; a newer snapshot aborts +an obsolete load before it can become visible. +Native subscriptions instead deliver the full LSML document and source mutations; +the source identity remains pinned while its temporary render variant changes. +Native scene_version is the source LSML address; x-orion-artifact-set is the +separate admitted Orion generation key. Both survive the preparation boundary. + +The scene record retains ZabCanvas's revision-pinned Blue declarations and +binding closure alongside the LSMLZ. This is the metadata retained in the synchronized local scene and Blue cache. The cached provider now stores verified +immutable sources; served-host startup synchronization is wired when credentials +and IndexedDB are available. Solar +does not execute Blue programs itself. + +The WebRTC compatibility dependency is resolved only to its receive-only viewer +entry. Its compiled scene renderer, Solar's old React renderer, bundle URL +resolver, atlas, overlay and editable-preview sideband are removed. + +Run `npm run typecheck`, `npm run lint`, `npm test`, `npm run build` and +`npm run check:bundle` after changing these paths. Physical camera capture, +authenticated production access and Pulsar composition still require a host +smoke test. + +Qualification and maturity: docs/runbooks/qualification.md and docs/development/maturity.md own reproducible proof procedures and the current evidence boundary. The Vision asset manifest and scripts/check-vision-assets.mjs own byte pins; scripts/check-source-usage.mjs owns first-party module reachability. + +Derived file/symbol/import/consumer index: [code-map](code-map.md). +Maintenance and historical-tool boundaries: [Solar tooling](../../scripts/README.md). diff --git a/docs/development/source-cache.md b/docs/development/source-cache.md new file mode 100644 index 0000000..81550e2 --- /dev/null +++ b/docs/development/source-cache.md @@ -0,0 +1,69 @@ +# Cache de sources immuables + +Propriétaires : src/scenes/cache.ts, startup.ts et browser-store.ts ; stockage +Node : src/server/scene-store.ts ; démarrage servi : src/host-entry.ts. + +createCachedSceneSourceProvider compose un fournisseur Canvas et SceneSourceStore. +Une requête sans version découvre la révision sur Canvas. Une requête exacte +utilise la capsule locale vérifiée, sans réseau ; un miss acquiert puis conserve +la source publiée. Une capsule corrompue échoue explicitement sans changer de scène. + +La capsule solar.scene-cache.v1 conserve les octets LSMLZ, ou LSML et assets, +ainsi que le manifeste Blue complet. Chaque lecture revalide l'adresse LSML, +le digest des octets source/transport, les assets hashés et la fermeture Blue. +offline_ready et binding_closure_complete doivent être vrais. Modifier le LSML +sans changer son adresse est refusé même si son digest d'octets est recalculé. + +FileSceneSourceStore publie une seule fois par staging/link atomique dans le même +dossier. Une publication concurrente identique est idempotente ; un contenu +différent refuse SOURCE_CACHE_KEY_CONFLICT et préserve la capsule acceptée. +Les clés sont SHA-256(id, version, format), jamais un chemin utilisateur. +Les tailles d'archive, d'assets et les expansions ZIP sont bornées. + +```ts +// Hôte Node/Electron ; transférer les sources à la frontière de rendu existante. +import { FileSceneSourceStore, createCachedSceneSourceProvider } from "@zablab/solar/server"; +const provider = createCachedSceneSourceProvider(canvasProvider, + new FileSceneSourceStore(accountOwnedSourceDirectory)); +const scene = await provider.get(sceneId, { sceneVersion, format: "lsmlz" }); +``` + +Le host Solar utilise directement le fournisseur Canvas, sans synchronisation du +catalogue au lancement, sans préchargement et sans store IndexedDB. Un changement +de scène effectue une nouvelle acquisition de source ; seules les sources et +ressources de la scène active restent disponibles pour ses mutations continues. +Les adaptateurs de stockage exportés ci-dessous restent des surfaces existantes à +retirer avec leurs consommateurs ; ils ne sont plus raccordés au host Solar. + +BrowserSceneSourceStore partage une transaction IndexedDB entre les deux lanes, +avec partition SHA-256(API, credential), 64 entrées et 512 MiB, éviction par accès. +Le credential n'est pas écrit. Sa rotation ouvre une nouvelle partition, et +IndexedDB dépend de l'origine du host ; cette politique conservatrice n'affirme +pas un cache unifié entre origines ou tokens. L'hôte Node peut fournir le store +de fichiers par identité stable lorsque l'intégration applicative sera reprise. +Prism n'a pas été modifié. Le runtime ne sauvegarde jamais le LSML live muté. +Le cache de rendu n'autorise pas Blue : Orion conserve son admission des capsules +signées/programmes. Un manifeste valide par digest n'est pas une signature. + +L'adresse source `scene_version` est distincte du `artifact_set_digest` admis +par Orion. Les documents Solar natifs conservent la première et portent la +seconde sous `x-orion-artifact-set`. Le manifeste Blue porte l'adresse source ; +le catalogue Orion conserve le programme signé et les déclarations associées. + +`scripts/prove-cold-start-cef.py` exerce FileSceneSourceStore et le catalogue +Orion après arrêt complet des processus : deux nouvelles instances CEF reprennent +Program/Preview, source/assets/déclarations inchangés, mutations live abandonnées. +Le harnais sert le cache Node vérifié aux CEF si nécessaire. Il ne certifie pas +la persistance IndexedDB entre processus. Sans `--capsule`, le manifeste et les +signatures sont des fixtures de contrat explicitement déclarées. Avec +`--capsule`, il consomme le résultat HTTP/SQL réellement validé et signé par le +Canvas local, qui a utilisé l'identité ZabAuth et la Blue publiée via ZabGate. +La clé reste locale au producteur ; ce n'est pas un déploiement Canvas distant. +`scripts/prove-authenticated-canvas.mjs` qualifie séparément login, référence +signée réelle et acquisition source LSMLZ/LSML. Le locator broker-only refuse le +bearer opérateur comme prévu ; sa récupération mTLS est une preuve séparée. +Voir [maturité et certificat](maturity.md). + +Tests : scene-cache.test.ts, scene-store.test.ts, startup-cache.test.ts. check:source-usage couvre les +modules accessibles depuis les quatre entrées ; tsc/eslint couvrent les symboles. +Cette analyse ne promet pas de détecter tous les usages externes/dynamiques. diff --git a/docs/lumencast-runtime-pin.md b/docs/lumencast-runtime-pin.md deleted file mode 100644 index 6271835..0000000 --- a/docs/lumencast-runtime-pin.md +++ /dev/null @@ -1,26 +0,0 @@ -# Temporary Lumencast runtime pin - -The signed `v0.18.3` tag did not publish to npm because its GitHub Actions -`NPM_TOKEN` cannot publish `@lumencast/canonical`. Lumencast PR #135 is merged -as commit `6e04ec7cb205559045bc75737ca5a7ad0c7e6a11`, tree -`7e1dd919bc32bcac3e2073b22168f3cc66b428a3`. Its workspace package version -is `0.18.4`, which is **not published**. Do not tag a release until npm access is -repaired. - -For now Solar's Vite library and browser host import the generated runtime -chunks in `vendor/lumencast-runtime/`. They were built with `npx vite build` in -`packages/runtime/` from a worktree whose tree matched that merged commit. -The upstream Apache-2.0 license accompanies the vendored files. -`manifest.json` pins every generated JS chunk and source map by SHA-256. The -host-bundle gate verifies this inventory and the `supportsHostAssetUrls` -capability; an installed npm `0.18.2` entry cannot silently replace it. The -installed package still supplies TypeScript declarations and Solar's existing -postinstall compatibility patches. - -To reproduce, check out that exact Lumencast commit in an isolated worktree, -install its locked dependencies, run the runtime Vite build, and compare the -seven JS chunks and seven source maps with `manifest.json`. To remove the pin -after the matching version is published, update Solar's dependency and lockfile, -restore both Vite aliases to the installed runtime entry, remove this vendor -directory, and rerun Solar's build/bundle/typecheck/tests plus Prism's integrated -PNG E2E before merge. No Pulsar code is changed by this pin. diff --git a/docs/runbooks/qualification.md b/docs/runbooks/qualification.md new file mode 100644 index 0000000..53cfdb9 --- /dev/null +++ b/docs/runbooks/qualification.md @@ -0,0 +1,106 @@ +# Qualifier Solar, Orion et CEF + +Propriétaire : Solar pour rendu/paquet/récepteur, Orion pour exécution/publication +native, ZabCanvas pour admission/signature/source. Chaque preuve reste dans son dépôt. +Les worktrees actifs et les sessions applicatives restent préservés. Ne jamais +écrire le document live muté dans la source originale. + +## Candidat stable + +Arrêter uniquement les processus possédés avant de reconstruire dist. Construire +le binaire natif release depuis la révision exacte, avec le target-dir sous Solar ; +le checkout Lumencast reste en lecture seule. Le paquet installé n'a besoin ni de +Cargo ni de checkout Lumencast. Le manifeste vérifie le SHA256 du binaire. + +```powershell +npm.cmd run lint +npm.cmd run typecheck +npm.cmd run check:architecture +npm.cmd run check:native-client +npm.cmd test +npm.cmd run build +npm.cmd run package:native -- --binary --platform win32-x64 --revision +npm.cmd run check:native-package +npm.cmd run check:bundle +node scripts/test-reception-recovery.mjs +node scripts/test-reception-server.mjs +node scripts/test-reception-install.mjs +``` + +Le dernier test charge l'adaptateur existant Prism en mémoire avec un résolveur de +racine installé, reçoit un LSML complet et une opération, contrôle Preview/client +TCP Orion puis récolte le processus. Il qualifie dist et cet adaptateur ; +le cycle complet de l'application Prism installée reste distinct. + +Pour qualifier le tarball npm, installer dans un dossier neuf sous Solar/build +avec la politique npm allowScripts locale autorisant le postinstall Solar. +Les scripts de patch doivent être présents dans le tarball et retrouver les +dépendances hoistées via import.meta.resolve. Vérifier les exports navigateur/server +et tous les hashes dist contre le candidat ; ne pas modifier la politique npm globale. + +Dans Orion, utiliser le toolchain déclaré, exécuter go test -race -json ./... avec +un compilateur C local, go vet, staticcheck et go build. Les opt-in natifs sont +exécutés séparément par le test de réception. Dans Canvas : uv run ruff check ., +uv run mypy src et uv run pytest ; garder les skips externes visibles. + +## Admission réelle et reprise froide + +Depuis ZabCanvas, le script suivant travaille dans une base locale isolée. +Il lit la vraie scène et l'identité à travers ZabGate, valide la Blue publiée, +projette les sept assets de l'archive complète et appelle la validation Vision. +Il publie/projette/valide/signe uniquement la copie locale. Les credentials restent +en RAM ; la clé de signature locale privée n'est pas exportée. + +```powershell +uv run --project python /scripts/prove-vision-source.py --session --credentials --scene --archive --output /evidence/local-20261005-final-certification/forge/-actual-vision +``` + +Le rapport nomme producerArtifacts. Il contient scene.lsml, scene.lsmlz et +capsule.json (programme/manifeste/signature + trust public). Le driver Solar +consomme cette capsule, sans verdict semé ni clé créée par le host. Regénérer la +capsule juste avant la preuve ; conserver les durées réelles de cinq minutes +pour les refs et une minute pour la readiness. + +```powershell +build/proof-python/Scripts/python.exe scripts/prove-cold-start-cef.py --pulsar --orion --source --capsule --output evidence/local-20261005-final-certification/forge/-canvas-cold +``` + +Le driver vérifie deux phases, des processus neufs, Program et Preview en CEF, +les surfaces/images/textes et la caméra, les capsules/déclarations/sélection +inchangées et l'abandon des mutations RAM. Les commandes sont découvertes depuis +le programme reçu ; aucune liste LEC/LCK particulière n'est un oracle. +Hors ligne, les requêtes DB échouent explicitement et leurs erreurs sont vérifiées. +Sans --capsule, le driver utilise des signatures/manifeste de contrat de test : +ne pas présenter ce mode comme une admission réelle Canvas. + +Le cache disque est vérifié entre processus ; cette preuve n'est pas une preuve +de persistance IndexedDB ni d'autorité Blue indéfinie après expiration. + +## Caméra peer et scène complète + +```powershell +build/proof-python/Scripts/python.exe scripts/prove-peer-camera-cef.py --pulsar --orion --meet --source --output evidence/local-20261005-final-certification/forge/-final-peer-camera +``` + +Le harnais lance le vrai serveur Meet, un publisher caméra dans un Pulsar séparé +et deux Solar CEF. Il vérifie RTP décodé, absence d'émission vidéo des viewers, +arrivée/départ/retour, réaffectation Program et libération Preview ; dix images +1920×1080 conservent les surfaces et assets complets. Les affectations de cette +preuve passent par les mutations natives du harnais ; les contrats Orion assign/ +release ont leurs tests séparés. Tous les processus possédés sont récolés. +Cette preuve est locale : aucune qualification WAN/TURN n'est implicite. + +## Frontière déployée et consolidation + +scripts/prove-authenticated-canvas.mjs sépare identité/JWS réelles, route source +et locator broker-only. Un opérateur refusé sur ce locator n'est pas une preuve +d'absence d'artefacts. Le flux mTLS/delegation est à exercer par l'intégration Prism. +Le Canvas déployé doit recevoir les nouvelles routes avant la preuve distante. + +Le générateur du certificat antérieur est conservé avec ses +[preuves historiques](../../evidence/local-20261005-solar-audit/forge/20261005T183100Z-historical-certify-chain.py). +Il réconciliait les hashes et baselines propres à cette phase. Il n'est plus une +commande de qualification courante et ne doit pas régénérer la documentation actuelle. +Voir +[la maturité et le certificat](../development/maturity.md) pour résultats, +frontières et branchements restants dans Prism. diff --git a/docs/runbooks/release-deploy.md b/docs/runbooks/release-deploy.md index 2f4000e..ce4e6c3 100644 --- a/docs/runbooks/release-deploy.md +++ b/docs/runbooks/release-deploy.md @@ -1,154 +1,86 @@ -# Runbook — Release Solar et consommation par Orion - -**Restauré par** : PR Solar #27 (`879510d`) — branche `keeper/solar-release-pipeline` -**Date** : 2026-06-24 -**Scope** : flux release Solar → GitHub Release → déploiement Orion VPS - ---- - -## Contexte et dette - -PR Solar #26 (`7fd68de`) avait supprimé `release.yml` et `ci.yml` en basculant sur -des hot-pushes manuels de `dist/` vers le VPS. Conséquences : - -- v0.2.10 et v0.2.11 ont été installées manuellement sur le VPS sans GitHub Release. -- **Une fresh-box est irreconstruisible** : aucune source versionnée pour récupérer ces tarballs. -- Le contrat d'asset (`solar-.tgz`, layout plat de `dist/`) attendu par - `Orion deploy.yml` n'était plus alimenté. - -PR #27 restaure le mécanisme de release automatisé. La dette résiduelle est listée -en fin de document. - ---- - -## Flux release → deploy - -### 1. Déclencheur : tag semver sur Solar - -Un push de tag `v*.*.*` sur le repo Solar déclenche `release.yml` -(branche `keeper/solar-release-pipeline`, à merger sur `main`). - -### 2. Build sur les runners self-hosted - -Le job tourne sur `[self-hosted, vps-ovh]` — le billing GitHub Actions est gelé -sur l'organisation, les runners ubuntu-latest ne sont pas disponibles. - -Séquence du job : - -1. `npm ci` (pas de cache npm — les runners JIT sont éphémères, le cache ne serait - jamais réutilisé, cf. note Orion `setup-go cache disabled`) -2. `lint` → `typecheck` → `vitest` → `build` → `check:bundle` -3. Playwright Chromium installé sans `--with-deps` (runner non-root nosuid, - les dépendances OS sont bakées dans l'image du pool) -4. `test:e2e` -5. Vérification que `package.json version` correspond au tag (échoue le job si dérive) -6. Pack : `tar -czf release/solar-.tgz -C dist .` — pack plat de `dist/` -7. Publication en GitHub Release via `softprops/action-gh-release@v2` - (notes de release auto-générées) - -### 3. Asset publié - -Nom : `solar-.tgz` -URL de download (publique, sans token) : -``` -https://github.com/ZabLaboratory/Solar/releases/download//solar-.tgz +# Solar release and deployment contract + +Owner: Solar packaging; Orion and the application own installation/activation. Source of +truth: `.github/workflows/release.yml`, `package.json`, `scripts/check-runtime-layout.mjs`, +`scripts/create-runtime-manifest.mjs` and `scripts/check-native-package.mjs`. +This runbook describes the current candidate. It does not claim a release was published, +a runner provisioned, Linux qualified or the installed Prism application migrated. + +## Build and qualify + +The workflow triggers on `v*.*.*` tags and uses the configured `[self-hosted, vps-ovh]` runner +with Node 22. It performs npm installation, lint, type checks, architecture checks, native +client pins, tests, build and bundle checks. The package version must match the tag. + +```powershell +npm.cmd ci +npm.cmd run check:architecture +npm.cmd run test:architecture +npm.cmd run lint +npm.cmd run typecheck +npm.cmd test +npm.cmd run build +npm.cmd run check:bundle ``` -Layout de l'archive : contenu de `dist/` à la racine du tgz (pack plat, -`tar -C dist .`). **Ce layout est un contrat avec Orion `deploy.yml` — ne pas modifier -sans aligner simultanément le step d'extraction Orion.** +Postinstall patches only the receive-only WebRTC modules needed by Solar; its scripts and +helpers must ship in the npm tarball. Dependency resolution supports hoisted installations. +Unit checks do not replace the [native/installed/CEF qualification](qualification.md). -### 4. Consommation par Orion (deploy.yml) +`npm run build` clears dist. Package unchanged native binaries **after** building: -Le job « Install Solar bundles » de `Orion deploy.yml` tourne après le rsync du code. -Variable d'environnement `SOLAR_VERSIONS` (ex. `"v0.2.8 v0.2.9"`) liste les versions -à installer. - -Pour chaque version : - -1. SSH vers VPS, `mkdir -p $APP_PATH/solar/` -2. Si `.installed` présent → skip (idempotent) -3. `curl -fsSL --retry 3` vers l'URL de release Solar (anonyme, repo public) -4. `tar -xzf` dans `$APP_PATH/solar//` -5. `touch .installed` (marqueur de succès — un download partiel laisse la version - sans marqueur, le prochain deploy retente) - -Le rsync Orion exclut `solar/` via `--exclude 'solar'` : les versions installées -survivent aux re-deploys sans être écrasées. - -### 5. Service par Orion - -Orion monte `$APP_PATH/solar/` (alias `ORION_SOLAR_ROOT`, défaut `/var/lib/orion/solar`) -en volume read-only dans le container (`docker-compose.prod.yml`). - -L'handler Go `internal/api/static.go` sert les fichiers sous : -``` -GET /static/solar/v{N.N.N}/ -``` -Via ZabGate : `GET /orion/static/solar//index.html` - -Consommateurs : -- **Pulsar CEF** (browser-source antenne) : pointe vers `host/index.html?orion=...&mode=broadcast` -- **Prism** : vendor les bundles Solar dans `resources/solar/v{N.N.N}/` - -Health-check de présence d'une version (suit la redirection 301 → `./`) : -```bash -curl -fsSL -o /dev/null -w '%{http_code}\n' \ - https://zabgate.cyell.dev/orion/static/solar//index.html -# attendu : 200 +```powershell +npm.cmd run package:native -- --binary --platform win32-x64 --revision +npm.cmd run package:native -- --binary --platform linux-x64 --revision +npm.cmd run check:native-package -- win32-x64 linux-x64 ``` ---- - -## Procédure de cut (publier une nouvelle version Solar) - -1. Bumper `version` dans `package.json` (ex. `0.2.12`). -2. Commit `release: vX.Y.Z`, push sur `main` (ou merger la PR). -3. Tagger : `git tag vX.Y.Z && git push origin vX.Y.Z`. -4. `release.yml` se déclenche sur le tag. Vérifier que le job passe sur - l'interface Actions (runner `vps-ovh`). -5. Vérifier la GitHub Release créée + présence de l'asset `solar-vX.Y.Z.tgz`. -6. **Aligner Orion** : PR sur `Orion deploy.yml` pour ajouter `vX.Y.Z` à `SOLAR_VERSIONS`. - Merger → le deploy Orion installe la version. -7. Health-check via gateway (commande ci-dessus). -8. Pointer Pulsar browser-source vers la nouvelle version si roll-forward. +The release workflow takes `SOLAR_LSDP_WINDOWS_BINARY` and `SOLAR_LSDP_LINUX_BINARY` runner +variables. Missing files fail assembly. The manifest pins platform binary digests and source +revision; the installed application requires no Cargo, source checkout or esbuild. ---- +## Artifacts and layout -## Rollback - -Le mécanisme de release Solar ne modifie aucun état sur le VPS seul : le -marqueur `.installed` et le répertoire de version ne sont écrits que par -`Orion deploy.yml`. +- npm package: ESM/browser API `dist/solar.js`, declarations, workers, Node server entry, + host/Vision assets and the required postinstall files. `prepack` checks the combined layout. +- release: `solar-.tgz`, flat `dist/` contents, plus `solar-runtime-manifest.json`. + Both Windows/Linux native binaries are mandatory for this release manifest. +- standalone browser page: `host/index.html`; assets resolve relative to the page. + An installed host may promote the host directory into its runtime root only after the + collision/digest check. Flat dist packaging does not itself perform that promotion. +- server: `server/index.mjs`, declarations, `native/manifest.json` and platform binaries. -Pour revenir à une version précédente : -1. Pointer le browser-source Pulsar vers la version précédente (ex. `v0.2.9`) — - les versions installées coexistent sur le VPS, switch instantané sans redeploy Orion. -2. Retirer la version cible de `SOLAR_VERSIONS` dans `Orion deploy.yml` si - on veut éviter qu'elle soit réinstallée (optionnel, le `.installed` l'idempotentise). +The runtime layout guard rejects missing or duplicated Vision assets, changed bytes and +collisions between host promotion and library/server files. Its tests cover adversarial +layouts. The release runs this guard immediately before archive creation. -Il n'y a pas de rollback de la GitHub Release elle-même (l'asset restera -accessible) — révoquer un tag Solar n'a aucun effet sur le VPS déjà installé. +The archive name, flat dist layout, manifest schema and Orion static version route are +integration contracts. Changing them requires corresponding consumer work. The archive +contains application JavaScript and a native receiver; it is not a compiled scene bundle. ---- +## Publish, install and activate -## Dette résiduelle +For an explicitly authorized release: update package/lock version, qualify the exact +candidate, commit with the configured signature, and create a **signed** `vX.Y.Z` tag. +Push the tag only when publication is authorized. The workflow publishes the release assets. +Verify the release archive/manifest hashes separately from CI completion. -| Poste | État | Porteur | -|---|---|---| -| `SOLAR_VERSIONS` Orion (`"v0.2.8 v0.2.9"`) non aligné sur les versions disponibles (`v0.2.10`, `v0.2.11`) | En cours | Keeper | -| `ci.yml` Solar non restauré (gate PR optionnelle) | Différé (pas bloquant pour la release) | — | -| Premier cut viewer WebRTC (#3/#4) | En attente validation porteur + RC-Q | — | +Orion's deployment or the installed application then downloads and verifies the selected +release, installs it into its versioned runtime directory and activates that exact version. +The configured static route is `/static/solar//host/index.html` for the flat archive; +a host-promoted installation serves its own root page. Confirm the consumer's actual layout +and URL before use. Supply the local native WebSocket/resource (and exact selector for +collections), plus the trusted Canvas config; old `?orion=...&scene=...` URLs are obsolete. ---- +The Node/Electron application owns the shared reception host, readiness, TCP/WS distribution, +recovery, credentials, logout and shutdown. Completing this Solar package does not wire the +installed Prism application's lifecycle. Deployment/activation and a real cold-start/CEF +smoke test are separate completion states. -## Invariants du contrat d'asset (ne pas casser) - -- Nom de l'asset : `solar-.tgz` (tag incluant le `v`, ex. `solar-v0.2.12.tgz`) -- Layout : pack plat de `dist/` (`tar -C dist .`) — `index.html` à la racine du tgz -- URL : `releases/download//solar-.tgz` — download anonyme (repo public) -- Destination VPS : `$ORION_SOLAR_ROOT//` (ex. `/var/lib/orion/solar/v0.2.12/`) -- Route Orion : `GET /static/solar//*` → `ORION_SOLAR_ROOT//*` +## Rollback -Toute modification de l'un de ces points nécessite une PR simultanée sur -Solar `release.yml` ET Orion `deploy.yml`. +Keep the previous verified runtime installed. Stop the owned receiver and switch the browser, +server and package to the same previous version; restart from admitted immutable source and +selection. Do not carry incompatible RAM mutations or mix native manifests across versions. +A signed source revert follows a reviewed PR. Deleting a release tag does not roll back an +already installed runtime, and a successful download does not prove activation. diff --git a/eslint.config.mjs b/eslint.config.mjs index bb1eb43..84bb854 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,22 +1,22 @@ import js from "@eslint/js"; import globals from "globals"; import tseslint from "typescript-eslint"; -import react from "eslint-plugin-react"; -import reactHooks from "eslint-plugin-react-hooks"; import prettier from "eslint-config-prettier"; export default tseslint.config( { ignores: [ "dist", + // Generated builds, proof environments and isolated installation fixtures. + "build", "node_modules", - "playwright-report", + "public/vision", + // Byte-pinned Apache-2.0 upstream browser client, checked separately. + "vendor/lsdp-native-browser", "test-results", // Local profiling captures contain generated/minified host bundles, // not Solar source files. Keep the full source lint intact. "evidence", - // Reviewed, generated Lumencast chunks are integrity-checked at build. - "vendor/lumencast-runtime", // Agent worktrees / harness checkouts live untracked under .claude // and carry their own built dist — never Solar's lintable source. ".claude", @@ -29,32 +29,18 @@ export default tseslint.config( ...tseslint.configs.recommended, { // Node-side scripts run in a Node ESM context. - files: ["scripts/**/*.mjs", "scripts/**/*.js"], + files: ["scripts/**/*.mjs", "scripts/**/*.js", "tests/tooling/**/*.mjs"], languageOptions: { globals: { ...globals.node }, }, }, { - files: ["**/*.{ts,tsx}"], - plugins: { - react, - "react-hooks": reactHooks, - }, + files: ["**/*.ts"], languageOptions: { ecmaVersion: 2022, sourceType: "module", - parserOptions: { - ecmaFeatures: { jsx: true }, - }, - }, - settings: { - react: { version: "19" }, }, rules: { - ...react.configs.recommended.rules, - ...reactHooks.configs.recommended.rules, - "react/react-in-jsx-scope": "off", - "react/prop-types": "off", "@typescript-eslint/no-unused-vars": [ "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, diff --git a/evidence/local-20261005-burst-consolidation/forge/20261005T144638Z-qualification.md b/evidence/local-20261005-burst-consolidation/forge/20261005T144638Z-qualification.md new file mode 100644 index 0000000..c2c98ae --- /dev/null +++ b/evidence/local-20261005-burst-consolidation/forge/20261005T144638Z-qualification.md @@ -0,0 +1,41 @@ +# Compteur LSDP natif — preuve CEF réelle + +Verdict : compte et ancrage vérifiés pour le flux cadencé ; **fluidité de 60 nouveaux états par seconde non certifiée**. La rafale immédiate n'est pas qualifiée pour la livraison exhaustive des états intermédiaires. + +Le grand compteur est la feuille LSML `/defaults/__lit.text.text_msxz43eh_1`, rendue par Vision. Les diagnostics DOM ne dessinent pas ce compteur. Pulsar enregistre sa vraie source CEF à 1920×1080 avec la caméra physique et tous les assets/surfaces de la scène. Les 60 images de l'extrait proviennent de 60 images consécutives de l'enregistrement ; aucun nombre/image interpolé. + +| Mesure | Cadencé sur 850 ms | Rafale immédiate | +|---|---:|---:| +| Transactions et opérations produites | 512 / 512 | 512 / 512 | +| Acquittements natifs appliqués | 512 | 512 | +| Dernier acquittement natif | 868.8 ms | 506.8 ms | +| Mutations reçues/appliquées dans Solar | 512 | 108 puis snapshot de resynchronisation | +| Soumissions Vision couvrant ces mutations | 23 | 8 | +| Valeurs du compteur réellement visibles dans 1 s | 23 | 7 | +| Dernier compteur dans l'extrait de 1 s | 512 | 247 | +| Première apparition de 512 dans l'enregistrement | 979.8 ms | 1312.3 ms | +| Incertitude d'alignement des pixels | ±34.2 ms | ±25.2 ms | +| Frames avec surfaces grises manquantes | 0 | 0 | + +Dans les 100 premières ms du test cadencé : 60 ACK natifs, 27 événements LSML Solar, 8 reçus couverts par 3 soumissions Vision. Les six images encodées montrent 0, 1, 8, 8, 8, 27. Ces frontières sont distinctes. Le binaire natif n'expose pas le temps interne pur d'application : le débit des ACK n'est pas présenté comme ce temps interne. + +Le flux cadencé possède les 512 identités dans l'ordre ; les 512 hashes intermédiaires correspondent au producteur et le dernier hash de la frame couvrante correspond au hash natif final. Aucun resync ni erreur de rendu. Le tampon `broadcast::channel(64)` et la file de transport de 16 événements expliquent le `SUBSCRIBER_LAGGED` observé en rafale immédiate ; Solar récupère le dernier état mais ne fabrique aucune preuve des événements intermédiaires absents. Lumencast est resté inchangé. + +La copie du canvas complet vers CEF coûte 0.2 ms en médiane (3.7 ms maximum). Elle corrige les surfaces partiellement échantillonnées ; les mesures ne permettent pas de la désigner comme principal coût de la cadence. + +LSML/LSMLZ d'origine inchangés, mutation uniquement en RAM puis restauration, caméra conservée, même PID natif, Prism inchangé, processus de preuve récolés. Les validations locales couvrent 185 tests sur 24 fichiers, types/lint/build/bundle, 33 modules accessibles et les pins natif/Vision. Le hook global du workspace a expiré après 300 s ; aucun succès global n'est revendiqué. + +- [Extrait cadencé : 1 s, 60 images](D:\Documents\Zab\Artifacts\2026-10-05\lsdp-512-verified\20261005T144202Z-paced-512-lsdp-cef-1s-60fps.mp4) +- [Enregistrement cadencé complet](D:\Documents\Zab\Artifacts\2026-10-05\lsdp-512-verified\20261005T144202Z-paced-512-lsdp-cef.mp4) +- [Rafale immédiate complète, échec conservé](D:\Documents\Zab\Artifacts\2026-10-05\lsdp-512-verified\20261005T143355Z-burst-512-lsdp-cef.mp4) +- [Rapport détaillé cadencé](D:\Documents\Zab\Solar\.worktrees\forge-local-20261003-source-resolution\evidence\local-20261005-burst-consolidation\forge\20261005T144202Z-report.json) +- [Rapport détaillé immédiat](D:\Documents\Zab\Solar\.worktrees\forge-local-20261003-source-resolution\evidence\local-20261005-burst-consolidation\forge\20261005T143355Z-report.json) + +Reproduction depuis le worktree Solar : + +```powershell +$env:SOLAR_BURST_MODE='paced' # 'burst' pour la rafale immédiate +$env:SOLAR_PACED_DURATION_MS='850' +python evidence/local-20261005-burst-consolidation/forge/20261005T133902Z-reproduce.py --solar 'D:\Documents\Zab\Solar\.worktrees\forge-local-20261003-source-resolution' --source 'C:/Users/Mathias/AppData/Local/Temp/codex-zab/solar-vision-localproof/scene.lsml' --pulsar 'D:/Documents/Zab/Pulsar/upstream/build_x64/rundir/RelWithDebInfo/cef-local-proof/bin/64bit/pulsar.exe' --output '' +python scripts/proof/analyse-native-counter.py --prefix '' +``` diff --git a/evidence/local-20261005-final-certification/forge/20261005T123800Z-final-chain-certificate.json b/evidence/local-20261005-final-certification/forge/20261005T123800Z-final-chain-certificate.json new file mode 100644 index 0000000..d2d8d04 --- /dev/null +++ b/evidence/local-20261005-final-certification/forge/20261005T123800Z-final-chain-certificate.json @@ -0,0 +1,1359 @@ +{ + "stamp": "2026-10-05T12:45:58.595314+00:00", + "result": "PASS_LOCAL_READY_FOR_PRISM_INTEGRATION", + "authority": "finish local readiness except Prism; no agents, ADR, commit, push, merge or deployment; Lumencast read-only", + "producerAdmission": "PASS_ACTUAL_CANVAS_VISION_ADMISSION", + "candidates": { + "Solar": { + "head": "537b606f9ecf97b5e0ce0f5bd80e88350f0823d5", + "branch": "forge/local-20261003-source-resolution", + "files": { + "docs/development/maturity.md": "dea05c6fe5d9547c93b67d85f7021663f89b9691da39a794c86e657b65ddfb64", + "docs/development/native-lsdp.md": "6347963290333185c56ab3c0ba26fed0a40bd4d300bbeb40ac0ce1564617ed1e", + "docs/development/source-cache.md": "144db13ad06e1e9ffab430f6287c1e021fe507184cd0974882fd32a470cb2f55", + "docs/runbooks/qualification.md": "8a04b68d0880c5f9771ab8e8779faa7456d8f8b41cadefd5bd312e61bc5afdfc", + "public/vision/pkg/lumencast_vision_web.js": "971569818b7296782210e830553cf56b273cf611efae4da31ac0934139738f5f", + "public/vision/pkg/lumencast_vision_web_bg.wasm": "dc76aeaf8d2d949ff6709f3d16060bd614dc57c7a8b7263a0c1c335b4729003b", + "public/vision/ui/fonts.mjs": "d49ed45181a8aa714f675aaa24f5439e9547b274d6eba6cd2bb995c981765c5f", + "public/vision/ui/mainpresenter.mjs": "92cd1fdc5ab30b90630f0cda648144c877f4dc86ec6dc102634eb8be5dd65be8", + "public/vision/vision-assets.json": "1297bb83f838c1a643f033d6ba0275c44d046d4fdff665c6e2bbfdc52c489864", + "scripts/build-server.mjs": "c8fd800b02d30753a58cb15665adc1f8b8ba23cccb79b3b365a62d7547dd8c69", + "scripts/certify-chain.py": "531b7fae2b372c627851a44a0648426da9c45ef39034e2853b2ea06983880bd4", + "scripts/check-native-client.mjs": "b12bc82af28f634c177a568a9795f0ea491dc0f1a7f8e07944b238a345493262", + "scripts/check-native-package.mjs": "b7d7c746fd9301f47773316186ac2fb7762034592e0e7e866b4cb8e2b6c50ef1", + "scripts/check-runtime-layout.mjs": "5b9b4d2e6be31365f819f28a9800351dd10a1b12d4db54139c63bb12e649f6d8", + "scripts/check-source-usage.mjs": "66bf88e99a456e22c8143328a02d65a59786261798377c1e081f71c20b8aa7b2", + "scripts/check-vision-assets.mjs": "4b251114d0c43f88d485864e87b05a0c03e4d95824a6ceec27a215e5d97a62d1", + "scripts/consolidate-canvas-offline.py": "fbbbf3dda4117b3327cba1461dfd678a7d23677d3073ef92942af1f4fb2eecba", + "scripts/consolidate-maturity.py": "b4af27dcde877c63f958e3cca3a1c9a28e1865a1e154ba2a2aa4aa22230e93b9", + "scripts/native-lsdp-local.mjs": "7a8e4baa01f76ee31aaab1c0cea721bc260727b3aea727912f9db4ea08c9d594", + "scripts/native-lsdp-tools.mjs": "ca2528f2d9a2be0472c8783e569694e731abc7a2f71ffcddc9165112c153ea85", + "scripts/package-native-server.mjs": "acdbc4619240f0305fadd7749e6d896ec2d250d3df73f81162769827fbd154d0", + "scripts/proof/cold-start-host.mjs": "0d38c9c17ceeb4721e968475ca758e0c1f6d99075afa39b1a183a6baccd1eb1a", + "scripts/proof/meet-peer.mjs": "2fdee2db7c1309267f9eca7e06b45ba3e5c8df4d8531e2b0e964ec8c38f771c1", + "scripts/proof/pulsar_cef.py": "8ee69e5b9697e3cf325baeba0a328806e295b349b1d47927edf2690c51689067", + "scripts/prove-authenticated-canvas.mjs": "a725db89653c9287ba6cd027667a39cc307e3737d5375e6260e2175f66269a94", + "scripts/prove-cold-start-cef.py": "8a97b509397dd66d4d008c0629e49495df72b936fdb91ad901f1063f0a7749b8", + "scripts/prove-native-cef.py": "3e350da1073b99a8c5469717f65e414f88ddc5246f4a2659bce1e6335f788b27", + "scripts/prove-peer-camera-cef.py": "fbb48f21f26988dc5d989b03286a7bb50ddafdb12a5bb28c881cd39a5ec6e0d6", + "scripts/prove-read-query-gateway.mjs": "f9273645b7af0c5739d212be70a02ee6dc90e6757be82fd84f9a6049103509b6", + "scripts/prove-scene-control-cef.py": "44c0ecbe018f8ea8fdd6cb7919c21823ed52b0f2e6da70bce29b6c744fa3c6bb", + "scripts/test-native-lsdp.mjs": "0395b002ee7e315ca7b50426799fe71a7c11cdd0468f688453d41b7fe6d43f95", + "scripts/test-packaged-cache.mjs": "2f941de4ac9bdad1966b09b223f6bc4ab5cd8ce552e72672a6209514dbf67374", + "scripts/test-reception-install.mjs": "ef01c6891c35666726e79ebf17ff11f370d224217f83272001f23f42b1c33cfc", + "scripts/test-reception-recovery.mjs": "0d47321e7c2e7146e93edb83b33eaa4aa08284089c0ae0d91e6982d2f30900e0", + "scripts/test-reception-server.mjs": "dd40dfa027dbd5d015f457c585bac382f27b17ac92a6b61b94f44be84c6edc25", + "scripts/verify-canvas-acquisition.mjs": "010af63cc8446b86700186485693bcc0f000a7b6e0ab67bad00c40a314681373", + "src/dev-entry.ts": "44af140907385d2d3f3ce426a109c73d207ec5268708419a26620d44e838d1ce", + "src/engine/README.md": "fbf769845acf74253f10ba3b28663839c5df18a3ec1bce6de0ca2b14d11405f3", + "src/engine/animations.ts": "5980cdc134e6c5b2ef6ed1ad6d8cfe2f11120045a417ae304b7298ec151fa9d8", + "src/engine/control-feedback.ts": "fd0011cb4d2aa29d650a8c8b15f2026b47fc92418f6d87880e0d7b4d889bfc6c", + "src/engine/live-media.ts": "7fe5affed7edb12e6578e86c702a354aa1438e7abd0e074fe70c40de93e506ae", + "src/engine/native-lsdp-runtime.ts": "c7fb6a132e6c52b0ea7c85e4fe3d66f74752983e61f66aaf6136ee1b906d502b", + "src/engine/vision-presenter.ts": "6b3702cbbd1c8e46a29250582250c5428dfa059ce17671c721793edb69eefef7", + "src/host-entry.ts": "50e90a0329d7dacd677ad67cf815985e8dfb4332241faf5de04b04d1e7a26a86", + "src/internal/native-tree.ts": "e037e4bb935e35f6212492a1d1668179009c3a73cda7283923eee7df401bc05f", + "src/peer-viewer/README.md": "a47bb6ea1aa3859e77e2957f1363615bafd043b6670f9d3064455bf457b5cb6e", + "src/scenes/README.md": "8d82bfa257108ecdc62e13522bbc2c825b596dfeb8f8315b0712a10f3b787d59", + "src/scenes/browser-store.ts": "ba3c26a39e3b84315ab62dc000d8b92964ff1b97c368f5eebe335f1fd43a4dd6", + "src/scenes/cache.ts": "d6181cb3057f00b0212fd8e16922c4ec8ef028c3ecdd59f1b6ed15598cc3f01b", + "src/scenes/canvas.ts": "49db42142a5a22ebebf18b42eb7f6ac7a6a56469e493d47613fb9fa6cfe55ebb", + "src/scenes/native-document.ts": "30f30aebe44f1e49b77a5f9bb36de54f7483f9cb150e741bfaae3b3b9d143620", + "src/scenes/startup.ts": "689ce0526451e4ad57fc5ac2a2f7cb9583af18658c2f2315ef2e0e4597a5c960", + "src/scenes/types.ts": "19217b7b59820e32c5273870031c03eba5d13c8f016c5923f47196cbf126be08", + "src/server/README.md": "e7971dd6f1703eea7e71f593b3d456ec614bb37c746886bba2f61c3e4d795889", + "src/server/index.ts": "3120fea814d5464ebe2467b220154d2890c67a105c1f4c254ba410ec72eb5cef", + "src/server/native-server.ts": "5e08fe6fe255a9dd51904472501f7628a4b4b521ddd82bffe9f524858b3fc438", + "src/server/scene-store.ts": "d3291b51eef550e89ba19eb4dc89b2a7b0bcbfc7e5a7d43088d4f720b451a758", + "src/sources/README.md": "379fccd61484d97b3fd3e928080de4b2a20fdd6d1c78402552cb3323f652a45e", + "src/sources/capture.ts": "364049716e454f8201d7e81c37837bfdd4606bdc2d5f66eb43dbe9907d88a5e4", + "src/sources/peers.ts": "d6f49c55bd77832d64a31c5daedd2090eb57fb2503c604a04bffc165a2eca181", + "tests/helpers/cache-source.ts": "2b10920e4367da69e20eb3db78d0f44030b345a5e340b4f19f39a87d0eebfdbe", + "tests/helpers/native-source.ts": "196f94e88041d8d203332f84aacf20b5813390e5af45ceba54a19832993d2d26", + "tests/unit/animations.test.ts": "6b677a6e7d855e6853b4d9a6d7b3513f25d15b3376529d22ed87f059b0fec4c6", + "tests/unit/control-feedback.test.ts": "36728a713d928c18b4003f57b69d86ed6adccc2b84dd026169c601afb9d70980", + "tests/unit/live-media.test.ts": "702764d0be3c470098b0349b35d2d45e137752132e637f4b9568ac87a981e3a9", + "tests/unit/native-document.test.ts": "a8f2015b47061138bf8137408bdf22c29c747129a1f946bc30f03502c2c604af", + "tests/unit/native-lsdp-runtime.test.ts": "c77fee04346366380f55039d55a340c350dba0420cbe3ec5afb59a95d786f3f1", + "tests/unit/native-reception.test.ts": "401ce6b537902eb28af988727f70605b903e0148bcdc9e7b179c12a5191a8b48", + "tests/unit/runtime-layout.test.ts": "c63330576579e5f294d81bbb15944daedba9c80d155fa76d8485003804ac8a02", + "tests/unit/scene-cache.test.ts": "d12198f09ae0054b405cc17d20787e093aeb1ecdc4e809f4bb55f0e529a35350", + "tests/unit/scene-source.test.ts": "03087146ce10bec5a79eac5e904946b61761369cc616cedd418b9adf80e86ae0", + "tests/unit/scene-store.test.ts": "c2e928446ab4fca7599869173faca571652df5ecc4d31619dece5f08247272ef", + "tests/unit/startup-cache.test.ts": "9dd0d032d8ebb41f71a24eecce0ba289037160cd23ab4c49831c2d5a794e843d", + "tsconfig.server.json": "9401bed3ab3a611ff69e8ed7ef31fe795ef865c34792ba21de728903fccb3a9a", + "vendor/lsdp-native-browser/LICENSE": "d42cfa19474fe4e66013b60010c87aefd0ef7df1f938306e6095ff02432ef002", + "vendor/lsdp-native-browser/README.md": "12ceb7c9a264d9d1a1f421b499cd14008abd1bd149f637dc73b2547c1b313eae", + "vendor/lsdp-native-browser/src/browser.d.ts": "666ccee3e1bc82a55447d0dcaa8f9ce71c139fd78d1b678b15ad7840644874eb", + "vendor/lsdp-native-browser/src/browser.js": "b9c61d66f4ac80d1ed70c2f894aa8de91056548274e2f35b963e964297eca85f", + "vendor/lsdp-native-browser/src/error.js": "53bd68fb8bd16b83cdfa573a5335153c5573609c14e4d46dfb81212cd833c7d1", + "vendor/lsdp-native-browser/src/strict-json.d.ts": "a0408c51de39b03ec07cb1135f27214a803135fead4eb1429f91da4d25ea4e00", + "vendor/lsdp-native-browser/src/strict-json.js": "b715e31adf77a69fd567a7c6dca5cebace20b17319b066adbcfb9b2227f6760e", + "vendor/lsdp-native-browser/tree-vectors.json": "e4066846f38b9c9c6c2171f008470242334a5660c92c066f91919c688c85237f", + ".file-size-exceptions.json": "b09d55bb08a3f80c129d91f362133a47eaecfc7d4eb17b9917aa3596d67c22dc", + ".gitattributes": "5e6f212295ff04b087668436a3bd94cbc06dc1494b519d0ba5a7107d864f3463", + ".github/CODEOWNERS": "f1f91dcaa95f220941be043ed2df66dc3439544952473f679f2bccc52c6028d1", + ".github/workflows/file-sizes.yml": "1057fd7d26fa06bf28243ba5ae6f27384ff68e0ef0065ab9822c0aed7d1715d4", + ".github/workflows/release.yml": "699e7ec08cd4e7f2f7763bff1ad142a543b42c38c4cf39b862a3a14e0f44087f", + ".gitignore": "9a7aeae9fa7d08bd799e7cca339f0ee52a7614f32185a27499797151b8614f65", + ".prettierrc.json": "72a1a7ac513e0c8f25f01d7cd920d27c16e5a21af1caf21c510f7c74833cdd4c", + "CHANGELOG.md": "3c71078f912bd7f09418678815b6195604a4a5e3a215bac2f8c3262a34a7be95", + "CLAUDE.md": "4ee217058d0f5a284d1fa251d63b39b9269fdc4694f6d312cd7d8d4150e4fec9", + "README.md": "0e7ed8be55bc58ec48234ecc7c332b64a0f07dfd32f0663bb8945305a601c45d", + "docs/adr/001-dual-build-host-bundle.md": "fb0486ea6474dbf810d6c8d71bd30ca64f0c72160de8f0de33fcf2f9d030380e", + "docs/development/file-size-policy.md": "13b3cff93a235f93a80bb811568ef8d648fb2a0efe468229eca42e5410ae22b1", + "docs/development/scene-feature-map.md": "2198bcff13bc56b5162fa964c634f773f6a228bb83afb518ceb9d834836ad758", + "docs/runbooks/release-deploy.md": "697d819a1a8f6622745b75d96259ac50667bb7a33c4882290f1b56bfe6b49033", + "eslint.config.mjs": "5872a6b30e521fc1acc5d6b6a1901e89ee74aaa93d19e8ae66ce9109d8b4f70d", + "host.html": "75cbe158bfd42d98752a51b5d46db6ce0212d984e459180898bb7b8ba1a60258", + "index.html": "bd5c236a71d5e7f2d8a6157c10ef4e1d65f71d035c55a878612955a804f3643f", + "package-lock.json": "b3225347f93e6b6d8d479ea412f1d67032e2d02a7f0032583760d0c4e2170cf9", + "package.json": "51585e22e4bfab866211d9129a1dd242a0329106336df23499e062ba97753c80", + "scripts/build-host-html.mjs": "ea10a9ebb561255ed11b25ea625fc83cee129801e529670d14faf453c90c162e", + "scripts/check-host-bundle.mjs": "93d314d0c1daf52af11c51e58dc5b575281ffc64796b8607810f4293e0e22f15", + "scripts/check_file_sizes.py": "56bc342f324fa1c67b9ebbce8b4a513bd1dc38f41e479cf85bf482613c3df36a", + "scripts/create-runtime-manifest.mjs": "7711bd6d86fa00928d77460c99f3b63cffd1e448cded38d6d8452056a361a85d", + "scripts/patch-known-peer-roster.mjs": "b300e7d6b5b61a6e0c2334b2d86d73547048d7ba07c5283d3d2324faf7864979", + "scripts/patch-lumencast-protocol.mjs": "14451e8d877ae452cd71225bc8df85b1608f2f7a64660d20f3035e9a35572b72", + "src/index.ts": "5e25074ab60ebf5ca62cb15b6b5ec6c87766e5a40a0ee9a7efae129a1a7d3d5d", + "src/internal/validate-options.ts": "fd16e087dd779322a35a2fbbc1824f42328597a385272523414b322a435ed18c", + "src/mount.ts": "0af6e321140cacd04b6605f9102de69292e7e07344412037cfbbc4455ec28aca", + "src/peer-viewer/antenne-controller.ts": "5bcbbeb0a797fc1564b85414279827c7d4c33b7744f35a6ca7e57a74af3184e4", + "src/peer-viewer/injection.ts": "dce9a5c4a002443b2d14aead7da9102bd6c38d1b142b24445d9cf7bd07d0561b", + "src/peer-viewer/slot-binding.ts": "a5a3f50f7d2c8ac41521cb89ecc17948cad1c613a7cbf24ff1741480004af1c7", + "src/types/index.ts": "266ecacfed04ce8f0a7ac0ab3e2cd78afebd939cb3fa0344eb0374f63d84ed10", + "tests/unit/antenne-controller.test.ts": "d5a804e75b00900c3281ffc190b6b95803b008ba1c11dce8d2c9ba639384f059", + "tests/unit/capture-resolution.test.ts": "7a39d8e2438ce1e6f5f4259ad607abbafe1ac28a24044697b6a587d7e338613f", + "tests/unit/meet-viewer-identity-race.test.ts": "ac1e424332edb0980f4cc923db996aad991de0c311b03d4aa5782756e896b97f", + "tests/unit/mount.test.ts": "1d4105de52b7d63601bd550f8fe2e7393ce77d76275aba141806bbf5eab7d5b5", + "tests/unit/peer-viewer-injection.test.ts": "0bf8c4fa0360cf625a7656d17154932ef253805d652150ae8f1c4747c3c0f87b", + "tests/unit/publisher-offer-policy.test.ts": "a81bde39bbeaf872ce723dad21d764eb432d5b98b63843847d8606c7c42212e1", + "tests/unit/slot-binding.test.ts": "7fb8189587bb9ac526415cded8187854da51f1bd37044825b3bb7a08f6e6d011", + "tsconfig.json": "de2674b8ff2e12514bb72f21b3fe48b2e09cfb6238331a8ae56a28e1a3a3c39d", + "tsconfig.lib.json": "c224796895324d7ce1576ced60833259ac76bc3ecd1287822641a1a1c8b8c903", + "tsconfig.node.json": "69d5df7e3549cd50e94dae488c25c92e9f0e093d8c60daa5191624f09085e15e", + "tsconfig.test.json": "6d767b79aaa950ae71e9b03036a4f1f90e0ed0209408cbeffd24a12ab0e6de54", + "vite.config.host.ts": "ca99a724754d90cfe7a02392ad1c47df90a6383b59bdecd163c7280812240106", + "vite.config.ts": "6ed183273ff4bbe72a693ff1e8b1a1d4098e6b86f866b6b539e0c3066386629d", + "vitest.config.ts": "55a3b4bc2f74ee494f51530bb4e42926f3e6492d05b2691a8b6ec0c1078a9bad" + } + }, + "Orion": { + "head": "c71336b1da17d9144a59f7753fe1dc846450d2d6", + "branch": "eleven/local-20261003-source-delivery", + "files": { + "cmd/orion/scene_control.go": "7c33677012383a286e525879f6ffac2d7f54a1d3ebb6f9e719340c974ea77e6e", + "cmd/orion/stream_control.go": "f5951c378ea39e4feb390063cef81b0401e79c405cf111dcabccae153748c0d0", + "docs/development/extending-orion.md": "ac28ca304cdf6c27269e96cfa87d48253663a3c96d6f9822e7133cee893544a0", + "docs/development/maturity.md": "dea05c6fe5d9547c93b67d85f7021663f89b9691da39a794c86e657b65ddfb64", + "docs/development/scene-control.md": "7091f5951bc660fdc47c023a46cf816ec3b21611c5b853d8f34c4a5cd89b41f3", + "docs/development/source-render-boundary.md": "85be15918e6c6c32d61188f59bf03b75f512be186429488abafab3b2aea2dd36", + "docs/runbooks/native-recovery.md": "ec4c0961bd16e46429c20978a5b18d05af55e862e49cafe9a3887f042f0ed5b8", + "internal/api/README.md": "6e5872bfebacbfd4c1d0afc2d14962df05bc9757b5339f8da4deeac9a47579f1", + "internal/api/cockpit_source_test.go": "af7559626bc9dc6ac52f02b84d440673821f67c77164d272d8f0ab60f2d78a63", + "internal/api/host_surface.go": "ff7712dd35eed27f2d7ca6afc86c8cd4ca1591cef38acb64bcf658f644573d01", + "internal/api/host_surface_test.go": "6493b8e0f351f4833c13cc3737727ad8694343a222f9dd36be6a77c35ab27a13", + "internal/api/native_delivery.go": "c66cba64be1c046315b074f394050ebc1bd1de9589d0190adee07c5b59d4795f", + "internal/api/native_delivery_test.go": "0ccc62b0c29e8dcca4317f57f1888831f1a092423395eaba45e2d81888ab7572", + "internal/api/native_operator_visual_test.go": "79644dc94fbf9aef8a66ed1710c32d3d21c65cbe18ddd15b3e43ebc2a1398d9c", + "internal/api/native_reception_proof_test.go": "b2411ead4f7da626a41c45b4ea1830f980ac9fdd0a2a30d4dc74b9454dcac859", + "internal/api/native_reception_ready_test.go": "c69d6124f9e37daa2a3dc5ee94196fd279db3d6b6f68d78330e12675f6e14cf3", + "internal/api/native_scene_presentation.go": "699f18ccad38354de174a981b4bea406dd0a194b74a983474ad0c454ca45ee97", + "internal/api/native_session_test.go": "c58640c928b72af1bca0af16e1dbb33dc401cf879665d10961c556e787eeead4", + "internal/api/operator_execution.go": "d2a96d8f28c0f8e994206c48f8c7320b06d7a92542a3df5e0feb6f4f09caa027", + "internal/api/operator_execution_test.go": "32c5f1a5f6a8755814ae841ebb6d6e651bd9f671e596124ea79016d56c213477", + "internal/api/scene_blue_manifest.go": "b71a47524a246bb604777177aa64acae0e9cf8df6bee839f3bed48a3c0092d0a", + "internal/api/scene_catalog.go": "3b567101e0f8de07057178d7a198019a176dc8b57fd04561fe3ca8a4bf220ec2", + "internal/api/scene_catalog_test.go": "0aa73449c029812a5a52c90c2eba7e95e26208df464a929956efb5ef6be3eb3d", + "internal/api/scene_control.go": "f643ce41ff90715b26572901495ff94e7663526df72365ce1cc40ad9e88a283f", + "internal/api/scene_control_native_test.go": "0d23f3ef87bc019f7aa2667a6eecf57240d82487b2dc105ad6aa1fda12469143", + "internal/api/scene_intent_concurrency_test.go": "4f4080df88b94a4806089263b993b8105a823e4fccb295722de7f59153fa76aa", + "internal/api/scene_intent_current_render.go": "8b53fb333933f5cd36d84bbf3e457e014f4821f1314b64c88100c953aac3d1e1", + "internal/api/scene_intent_identity_test.go": "00319166fcb276674ed6f1dab0157beeef42b2a7ed118e5df21e76919f3257a5", + "internal/api/scene_intent_source.go": "9fddf431c3719613756411146fe458b4e6d83c924705034380917596ae306937", + "internal/api/scene_intent_source_test.go": "6166821333e54837776bf574b822292644901413df9b6c8e7b2d33bd99f9ced3", + "internal/api/scene_presentation.go": "cfeafba869d89fbaaf327c122e9b0b9fcf76a8c0b867433ee13c178bc8aa251e", + "internal/api/scene_presentation_test.go": "a30b74b34256a1f07fb1f92a999d51551e305443e9b4aeec522678be5d9d5e3d", + "internal/api/testdata/scene-lec-lck.README.md": "70a4d0beefff49a9a2bd94d4958e514ba30a717f655df82099a73a55748cbd70", + "internal/api/testdata/scene-lec-lck.program.json": "9754a0b16acbaa5f061e9b45189b9a21443deb97ed37449d277b339f77a2b7b3", + "internal/bluehost/README.md": "3bbbe9f643b41a9cf386290e71e4de6501c8f266e9767be32d9c1b7221819699", + "internal/bluehost/effect_animation.go": "aa28607a06f5cfbb78275788c107b38b99066591372213ac591cdb1d21a30cd6", + "internal/bluehost/effect_local.go": "d45f075708fa76a9be8223a2564beeefdde1bb3b6de35e67ae581035ec9faa58", + "internal/bluehost/event_router.go": "c8d1b848c10cee37e5117ac9d7636f491c79baaab9bf4db36e452269dcbc016b", + "internal/bluehost/marker_process_test.go": "99d72d1a729c9102b334a89b1a6050c8479e064da25ad5ab2aecd336e08b18ec", + "internal/bluehost/replacement.go": "25d1c7b01bb6e0e2c07dd18144a14e787154c49ce2a11623e1144798324b837a", + "internal/bluehost/replacement_test.go": "4afafa0c36511de35645342cd000293d7c5cb30d548897c05ec25a4229b2402e", + "internal/bluehost/rule_store_test.go": "e7a7da95b48eafe30c31cde9bdcc2aa98af0052a9aeb947498dcf67b1438212e", + "internal/bluehost/rule_topics_test.go": "a74ca7dc1a0d0dfb120490764dc73dbbc657e7742658ba19ac5f5903a8705cba", + "internal/bluehost/scene_commit.go": "290750226b87349b7809b6d95aa543cb65e9aa6e3d7af05f453e6402acab777d", + "internal/bluehost/scene_interface.go": "f064e099eb7bcb425488d152e730fd700affc890168dbb29c0f6d06455262ffe", + "internal/bluehost/scene_interface_test.go": "2e365678555986166bb362558217ec01653835ad874126543fdea622650abbc7", + "internal/bluehost/surface_test.go": "6b2cc59fb3f546b51ff5caaa4e400c9d2a0fa6e79cdaa0d6636336c5080e4764", + "internal/lsdp/native_surface.go": "909f994ebf8a12be208064cc3b4ab218b78074f303576ecc709dd9788e21a37f", + "internal/lsdpreception/README.md": "d4484454942280214757dfd25dcfd3041896c6c4e9ff6b4afc61efa800506382", + "internal/lsdpreception/animation_native_test.go": "a17b8b0406043ee70466df26ae42793e2d26d6762e3f23d9c8b3c3b9365ee741", + "internal/lsdpreception/blue_native_test.go": "2d42f89714d6f3c0ee8d4c1e6f28c2c75f2f3817c6c118c28f18628a8c810bb0", + "internal/lsdpreception/control.go": "d12d078390f66c77508430ea0da871c0ebabd4eefb1a997aaee7163c8833ec56", + "internal/lsdpreception/mirror.go": "c385fccd3457db1e5e9c7398809440beb6ab13b8596e7b80d962a865cd8e1cc5", + "internal/lsdpreception/mutation.go": "02945580baa9dae8d1c58ff139e5d3eb1e36daa2009ac28a31e3da95061f6cb3", + "internal/lsdpreception/mutation_test.go": "b6e2790f50a76ff41e2523a68f6a3c0f1437bbb11f98d09f0b2c1e7a922b6dcb", + "internal/lsdpreception/producer.go": "d2d252e7d1ccba8951c68e7c64404976debe3f1a59d6e6a9a260ead91ca84784", + "internal/lsdpreception/producer_test.go": "8f3384f04a42397b4d5407cf8ee19f26a118ed126b1e51ed6c0c220dffa2f00a", + "internal/lsdpreception/reception.go": "fc305d8da1ab330a28f1c22c68afe430952cbddd59430af6c7eac8281d0dd0ac", + "internal/lsdpreception/reception_test.go": "c08afda5eae9c8e51d92f3f0e57823b1698d509e0b2f733ae7c31d594c8cbf3a", + "internal/lsdpreception/recovery_native_test.go": "c8e1361aea298223cc629df4cda205a6bc7e2c92ebb31a56328349b5ca1bd2bf", + "internal/lsdpreception/recovery_test.go": "20686df13318c9feab7dab07e50d0c7e25a16f092481d700064771ae7fdb1a9e", + "internal/lsdpreception/session.go": "33facbd24521939e8c93a3de6d3fd66cf72d72f106cd64284e77a1ecaf4344e5", + "internal/lsdpreception/surface_native_test.go": "7b3910a956ff9d7f5f8925165cc4022b512d1a0b47fa487a4ad66ba508ae16d0", + "internal/lsdpreception/transition_test.go": "1751911c7bea563bba84504eeb4f99035c7ce5d0dfd83e8d29bd7df2485bd32c", + "internal/lsdpreception/tree.go": "56f4a1ca4b666c818a487b88887c7eea8fd9403632078e7e48f5426481f55f71", + "internal/streamcontrol/README.md": "0671861ff2467c96836a8e47f771b01068e02977fceae3f93b6e8d59ed33cb04", + "internal/streamcontrol/apps.go": "748f86b84534e6ea086e59a7e9ad0657d22e48ac2572c2c8ddc71acae07426d8", + "internal/streamcontrol/apps_test.go": "af20dab2b47655d0a1537bed99285a4446743d9ba50a745fce244d143e136889", + "internal/streamcontrol/controller.go": "0d79321ae7eda17f1817384049b2383da516db86cc1a67ae0d166181ede17ed1", + "internal/streamcontrol/process_unix.go": "be93e995a261eb95d894b28c5553de7fb1ff345ddea84847d7f5654ae974cafd", + "internal/streamcontrol/process_windows.go": "b9f681bbecb648526d6738d11440fe7d7403692d86c6cb302617292b07a8f1fc", + "internal/streamcontrol/store.go": "2fe076d99758cf5ad07a82d6c84cd2828ff0b35c42d85b7ed2bb49c9521e16c9", + "internal/streamcontrol/store_test.go": "479e8ba9fddb3acf80b730e97b856880f94b1b585a30183d46527bfcf3d55d0a", + "third_party/lsdpnative/LICENSE": "d42cfa19474fe4e66013b60010c87aefd0ef7df1f938306e6095ff02432ef002", + "third_party/lsdpnative/README.md": "1c31d7fb3f77a07e27742f8d97aab227c4a22ccbee578219bf44d0543b83ab38", + "third_party/lsdpnative/client.go": "abd8bb2e9bf8b633e70d911b02f1551d45889f7231b0694aff06abc0a627191e", + "third_party/lsdpnative/subscription.go": "3df50fcb11ba1b1195bc26de064290628c2a5c677490d170f4a99c181a8fa3e9", + ".env.template": "583a943bfb5752693d6c071a69d3a078abc4daa540146cf049aba4348d36d386", + ".file-size-exceptions.json": "a3f23db50b34cca770c8f3e62f99d452937bc167071ecf6c9981cd75d08f8d01", + ".gitattributes": "113c1fd4258b2ef80f29c07548b3ba91193c1eba361f6a7026accf1e5c476c45", + ".github/actionlint.yaml": "6cfd1ee44b9768499c7293ae5ce3036ae988e882e888104bd99b88d8c1c4f1d7", + ".github/workflows/ci.yml": "a3a1fb84f3a788f062b0ea899433d6e0a1749116d0b5b9f98f7fe831214cd0db", + ".github/workflows/file-sizes.yml": "3f8a6aeb0bd4661fb01955251eb05ee490952a76a8f2bba3aae0d76aacf47609", + ".github/workflows/release.yml": "31dd91c48d34b6a3f6043a4bb6198229047743ca69af34c934cb4d2057c1e1cf", + ".github/workflows/win-sidecar.yml": "5cccab5c6b0028ce73ccd874e7a530d6fc661c5f906e82f5cb85a28d4faff51b", + ".gitignore": "99eef2af7bbdbbec03ab7166094697c10f507ad0bb470df6a651eab1c6dff576", + ".golangci.yml": "a31f5841d69149910f5417388f1f40e66f43b4e46034ab23d8d58bf26dd86f1b", + ".health.json": "f10d3b2aa764ea79d5733fa0dcba557205cd7888d1759c7e23d8da63249a7a25", + ".pr-body.md": "9528e0e3ecdd8f0638d3c39239fea5f613d7bf28dc09cd845dac31fb942697c0", + "CHANGELOG.md": "52f58e0700dc584c9adca714a7ee594e53934210e73497a3c499c639e56f3e70", + "CLAUDE.md": "5db617bc08ba97babb7356f7850871a420c23b3531cefca664a1aaf28d1e8952", + "Dockerfile": "914a67e284a723ed2feef423bb37a426814dc80d529533ddd7bac00730f7dd33", + "README.md": "152592a479fbaa2aac15885e9b84ae8cf68f1b90d17cfd3a2580387c51e6a3cd", + "VERSION": "c28fcca53637bc88e124af1725df13cb98c69dedefd62fb3cdbe1cdb6b760624", + "attestation2.md": "78fbf5701e12291f0bcf64f99812285b3f6861237c698b7b8c66932c03b221d2", + "attestation4.md": "a815d6030158c23c8ec773c7946b31e49b39bbb5629a8d72db273328a42c01a4", + "cmd/healthcheck/main.go": "4a919546eb9de017663cc87fea5e9e9ebc781334a41ba2d809faafe613e6cc71", + "cmd/orion/camera_slot_mirror.go": "e14dec0cdb4c4d0fceb729197b5eb93dcb6aef69594c91941f069be011c8eb60", + "cmd/orion/egress_boot_test.go": "00cb86327dd54cba7e9980806dc617eb470393559d0431c257b4fe8ef56fc9df", + "cmd/orion/egress_wiring.go": "57f36632a521172f453f755d44f7288e25aee7ad29b8609e036815153bf920bf", + "cmd/orion/egress_wiring_test.go": "832f1106fe0acb9e0e36cdd905ff5851fc25cec12ae87e4d118093c06a77997e", + "cmd/orion/main.go": "3555f4cba5f7c7c8333b8489bb7259b340dc7f38a91052499bdbd17f1691ed2d", + "cmd/orion/mirror_wiring.go": "fe06c093b2fd67d2a36d0540aa1ee4b3de9295c3c8ee851f214703cff50dc21b", + "cmd/orion/mirror_wiring_test.go": "55eee7414eced91f100746685c64cd955526e277fc203702f9477a9d002bcfbd", + "cmd/orion/platform_event_sink.go": "4d9139452bd8f84c837ed5a2f5d20ad371ff6e5bb550f12336d2b0849e911823", + "cmd/orion/platform_event_sink_test.go": "28ac9e8e3ede7ffec165c8583c2295e24fa38aae250faba00c93c65f34ec0207", + "cmd/orion/scene_intent_local_test.go": "a4466882f5a27764b03622169a6f058ef77c65c1d586d4deea9cac65f88d35fc", + "cmd/orion/scene_intent_wiring.go": "3ce6314b4416f9788056c46bf5d7b75d3efd40c338cec82d0ebc1d3c5a6a60dd", + "cmd/orion/scene_intent_wiring_test.go": "9b8e18185baf4fb0a0f42c1056b08d0a617bfc852f72fb7a324022bc7eb95860", + "conformance_allowlist.txt": "9b991084dd0767b20832b9d4b9b5560ddc76684705616ffea4d0199cd63945f2", + "contract-fixtures/core.http.request@1/http_request.v1.json": "a92dec30a61973a5b2b1920fad7770d7f063f8414df24d9e4cfff1511050787b", + "contract-fixtures/core.http.request@1/http_request.v1.json.sha256": "5750ca59a72f14c8932cb67fd607a1f414219eb67e5eea0c0a726635465d0b8d", + "contract-fixtures/inventory/e3ef100a4e0f.json": "9872b82fb79e49a6f17e712c53183c167988424c7516ea29a1891747cfa96a8d", + "contract-fixtures/inventory/e3ef100a4e0f.json.sha256": "330dc1a3d5b2b7abab2c4bb44ac7d0b5a42dcbadef738b8709ae2cbef8e9cea3", + "docker-compose.localstack.yml": "cd2ea3937938b93fb6eb5962be134a50e5a3cd1d2663fa0854d278b4daeb035c", + "docker-compose.yml": "1b623d4ce9dadda972177bcd0c3b442912f9c2b15df10bc638a9ff6bdf2c8d15", + "docs/adr/001-multi-blueprint-push-envelope.md": "65af77545aa43e799790a0dab3ab28e5bf6a655b48d68ccafd2b0fc40ed41dc0", + "docs/adr/002-scene-lifecycle-canvas-orion.md": "c4fddd18b192159f6416cbbfdfe4250d7440b4352e151b813ab52e9e7c75ce3f", + "docs/adr/003-full-blue-execution-engine-and-scene-validation-gate.md": "fd8f12fd51f517958ccaebc37492ba53b5b24f052b657d0cf34a88d22a5b8cfd", + "docs/adr/006-exec-activation-r9-lift.md": "a52f81cbb6f52aad14bc5b99af26bc4be262b71da18c21c4cd126711ccf33cf9", + "docs/adr/007-db-atomics-dataflow-descriptor-builders.md": "d9f6d3275410a604234055d4f5477520631741f23e82a4e3b6f54267488704bf", + "docs/adr/008-active-scene-only-execution.md": "3bf6d979aa812bff2f6b6ba6a51529460ce68b2299e6f6369e133b04bb112690", + "docs/adr/009-stream-level-blue-rules.md": "67eae8fc4609c92d41d5a94339a4d14dcd72788ce97df7895529e1c91213c671", + "docs/adr/010-http-request-executor-hardening.md": "d3d57cbe433509fcc2a4742b32d6fd846e08164e91280f772825776aa618ae76", + "docs/adr/011-animation-play-keyframe-lowering.md": "ff04bbd782d5d28f9570b9ebc3f90ce4142ee8e84dcaaae447108aa119dc4de9", + "docs/adr/012-source-read-reclassified-compute.md": "af8131665f8e3c3cd0d00315dbd2bb41b22730c76bfb01ec8747bb4288b0f9f9", + "docs/adr/013-platform-event-exec-entrypoint.md": "e29c31b23a2a2eb1fd6527c72081441cf635a77e1af24d9b08dc7172060fd8c8", + "docs/adr/014-blueprint-reference-compile-expansion.md": "2ddce915c51be7c7dd9d0df2bbfa7df81cf858a79493131f0cfcad0f0e271482", + "docs/adr/015-service-scoped-simulate-endpoint.md": "4fb231ee9f8e3999b6124b6e1b65cb50faa702fbc577c61adb7cada002044af6", + "docs/adr/016-embedded-local-execution-profile.md": "b0d19fd383d32a6b927ebc96ca62ac2dfbf9991327024f2b7a5d60ec8d3694cb", + "docs/adr/017-blueprint-direct-full-graph-compile.md": "e6529489f0f84ba404ecdf36c7ef10411f116933888229413d5f70716d8177e5", + "docs/adr/018-ci-postgres-substrat-runner-zab.md": "0493ca239a7212bc979ed5cd1f3e383da065b039c56dc40b208e40e7efda1852", + "docs/contracts/adr012-source-read-compute.md": "c7afd62329e7e6d45445af99cba13e79ad68a3a6e4ca696061333bc97e3226ed", + "docs/contracts/embedded-local-contracts.md": "4f5edbe8c6b4d1caec3a14fcfc09deaa1b768997f9b0825200929f1377fcef31", + "docs/contracts/phase3-wire-contract.md": "630528324da728c71d92c6788074eab1333f93c9a30118f7fd024a1f58a9aa39", + "docs/development/file-size-policy.md": "9b25c85334ba2df209397a57eef923fcb2adeb77c114aaca0c186206c4362388", + "docs/development/scene-feature-map.md": "5a1fafbcc39ff9ae6e28cadc28f21fb39b500fc5fa2db687c827fb58b21c03b0", + "docs/proposals/319-postgres-ci-canon.md": "ce2ffafef53d2b27c11bf5a11b252a7e55b98bbfbbafff4aa5328b33b41d5b04", + "docs/runbooks/canary-scene-payload.json": "c0dd67984bc9f11a33929525eaad066770dfcfd5dbab55a8f8c2a6b7af965d52", + "docs/runbooks/canary-scene-payload.md": "138849c5334f65ed7e9bd377f9a785136dbc33e59e198a9e582d1851b73d5552", + "docs/runbooks/canevas-chat-sponso-black-screen-recidive.md": "ce77e9abb3f0ab05534dfbc7591496564004eacc373d7b471bffd15b17aaecb2", + "docs/runbooks/ci-runner-orchestrator-backstop-poison.md": "b3e1761dd3062775ff59424dd45e42dff3c7a762fe087093fba38713081fc3e6", + "docs/runbooks/ci-runner-pg-image.md": "70be77fbac89c7404a01d9ffa0a6926e72810fe7182af6f4324b7de3e50657ef", + "docs/runbooks/e2e-pg-idempotent-bootstrap.md": "7ba9146ba1d89e15bd9c65c9ff9d5e68ac660142dcd1efd89c2aa3fe103255ef", + "docs/runbooks/e2e-shared-db-schema-isolation.md": "48171a4af590ad8282270615068057c289b46e481cb5e12604692894da325875", + "docs/runbooks/legacy-store-removal-deploy-credential-cleanup-2026-08-13.md": "c4e5b3c477615628c1c2be103e59dc4aa9ca3935f4c3f17f456f1ad66e607445", + "docs/runbooks/local-execution-stack.md": "bfea3d2977b74f00d82e9dcc96519a56e3a23eb08b1e9224fa122319d84d7b7a", + "docs/runbooks/m1-reactive-chat-scene-payload.json": "716c1856cb33d28f230779d7e7fcdd48c2d722a9e88a8d22eb62c2b4554fa920", + "docs/runbooks/m2-live-data.md": "1e0fa213029b796f809f94a83c2b04648ba6635a2fa9b740898df828097a469c", + "docs/runbooks/m3-named-leaderboard.md": "d2ba5988208ec6d53b0f867657b27415655e4b0ab55e6b1d7406edfdaf08a5e0", + "docs/runbooks/ops-orion-thresholds.md": "52d890b17ddee111035d9c3e60a969a4cef8727a7339ef3280ba7e163a2f137a", + "docs/runbooks/orion-ci-gomod-cache-fix.md": "b5aa9ebbf4063a69e4d5ef62e536df7e2cbc47bf2eeed9b98b63b81657d4cd5a", + "docs/runbooks/orion-cpu-pathological-scene.md": "c4799898eaea8cf585b5e119de9f35f3887116c2221fa239aeb3452315742a65", + "docs/runbooks/orion-exec-first-flight-canary-RESULT.md": "c8032e6cbdc4e61dff0bea9eafddceacd9a8d1aea34d7286b77f136930171f82", + "docs/runbooks/orion-exec-first-flight-canary.md": "87cf0b62c9120158bac562cf146807b5423bc088513e126202c95e40371a968a", + "docs/runbooks/orion-rollback-posture-forward-only-2026-08-15.md": "f2cb59a39f848cc4ab0cc921f470a082e7585072aa86e7d371d1769b31eb2a1b", + "docs/runbooks/orion-vps-retirement-2026-08-24.md": "2d203abf6d8ef97fb32ff9a2d838693b38e662061ad1bfc349bae38f31b95865", + "docs/runbooks/orion-workload-identity.md": "c9f7936736b30dae5b8f823670babb780f5113cc6f04a4ee9687c064a65e059d", + "docs/runbooks/quasar-finale-scene-payloads.json": "08c7b6dc43103a38ff2e1278afa0257e7fbf1c4859f29cef6d3cc7ec6195a3dc", + "docs/runbooks/quasar-finale-stream-level-reactive.md": "29a48585c594aaff0ed2e46693cc95cba894737872e4416b0e98ba7f3f3699d4", + "docs/runbooks/scene-recompile-after-compiler-fix.md": "fd561f533cdc5092ca0dcc1b694d887721cff1051c102f1f788bdea9e531234a", + "docs/runbooks/service-token-cutover-2026-08-08.md": "a5fdd3f5b38ffaa6ad8b82cf16e9e9a94d2c685490635886171c13682987f4e6", + "docs/runbooks/solar-deploy-and-lsdp-flip.md": "4a37d8813de54a272bda659d0e17ae20d44242b71dac762e8066681adc1b4cbe", + "docs/runbooks/solar-deploy-host-subtree-hotfix.md": "457d147196829e0dd175c16889dd52268b4c6333fe56378f683cbe4d84578c5f", + "docs/runbooks/solar-v0.2.11-capture-transparent-deploy.md": "da82388a75cca7dc4c945e7d7f4141705e602c2fd67e3bb940eeacdc4ee79363", + "docs/runbooks/solar-v029-animation-fix.md": "638dcc55645ccbe117dc74c1c626a58c307c9a0ac51f05fae474339600d87bde", + "forge-agent-report.md": "8021af1e06506a2c803736275553eeaecb64603366e1da75e6900c155dde5fba", + "go.mod": "07042312b6d7e4a85e0ba8a062e5620199561e2e85cca5f7ab8e0ef24a65b350", + "go.sum": "cd4dec4eb3ad2ac72efc4d9df26b02736969c2fed01490f6146f9b86db828b08", + "handoff2.md": "eae0a777b188f9d0827a9cc965cb48c3444cc13d5252fb64d4ad1bdcf7ad79d0", + "handoff4.md": "261e07446ac42d27f6fbd37e12454b84c92b0c709d8dec65199d9303088bfdd6", + "internal/adapters/events_dataflow_compile_e2e_test.go": "566a25fae1add3bae978bcbb40aa200346f666fab0a2c73d05861e88a3b1b234", + "internal/adapters/helpers_test.go": "b9a0453f3fe95efcbc49ad768fc3a0f294a2bf08fc9e0b9e91f5b4c568a59b6e", + "internal/adapters/inbox.go": "2237eab87bf191aca9196f4877467ac0ecfd1cbcd7b10b2f9fc20b98e6141bcc", + "internal/adapters/inbox_platform_probe_test.go": "e06af91c46999ad49e0460516a43265aa7ddda15a11570c9466322b0cc588513", + "internal/adapters/inbox_platform_test.go": "a6ea382a7b17f8a0b7bc12aee023874fd6ac336e39dfaa267d2f05159f166f7b", + "internal/adapters/inbox_show_emit_test.go": "661691aa7c6c67d014e5f6fbeb01b5d12acabe746002a89b53d46cbd80924e84", + "internal/adapters/inbox_stream_rule_test.go": "14da7cdd2f6fab142ece84ec955cd38b648d94fcfe5874b0619a8312542168f5", + "internal/adapters/inbox_system_probe_test.go": "dd1fcf1aa66f904f424bf2bfd9ac9a87ebdf5e99c0c18b594ff41716d7c929c8", + "internal/adapters/inbox_system_test.go": "8ca114b131de337f2d03b4b8bddf3c3e6e8d01073e0e249beba44e5fa7d7b1d1", + "internal/adapters/inbox_test.go": "5ab6e791b85cf3c9fbaa1169344e73c1822c3ef52a58aec6f2df484b7e4a9381", + "internal/adapters/poller_test.go": "9ddb0c2488af988a67c724b6687a7f044c2b7e77fffd3b4ec1394580105464bf", + "internal/adapters/stateless_platform_sink_test.go": "23ed8d3a7f36c5680e890ee936b3fdd560b6ea80e7c6bbb84905139dd7d5d6d0", + "internal/api/auth_seam_test.go": "a008bf91d45d1612b2549026a6d55c4c396e7d726a53091856522fefe84aee02", + "internal/api/camera_slots.go": "b1113d659e17c2bf335b07aec8dfe6e328f10a42bca71fca282889715058d2c5", + "internal/api/camera_slots_test.go": "8d93ccd99774464f2a4e3672b5ab2459dd380f6752cabe51e2ecda19a95534d9", + "internal/api/cockpit.go": "552756e620972ba6d3860a4d5e52a1352055c62d297cfde29a084db00dcae38d", + "internal/api/cockpit_test.go": "8c2563b50fea8099de7ad5c3e58313f1f499e2be06d883e6bf43b91cb10a9626", + "internal/api/db_catalog.go": "ed88cdafc8ca20419db728b5375777e88bb293f1333c2879dc52f5cc5d8155fd", + "internal/api/db_catalog_test.go": "09e3ee062092cf167584c9bea7dd1d8cef1a70a4fa47e54df35e82b32ed79f24", + "internal/api/editable_preview.go": "d31b5814bf9275987931848b6f0fbc77c0532a1c7b016840e747499a555d132d", + "internal/api/editable_preview_compile_test.go": "b1f2636d87b5644b77e80fa7b96a9a4692141d5e717c20659a825beeaa2a26cd", + "internal/api/editable_preview_test.go": "338bd9aa4e8e32ff08826ebea579b9d740bc9de349144817e3905952dfc7aee1", + "internal/api/engine_b_fixture_test.go": "1b2226ba778898e32653932278fa4297ccb230362c1dff1808bc5f0c1c2002be", + "internal/api/exec_completion.go": "7976ee6c0aaeaf84c35394f74111245978100b1da7327643a9d3ecef47f3dfc2", + "internal/api/exec_completion_probe_test.go": "bc4bea9efcccf0bb90287165d3d91647f3ee7e16160ed90aeb8146a3d2add858", + "internal/api/exec_completion_test.go": "309c17b058918e640c052d2f8a5192dba6515133ab43627a4204e4e94fa3fb23", + "internal/api/lsml_get.go": "ba1f0bcf2a2538cac4b1a9de3f18eec0143163a9545120f0a4d3be2a370bf108", + "internal/api/lsml_get_test.go": "975d2b78599d57963d63ac80d04c98685e5a73760059b09b8d0aaa0a31dd926a", + "internal/api/operator.go": "79c876f4d7ebecc83f2107326084d97dbd81105f81eb8cc4c18e07f632729e8a", + "internal/api/operator_authz_test.go": "42a29ae73ee8e94cb72298a51096623f427709435e609963c03f5f0b34a94dde", + "internal/api/operator_dual_slot_isolation_test.go": "aaef0911e29254949fb5ddc8bc2766ab4f727b1ec1d22be5e2b3972603d5c0e9", + "internal/api/operator_preview_engine_b_test.go": "64c292980b9d87a6d89bfaf1580e0f43e654e072181cd926fd5ee7e24e3a0304", + "internal/api/operator_route_keying_test.go": "787a08a12ef1e05ca368c99d207fd465d5562b8430a52fc354de3acf8953b05f", + "internal/api/operator_target_unknown_test.go": "c2da3fb1e99aac149f72eb70748f39917ee4a72dafa095012a0bb922615bf365", + "internal/api/operator_test.go": "dd0f44f5fd43b38f45c94142ee61de633b8967faa3f2a947e51b594cf69c8c7d", + "internal/api/public.go": "b83bc48c0433c23e55a209f7289281ec3a9d7816f97c8c952b0cd5603f78506b", + "internal/api/render_bundle_validate.go": "26ffe334d47a79065747c45067d8f56cd9b10c8268de8f95d070a90a7327d204", + "internal/api/runtime_descriptor.go": "c3834d0fd4ac74b846c31898e4773fb78eb0f66b70bc1e06329a783dcbaceaa9", + "internal/api/runtime_descriptor_test.go": "f31522918cfb76c003ee3bdd8aefc79459428b7a2c7d0368075045129731bda9", + "internal/api/scene_intent.go": "1a39159f302354241ecc8a53f143feaa966a03518b8203c5b8c0a399270ef1b3", + "internal/api/scene_intent_artifacts.go": "3fb5216b328df3821c6467ac4b44d5a03d887fc7eddfefe0f0fb50b52e85c6a4", + "internal/api/scene_intent_bridge.go": "0f7a6bdd45a7aa5aedaa06cff6589831cf9294a095945fae0fe6c5701ef943a8", + "internal/api/scene_intent_bundle_test.go": "19a280473d5b88da00e08ee245021d87d633508fcff7b5d696b7f0c9bb519904", + "internal/api/scene_intent_databound_benchmark_test.go": "0cfe9f1558e79ae3963fb671b61e4723b740c093f4e2d4701260e768eef35bec", + "internal/api/scene_intent_databound_test.go": "d43dfdb27ffc393df03995c9e0e03766d3a9db83e9e4c058943672240115ca59", + "internal/api/scene_intent_idempotency.go": "cb821ce0b4e5c832bb226faf4cc87337848f9fec7f36268b68135c08d0f17350", + "internal/api/scene_intent_local.go": "8f7a7b24ebc93062d645a104b57057d8b9b09018bf63f6ad71dcce53933eaafa", + "internal/api/scene_intent_local_test.go": "5545da952c3f65bb5fceb3919295b690f587ab3bb0ee5faa437db9497d1099f0", + "internal/api/scene_intent_noblue_test.go": "a342063099385256fabbe970ca65a0a2dc330738c9ee0db38f5d548355456198", + "internal/api/scene_intent_providers_test.go": "ba2314e0c840623b47624ed72b44d1c65c0777eecc56075c35e6aa18401f606d", + "internal/api/scene_intent_static_test.go": "1de9fedfa74b106fb14b61cfc2d122c9b1c1bd3ab883842201980c2b44312573", + "internal/api/scene_intent_status.go": "e0148a10a06a4a9e3e6dcb7ba8534b9834f340db115253e1e029aeab2e892ec4", + "internal/api/scene_intent_test.go": "88a8a7bf51370c1dfe66f94385b1dd06bdf4318743265a86f08b4a3fa5ef4dc6", + "internal/api/scene_intent_verification_cache.go": "783abd4dc0493d94e53da7c8387ba4c239efb5649200d19181e62f1daf12601c", + "internal/api/scenes_get.go": "5e775a6b42ccd88fe8324aab27e165f3b5e21d71ca4a664fabbbac6151dae20b", + "internal/api/scenes_get_test.go": "211c45381944122cf80fcc65d4b26fd716279f4776875fcf1fd3e74194ee6063", + "internal/api/show.go": "a0b3ec6faf127285df3859eb8a95637c436af56a6f1e99b13e21286a341c2a9a", + "internal/api/state_snapshot.go": "810f1f7d939686d8c83681f259984a2e5169bde6a24fc5f56360ebc5faeeb62b", + "internal/api/state_snapshot_test.go": "ee27fc073fcbdfe8a875447379fac1f3b185465e67b3f9a20167f7268ffe27bf", + "internal/api/static.go": "cdf3384a3d264c7ab4db8924ff90b6537afc477f2a987c3e08102a1ac2853d40", + "internal/api/stream_rules.go": "59d557227e94acc46883d8aa2658f1be5e459484654dc804fc23259026d55e5b", + "internal/api/stream_rules_test.go": "b7c57651044fd4f1dfc7ebe7335a3760356ff211df6906f2c1a5a8d5411b388b", + "internal/api/testdata/orion_181_chat_overlay_program.json": "fc5cd671729f44bbe09ad278d93a8cc0ac0add9c275c541df2aa6b16f4a3b97a", + "internal/api/util.go": "5a7b5c7f02798fcebd1ff236df69860cdbb4ec15b616254bd5631d6b9276c07d", + "internal/api/validate_program.go": "a512bd9acdd1d0057393f01b864594514892dec6df60fd289296e091b7e145b7", + "internal/api/validate_program_test.go": "bad33285b357acb7ab46f9773780fc959ae8c6924f30e6b29eccd69d0c9c6ce2", + "internal/api/validate_simulate.go": "da31d768e6f07a1bcdbd787121948f4853a65fe0fb3d11c360ad306a2f7055da", + "internal/api/validate_simulate_test.go": "0562917433c4d128f2ed3376c5b516d0cd24a4bb47605689d82d245f29760d80", + "internal/attestation/verify.go": "22ee1d62ee71d6471e79522844363af307bf1b1d63fbc05d86e7c6d39f0d1347", + "internal/attestation/verify_test.go": "e92542ac22afade46a3481890412635a93fa3581c1c0581d3a5ed728f35f2f2b", + "internal/auth/auth_source_test.go": "31c8b9e0fa8968d25ed9c3c37a6d5924ef81e75775c9f4a66c50bc4f5743996d", + "internal/auth/identity.go": "d204d500da2c7e8bcfc61717ba6b58021fdb743f405035e1199e71bb4699b1a5", + "internal/auth/identity_platform_probe_test.go": "2b36b95a005457c08c4132e09d053c1ca375613ea0be3d705a5d417faf206131", + "internal/auth/identity_test.go": "b4660b657cbe6cde9ba301a585181134505e7a032a2bbdfc8e5bcb1ef8693751", + "internal/auth/local_operator.go": "6ef64d40da7e8a491d03e5661d4ee5893e6cf651a1e65aedc807c635c8cbb6dc", + "internal/auth/local_operator_test.go": "0a2e29e06f5a458c1a5c9db4d63bd8b035bb3566371dd6f15fa784c51bc91c20", + "internal/auth/local_service_token_store_test.go": "c4388b148ad0d434d2a4e8dfa847b921e901925bde7243bfd678c84b2474d8ed", + "internal/auth/probe_test.go": "636f8a9f048af2721f0e5c2243f3ba0742c7dc368f6169324dabf7b298fdbcbf", + "internal/auth/service_token_manager.go": "5bb50a9e52319a55c64111921584a4f767941d8fda46922b3f763ea6de9657be", + "internal/auth/service_token_manager_test.go": "60e304bd95c96c312c5155eea0c8091c42dde7de243d7a51325df733cdf1f090", + "internal/bluehost/effect_assign_slot_test.go": "717df583e5e13a1a202933ff774b711fd76e986b6d382b09ba4ea00b59394cea", + "internal/bluehost/effect_http.go": "d046e850be5c2a92f7b10b00a3aa49aa8d08efd49c6b2f48fdf508309633d987", + "internal/bluehost/effect_http_gate_position_test.go": "c43267bd0180cdb7e62f2b844e3b17ca433c74bfb3e0e51f633f5d7104ea17d4", + "internal/bluehost/effect_http_integration_test.go": "d594e98eaa337c4fde9511697f9bfa774fd9d2a6548d11bb7b62f5fed829b235", + "internal/bluehost/effect_overlay.go": "d15d91a08d60c836036193d0a4b37f58ccf822cdf54ccc3eb7d7ebb9de68b14c", + "internal/bluehost/effect_overlay_test.go": "864ccce16be97c4d2171fff51af448b06fb8699119ecf41960578436d1be9fc3", + "internal/bluehost/effects.go": "8de0efa696ae2343e8f9280f619dc09871b1b328237217f640cc15a3c9bd3200", + "internal/bluehost/effects_preview_db_test.go": "56aaaecbde641ecb2544ea7def7748355448186bbb8470e4603301fa43291caa", + "internal/bluehost/effects_preview_read_test.go": "c65331b5c70ff23c0043b80f5c97e2b418e02e275d52bd69fbee503f4e55139e", + "internal/bluehost/effects_service_parity_test.go": "e7313a819273b7fa687fdc8f75fab40648bd13f2d875a449db60e29084c210ba", + "internal/bluehost/effects_test.go": "e6876dbdaa7b199970038f914f21db7d7efd5fdd04f8d68f8de14163dae80291", + "internal/bluehost/host.go": "573dad62afa5d38aad7f55de18175c6d6f5b3574fcb8b6c686690328429a9b1e", + "internal/bluehost/host_entrypoints_test.go": "e05ba6b721b918dde73fe9ada2d9d1ffec408dfd16c3f738b609a7e21dc581a5", + "internal/bluehost/host_static_test.go": "d70ded60b49fb8a2ba680c3a832dfbfd18c606e21482ea97701c11512f1eb6f8", + "internal/bluehost/host_test.go": "4abdccc94687cbbe7728ab4466639a69738ac7a683cc3b8f171286c35a804e15", + "internal/bluehost/http_transport.go": "11b3b1f056a025aa6dec28a1c9fe633ea679b8821cff7b883f736887ed22b03d", + "internal/bluehost/http_transport_test.go": "1acef749c92f952f5158fe90f2edbe0114257809e995c207d8f1e51b21617d41", + "internal/bluehost/outputs.go": "03d185788e18a81a13b537d81e6abbc652e4d2f0e271d5c98038bde4cd1d1a27", + "internal/bluehost/outputs_test.go": "26c30362b1da2509a392f329abab6a6b027bd508622b606b91b72d8c1837f5d5", + "internal/bluehost/program_metadata.go": "810739a3dd57556ecba6dc54e48149d15077221ba222f88932fd9163885db2cb", + "internal/bluehost/projection_test.go": "b55df2dd287f5cb05c98d0ae2a954ed06ecd597bf65a6f99265c88adf62016a0", + "internal/bluehost/rule_plane.go": "a450892ef75d33a6fa233837ed3572e7e19a7a644c319a33fb0c63580dee6768", + "internal/bluehost/rule_plane_test.go": "c38e4497d6e1c086474fd741d5d2398d9e46543ea49245c666a25ae09dd19f54", + "internal/bluehost/show_emit.go": "b7189cd2df9905c0da21f1d078d96e3eb71ff991f5ac14661019c71a4a874a04", + "internal/bluehost/show_emit_test.go": "09b49027550de2e8570dc625d870f2f595cbdeeae55f827db4f75168647817e4", + "internal/bluehost/validate.go": "107aa5b98411be26091fb29e233aa20dc31ccc6fbf5d1ae61b99acc03e03c262", + "internal/bluehost/validate_budget_test.go": "250d9c892b66a42e4aba1e5a5f9c555da3dfda7132e431939b4bc159c04fff38", + "internal/bluehost/validate_effects_test.go": "0020eaecb9205ea8be29897d6cc5612ff286ec71a550744d9c160b9b3fc12cd6", + "internal/bluehost/validate_test.go": "10f0038c9ac9d905dc24ae33a87f30cc7db9e8d9e165c4bb22940543f6f0d42c", + "internal/blueproject/project.go": "3746630ebff83bcd8b1be1af8b454b24692c2cd0bad5c888dc64246dc16dce2b", + "internal/blueproject/project_test.go": "899d234714107cc1b69f950c064610a237c864f9b3b8670047b8070c9ffc7c6e", + "internal/bluespike/spike_test.go": "f3d62d3c0468249bc75fb0b97d9cd9c1c75b3ed7c220057ebd4f60a2084c780f", + "internal/bluespike/testdata/01-minimal.program.json": "cc4491390a81b7de1a278d8a56cae167b5bc78e523f1391665a157dad2f655e1", + "internal/bluespike/testdata/02-http-requires.program.json": "48cfdd602a27de7535faf9bfdf2390a3d5cc88348c7a39068885ff8cc380b770", + "internal/bluespike/testdata/03-overlay-app-stream-rule.program.json": "4b2c5427826de37ae51471a2fc9849ddd688471e028f94d1fd10cb5d59ebd6a3", + "internal/bluewire/bridge.go": "74769c4fd9a5bf0921c432b8c6eeeb65e297e8b9c5747d65ab19e15cda37c236", + "internal/bluewire/bridge_test.go": "2681f22217b52f031e35dae356fc8656cf3b854a7a15540e80c5ee8179d12bab", + "internal/bluewire/registry.go": "d205ceedea6f4d220c6f0018b4fe4dabd8dc3cd475fdc89969ca15f614a95739", + "internal/bluewire/registry_test.go": "a480f194250967ceb3bf38e9ae3cf2b50aae123d047494f73275a2c01e72c94e", + "internal/buildinfo/buildinfo.go": "692f45385706ba9d29a68c0a9cc14f31f0d63af82772e33e4e6b4b3038186409", + "internal/canonical/canonical.go": "907296f37b8d513b42225aa3f8180ed344a69cfb26ef49c4e63dc94de17a5521", + "internal/compiler/authoring_gate.go": "11aba58fae79ed763d4e69b3c35f2a752e723d6029f3f71d1b8495724a8e7972", + "internal/compiler/authoring_gate_test.go": "48400de378276af943a07c6d3c95d24f73b762477179a0add5266629941d33f6", + "internal/compiler/blueprints.go": "b629217003665155ef7a8635d56f71d54fda2e7a76096b51de1f25314bd25f7f", + "internal/compiler/bundled_fetcher.go": "049cbf95a2a97d720cf853c047a83cc77eb20b9ad328ac45a0be7911142ca64f", + "internal/compiler/bundled_fetcher_contract_test.go": "a7b91b9d5b9008a78fcff725d1fab9e983440313e8be7e35a109e8df000d4024", + "internal/compiler/bundled_fetcher_test.go": "64dc632ed297f75fbae63c766898acf453d228bb6159cf3508db04569efc6230", + "internal/compiler/compile.go": "c33b9846f7aedff760c7f86ce1dcadf52810d25f3f26ec141813b6e01d5b32f5", + "internal/compiler/compile_blueprint.go": "4f850eb0ba94baf716e98f64d7dbf912be85d2c8db8720231012a60e4f712942", + "internal/compiler/compile_blueprint_rule.go": "d0691d54f6ee2996d9ec6231dac4bfc2ad9bcc55063efdc620b2effeb9c8ccc1", + "internal/compiler/compile_blueprint_rule_test.go": "9163a3f05e7ef7390ff2cbb73f8311d62eae0bdd662fda5b12494dcf3b3b316b", + "internal/compiler/compile_exec_inbody.go": "a3450138af40b5fe5701f19fe2edc96e3c89c95e37342aeb76125e3ee18171aa", + "internal/compiler/compile_exec_inbody_test.go": "2708f3b1bab16447c49c8cf5e7604af48b1b993e3ac6a71377e20a246e96d902", + "internal/compiler/compile_graph_helpers.go": "04de776ed435170e07c7ac7282ee66ac202c6ede46e5eedee69f93ae2b81b320", + "internal/compiler/compile_multiblueprint_test.go": "a4847c9a244bd638de0907bb24733497f1af9144aa53a92de6f7ae9189638531", + "internal/compiler/compile_platform_bindings.go": "9724849fa79ad1a8212309292ff48efc341e43bb17573adc4e61d7759b6fdadc", + "internal/compiler/compile_platform_probe_test.go": "4c306183e96f0bac6593cfe2a690ed6a205171b3f146d6e645dabdb360522ae9", + "internal/compiler/compile_platform_test.go": "90afcc8ae444f3347548074362de7db11c36f4d67ca8505c9329c96aea65df6b", + "internal/compiler/compile_test.go": "7924d36a516c38b256a87ea9f77b7918d60a22f8ec37ecdfee68d8183db93568", + "internal/compiler/compile_toplevel_vars_test.go": "4b05a3296a8c2959ee5aa41a1a00b5d6cc08889ba7e1639e04c2b7bc371d88fc", + "internal/compiler/diagnostics.go": "b0dad705c9e150f80f3282adeda4bf418ae82804417926af3d8f3c010e701776", + "internal/compiler/egress.go": "422ae6406080b02f7504475b67aaf219554efd8d189c1303885999b1df67645e", + "internal/compiler/egress_test.go": "839632d155b954771adc8d8397d2624d979a213f7d50f0364e208cc91ca73bb3", + "internal/compiler/emit_lsml.go": "b0032fdb2728a6e36ed1825cc5164e5d116d43e2ceac88855667b532bd90fc5f", + "internal/compiler/emit_lsml_1_2_test.go": "a6a52401862e4db34e73ca71f8ec3145e4d2966910cd099b6efe2d16cb24fc93", + "internal/compiler/emit_lsml_authoring_test.go": "9ff149d7dd5ed1ab6666d302115415ceb33d7d6aac12441c0f134404c001ffb5", + "internal/compiler/emit_lsml_test.go": "aee42813d35124e0121ce63ca2a58362257837c95da0df0e05cb86ab3cd4bd3b", + "internal/compiler/emit_lsml_xlang_multiblueprint_test.go": "cc1764eaad599fe9c85bc8f1d7a33dbd195552fe03d79bb0440c469cf0f07ded", + "internal/compiler/emit_lsml_xlang_test.go": "b5371e01b2275d4c0d11632724ffaaa3d4e528419511f43ea3f6a7486746617d", + "internal/compiler/event_topic_binding_test.go": "47940de79acde66f8d0554f5a41f49e1c69b2e2ee2b92731b6eed7845daebcac", + "internal/compiler/events_input_binding_test.go": "ca0256d56f48bb17eca65938d71903203a5f594255b72e4fc43ce2767fc9c055", + "internal/compiler/exec_partition.go": "f42700ef8b6de1aea9d0631737e76fe85bbba2a90d42b866719efe2e205c28d7", + "internal/compiler/exec_partition_probe_test.go": "e9c419997ee6d5332dc32f9fa518bfd7000b5b4f1c5bfb93cda7974b955e6bf9", + "internal/compiler/exec_partition_test.go": "286f476545e1dfd099d6f02805740d8823e6664a6a2bfb35d940285b61f75ecf", + "internal/compiler/expand_reference.go": "d7b95cc7fb0dc2d07dc77049867063720e71495683c7a9cee334e0d62830e4a3", + "internal/compiler/expand_reference_exec_test.go": "a5803a381d564f381b156802c619c82410b771e6aea73c56fd148d3186c5fc8b", + "internal/compiler/expand_reference_matrix_test.go": "9fd96644d7683151a244fcba44bbab177d97958f3742a0192d25ecca47ca3d63", + "internal/compiler/expand_reference_seqchain_test.go": "0f052164b220ac82aaba2530aad677f6914530feee8afe7712f73a31ad02c860", + "internal/compiler/expand_reference_test.go": "32be3b831abe3d49448feab37ea7108df10af06126995f0c836c83e1f7bb91bd", + "internal/compiler/expand_reference_varinput_probe_test.go": "461cf8d23a094d9229bae37be4cc23dac5830955abd25b38aaa64f4fd7ca1e80", + "internal/compiler/expand_reference_varinput_test.go": "8ab2fbfaff0ecdd659740354531a064bedaa6164a74fca61e80b0782802cd74d", + "internal/compiler/expand_reference_varseed_test.go": "7c5545047d6c80f80e07498f2c08339a34133413f643f8de094cdad65e18baa5", + "internal/compiler/fetch_cache.go": "80e359eb29bd25fbb6c01ec12e330eb0a36a063c61a04783bc450553a88101b2", + "internal/compiler/fetch_cache_test.go": "fddfa08bb99dafd3af89b760dd5c527186e79df79fecd97ed84634753fa9ec3b", + "internal/compiler/fetcher.go": "c55914a0056758b80ab4f5e36666f867c961a1d1c4b06b7f9fcc0f90aa81cfa3", + "internal/compiler/frozen_bundle_exec_ports_test.go": "d331aa1ccb40c97e727f75df6e5cab0863bc5217283ac31b98f6684d11c9fb3a", + "internal/compiler/graph.go": "f9d605dc7a6bd909101215d0672e3be68c88095c9a948f812f93a16a24f670dc", + "internal/compiler/http_fetcher.go": "39d9b21a62c6bdf7f30bae0aecfd879bb616ccf1ee701bc2a7fc27b3f8fc3ae1", + "internal/compiler/http_fetcher_test.go": "b20e9ec8fb86e09567926afa72f4ef5aa163fb0d2ce3f019096c07b035967af1", + "internal/compiler/http_fetcher_token_test.go": "a1a392e1eea0b508c9d03323dea0e4929cd12fd36b93d624436b2cb3e0f59d0b", + "internal/compiler/layout_cache_contract_test.go": "6dac0cdfbcae2a5b601e066e5b8c5c948528624f14ad33f9422cd9899fb8bd8d", + "internal/compiler/lower_animate_initial.go": "1ed7e4d26d3525d5291ee60343ebbae92c7b6c504231c8eaca66fb726d91a1ec", + "internal/compiler/lower_animate_initial_test.go": "4de430ce8b55d7d84c2ef98c4c9c8b8753fdcae66088437e9fe2b0604524dfe5", + "internal/compiler/lower_animation.go": "d294c4a665cf07fae32d0aac6b023a277ea5c5dad7681723c78d66f5854de17f", + "internal/compiler/lower_animation_test.go": "bddcd1a1329291ab316e90f4e0f6abb2824a0f66b9090f451dd6a097700e12dc", + "internal/compiler/lower_render_props.go": "f35956bc6fd05a4bc535156a14c87807997969a104b448a52d5210367eab435d", + "internal/compiler/lower_render_props_test.go": "251bc6fac71fd01b04f1af514bb97954293d6d8d94c97971a7c1ba553dd1bf16", + "internal/compiler/lower_transitions.go": "9bf83f51291a28456816e8807e03109965e2b117a5171ea521263a4f7d22bc1d", + "internal/compiler/lower_transitions_test.go": "eecbbc96f98f60826603adcac366db5c6047bfaaeb9caa2b63cd2cd108aece96", + "internal/compiler/lower_wipe_cover.go": "b8a0faf70c9f64afc711bd2f6d2c489b6e4e1dba043ea35ac5fc676f6d6bcbe3", + "internal/compiler/lower_wipe_cover_test.go": "f67aadd24e079ffb6577b4b2e8a3ab8fda91e8a1513431b3e4c23890c23b93ca", + "internal/compiler/named_ports_test.go": "64c802a7887427c835303dd9b831c83a554b9cab8da24dc66183cf01f6ea2ea1", + "internal/compiler/on_platform_event_test.go": "ec3548786f2ef618ef55dca03d7e966f2747800ce1af210ff65789fe13c03004", + "internal/compiler/push_fingerprint.go": "be64278291dc72b5f3220297a73c16aa6b96cd6a2dbdfaa10575c31c3e833638", + "internal/compiler/reserved_blueprint_key_test.go": "5ff3047e7b88ec5988b19c9c0ad8bf018e2fbc4431fbef3e7d7e06d0ffce6f0b", + "internal/compiler/source_resolve_test.go": "7a767c433b702ae4364f8992e4f3c213a442f82e29bfeb77f4e50784b5e434e0", + "internal/compiler/static_bundle.go": "262c87f410ee761acafd4a78428803ffc44f26ff55143cbeb19d1d9ed92dec9a", + "internal/compiler/static_bundle_benchmark_test.go": "355009206fedad3940802cdc1d2a946fbe4fc85f58e7b5f91d4b1742a5845e35", + "internal/compiler/static_bundle_test.go": "57f7c5d61380aaa59e3e401e2852ac55c5b4ef73e0f630c20f4383ca5c4ec013", + "internal/compiler/static_bundle_value_test.go": "a06991aeb87591f0d375090e388dfd85eb8a58e4a694ad38d84d27f205c0c8b4", + "internal/compiler/static_node_fields.go": "885863621224f2a64fe1b77373dce088f83ee27ed831f78c172c148b481661d1", + "internal/compiler/static_node_fields_test.go": "90fcbf9c2d79cc79c56c4e2a280cb92bd932b53a78e366c68121e05c0e500e6e", + "internal/compiler/testdata/blue_compute_manifest.json": "c7298ce473c12194a2f68c47278b2200e465ed9a3f6658b687bd1854bff196b2", + "internal/compiler/testdata/xlang_golden.json": "8928d2530c46f832d64a986fd5877eac2d0c921ce2aa9c03fdc628b6d43b1240", + "internal/compiler/testdata/xlang_golden_multiblueprint.json": "bf9bd613dba8325479b70ff62d17899fa229c324e1961722511bf378db939ab4", + "internal/compiler/testdata/zabcanvas_user_component_pushed_version.json": "3b0cee599b27a708c42f07cca9def4ad1a9430a8e1ac8dc4feea3ec8bc0ed54b", + "internal/compiler/types.go": "0578005bf3d5903dff7e22eb52e3328c38266e6e5b36e03e934c476d88d7233e", + "internal/config/config.go": "76f09f7869aac4961641cc7dc57c0fc20ae07be827433181d098b2af17af4477", + "internal/config/config_lsdp_test.go": "1ac3fd315acb8d36f95300b40ac6965b3d9e93d4aa6db57e467c6f384940518b", + "internal/config/config_profile_test.go": "429255c5917c5ebe37f8c90f05e2b3c2c3ceef2f3acde757ac77d10bd1e3533f", + "internal/conformance/allowlist.go": "64bda62caa1a73250112919375365b47952b2b7a8b04d887328376c74b0a76d7", + "internal/conformance/conformance.go": "5f03f71fc72fb8a7899ea3b5f03e1829796075fe9fc1d66b33803908b3744c4e", + "internal/conformance/conformance_test.go": "5e03de0e43dab9c758090fce54812dacd391b550ea52da057c3c63bcf918e3cf", + "internal/conformance/manifest.json": "b08ece5698f1fc764641a0a4b384d14cfd2a5317131dda733ef909d52e583e5c", + "internal/conformance/querydescriptor_golden.json": "23e10e33f0ebbb01a212c038b3ec24c3b546e81f7ddc4251009856907026c478", + "internal/conformance/signature_parity_test.go": "b56af86b213e43110d917e242dcc865eee5f11fec2da1d97a8591df9cecf78cc", + "internal/conformance/signatures.json": "bbec68cbcdaaf73c33535fbf62b1892d355d918c533ba7aa74cccee112c289c6", + "internal/effects/dbquery.go": "5d2a719541e9c2fdf97039826f29918b2d699558cfa9bbdc54a0abfbc289ee27", + "internal/effects/dbquery_local_contract_test.go": "8e38a04e6a48b1be0fa3984f23dc7f43b2953728c374956540b5ab11fdf3c510", + "internal/effects/dbquery_test.go": "64c2e3036be50bd01b5a19d6206ca67146b35ba3df7de9652aa41d68239c8a18", + "internal/effects/dbschema.go": "57698603e8bad10db49390f0292aa1f6e7ac1134db9320b12d718b4eeb01055d", + "internal/effects/dbschema_test.go": "cd9a7d1ece220c33d5bb4cf5770cd531f7e2e21bd4802a069893a49d780c3d30", + "internal/effects/egress.go": "8083f2f43b1ab409560a34cb7396f6437d7f4f399836093b731f5554b37753ed", + "internal/effects/egress_budget.go": "36af9a20f29301caa4ecd05b25015a3abc8e792b942b96e11f1448bfb325c533", + "internal/effects/egress_budget_test.go": "6c87efb779a77f66ae2133dff2afd77735bbbc06f9be95972554f08c9a7c6521", + "internal/effects/egress_probe_test.go": "4ad8ebf04262c40433d6dbcf23e97216e1a22ae5a8b721261af69943c81698ee", + "internal/effects/egress_test.go": "dae15108d87713b228a4de49d28acc1c303e82b35926d6ca9cc223b79acde6f2", + "internal/effects/runner.go": "a889312d28e2d113f962f436d5ab782311362b9ac3fa8ac532d9f189fbe4df5d", + "internal/effects/runner_test.go": "89bea0b6403b5ba2132721640d455b3e9d0c58cb7dff9ebaa3fbb9d87cee49c2", + "internal/effects/service_token_exchange.go": "8433f08369240c239158a58d84ea34f07c0893a5ac74488771a704dac50ae3c3", + "internal/effects/service_token_exchange_test.go": "8e2a4144905ca26efb1e9e181907566c81b22ddb09fedb9dbb702509aff344f8", + "internal/effects/servicecall.go": "f1b966f8e899a37cce3ae9259395ec90e3cab6286221401e481d8162d8333bf7", + "internal/effects/servicecall_test.go": "399babee4ace54b9189ac38d698855d55f09730fcad3cd2e5eb217d3a56da503", + "internal/lsdp/bootstrap_state_test.go": "0e0df812e22c5c96007ef8b8294bb61bcf4fc6dd03b9e11218f8ce5c4b778ee0", + "internal/lsdp/bound_leaves.go": "1faf74da6d4f3be8c85afbd98152a56652567241d6575aa35bc2993254b4822b", + "internal/lsdp/bound_leaves_lsml.go": "3e867b370a2685c77a92df21c71452f757f62c882eac7fc865a84b76267ddf6b", + "internal/lsdp/bound_leaves_lsml_test.go": "34156f89cc89c9ac907d61eda8d5d91a3467556a539dfd51bff575fdee1cfb66", + "internal/lsdp/bound_leaves_test.go": "f3986dbf21da9497b96f488f9f1c1aa0d1f180962a014ad9bbe8390b742c715c", + "internal/lsdp/generation.go": "fadaf683047134b4e60eb40ee3cfb73fdf09e6614215925893e3b02723ba271c", + "internal/lsdp/generation_test.go": "2198c4d5e6688e9cd75ec10eb82cc89279f34536ab6f106b1046d1fd9040174e", + "internal/lsdp/helpers_test.go": "b2c04102309396f5984b3017516ef40f7604db13392ee898a1983f07c0231328", + "internal/lsdp/identity_authsource_test.go": "4bbb01ff1fa58acfb97ed2bf3a0161749d3ccb53b062df7e313b031283e94519", + "internal/lsdp/keyframe_on_join_test.go": "59c45c653a4364a5a481611cfd78039776a40259bcb61b9a54284300b9590fce", + "internal/lsdp/keyframe_switch_test.go": "c3641cb38f539af2d5785bc28d1c88cdc2b4ffb5703ff26be43072bf7cf8d9b1", + "internal/lsdp/mirror.go": "5ce9aad970770bca02bb711bd911dec7fc96d4adf8a851d3944846a09ac47623", + "internal/lsdp/overlay_mirror.go": "1ddc5627169c38c4df84634b630a9032a4c2a6f0d020371120b3d0a4973abbbf", + "internal/lsdp/overlay_mirror_test.go": "3303982c0455f5fdd7a2176d67d20eed6ec6465f341c0b5598fb4aa1f9c07dfb", + "internal/lsdp/overlay_streamrule_test.go": "477b85e6532023fde93294ac487283ece5daa335a25fdfcbe2eee95c7947ff61", + "internal/lsdp/pipeline_e2e_test.go": "3510a0158884b0aa223b912f7cb1036f7795aae1cf4d912a95445c1102eaef5b", + "internal/lsdp/preview_isolation_test.go": "e1da14b540a78e2e676c7757f9838430cfd397b9302d971c73a48ecadbeff0cd", + "internal/lsdp/projection_metadata_test.go": "0a2bf8de202a7fcf781f4bae5f28747de01a5b5779771da51b8f68912e3e532d", + "internal/lsdp/roster_wire_test.go": "63aad8672b2dae57cb6771ea181d089349ba41e1924fb6a9a69dae1fd626a40d", + "internal/lsdp/scalar_filter_test.go": "fa33d739e5fb1979ac5225cbbde2f888c1f966188d4c0234db915dad11f66437", + "internal/lsdp/security_test.go": "cd7221ac19f262cacaa4b7324dc788a0211777c1d44cf6eb3074ac06d29b9a64", + "internal/lsdp/session.go": "f4e8dca712988c40c7077395d4108604112e49c37651dcc4f5345b59a63f720b", + "internal/lsdp/session_isolation_test.go": "f735129113fea2527a238490647b55e09d230d604f7617ed5f163cd91e0213d9", + "internal/lsdp/slot_mirror.go": "7db168e851dc5d31d57de66b7e0873c41f85a100f86d590b4f8c9660f9531886", + "internal/lsdp/slot_mirror_test.go": "d885264d97708c060cae5679f911d9bb18d945d0f390ddf3f24d44949fd1f095", + "internal/lsdp/snapshot_identity_gap_test.go": "56848265e410f24e762e3487e681818a8de2179b2a742794fcea0367264859a2", + "internal/lsdp/viewer_arming.go": "a7eef07169ab3717c243b83297f9f014296a2cee1c0abf4c08aacf4377ec1e49", + "internal/lsdp/viewer_arming_test.go": "f3429ddd2d3064ece489301f807c5b4f03a22ab10618335a482a932481cbcaff", + "internal/lsdp/viewer_creds.go": "3e85ad83843928fbb340b280825d5b28c1a95955899c24ef33ae9602ce243089", + "internal/lsdp/viewer_creds_test.go": "d1654b27501c1fdebd96a3964f8044e05a5f8556e40307a6b336a275b8621b92", + "internal/lsdp/wire.go": "bdfd544d7379c87f355196428ad70f59face5e307de01f6ae50ca2739ede144e", + "internal/lsdp/wire_test.go": "ca63ca4af7dceb10717626614ba05e5e1d6856d02aa7a3e0e960adba7c9ea3c4", + "internal/obs/logger.go": "993c6fc53fb8861bd289531f0b61923ff061c2617fb507d2d7198e5887e34d5d", + "internal/obs/metrics.go": "0bb2dafa79d7be3266554cd89d7a6d6c8d73d3d3eeb250dd6fece25d15425d1b", + "internal/obs/metrics_test.go": "d8068299d04400b34eec5b63e52263af0c399d2388c4b4f1063c59e53afdaa9c", + "internal/obs/prism.go": "9fe3124096b11399863936f161f164773ba3350c7b9fc1b5e958d0c449c88b15", + "internal/obs/recover.go": "edf6aa2b30761292523e809e111af45ab77161ef408181f3ca95da74a4a63ab8", + "internal/protocol/fixtures_test.go": "007c1c00bf34af96342d72ee7d9a47ee94e21d5c4f7b5c0aa83a7f424754865c", + "internal/protocol/messages.go": "833b96029868407f58fb9fcb5d21e3356f33e9c2293741bda6b4ab1696c5614b", + "internal/protocol/messages_test.go": "358ca4f6b76a05de895fc4f09011d803776e73716349671a09a9476bb853a072", + "internal/providers/canonical.go": "b14653c9965527c7dd2febfab17df5928ea8911001cb1723cb880925a326b46e", + "internal/providers/events.go": "615ba6d918057dd425bdc6ee1e2b4da8fc45a7006b3af3a5cd1d3ba24633884d", + "internal/providers/events_test.go": "4a19e445a9bde44a65f9f2be01d1ec1cd567f3ae007619bd1d36003ad62408b4", + "internal/providers/isolation_test.go": "41e1f00f43075edf843553e0bba62255ef620f06a0faa08ef78c54893e5d22df", + "internal/providers/policy.go": "b0458a6328c5fe0af45f22c9730bdd4cc01f352ce8c03cf5e5e0512f9907fe40", + "internal/providers/providers.go": "1ef4f32c7af54b95e14bbaf724a2a3bd2e4da940c8eb39748a935e74c79b3937", + "internal/providers/providers_test.go": "f95e483e79199b0a9e26777ba034ff8b9a3fcdf4606835d5cd7e8463beff1c3b", + "internal/runtime/canary_firstflight_harness_test.go": "46b0d33c9e06550dfa46e509e630cef699685bb9913bcacb500b4282a9b5bf35", + "internal/runtime/chained_ref_db_exec_test.go": "90d6218ec47b6f3cc3590fdbafbc99b59dff634f376af68b0581974a6d81d376", + "internal/runtime/clock.go": "a2eb33e29d045291959df20d476217ce14654f8223bc5b6f3337a5b2ce1bf74d", + "internal/runtime/compute.go": "bfb7ad6b99b9700270d3186817322bb81cdd94c799bd7ba5a0dfd0ece78c2119", + "internal/runtime/compute_db.go": "cde0bfbf4dfc48acb9c16fdaa722f19d2ae0c703b2e9f8bf9a914a1ed4c160f0", + "internal/runtime/compute_db_test.go": "41d79f7a5c84a42f2db3e0e83902928fef204cab261e9b2cb8daebecf1387bd0", + "internal/runtime/compute_db_wired_test.go": "7828ea5018c9efcd94fbd9b95d2c501738fc306303fde0148932dc3d7333d51d", + "internal/runtime/compute_equal_test.go": "4ee613c493da93f26b5d02f985ac81fdcdd93427fddb8a89af545f6baf3224ca", + "internal/runtime/compute_pure.go": "d75b0fe0352b20c54a08d589542b1948a4fda6b62aa1db3b5fc3379b087ecb93", + "internal/runtime/compute_pure_probe_test.go": "d066945349a21de7bc87dcaca38dc4037d9a0c09b827cf7f963cb42f56f2a644", + "internal/runtime/compute_pure_test.go": "b595d969991d0317bd0d55eb516dd49a08e41a32568717aa3791ec1de2642457", + "internal/runtime/compute_source.go": "8d74ccf671d2b2c4259ead29f60236ec8fe66a932959fafb4e2ddbb7b36fca2b", + "internal/runtime/compute_source_test.go": "2686a9ae9b33b45945dc41c588e0fbc081ef6b22ba6e4a588b720df68e19dfa1", + "internal/runtime/conformance_exec_test.go": "5d6367062946ad9444479fc9040571e1f0d5e5181ed5e822a77896322d69eca6", + "internal/runtime/dirtycone_test.go": "396e9376afc3ec140e97ed5b77485f04c9aedca0c30135cdd9f1a8dee910c1be", + "internal/runtime/draft_switch_test.go": "855c85906e872a99b1aa5335ecd8af0a6d8875fa4f06c53f237ecc7d038260ea", + "internal/runtime/engine_ab_parity_87_test.go": "de5ad05236724a9dca9cef20d3b7a52b1e7afab756fc8a8384b303549019eb32", + "internal/runtime/engine_ab_parity_extended_test.go": "c97734eb2583af5ffcbba9f0083fae08ff34a1d97832b9fa2985f8dd59207f61", + "internal/runtime/engine_ab_parity_inventory_test.go": "ca82ccbf10a46f32c203e1f01468e51c00599728b7b0896a4a10284822433c51", + "internal/runtime/engine_ab_parity_test.go": "f026af2728ceacd32d38e1296855fe811ff60a5b26d89683fa7e1f2580161228", + "internal/runtime/engine_ab_parity_transport_test.go": "306efaf0611677766af0ff2131dd4234e6e5e54950c0b3719cb36bdbf7a6f44c", + "internal/runtime/event_ingress.go": "32d526d5bd40b9b7b36b193388db59435ea662349d76df246b0a9c8495f01974", + "internal/runtime/events_input_reactive_test.go": "9d588105f8651b993464feed373614cbf60df0e248623802b3874a6e9003a3ae", + "internal/runtime/exec.go": "a7df78085416c6b5b05fcf89d339752421c97144cbd5ac2d74aa6379432d9ba6", + "internal/runtime/exec_activation_probe_test.go": "5b6932679a769156bd43dc05331fa62c58e20f145c08bd722f1b030bd4affbe7", + "internal/runtime/exec_anim.go": "6680b7a52b10a4f8fe1c7808ed6b0834e87784aef28f1639e26aca68c373321d", + "internal/runtime/exec_anim_probe_test.go": "27972c459ad17e698d2d9f5586824ae075bd5c0d6774df103bd631dc2ff112d0", + "internal/runtime/exec_anim_test.go": "ea28afb213773fd6f5b0adc201ad9be8f753c1f73c8d028c26535374b51603bb", + "internal/runtime/exec_assign_slot.go": "632af97f18894bbbb920c90c7f3c5a50cbd4469a39816647be02e94e3549b6da", + "internal/runtime/exec_assign_slot_test.go": "a163df8c475b99891a0e4cd42ddf77e377daf18a456a720934fdc4e30c2c360a", + "internal/runtime/exec_contracts.go": "1f127ba255c546adac23987622718f2b94b95d696834de5c254f8ed79b26cea9", + "internal/runtime/exec_counted_loop_test.go": "2ddfeb2ef9f13107b840ed8f4e58c8eaf2e6d412fb3d3acd8ba72a9b27830cdb", + "internal/runtime/exec_cpu_test.go": "bc3a94d093ac651c4e1070b6248f1debe5097be2d01d7bb9c911c5a0bbf99623", + "internal/runtime/exec_effects.go": "c65fff1792b130b31a17d051eac4cbc36bd3d739ea9efa13909bcef5acd2732b", + "internal/runtime/exec_effects_probe_test.go": "3c7f58e533c31419f8c158de49ce5520936fa8260e170f52e29e51704b49002f", + "internal/runtime/exec_effects_test.go": "753718a189d749b689e0eec7562f6f338ee99a28523ec93a1b1d462e707a1bf2", + "internal/runtime/exec_effects_wiring_test.go": "aab7c6e9272f02a78d01ab49dbc5a3ad0d3b232ad1a952782647b311ceccc80f", + "internal/runtime/exec_http_hardening_test.go": "44f515cdb7c195b8e4a8f128fd13b9ad0e2fadbba5f404d09557d8fd712b7878", + "internal/runtime/exec_interpreter.go": "a2cb3fbdbfda76ad578d01c058f038117b540ea6cfbc06cec75fc2e1a136310d", + "internal/runtime/exec_multiprog_probe_test.go": "dc5d4191157303e76a0cd68812b6c2a904d9b7c1b8e9fd132177def7b169457b", + "internal/runtime/exec_multiprog_test.go": "c207739cdd446e134bf06b39c91c99d7b754a4cdc5a385c5e0271ff28c878928", + "internal/runtime/exec_on_platform_test.go": "022ac1b1c9832a57a6976a7d84a436b1db1fce53642b7c7068c62f5c0764abfd", + "internal/runtime/exec_onair_test.go": "46ab66705348f844d076123df46c967db9b8cbac861be4469cbd23560f406f40", + "internal/runtime/exec_operator.go": "d1b76611173a10d70943783826e9b04b2656451a39e733e614678327b06b01d1", + "internal/runtime/exec_operator_test.go": "50fa0d3edbf261b2b77a6874b461495d354653fdc4556dc4d29b292e26fec267", + "internal/runtime/exec_overlay_app.go": "1dab339278ce241e30a19a4e62fe39a8db4ce6ced53e5315581c64e9f6512fc0", + "internal/runtime/exec_overlay_app_test.go": "8e554d817243cc3e9788e4c1e5374e741befa3848e4e05a7c07b332d3b867edc", + "internal/runtime/exec_partition_roundtrip_test.go": "177b449e22ba197c3646f45778c9128c8766866c6502f0c8502097e3cd761585", + "internal/runtime/exec_probe_test.go": "0fd396c0408dc9d0603d2705759bce95273b9575b31f7aaf33ae0094eeb11efd", + "internal/runtime/exec_service_call.go": "e463e657b0dc347f44a813f9df53bf05e39151e2e36cb90fb2a8f3769ae192f8", + "internal/runtime/exec_service_call_test.go": "47660de2d7984f3faf5515e042a904aedb60c9dc446fd34f946f9d3722ff77cc", + "internal/runtime/exec_show_emit.go": "ba483e73d0041dce76d62a49b1c788a2bd326aeab62cfc839836aab0dbb78f04", + "internal/runtime/exec_show_emit_test.go": "aa53c0b12070a0eff506701a9a1e9d387137ed04cdec5da76d061591993b544a", + "internal/runtime/exec_test.go": "92a66d015d705df4c0ad052c0f94e839bc6c307de080b143361e4473d6f4a898", + "internal/runtime/exec_timer.go": "061cc23d8be527eaa1868df7c26f058ed6bc91d6cea013240026d2a6f74d73c4", + "internal/runtime/exec_timer_probe_test.go": "8149a21335d30dee06a976cfa21eddc2ed9e5ab6b4c3b501d75a3819855183bb", + "internal/runtime/exec_timer_test.go": "b6c534fa689aaad94b09acc2df17a9ce035b9eaaa6253c2627ef6a2c2d55b92a", + "internal/runtime/exec_validation.go": "e1bb3a850b5e88931903724f3782c35c8b2fa0c27f8ba4c6f7a57e46d3302b2a", + "internal/runtime/expand_reference_runtime_probe_test.go": "779131d0a3e193878683143250dafb4c6bdc9f00eabff56bb29bdf0f3d987b94", + "internal/runtime/named_leaderboard_prodgraph_test.go": "8735724211ecd2c474611ce6a09c31641fd1fdfdcb53d0d7360dbebd25725c84", + "internal/runtime/named_leaderboard_reactive_test.go": "b63fb2667c3d352bbe5915a0e47f4ff1630409cc9f0226c32627dbe01aa2e278", + "internal/runtime/named_leaderboard_test.go": "387623060021cb2a141d9c7568c8edc6fa48922ef1cc7d5817edac078c7cf80b", + "internal/runtime/named_ports_test.go": "40f471d8361e689c93203fc0fb64248c7fb57629110e364fe9b790c5e424632a", + "internal/runtime/perf20k_test.go": "0f84e571bc90637ab30e386046e98e3d9a7d810a70c652b1bca43c092b31f76b", + "internal/runtime/preview.go": "2e09dcaa1e23e9a7054f87a49506271d1c963b35f0220add32b1b424038ee2b4", + "internal/runtime/preview_editable_test.go": "7cc503e6ead1b904c761746a2e2d18ff716630f5afdbcd083c9c2796a100ea5c", + "internal/runtime/probe_cone_test.go": "39abf881dad0ae5f609962369d91a7e13d95ee37308a1084ab52a573d3726088", + "internal/runtime/race_off_test.go": "3a318ce34760c2e1aa2616b5822e31a86324e59c420bf6188c26d8c969a3485b", + "internal/runtime/race_on_test.go": "c4d6bad7295c55e560599ee0d00c74c453763022ce3f4bd3f2b7967709e91ab0", + "internal/runtime/reactivation_onstart_test.go": "4c26f059368b831d6869e6db33b64864e5254a52e54ae694222d4fdab0a431c6", + "internal/runtime/scene.go": "b968f4d6645d143b89f771e4ffe3f858c22f88d05927fd42729d977fda10fb87", + "internal/runtime/scene_emission.go": "aa37ec9aa03695deadfacb3a117994ed26c1f137769d910d7229383e6a411f7b", + "internal/runtime/scene_evaluation.go": "b61ae0341e4cdb8d59e107ead3001e20bf6ffc76e162c181d5cb103809d7aa0e", + "internal/runtime/scene_index_test.go": "bfb0d1bfaf5ca280c9950ac902ec6e1891770a27251188836062f38ebb20a456", + "internal/runtime/scene_test.go": "84a5b8ce0e77322a6bcab9ad706d7446678e89409858a78d99967b4e304bf6d9", + "internal/runtime/show.go": "d1c98c1151456b8bef3779863a57c295d1bccb304b92532dd17b873bb320a4c3", + "internal/runtime/show_roster_test.go": "e7efcb3084830546a618db91f9613cb85e8bb923bc7258474732d116d819655a", + "internal/runtime/state.go": "54c1024e690af10e87cd8a969ebc4a8d7c424523990554d3f9cf15d955d3b4a5", + "internal/runtime/stream_rule_test.go": "5ca2900f51d49e35eabfe5af6beb6323d63b396518032e19745ec973ae92b8bf", + "internal/runtime/test_session.go": "9e9fb1a842312b2c65a409fddfd56f0fb0920aa1fa5b7cb8fa06dcc22b3740ca", + "internal/runtime/testdata/m3_prod_graph.json": "e92e49c0dfffae7c13515351477fde78a8d3b0cb10805840184c33dcfe762b67", + "internal/runtime/testdata/m4_draft_switch_graph.json": "a843688f1efe00df68397729e8edf6bc5cbf886386e1b901a9ae34fb6c7e0ca7", + "internal/runtime/tick.go": "c11222c4e6d6274d7bb09b31cc9a2dffe103dd77f6ef8fd3eee0f8e767ed895f", + "internal/runtime/validation_harness.go": "73638c80da6ea90112665475fdfcbff3ed4e89d03e72efc36f9149f2762f3772", + "internal/runtime/validation_harness_probe_test.go": "068c3f64c7a640f589146758053c9d0f613aad06723423db3cb4da09e354f734", + "internal/runtime/validation_harness_test.go": "9d2accbe4ad47b489fe95cc4c3001341d7f447a6496a6cbe9f9ad88c04e8560c", + "internal/runtime/validation_simulate.go": "d9fecb8e7df136ba72b8899307fdef68eeefa4562443dea5991b0aabce118f79", + "internal/runtime/validation_simulate_test.go": "73eb738227fbcfff8b446a56a72f69b4ec7af11bc934f603c1d1421c13c2809d", + "internal/workload/client.go": "388bb177061c92a86da1e82063bb64c12e3ab0fac52d1bb47ca4eb16acea268d", + "internal/workload/client_test.go": "be9ac6100834e93fcea15aeab4da3cff119411a2c6d845b7c92160f0dd7fa6d6", + "internal/ws/connection.go": "d773e7fdd66d522cd1081f03c0b5fc85c1f9a4dd291120ddea83ad98de22e0ed", + "internal/ws/server.go": "364ec54691f65dc0fbb377877ecb1ff0be7915b952d830ca2466445316e6202a", + "internal/ws/server_authsource_test.go": "f07b2e25b1b3d0222648fdf5fb4c3017be86c1e4c4a85fdd60a2a51f9d26c99f", + "internal/ws/server_platform_test.go": "dbf4e8f6ed350397cd14da27a39f944d6f91e0686f4c44e5aa82f03d3f18c49c", + "internal/ws/server_test.go": "321bae19aaba23065984d4247b384a1883439dc528894ba6263c7b01e1ca775b", + "scripts/check_file_sizes.py": "56bc342f324fa1c67b9ebbce8b4a513bd1dc38f41e479cf85bf482613c3df36a", + "scripts/extract-changelog.sh": "c4ab684fc2994dd7650acf898699ab330e37d7540cd45b80ebf52e9ac95839f5", + "scripts/gen_conformance_manifest.py": "3d687b91e38e614857d2baf1dfa8472e438e1c3aea07aede80c4c2349feb3bfc", + "tests/e2e/README.md": "e059a4d14a51bf3d052aa62115319d5bd9b9313c0c34e1fabb5abe73d907479d", + "tests/e2e/canvas_layout_contract_test.go": "662a595313970401971bd61f42208d7c93f207d7ad36d68f362d312282751158", + "tests/e2e/e2e_test.go": "dac60408ad6bc5d94c64887df3cdbba9e2565b0d70ab42f8dd915f59687ffa58", + "tests/e2e/testdata/canvas-chat-sponso.scene-bundle.json": "e862a652f6725b78e7937f22c885d73892da256a989686f348696f588a0b8110", + "tests/e2e/testdata/canvas_layout_from_canvas.json": "c086fcc2c67bb2af00eac8724bfee995df2921dfbc8a13dbfac86fa348cf3cde" + } + }, + "ZabCanvas": { + "head": "27a3caa20d4a6461e892c4aa7370794622abc035", + "branch": "forge/local-20261003-source-delivery", + "files": { + "docs/development/maturity.md": "dea05c6fe5d9547c93b67d85f7021663f89b9691da39a794c86e657b65ddfb64", + "docs/development/source-delivery.md": "0cd270e3f27430504509970e7e5b1872b8de65d2524d0b246d6701fdfc8ccc88", + "scripts/prove-vision-source.py": "1d6287861a7fea543cb514671e1f626d7012ad26fb18525a1ce4c014c9edaf6c", + "scripts/verify_vision_delivery.mjs": "7415e72ee0a813890c3327c11006b45123402aa98d53cab91467c12ca5f3d663", + "src/zabcanvas/routes/scene_sources.py": "856ca5b2ef385b09793f7d5ccd8e05d53ae91dee51a877b5acbb88d1194ecd35", + "src/zabcanvas/routes/scene_validation_payload.py": "12ccecc06de6d9437ad88fda67cc07ccac39031c4834c0e4eec38f1fe6c4272f", + "src/zabcanvas/services/scene_blue_manifest.py": "896f475f138003043bf280c5fad7e6af331e13761f8706f1424377b2fa846988", + "src/zabcanvas/services/scene_source_archive.py": "7874afb75ffbaaf933dc16481fd900be831d8f724a1c0851530f974ffc422483", + "src/zabcanvas/services/scene_source_assets.py": "a4ea7e033b60e69641e849eca533b4d376e6270eecf4e751524ff0ef56f2241e", + "src/zabcanvas/services/scene_source_service.py": "d573d0cdd53b92e8f2a8a260b844906187a11ccdad807de8eabca2545fc110d5", + "tests/serve_scene_source_fixture.py": "9a8c5184cc0b49955438e367766f9670f93ede8fe828395c0dd43584772cb904", + "tests/test_scene_source.py": "851b8a3c1a0d2c65081c5e3fc8d573ecfa19407e993b4e14a6a791af70b0131f", + "tests/test_scene_source_delivery.py": "57243078102ba9e31b92ff286f7136db31a550e31536d76614934824197bf5e4", + ".agent-checkpoint-327.md": "726967791a5e25a80633ed5e0e460441db4c947ad7f7790d99bc123ee0b57d43", + ".env.example": "2517d322a031a387c735ec18758c32ad693b9158839428ad84431f0d9430c487", + ".file-size-exceptions.json": "c9b88988700d5a466ece59612044b65659866d0d992e1b0703425fc676b2c125", + ".github/CODEOWNERS": "945bae1e4d4dafee6f6ca18951a8cf3d73fa353deda16ad23181632bb3d31092", + ".github/workflows/ci.yml": "3742feafc152ff9131477227121f910efbea2d261529237647f62a1cf1440e14", + ".github/workflows/deploy.yml": "a25d7e584848ba55f72732167a0a82b545c1ec3df76006cbdf35f29c0762f742", + ".github/workflows/file-sizes.yml": "a007cb85357c2557e015b4f738e5963f4cb502a7ac1be9eacdb5c49ae65e813c", + ".github/workflows/integration.yml": "4d0d6b6b032a827b909b2c6c5a16659e7dcafb76366bf4f2e2ceef90c3015c06", + ".github/workflows/release.yml": "f2215ba1c9666140688dd8dc8933a4a51fa61e0954618686a489f08469d7abd4", + ".gitignore": "00958c7ada8bc334993a8584c3dca4cdc26c7c9e2789bf66237e3741a559b062", + ".gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "CHANGELOG.md": "e4e194ecda5b6b2f656e5edfdbf35d8caec80ea22205738e73ecd693672ff1f3", + "CLAUDE.md": "adb9f852cb6c82eaff8e7cea2e862b1403ae7fc159cda23e6a25c319f0c6008c", + "Dockerfile": "91a13ce183993dd10bb69da55c093f959a328c3e16610b5ed59f273911029f53", + "Makefile": "e05f391a3b11c691054d6c203de9fbb39c8fac2e3cc0e35b6b52b16ec93c6f77", + "README.md": "116b6788d120a263b5f9996b7c32ccd673f8490ed0ac4f961567bb69ab7a9835", + "alembic.ini": "8d8691f595d63397075ca9c11c5983730276d0fb68a4529bc15b8417571cd5de", + "alembic/env.py": "99524627a524f3e18460a64aefccf33901ea860c5b5ea3e8993479b635ad6d8f", + "alembic/script.py.mako": "a60ec52443699fdc2aec93254f06dfdcdfb7f9a5fc7432b7a7e12b9c253713db", + "alembic/versions/a1adr012g304_add_live_scene_graph_fts_index.py": "d7d198de6b5ba0c4849fa55f08b227858de68e9bbf8d61fb9a7502a561d14015", + "alembic/versions/a1b2c3d4e5f6_init_overlays_table.py": "9ee4061b23c9fa75bbfdd5054e04d3accc416d877d267217ab6ada5e159f1917", + "alembic/versions/b2c3d4e5f6a7_add_composite_components.py": "4b0351e4db40e8850ef1b14a1bf16c6f7618fb3adb84f12a7266a8feccbc0104", + "alembic/versions/b325rsref01_add_resolved_scene_ref_issuances.py": "bee764f9e81591a4d7d4066daea479345d1c6211adaa15aaefb3ea696bf89f9b", + "alembic/versions/c325rslookup1_add_request_key_lookup_index.py": "82ef0622ce3a7ee08aeaa3e9a3f51e80909ba98b73c8f096a643778ab4040660", + "alembic/versions/c3d4e5f6a7b8_composite_slots_schema.py": "d33651e0393efadbe22c17fb693937a610e80255ca2d59e1df63d6d5731df1f0", + "alembic/versions/d325rskbind1_add_request_key_aliases.py": "493f1c62936cff662b390188c7ac0931586f87bb7f7c9a504b6ba4cdb63e4622", + "alembic/versions/d4e5f6a7b8c9_composite_data_sources.py": "2fc01e8c636e1f1b9e9f9e191e4f98046681e65bc4ba4089f3d354ad5018fc5d", + "alembic/versions/e5f6a7b8c9da_add_scenes_collections.py": "cccbdd2db38b2f06c747a4aa51c287601536d9454ef35517b215b3b09cf25305", + "alembic/versions/e647vldn0001_add_scene_validation_records.py": "07a1ace0e47c578acd5fe31f9ac5243ab0a73d568b40dc593db5fcfbb9818331", + "alembic/versions/f6a7b8c9dadb_composite_tree_model.py": "867f687dbcca620feaa563fd915368d22e9d437835204e5735ec54690265989c", + "alembic/versions/g7b8c9dadbec_scenes_counter_trigger.py": "c3aaa4d495ba96d15687e7bccc6d39d2ad8bd66411776af5aa3c1824cccbcd46", + "alembic/versions/h8c9dadbecfa_save_push_split_and_user_components.py": "3d56ab36e32140d94f9d950d6bee8f322b5ce193836a421e7f7de9fe82ff78f8", + "alembic/versions/i9dadbecfab0_add_lsml_bundles_table.py": "3491a6081d9069c4dc081ba3c1fe18bf1289d6446715a21bcc69a6e17d530b3f", + "alembic/versions/j0debecfab12_widen_scenes_latest_pushed_version.py": "92fa558471aeb57bb2dea3a853933b225402fc2710a87d888d306843a350f210", + "alembic/versions/k1efcfab1234_add_scene_definitions_blueprints.py": "25ed057fab5cb60a0c200138cd49571c8705069854d6ee431023243430a01882", + "alembic/versions/l2fcfab12345_add_scene_authoring_revisions.py": "341d5e7a8cb04c2e59ceccdafe1787dc2b590869bc78deaa655f51205b871ef9", + "alembic/versions/m3fdcfab1456_link_revision_to_definition.py": "2307300c4fd6ca054a4002e32ea31c169813f29b91c1f9322c9dc32e7adccff7", + "alembic/versions/n4fecfab1567_fk_lsml_bundle_hash.py": "c710a1d1a6c6ddcfddfc548d123e1a16faf6c327d6c8eb7818388636cbf65eec", + "alembic/versions/o5ffcfab1678_add_version_verified.py": "4b1f20b7ca52a4aa1b29e883919507b4277dfe42bf146b662049e5164cf8459f", + "alembic/versions/p6ffcfab1789_add_scenes_render_schema.py": "20a72973456047d58a5e1aa03ffe8c0d9da1839f776387586b65f664b65d37a6", + "alembic/versions/q7ffcfab1900_add_scenes_binding_status.py": "fcc153d8b8491b2668db4f21ae5201b4fc4a4f80576aebc1e41d357501cf3536", + "alembic/versions/r8ffcfab2011_add_scene_assets_table.py": "ff2606c0e562f3d817799f2cbd220af1f83dafb7c6580767294815298654c631", + "alembic/versions/s9ggdgbd2122_add_categories_mandatory_axes.py": "803469eb1d3483615737a683f3ef87bf8f56fb1a56ba9efdb82bab7cd05d73da", + "alembic/versions/t0hhehce3233_add_scenes_custom_thumbnail.py": "a469c866e3e8c84ee85696a3959f2ce57e12fe236f4db18af1962b8ba969568c", + "alembic/versions/u1iifidf4344_add_scene_assets_storage.py": "7259234c07c8cea4d07261ad791e4871f184ddc686ad9a5bf696ebf8ed8b2e47", + "alembic/versions/v2jjgjeg5455_add_revision_pointers.py": "96a7cf0d38d5528953dfb548bd4f4838ef387d74835ce091d0f459e1de90348d", + "alembic/versions/w3kkhkfh6566_categories_cascade_on_collection_delete.py": "2ff1c0d4306a38124c578d582297464a472b4da1489e89f400f6283dd6f29de0", + "alembic/versions/x4llilgi7677_reseal_legacy_lsml_bundles.py": "d9ff406c8557e8e55cf44bc2906d7e48f4431f3064516a5c3d0f0fcc0994d687", + "alembic/versions/y5mmjgjh8788_add_scene_revision_projections.py": "a180be84b0118f66c33b7836d41e0c599ceaf5d39977304d03f99972d3c730fe", + "alembic/versions/z0adr010i2_soft_delete_scenes.py": "e12028c61b26f27688d6d3b4a09cdfa73c73cd01b2589436568f2e4e9385b743", + "alembic/versions/z1adr010b5_duplicated_from_scene.py": "84a7eb259beae4fb3d157ca966e823a2bc0e0c58a216a3a84e476d7a2b2284d4", + "alembic/versions/z6nnkhki9899_add_pushed_scene_version.py": "bbd0714f782f15543c1dd874b7ecf063bb47e1557ff02522beea7f55ec23e164", + "alembic/versions/z7runtimerb001_add_validated_render_bundle.py": "ed05ea1e78883cc6ef5682ca5a560b464190828c5c3265ee38e5454003636a21", + "alembic/versions/z8editable001_add_editable_scene_head.py": "7dd7d48a6a367a3581afa3fdc29e0f330d6e29a4c5689c10699a7a6360c36708", + "alembic/versions/z9bluecomp001_add_blue_component_contract.py": "71e784e75d58e72844b31957ace7fb74a10f722b9579c3e08cd42d20846b2aa0", + "docker-compose.prod.yml": "1223eb3ea1c10c33577a83f961558ac5ab17ec0d02753be47d4459c7b0b5fb62", + "docker-compose.yml": "45c442c174943ba2276c2585362fbc2baaf776f27898e2c3d73a4adc8e114bf6", + "docs/adr/001-jsonb-shape-contract.md": "be5d487d75a63d21a6b76146bfd714652520448eb0947e5330ec58338b559fec", + "docs/adr/003-scene-roundtrip-lossless.md": "dc8fe897def777f3344f2cdb04c36a5e7a0ab6c7b9cdc3fb53cb9b4f01eb4334", + "docs/adr/004-scene-quality-gate.md": "35327a9ad92a8e6f1f0aa762c20f647015f882b12b3a3493af5fd20165a74d51", + "docs/adr/005-headless-render-host-and-asset-store.md": "a21c09afc7f515ae90a28a819e14bf1d24495f4a75a20977d042fa2a335a4485", + "docs/adr/006-rendered-thumbnail-persist-serve.md": "57799e57204e0623940d4435b2e3373ce7f7bf511842621878fec488b039d298", + "docs/adr/007-scene-image-asset-persistence-pipeline.md": "6691ec48de67d77534588e651d594a3f958f24b95297e0e530a0ab4a08807812", + "docs/adr/008-video-scene-assets.md": "3f0694b2377e1b2762cd4ac0125bb1b76b8d971dc45a3ee3e2aca7391429d96f", + "docs/adr/009-scene-lifecycle-publication.md": "c10b2808702fc56b79157ee6d5618b6d5e965f81772c933f6b64af8f8711079f", + "docs/adr/010-identity-lifetime-deletion.md": "38dfbc7a0fb284b13878d836c9d741e82b0f912035f378cc52db21340629658d", + "docs/adr/011-collection-scoping-and-call-authority.md": "f7a3491951444113aca4b6125b6d84facccb5cb359580db169756b9295273fdc", + "docs/adr/012-scene-graph-search.md": "1ae03fc58b342070ce22cbb2f3bdf9e71d777406a3b77724728533bc1fb9dc1b", + "docs/audits/2026-08-05-lsml-bundle-addresses.md": "353cd0491d55ea53b9cd2a7b0702bdae88dcf4f0e56a630413893e4ae5c068d4", + "docs/audits/evidence/2026-08-05-quarantine-9-rows.json.gz": "53df0a5ff6594ed74ed1ffa8651ae62644f4208ef95a49a38c6ca9ffbfb9b500", + "docs/contracts/blue-components.md": "c4f9d4e7c54296a0f43551c83424da2b4bad7fdfa0a1f061ae8df646eaba3aab", + "docs/contracts/lsml-bundle-store-admission.md": "a16c3cf114c19f9545549b55c9bb7b5d2483dbd34aa40c71525b7cd8e1166295", + "docs/contracts/zabcanvas-if-match-public-gateway.md": "2de773c45b86017261e2a93d7416ad6d6b60f05dae7d53bf2108cde697bb35bd", + "docs/contracts/zabcanvas-orion-scene-version.md": "4a5edc4ddace85e3ef07cd9c1a6a7fdde118d03018f4e9fcd10be006c3a1861d", + "docs/contracts/zabcanvas-zabrender.md": "85da853d4fba22b8e621b1751165ec30b1d03f858af7d7c322e88fc4f75ca839", + "docs/development/file-size-policy.md": "6ff44854edb74ab1a8381c4b4431667f62d16c21ad2e81338b5d22658137b374", + "docs/development/scene-feature-map.md": "e5a2485d4680c6b071e9d65be59309fbbbf859386737e38b809c223069f48ca3", + "docs/evidence/165/keeper-2-operational-validation-2026-08-08.md": "29c746ab0d6bf483c508006be209fe2ea18c8595cc9b79a5b046503d5eb2863e", + "docs/lsml-scene-authoring-playbook.md": "3b8ab83c421422c0f120a776fd17816fa9f5aa6969ca89c7a2c05938114c0264", + "docs/runbooks/2026-06-20-ci-vps-jit-onboarding.md": "bb64c5f0c453dcb6801f44cb9fe7c1746e4fcbca4597ee703f804793b7817b90", + "docs/runbooks/blob-volume-orphan-sweep.md": "baf175233d79bef3d8911d6aefb976f197ddc0c596a65eb4bffa50db181bd7f0", + "docs/runbooks/ci-runner-pg16-image.md": "04be483cf7e85609deb1e817af9965324ec80eb37e4c4bcbc027f37f8d081c5b", + "docs/runbooks/ci-runner-pg16.Dockerfile": "4de6ba7d780a778d20dc9ea0fb07fe27bfa764472516351a7e4a727689831974", + "docs/runbooks/deploy-secrets-environment.md": "e5649257dfa21266d279c769bce1efdf83fd208442b0151d7dc60855c522ab60", + "docs/runbooks/docker-log-retention.md": "d48e51487905db2cae401e33d920fcc7d652e89007aaf0884d0a948b8eda2c0b", + "docs/runbooks/gc-activation.md": "d20f4438930cd9b2d0b5b4b695e034a4ddb669b890938c1ed1fa738cb9da8c26", + "docs/runbooks/integration-blue.md": "adf882384c968bfb12ed4ca590350f4e761b2367a155707cdd14d8d93d5329fd", + "docs/runbooks/jit-runner-image-disparue.md": "87f1797f21e97f927e25fb4e8c5a8c883d1ce39c0414bcd172723c241f5930e8", + "docs/runbooks/render-failure-alert.md": "24f3d69e0f66bbeb6e14da0a68744fdd94df13cd36e20531624f52ddedd07ff7", + "docs/runbooks/zabcanvas-backup-volumes.md": "804249cd217e799e11c6652c56252537be72b7dc1516b72bcead2fa3c083df16", + "pyproject.toml": "82815324db40703b75b88da5d37610b91ec421c9fd45d2508516aadd5e5db262", + "scripts/build_canvas_chat_sponso_mockup.py": "70915cfbbf08dba233bc8c3952290e3f63e9bacfdcf6244ada92372b94508446", + "scripts/check_file_sizes.py": "0fd21d955e109ea9fbe65982f187bd7f65586e200918910bcfb520ccfed04e4c", + "scripts/ci/gateway_stub.py": "7a7c45bb8e4bcf072cfdff1482c3830429655820fe6a226dc0b4fc3eadc43ba6", + "scripts/ci/pg_migration_roundtrip.py": "c9f0d0fd998f10a904dd3cb6881977cae6bb584636b2c6e31a1130beb68fefb1", + "scripts/ci/verify_bundle_addresses.go": "208dab6bb641ab324706938a74209823c9fd7c9971a4e2ec4aa3526b148a32ab", + "scripts/export_mirror.py": "9ed0d4a3c3bfed518a58fb0e84b342a6c574d78c1eb09524d51f958f5ee7e388", + "scripts/extract-changelog.sh": "671725c57eaeb7903d67a10a5a89501f9aeac52d35d2b564ee64a72d9eef513f", + "scripts/publish_attested_fixture.py": "492309c6b44bb741e12ff3e8cd85ccb5b435e467061612e035d4995ef6315f67", + "scripts/seed_scenes.py": "0d76f165272aa4d03f02caed5d7635e32c7be54777465c975dde1f7faeeabe0f", + "scripts/sweep_blob_orphans.py": "5fe5034f19332af0267d03da937f09490afd46fc50dd127bdbba2760214c421d", + "scripts/sweep_dead_image_src.py": "ec74cea4b0b1e82c2532d468fa6746df4bb41242919bb663408bbe34907bc6cc", + "src/zabcanvas/__init__.py": "23c265fa2a146e0fb9b1ef8f3c586d95c8b098ba0c4819a6588e101c6aefb286", + "src/zabcanvas/asset_author_gate.py": "ae0f7a242c41497986ea0aa800b470db53d44c8496b782d429cbf023e53ce234", + "src/zabcanvas/body_limit.py": "1a4fd6450a6c191f604ea460802bb97828b44fa72e5a65328539fba0abd6b598", + "src/zabcanvas/config.py": "a3bf804049396423486039434959a98335dac2ffd0a1f611d4dbc28e19187d1a", + "src/zabcanvas/database.py": "a0bb47a4368927c678a269b63eac15eb7da5518890ff7ded58eefef84495fac5", + "src/zabcanvas/main.py": "fd03cd791acc3eb110ede77e6edb4f3c5e10a026653054b3e172db612e128cf8", + "src/zabcanvas/metrics.py": "e2567a1e77bdc17d70f2cb48a121d92c58a45b04e83e79dcd4a181b53cb7b548", + "src/zabcanvas/metrics_middleware.py": "a4c84d428ef0afebc54eb5d697f69fdcfa944aaabb2472f6a166668209e836bc", + "src/zabcanvas/metrics_server.py": "53a9e757ee4c0dbc8e88cbfb8418033ad1d406e7914d9e25f9567108339d2345", + "src/zabcanvas/models/__init__.py": "c106c69ecb84c4b23d5d8856e09fe9ed9f65b40018d098eaca4a8e016fd5f0d0", + "src/zabcanvas/models/category.py": "767f7d998fb3266b4ec0b30ee5d6371cddbb5aa75a3600a3a410214419a51c58", + "src/zabcanvas/models/collection.py": "a207400817e9062642d71927fa853b4ad937674e27b6444650597be22f90f103", + "src/zabcanvas/models/composite.py": "7d9686e8c89065c000d7dbf0152db3aaeab2e2ef015459b010219c6a0e2a5153", + "src/zabcanvas/models/lsml_bundle.py": "752d10835a82475503e8bef7eaf3b2caf742e7d0e89152abb77b24e64ef0e951", + "src/zabcanvas/models/operator_input_draft.py": "14c3bcfc55e66bd281488335372e3fdeb40f1eb2aad38829f27280f1a772f5b0", + "src/zabcanvas/models/overlay.py": "a70d9d1f7374ccfea92e988af46725bf861f77712b336de462e5de86526d0cbd", + "src/zabcanvas/models/resolved_scene_ref_issuance.py": "d88bc5bb09df4f136b3f176a1b3f76c9535e31bedae410de9dc2c551fcc28dfa", + "src/zabcanvas/models/resolved_scene_ref_request_key_alias.py": "615459eb850d2c1457df881972a33adb16d92a0eac9886ca53277c5f0ef82e30", + "src/zabcanvas/models/scene.py": "04392f9036801b67199a64fea4d0977153f1cfdaffe09d1a96c53802ac814ce3", + "src/zabcanvas/models/scene_asset.py": "b02e57b064c74de33a49c2275d9f387c2162da06bb43b6b7488d75e512d2b6fc", + "src/zabcanvas/models/scene_authoring_revision.py": "3cf20cc7b1f8e907be73757e22cac193fe2ed3bfe9ddc6addf5ff0e40554347e", + "src/zabcanvas/models/scene_definition.py": "80004e25932d71c3ca6e1caede3af96c411024eaef90dfb1b65d8ba023477011", + "src/zabcanvas/models/scene_revision_projection.py": "74ee4fa0c8143a28a970c63482b9d68027fc73e21557020d3e18a8f7f012f3ec", + "src/zabcanvas/models/scene_validation_record.py": "2cd7568cb14975b0a30145865702e4a62cb943e43156cb26325077df018810dd", + "src/zabcanvas/models/user_component.py": "847cc4c3943d2475b8bc8072c44f4ba23efceb1a8e52ebba7494ebecfcce1519", + "src/zabcanvas/observability/__init__.py": "f978b1c0719b75ffd16be6d49cc6ce958b1ae90f6cf812cff3c7f4ba30242e18", + "src/zabcanvas/observability/context.py": "3c9822e76efe6166d16cd30bfdd83c5baa2824efbd13e5095e4a7f0cd133f647", + "src/zabcanvas/observability/logging.py": "1950a0fb67c6f7a0f4df81b7a6d9aa4aea12d8d3daa804cac57e749bd6647658", + "src/zabcanvas/observability/middleware.py": "aa21682e18ad4cd98e621ba498e582f9cf0f367d1134da0a5e3c7bb8060f0fa5", + "src/zabcanvas/problem.py": "04b8ab5a9370d39ea1968ba24017a915fbe43bdb818b93bfc4d3b5e3687d9dd1", + "src/zabcanvas/routes/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "src/zabcanvas/routes/_authz.py": "3018f7793183847f0c5cd0761ae891c52923bbab211ad0ff2673d1fa258ae324", + "src/zabcanvas/routes/categories.py": "a694381716a18ed3c28ff6f0862be5e7955815231fe50e4f5b8ac3adb1e9b65f", + "src/zabcanvas/routes/collections.py": "a7503d2ee53e18ea0db7a24670ca0546b71a7e301003929fd5abc0f0ac753a83", + "src/zabcanvas/routes/composites.py": "1034c53d3992dd7f24c803aef38c00e3ab3c620e26001785b4b13a3a9f764629", + "src/zabcanvas/routes/health.py": "4fc74e96dbd3968c82c6676d1b7b7e5a26715e15dac93b925706d9ce4a439e84", + "src/zabcanvas/routes/internal.py": "6c91d90b552a6eb60d5f940efbe019456e73248c3c2b853207de07ad1e56ec8e", + "src/zabcanvas/routes/layouts.py": "e15b378de67fd99591ed24541e48a78ab4da18affae2246a8e84aeb16e922d51", + "src/zabcanvas/routes/lsml_bundles.py": "460b2f655a6cb770a65aa4a2a3f716120db53b90934db51ab1c83ad3f9a356dc", + "src/zabcanvas/routes/maintenance.py": "74be02e75c40fa7ffcc90cb02a5d05789c24abce962775e5997cd6b3fc16d401", + "src/zabcanvas/routes/overlays.py": "3b250da723ce0ce308f7d06479c9a96486bb7c561dde8a0aee96451734fee998", + "src/zabcanvas/routes/scene_assets.py": "e4728b8214e1e0843c33d05b706c0b1697bdd5dd29ea226f1286ae2cd0a34ff9", + "src/zabcanvas/routes/scenes.py": "97bf992699b8dd0ab0e0873abcf02a1bca985a11a0e1a2d80df8ef0c92f65c73", + "src/zabcanvas/routes/user_components.py": "fbdd05be19763439f96f2c73b1d13508cafbefe6432a28b9d3ef927fecc885c4", + "src/zabcanvas/schemas/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "src/zabcanvas/schemas/animation_asset.py": "41b2d8c7e8a608c1c998788f58c1333018ba5ad45cb9837af645274b594cd1ec", + "src/zabcanvas/schemas/blue_component.py": "5e3dae30599214f7c63dcc56d7b6393037bcd33216e18e1ca672d516a1736eaa", + "src/zabcanvas/schemas/category.py": "a6f1907c5d81e5387f3e44151e93aeae1fc2cba873dd809d4fcd0bc94a87fb5c", + "src/zabcanvas/schemas/collection.py": "21721ae6bcd1d964fc4faa9e3a7ea702d71f3368367d89b752716d8bd1b5e33a", + "src/zabcanvas/schemas/component.py": "551251278317dd3ba27c02a4e16af8caa4fde770625b5ac0897d3a251d01aec9", + "src/zabcanvas/schemas/composite.py": "0820469518bba8fe3ab6f95a35b6b8d0a97f6fe2956827cf5ec91579ac5cb59a", + "src/zabcanvas/schemas/lsml_bundle.py": "e79ae20d35322532fe299d98756fbf6d8cb37bdff0585b643fc53ef77f3de7b0", + "src/zabcanvas/schemas/overlay.py": "7804125fee56791ae66271e22073980cd886115118696041faa656f17b16719f", + "src/zabcanvas/schemas/resolve.py": "f6f6755b59f8ee62d3df9c83cb197e12b9fdddfa6c1656b95b3ab4c3b2202fce", + "src/zabcanvas/schemas/resolved_scene_ref.py": "37b5b7c2dda50f5048733e7d619727ea34b80554ba3a68a550201420b3199028", + "src/zabcanvas/schemas/roundtrip.py": "852550f331f3b47211240deceac8b32812b99ad35537e67bbb61fcb6db120ebb", + "src/zabcanvas/schemas/scene.py": "728dd6cb638f18d3bf342e0b8f9380336000f060e9497eb246ab93a0654b37a0", + "src/zabcanvas/schemas/scene_asset.py": "62a43ca671e5b7ee63f03bc6cdd6b28d87a903cdecff0b1694155688396c65d0", + "src/zabcanvas/schemas/scene_authoring.py": "6584c896e3cd39038352654f6ff87867290a07b35bac5bc0760d6465607d0211", + "src/zabcanvas/schemas/scene_definition.py": "d64064b3e5d85e1284130f10a32a947942f3d204a03431b08d08742d5e92a426", + "src/zabcanvas/schemas/scene_readiness_attestation.py": "0da94abf509bfc1b6640fb4b6fb15c1a37e5f6a03f874ab79b423fb1e28f3ad3", + "src/zabcanvas/schemas/scene_validation.py": "5e2ce2144a68a4e58519ef841ca57bdd37e333a3dcac9cca73a42c5404e450db", + "src/zabcanvas/schemas/user_component.py": "9e9fa08f04aa4419838b422f76a82d4529bc24b145504b7ecd29e5039ec63287", + "src/zabcanvas/services/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "src/zabcanvas/services/asset_blob_store.py": "f23706a5f73a1fb5378157c024080487a97257e78dfeda442dc747abc29bc8d8", + "src/zabcanvas/services/asset_orphan_sweep.py": "fbe47eed86ac562208a5fcd58a562006811f7739e6515426576d697f3b900b85", + "src/zabcanvas/services/asset_stream_ingest.py": "31a1c2bbf4217815f88d746b6088b364ce322562ed093cb5415e09117b7e4db9", + "src/zabcanvas/services/asset_validator.py": "20bfa8cb9dc0018142c6380d02c4c7eccf4b9cd198960a69e43789b0a543647f", + "src/zabcanvas/services/blob_scrub.py": "7eb7cc4e244c2b27d8aefbb506ed01db84558c6f60f6afcf59300cab2ec07ca5", + "src/zabcanvas/services/blue_client.py": "de5a9f01074b4dc851bcec1302a5349c12f93fd80de64a58e6ef626a8d1b17e1", + "src/zabcanvas/services/blue_component_catalog.py": "3889c47d63c1d1cf19d24c2c261796af7b561a8ba12530d4f46bfad916127b72", + "src/zabcanvas/services/category_service.py": "f75069f1e56794b7a6860cf4f0767618f67b1ef0d787028858dbb4e9a9fc4695", + "src/zabcanvas/services/collection_service.py": "e131ad9526f1b68968d31c33975ff262640117ea087897698e0cda690dca78f0", + "src/zabcanvas/services/composite_service.py": "82df564e7554b9bdc92a45b5a45154d21b047d833150bd5c4962e7df464b3e48", + "src/zabcanvas/services/content_hash.py": "73f96df096226190669386f3c2b3038e9108faad1c95e3e82aaa5b95ec592fe7", + "src/zabcanvas/services/contracts.py": "e490fc6130f4f7a13a3e7af88277779111f78499c7706fb7ede17da2b9570dcb", + "src/zabcanvas/services/db_catalog.py": "536c648b865cf2b8f97789def89924cdbc25dbd832a32c20f3e6c12583605083", + "src/zabcanvas/services/editable_scene_service.py": "f34a78a61046674f38fb46d7276ab8108b0484f0074f387d69d13455f9cbc699", + "src/zabcanvas/services/frozen_bundles.py": "79db0eeaeb0947394f8daf0c9fa269507bd790b07509e35450f1887fb995edb3", + "src/zabcanvas/services/gc_scheduler.py": "50ecc57abcf95a0ccf36655132297deeffa59adf214457723e2036bcba04a296", + "src/zabcanvas/services/jws_signing.py": "51fbc257f7c54487520968fd16ecafdabb3355b9898633070931c85ad2185bae", + "src/zabcanvas/services/layout_adapter.py": "fb4f6ff2c24581bf9435089d0c1d18a3a53f96eb9a80a3cf058eb0957d58f193", + "src/zabcanvas/services/lsml_bundle_service.py": "cbc52dec5eebcbbb4eb933ab3d2ffe3cc4ca59466f8ae6751c99bddd46f8a915", + "src/zabcanvas/services/maintenance_gc_service.py": "7aba6b116f8c24db12f698f195ceb067825ec2d71834ffa056b9918bc1aefe4d", + "src/zabcanvas/services/mirror_export_service.py": "649ea7af12122923f1bff43721e7dcb3506f4fcfc23f054402e52d758dbb0955", + "src/zabcanvas/services/operator_contract_service.py": "21795815c9737aaee3943781c20a6d0433aab8b5c7041f10cb11eee59a0e24e5", + "src/zabcanvas/services/operator_input_draft_service.py": "7e25ba8daf8efe917268df37ec5ebc99679f4a9b916ee5b7928026ab44c7769d", + "src/zabcanvas/services/outbound.py": "af5ba2626c208e8ed555d7e46607759c7fb0426191aa43aa22b8340e9605b52e", + "src/zabcanvas/services/overlay_service.py": "a6c3a52c4ba892e55e450849a54f15d51761957d678f57ac6c1efe7a24b41a86", + "src/zabcanvas/services/query_runner.py": "99dfa129054847476f8c80c41932bd1197fb0ade394e45b034d57cd87dbffd52", + "src/zabcanvas/services/readiness.py": "e0550a933fc98cde09047cd75e4606df5e9386da053d259382276e35e1d5e71e", + "src/zabcanvas/services/render_alert.py": "042235b8e6eacbf993f9d5a4882f71ed26f9409cb35bae0993e9eb1456e05e7d", + "src/zabcanvas/services/resolve_service.py": "d204a3336e916c98490658e0169179642088e12601277791799d9d12f8c0b3ff", + "src/zabcanvas/services/resolved_scene_ref.py": "57c7109809e273e2b3ab57d63af69230fec773b0ec1ea6d7770de256b3d40c18", + "src/zabcanvas/services/scene_access.py": "b896bd4e78f73c191b708589ce0cc4c98b6d676561cec8bbcb4794d5ee57f823", + "src/zabcanvas/services/scene_asset_service.py": "29dafda2a55ee16a01e9b3f86ddacbc45165c404aebba07e9249d9db53176ef0", + "src/zabcanvas/services/scene_authoring_service.py": "80bf1a27723cdb24f9d09d78ef51d080d6bda7262b69e8017899266b6f74aa9d", + "src/zabcanvas/services/scene_counter.py": "455774e34995f90cf09ffdff535eb817cb622ad4d3792ee426b6b9412b69d4fa", + "src/zabcanvas/services/scene_definition_service.py": "a349d64204c4a7d3fca24b3afdba32a33fe0fa0b0d745906c743162f5cb77714", + "src/zabcanvas/services/scene_errors.py": "2372103d432573e27ad1548538a94d94e1a8396e8d319b77ef8dbaef1b1bacbb", + "src/zabcanvas/services/scene_hard_purge_service.py": "c3f606e564a9e5dcf844eb94acc4e0cd081d4542caf416f4f649deb917e727bb", + "src/zabcanvas/services/scene_image_sweep.py": "7f3ff0f13eb61758757a54d7016604c0efa01d959681aec204475bf938ee45cc", + "src/zabcanvas/services/scene_lock.py": "bac6c42b10df61bba4797e9e840bc61cd2399bb3ca1ac57778b239f391dd4765", + "src/zabcanvas/services/scene_publish_service.py": "b599153c8dc4b0f085fd4df1ccb7ed68e8ab2b8febcecff1c020941254856cd9", + "src/zabcanvas/services/scene_readiness_attestation.py": "6b05c93cf8fec5258762dc823a2bdab877a12cf555b2270ce2603cf9c96eb750", + "src/zabcanvas/services/scene_render_service.py": "f51148755c7656cb2d59b91cf48d0cf3e0b6282c20023d5a6f8f9639ddbf26df", + "src/zabcanvas/services/scene_roundtrip_service.py": "59d31917fd8d5b0e8fe67510ee6625ac2c09b09a0a1a0a5ab230ad02537803aa", + "src/zabcanvas/services/scene_search.py": "e6e0357e0511b401f1e5bb081a548fb28df7393a26e7f10ee6f9f9c0a5b6a1d6", + "src/zabcanvas/services/scene_service.py": "195ab81dde9e77357287005a651c52922f4437b4523bef98d8c7ab42e22d5cd0", + "src/zabcanvas/services/scene_shape.py": "d03241fc01551f3dc6f2c5107675c8f9e1fd73cc67ca18ce4ec633dc6ffede62", + "src/zabcanvas/services/scene_tag_service.py": "f414faee6ce478e1ffca2c143df2d42f9d5a3d4425962d9492c484a0bcdd03b8", + "src/zabcanvas/services/scene_validation_service.py": "4619e4ac6d582c846a978eac0551e3c27f2bf1991d5e84bdec8d80790275a16f", + "src/zabcanvas/services/scene_verify_service.py": "4673c32fc7fae375c3ee513cc0e8b0aa60a9ea8ca5549c78b4c50a9c9cd3c0ab", + "src/zabcanvas/services/user_component_service.py": "42f157fd041a73c7135cc6ff39512ab6f16e09cde76744e650b7c3773ee0f5b1", + "src/zabcanvas/services/zabrender_client.py": "701793bf07bce29d8ab6148c8fbcc5ff6b7c9da60cf89cf4aeed0ea74cf0b744", + "tests/__init__.py": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "tests/_bundles.py": "617da498e93564ce7146167164a0526a795d8aeac28eeeb7877527f2c511271b", + "tests/conftest.py": "2f2c254c977152cc34022d22a290676735572ef808095bbeee5d8034f9da8fe2", + "tests/contract/README.md": "dfda43d9316386693224cdaf4110cdd2b824d7f4ba815f7c2227cc8e61870ce7", + "tests/contract/canvas_layout_contract_test.go": "0d097fff002587fac9eb2973cdf80315cabf660432881ccb350a0562b4d2d01d", + "tests/contract/canvas_layout_from_canvas.json": "0540c2cbc4a9c3cabb5d111d1f1a04973a23a057249d53bc29289c7799263c6b", + "tests/fixtures/README.md": "983127792588572a845ea57ddad3e42d956ac140813690eb9c9d98b2a46a23b1", + "tests/fixtures/adr_blue_012_r6_attested_authoring_fixture.json": "c105fc6a069b6d9563fb80a2ce775a5b60ec92cbb61c6b9878bf51f5799d3592", + "tests/integration/test_blue_wiring.py": "aebc38dfc472d39469b23349acb2b48d4fa7389cae30319490f7466da4ae3339", + "tests/integration/test_if_match_through_gateway.py": "567298bedd9756f74b38c9eed8b07a1932316840ffe2f268576666538a326d5a", + "tests/pg/conftest.py": "6abfc619e29db3d156db860cef5c72c1ab0e16361dc6c9a6e97ab1597dc74685", + "tests/pg/test_bundle_address_jsonb_fidelity.py": "ede8f80f71daa13185b2c9a1a9f560b7a291e697551f135c80a7df379d102448", + "tests/pg/test_canonical_number_roundtrip_pg.py": "46965efbf5ec33989b3a325ee6de2813acd9264bcbb6491e970cd92f84ce3989", + "tests/pg/test_collection_delete_cascade.py": "a9d1992a0bee377d2798bde2513c0f5d866c339288b5a21c1118c2df2a738410", + "tests/pg/test_gc_revision_purge_pg.py": "7c811ff85b0ec34c4f9fec1d562c4ed5e9412cfef0f559c1521eee956d32ade5", + "tests/pg/test_migration_data_fidelity.py": "06b5d0af2f88b721f4263be7d5ef7908ac8d5430c4886c78acbf650991fc2ea3", + "tests/pg/test_reseal_guard.py": "461286eabf297b25aa275e8cf1819e802faabaeb0449d743c60b38688fadb436", + "tests/pg/test_revision_pointer_restrict.py": "3743046d19a86ac6baf7235db354d3aac49b85dc82b6ef6aedc23ef8c5727009", + "tests/pg/test_scene_counter_trigger_parity.py": "01583bbd59e2aa4c3acdb0952e82faaa4ce89a08692c17cf9957ab632bc1c9c2", + "tests/pg/test_scene_delete_grace_pg.py": "9e94da5a88f1ec5aafc15315afca72822a1739070840e5ee3e3c112c2759de64", + "tests/pg/test_scene_graph_fts_index_pg.py": "85866f804a3fa559b292e06fd16cc7d70b16d6b8e21a2b5705e702787a2e8195", + "tests/pg/test_scene_lock_concurrency.py": "aae013a850f6df9e127b50665e360dfed5e45d9b818232edfe97f1ddb2960f4b", + "tests/pg/test_scene_revision_projections_migration.py": "70bab0a0edb540d24cd02bbc7f0242dc05e1daba6931e490581cc0624c123dae", + "tests/pg/test_scene_tags_pg.py": "8c58034e14f8691f89818d35a48fd964a0f3ad6fda5b402678a259c1429032b4", + "tests/pg/test_soft_delete_migration.py": "4f8f0d580a68c47c917a7330607a75abfd10704015ee8dd2e8d6eaa5b5ee43d7", + "tests/test_animation_asset.py": "0d5a6ffb4ec46e7f6b8679a997b6bb2a960e7b8c3aaf721831b9b2a5720fea3e", + "tests/test_animation_catalogue_store.py": "8953c9bf92c4ca0064f08885942012e0c680f4d4da2ff8b82ca5e7522915bf85", + "tests/test_asset_author_gate.py": "ea7bfcffbe2777094a6da04a294d614222ccb1b6ccf7bd934c5355d95646769d", + "tests/test_asset_storage_migration.py": "7c1db8f174c9abeda2b6c968d1aecb1cbb758ea9ce1f96dcdee672df5dd1338a", + "tests/test_authoring_revisions_api.py": "bff6c67dcdd91d01788d8ef7af01c64b3954bb5a6a8bbd836eeeaf8dd0a43e30", + "tests/test_authz_objects.py": "694190e72cf09a632b79cd6d1cb7bdd6535813047500b00aea8fcb36f59af8ba", + "tests/test_authz_role_refusal_log.py": "e8d71b2849a6e42f11b4aa278f3fc5037f075ebc56d42b248866c85eca2c86f1", + "tests/test_binding_status.py": "2f0223746d4bb61df7c909b8c6af6635489213c886cc6fd603ee1098989ae1e0", + "tests/test_blob_durability.py": "1baacd6d8cba733f265cc029da074b691df1622061be3f962a78a713bebb1139", + "tests/test_blob_orphan_sweep.py": "a4e50798dfe1aac86702ca1a3edc25683979e42f6b5d70cbcab385da51d090ed", + "tests/test_blob_scrub.py": "8499f5a819cc0c5bd07c3ffad2a1faabe3eceb7552fa50cbe894b7adaaa00f97", + "tests/test_bounded_and_revalidatable.py": "9e65375003785f55a16a0ad3f82cbd5bb942bf3e9105b3a5050074a859510ec3", + "tests/test_bundle_write_audit.py": "c2cfcb566a3d0b2339f7806e857cd65b616b8e43c197ec0097013898b0bf253a", + "tests/test_canonical_number_form.py": "e9be6309c4d21b2157e726df91b85a9a3f48854f83627a5b7592d3f76c9ce857", + "tests/test_canvas_chat_sponso_mockup_layout.py": "56e1e9e4515440b3727e2bb138dc207d95a0ec37a429824414a4cb3f91098b5b", + "tests/test_category_rejections.py": "684413c3fd5e5ed2d73d89227ccb61426b72aa30ccbb6afac9ce10d1610a26d2", + "tests/test_composite_chat_draft_scene_layout.py": "af55c08611593d4478e5117b73edcfb3e970e659f89382696f0c534333637db0", + "tests/test_composites.py": "002a48e202e668dd090c94c5d1d5d3c82e44b9dcafd76bc98023db73d8f29a5a", + "tests/test_config.py": "ccbd7e10a31e190e4c970bc1e71fe6adbb46ffe4acdc7eb18b27e23580475182", + "tests/test_contract_version.py": "ce955ef439098af85163187c61570e85b5b6db7172ea8bba1a8c7e323a384d29", + "tests/test_editable_scenes.py": "469eda2dff1ead5cec56d215fb7bdc3e200160780f587c15900a8478e6817c4f", + "tests/test_frozen_bundles.py": "328c281e6ac4341689aa075c16f8e59512aee62f3642233b30b18d635b4ac710", + "tests/test_gc_live_set_sources.py": "8a2e43a30939c548756740b5f7ab51d1f76fa7106d0c470db75434100e96275f", + "tests/test_gc_scheduler.py": "39180b721b5ca779f0a560e6b3ee89216520bb19d1e2a107fb20f22531e66fb9", + "tests/test_golive_delete_guards.py": "20e5fa5a271e23866225d5f5083dc44e3b488c5c56fe023662456476cfc2fd86", + "tests/test_health.py": "87b109d581a5ba17d20e0e36b023152ab32c7391c899752e3864b03cd0a4ac8e", + "tests/test_internal.py": "da861ee2af5d190d669b2cad43e5c7834028d1344f69f853161af0efa95bf9f2", + "tests/test_layouts.py": "752cf4134b1a28a40306a20d0a71b836959bb15bed7e1325ca19f7eb6e21a78f", + "tests/test_lifecycle_audit_log.py": "b196b1e08fc5c679af1d1b4de8877b69ad9d662c933efc2b39b8a0644a02cbdc", + "tests/test_limits_and_logs.py": "8c3ad0276c29f2b988eaedb61987762aeaf341b40dac38ff81ea2dddc66c9395", + "tests/test_live_scene_compile_equivalence.py": "b605b0cbed3e82bfeafc764f7e66e215ab33d268a0d1f9b0eb609d227aa55dc4", + "tests/test_lsml_bundle_hash_fk.py": "53612baeabd0c539be3cf14b7cba555d656343651767cb4ed38153fb1d256583", + "tests/test_lsml_bundles.py": "75575bb34dbed7aa524c281acc068ee2d168fa16b7b35c548b50d6ca9ea63deb", + "tests/test_m1_reactive_chat_layout.py": "84e1918a5c3c82fbbda2df729532cfa5d152ac40b2a085d957beeb45ce6b8682", + "tests/test_m2_live_data_layout.py": "29ed1b0d25dcccd5094c191bc9a1156df45c5a2034a376b3d094a7c0243dcd49", + "tests/test_m3_named_leaderboard_layout.py": "61d2918fd6337f4cc80b5d7258d68ba77c97141ab31f7d7852f95ca9b750de64", + "tests/test_m4_draft_switch_layout.py": "1e7cca1a66d773e1423f61ceedcb64832a70733fa36fbcc51062096ab6d4c220", + "tests/test_maintenance_gc.py": "09d102478fef3bf5a7e105e1d57360a4b6ff400843c2413b47f08df1a8db445b", + "tests/test_meet_peer_lsml_roundtrip.py": "3fdc3419bd555dee8aaae5423c5a4715a35fe123c6c72ce95e849602955118c9", + "tests/test_metrics.py": "aeed04e52d7bce02631cda4f2987e6eaeab5c06846b13764da8b53d9ca7cc7f1", + "tests/test_mirror_export.py": "c99d8d37d59d5125b5b6214fa042d40049f814845b3cda75732ff220ab654ebc", + "tests/test_observability.py": "ca83fd25a3a968b68937ef89f0f99b8e8bfe7f01542d3bb55ccb4c85b0321039", + "tests/test_operator_contract.py": "f0e596a2637238a26090c154a89dfb242830470783b178b04d6088fc935f68e3", + "tests/test_operator_inputs_draft.py": "6157802c686ecc7b8fde5506fbad9512c02caf77492543bd7d5b37f4befb93b0", + "tests/test_operator_read_bypass.py": "ef5d0c1a8a3754a8d24c43323b217d503cf242170f725d402851d533f2ffcfbc", + "tests/test_outbound_client.py": "338e8583f2cba4c8756fa578fb7b862eff4727d1f3d1acfb37f31f0447d9b5f0", + "tests/test_overlays_smoke.py": "2842b4c18d504708bdfa451fa70e855ab1b5db81135871087aa51977d0afa914", + "tests/test_ownerless_write_gate.py": "9313bd047e2d9b01108cc3b274f067bca50eab8b4e10da7c4cbe1c4bb7576b9d", + "tests/test_pg_fixture_isolation.py": "0eaae8931aef277ba08f0b1d4331efd8c451e4d717588fd1fcd83d348152752a", + "tests/test_primitives_log_harness_layout.py": "a2738a6fbb566288e4bec5d4270d5fc3531e2a62207af7e2ab26be23ebc8d236", + "tests/test_problem_catch_all.py": "e6324c0f0ce4042fc8bcb7a77704f66fb52dcae9c52d9e8a6f3c2ebe1ee61def", + "tests/test_problem_json.py": "c3412159838db88b90c4ca4178ae112410296e5af13c0b06cac62272d9fd7f4d", + "tests/test_production_runs_unoptimized_python.py": "bee0a38eab4de5660a556d29ac4355cd20554520d461e5796a330183bfd7904e", + "tests/test_projection_bundle_gate.py": "c80a67a8f5f4d3d75d4f9be83ecea989da5970a2efaad3c046083a7573b8e871", + "tests/test_publish_attested_fixture.py": "f710095d910d5eaf95f5510a6526a71280b50fdb9b246d616e34eb0794fc46b0", + "tests/test_quasar_finale_reactive_scene_layout.py": "17b54744d4f400db044ee1cd7fcd459abed722e6413dfa9bff3b2d387fb92d91", + "tests/test_query_catalog_drift.py": "8e84fed83c5aa7b6d2ff4ccf573400017edaccb220d163049fbe3aa2e5d62696", + "tests/test_query_runner.py": "d737666c4589f63e5a4f89cb7524168357ef990823205afcf046cb75ace287fa", + "tests/test_readiness.py": "17ba54e84e662672f1f77cbae57261884526382efc748d2f017b8f0c1644873e", + "tests/test_render_alert.py": "a68f86614dd7dfbed41876286d66d3bc11164e70b8128b5181c5c5116c3b9e96", + "tests/test_render_schema_migration.py": "7811b6d8fd634a6fdaea767a71148f617ce021bdf9e94b098a0bbe53ea4d006a", + "tests/test_rendered_bundle_provenance_contract.py": "01d3954029f575e08391dbc5f980ab8904e7b88a5cf1e146502074edc639b622", + "tests/test_rendered_png_persist.py": "70bd539b8e150ea8d335edaa268ce350ea56f13913526ad2e0f26a4294f384f5", + "tests/test_reseal_map.py": "8285c8e957bd8ac9845552c914a5432bcd8ec7703568ba9ec23a0a0d073142dd", + "tests/test_resolve.py": "b953f17c9a81d3869bea1442ff40211303f8170147a0378127352fa34aaa08d3", + "tests/test_resolve_service_coverage.py": "f4292e3246d9beccaf3936920a19e222068de56c70e6a3b1d45cd3a707905d90", + "tests/test_resolved_scene_ref.py": "37fa890d807186c885fc2b95dc19ae2d6f5fd4cf37d9351d19536f579f94e3ab", + "tests/test_resolved_scene_ref_locator_pattern.py": "8c144fb7776470628c9de42a94fdce72e63d856e89024cce3cabafd1dd381d01", + "tests/test_resolved_scene_ref_migration.py": "9324250f4e57a5ede10b89072c60f1933e867cbf6dcb8a6cc1890886c93cf326", + "tests/test_resolved_scene_ref_trust.py": "2034808ddacb5abe60d1f19f69e03ce1644ffb53dbe00012970d653c77529b17", + "tests/test_revision_definition_link.py": "95d94a09bd72720b1cc51bd887a1c53f13d26ec6b0e9dc0e1e21dcdc5b2a2a3c", + "tests/test_roundtrip_gate_preserved.py": "d2966067b35dcdfae5f598aea8d57343f02ba3f9a1ba345c31c4b793e0c2f499", + "tests/test_roundtrip_lossless_corpus.py": "6b4755c90ed39de3d1066eef49d3e655fba7a230e75db91574fb47d2fbc4ed65", + "tests/test_save_push_endpoints.py": "d465de5eee418311e5cd189854c11aeb232751fd66bc96cbd6f90f5af8c5291b", + "tests/test_scene_asset_bytes.py": "4d2016c6b1a487ee14c9d6f5a2022981fdd9328b803c7de7cb95ac8146469972", + "tests/test_scene_asset_file_storage.py": "e4ff444164bf099966a828d50c8310b019ab6885ea7210f49b4560eb2d9b7d52", + "tests/test_scene_asset_stream.py": "78db36919b611cb9399b063cb92cadec06f45010d787b06cb74faa719a5a4940", + "tests/test_scene_asset_video.py": "20304c3c3a58ce5f8782cf818fbb1624e18dbbc2af90b495be1000b5f5d6cfe2", + "tests/test_scene_assets.py": "095e9f060345699c7ab328867f7dbf01bdfcaec3cd3b827ff53bdf54a5ca7e09", + "tests/test_scene_authoring_revisions.py": "b0722463e780ef6ee864d07ae774b6e97bdb82e5ab0d2dc9551eeff6e78824d9", + "tests/test_scene_counter.py": "b533ee1500cdcc9ab81d4eeddf096c9b75dc5eb7f9eeb1571a0743856f61564a", + "tests/test_scene_custom_thumbnail.py": "bc286de55111b5e9e3dfbcaa3a9206e0bede0e736c54a44eb9b7438d824086ed", + "tests/test_scene_duplicate_export.py": "f7982b1da154483d95a712bd6b05ea68fe92bf5910cf66f66d7a2d32f23f1a77", + "tests/test_scene_filter_sql.py": "60c97ea80358466d10dccbeb5baa56f589ffac8de878b91c35b74557161fce49", + "tests/test_scene_if_match.py": "e3926e47f7f6e182fe3ea2d861165233fddbbcdce9476c776fc386c83e4ad224", + "tests/test_scene_list_visibility.py": "da56e8b7845f10f442b02e57fc86240d2d9dc0b35d1d404eceb16a779aa73bc0", + "tests/test_scene_owner_scoping.py": "863d75d7cc9e8a51b957703ee4a049aa1b070eb1a9d7267bd28ecdb73ad6acc2", + "tests/test_scene_pagination.py": "2b30bc38ffb1cd5a3626e1c74865fa9f6853ea595db96ce0a0da0dda0e98fc3c", + "tests/test_scene_publish.py": "aed715b7c8547800ebfc2d52d4f27fae8102fb96e61f47d329d7d0e9348f821d", + "tests/test_scene_render_assets.py": "9f74cbdfa75634832bcddbd05aae3429f87c04174a72e0ad8e81c346107ad378", + "tests/test_scene_render_job.py": "531d3adb0319d059f33c75ac7ff5e2b6f1b4d475c8522435befcf0af72d42613", + "tests/test_scene_render_schema.py": "84d755bba6968524bbeff440d2a14ff2564b89400e1385df200edbdc11b39cca", + "tests/test_scene_render_service_coverage.py": "3b0dc1b848f77e08fbcb5c4a391e7a7757077d5eb5dc7b4fbf00eb66c10a83d1", + "tests/test_scene_resolve.py": "0ff5afe483d99b7d0b72f23b6766b94ac0d81a7edc45ea425fb21a675bf71712", + "tests/test_scene_restore.py": "d880443c13965b9b5e240b808e95dc6be5e12fe32abbc16c0869a29a70131876", + "tests/test_scene_revision_pointers.py": "cc1d044b7d88d6d22a7e238bd203cc21d6ae39b36c1516d3218f716403dfdabd", + "tests/test_scene_roundtrip.py": "2796687c72d20238663beb824a3ab52ce91373278326065956326295be9d223b", + "tests/test_scene_save_authoring_revision.py": "6ee8967948454e6ee43fd5841b981642d716a50687cef005c8925c2e2399fffb", + "tests/test_scene_save_operator_inputs.py": "5d5b8938898f4ade84adf4bf606d8b62e1adc86f0863992532d9ced2d6ecb3fa", + "tests/test_scene_service_coverage.py": "67af11b0787bae4c7efbf37d7d2dbca9aa7455b3158bc4e303a6cee8abb54f3e", + "tests/test_scene_shape_gate.py": "ed35ce54ecb9b20c384598a8f3f200dce7f9bbaea36a9679bf0c2ef0db5b8d43", + "tests/test_scene_soft_delete.py": "dd96101885d8192635aaa3c1e7ec4efba79f631c36cfdf0d08fd2fff0a500f82", + "tests/test_scene_sorting.py": "18728c1ed77354450fe25a78471f565728e9cf56f465067151d5b2d0e1b11cb0", + "tests/test_scene_status_machine.py": "0b6c6da06147d416f23a6093fae43c5a4828d5e007fc919c284022cbc98f9bc5", + "tests/test_scene_tag_mutations.py": "87a235e74e0bcfa6686c4fc3162cd83b3798c477395d7e8b5e8f22b543de4364", + "tests/test_scene_thumbnail_cache.py": "d95349aa32e764bbbe956d48e093d28f54b1227fe7b33ee268794943746b9658", + "tests/test_scene_thumbnail_endpoint.py": "96f36c4c3b2af0e8c51e218049a5811461e3c357a646b0949c63a861baeadd67", + "tests/test_scene_validation_gate.py": "873a04079f5863516446f72cec30acdbb72ce96db30db94c107eac942fa04e7b", + "tests/test_scene_validation_records_migration.py": "b1cf5a1ad3cf42836ce583acc9b9f4e1b1981d599221959fd446e169133c851a", + "tests/test_scene_verify.py": "777ede740ad2c4024dd1d2ed86a5756341ed5b719edbf6abfebf03d4fae9d42f", + "tests/test_scene_verify_audit_scope.py": "116f0ad0cfcdd8113a1c61235193eaefba4141040ee0cadf512191c139fe10a2", + "tests/test_scene_verify_coverage.py": "719fab1b88a314cca0c6139eee18885a2fa6c4d87d43357688456b1320690503", + "tests/test_scene_verify_fk_contract.py": "5411f5e1a7c46f9f2c9409ac3917da94eecb39eed30c71e6d0c38c2b092307f4", + "tests/test_scene_version_atomicity.py": "ba27536bdfe9b9fe09884ff996b7a63e93f4a84199ce96abef0cedc3f6390ed6", + "tests/test_seed_scenes.py": "0a8bd3fd234c7894ce4038c46bb4eaf503e3e9d9af5bb0e4aabc6abd75b06af2", + "tests/test_soft_delete_migration.py": "5635c985a4b8df92f74b7e0d2d9bbec1008177fadf36dcb7ff3fdf99cde4f708", + "tests/test_surface_completeness.py": "01976de7993d04c8f2dc32a4f6f7da6425f4af65702a6e84f61781ccffbea996", + "tests/test_sweep_dead_image_src.py": "2066f1ab08c50203823eab18b860440114f6492b7f39b94433c4870d43ad21f0", + "tests/test_user_components_crud.py": "dc395dd85be3741fcbe758a3e0cd011c94218cba91e71b7c8f9d59d591c1d9ff", + "tests/test_version_verified_migration.py": "8c4f88ce5883efab8847e77f085987df7bc53d4d2791f96b2a423e605d66e59e", + "tests/test_volumetry.py": "5bdd12c57324d747f1849b97c8b258aa029a4723cc901156914674e29b8b85be", + "tests/test_zabrender_retry.py": "a49e74b39b4e2496a5c3eb70450487046be920d4357a6705606577c904dbcb0e", + "uv.lock": "967b668b246e84375ed2dc90e4f478862aeb170f4ab41ad42abd8c3691b23dfb" + } + } + }, + "bluePreparation": { + "sourceAssetsDeclarationsAndClosure": "PASS", + "assets": 7, + "programDigest": "sha256:ca2f21db4459f616c0f996fb7c03b3cf61e2a81d48bbc870d671f8fbda327bfc", + "validationAuthority": "ZabCanvas", + "executionAuthority": "Orion", + "businessOracleInOrion": false + }, + "coldStart": { + "result": "PASS_LOCAL_COLD_START", + "commandsDiscovered": 3, + "dispatches": 6, + "frames": 4, + "immutableCapsulesAndSelection": true, + "liveLSMLPersisted": false, + "allProcessesReaped": true, + "upstreamSourceReadsWhenOffline": 0 + }, + "cameras": { + "result": "PASS_REAL_PEER_CEF", + "frames": 10, + "allProcessesReaped": true, + "transport": "loopback Meet/WebRTC, separate physical camera publisher, two CEF viewers" + }, + "tests": { + "solar": 145, + "solarFiles": 18, + "orionRace": { + "testsAndSubtests": 1812, + "packages": 22, + "skips": [ + "TestRealNativeOperatorVisual", + "TestRealNativeSceneControlVisual", + "TestRealNativeBlueAnimation", + "TestRealNativeVisual", + "TestRealNativeProducers", + "TestRealSharedNative", + "TestRealNativeTransportRecovery", + "TestRealNativeBlueStructuralMutation", + "TestRealMarkerRuleLaunchAndLSMLIntent", + "TestPerf20k_CompileAndColdStartGate", + "TestPerf20k_SingleInputDeltaP95Gate" + ] + }, + "nativeActualOptIns": [ + "TestRealNativeProducers", + "TestRealSharedNative", + "TestRealNativeTransportRecovery", + "TestRealNativeBlueStructuralMutation" + ], + "canvasPassed": 1402, + "canvasSkipped": 85, + "canvasWarning": "pre-existing duplicate OpenAPI operation ID on asset GET/HEAD", + "finalValidationAdapterTests": "109 passed in 107.26s (0:01:47)", + "sizeGuards": "PASS_ALL_THREE" + }, + "installedPackage": { + "result": "PASS_NPM_INSTALL", + "files": 161, + "nativeBuild": "release Windows x64", + "nativeManifest": { + "schema": "solar.native-server.v1", + "wire": "LSDP-TCP/2.0-draft2", + "source_revision": "e695e14f9f664f430ffc2e0e75d083fab88abc7f", + "binaries": { + "win32-x64": { + "file": "win32-x64/lsdpd.exe", + "sha256": "7bd059c56b0daa56b3da24f0ccdb3b6a888709feff9618cd3b8cd2a045bdce60" + } + } + } + }, + "preserved": { + "Prism": { + "result": "PASS", + "head": "35de1404abb10fd9795cad2b898bfdc4844c5185", + "branch": "forge/local-20261004-shared-lsdp", + "trackedDiffSHA256": "79e64a20f1a00c52a7b7214b77d900da57b4dedf4ccef7573e4f36726b8b98ad", + "untrackedFiles": 16 + }, + "Lumencast": { + "result": "PASS", + "head": "e695e14f9f664f430ffc2e0e75d083fab88abc7f", + "branch": "codex/server-addons-20261004", + "trackedDiffSHA256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untrackedFiles": 0 + } + }, + "retainedIndependentProofs": { + "Marker": "historical actual process/window/native ACK/persisted intent/off proof; not a fresh rerun", + "sceneCompensation": "historical 11-frame CEF scenario; current runtime tests retained" + }, + "remainingApplicationCodeOwner": "Prism", + "prismIntegration": [ + "project the complete asset inventory including fonts into immutable LSML", + "own the packaged shared receiver lifecycle, origins, readiness, logout and shutdown", + "sync immutable Canvas sources/Blue capsules at startup in stable per-account storage", + "obtain and refresh admitted refs via the existing authorized workload broker flow", + "drive Program/Preview with desired scene-control LSML and observed renderer acknowledgements", + "consume Orion controls/stream-rule status and route viewer/capture capability declarations", + "qualify the complete installed application and upgrade lifecycle" + ], + "externalBoundaries": [ + "local changes uncommitted/unpublished; remote Canvas source route still 404 until deployment", + "operator denial on broker-only locator is expected, not evidence of missing artifacts", + "mTLS workload broker retrieval was not exercised", + "cached sources are immutable; execution leases expire normally, no indefinite offline authority", + "offline command dispatch/error reporting proved; offline database business success not claimed", + "camera proof is local WebRTC, no WAN/TURN or Linux release certification", + "PostgreSQL and configured external integration tests skipped; no schema migration introduced", + "no new long Pulsar soak, remote CI, production deployment or complete installed Prism E2E", + "workspace checks cover new owned evidence and preserved continuations, not historical global layout" + ], + "evidence": [ + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122602Z-canvas-cold-report.json", + "sha256": "dd51ae7f3ab5c4d4062392b03dfcd5b37625a774395330dd7e21d48b21d334da" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T122602Z-actual-vision-report.json", + "sha256": "3ac9463d6791b6d4b118f24ced16b9f93fcb372f7b9ffdbfbd9c39141fd77ee1" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-report.json", + "sha256": "fc3cdbd668dd31f2ca84e266dc1fa64c811ce8888c4ae744745a1ac0e77b4e40" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122602Z-canvas-cold-warm-program.png", + "sha256": "c469e0b104feae5bd1867bbb4c558084a4d283424022352503b387fd91df59ca" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122602Z-canvas-cold-warm-preview.png", + "sha256": "9dd96e4e296b35d8fd9d047d0558c33d6c6e75153bd38ad9f767069d3fdf8ce9" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122602Z-canvas-cold-cold-offline-program.png", + "sha256": "146eeb64d3910996f401c0a350e020873c983806fa40a303636492a1adccc8ea" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122602Z-canvas-cold-cold-offline-preview.png", + "sha256": "f9f262b431d3f2e71ad1fed7b64bce971926feddd8fafe71ebd4853053d0884a" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-joined-program.png", + "sha256": "eb42c0985d8cf9464fc011edd20923d262c92b63214e3271f7b6864d0de9c51f" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-joined-preview.png", + "sha256": "a017139ba20f3fcec5569e69ea5882d6ccd76bf558d4e863836bebb6d3e50a2e" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-left-program.png", + "sha256": "be82bf7453957f21d9e6b32abcc9ea3aca8ba92c3a3d7971e6a512ecc73f2cf5" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-left-preview.png", + "sha256": "be82bf7453957f21d9e6b32abcc9ea3aca8ba92c3a3d7971e6a512ecc73f2cf5" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-rejoined-program.png", + "sha256": "ed3e89aa235da7b1dd45120db9ac4996892b54a749a9c10949b6dfa7bb657e34" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-rejoined-preview.png", + "sha256": "756db9303dd7aa80797ea6c5c3df7d01c247e40e1dd23d6e546a814eb3937302" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-reassigned-program.png", + "sha256": "52df1e982b7acf2d4b7c1c81782245721b76d0155f997fc49213a704360e99e2" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-reassigned-preview.png", + "sha256": "561fc9b21bd3848b87420873aeed1f8e5c3a46484ee2a7f59993a2cde8ef9299" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-released-program.png", + "sha256": "bdb1a848f5c42512f99f350ab666052446b4398a6364f50bd84d8d3d24b7b505" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122814Z-final-peer-camera-released-preview.png", + "sha256": "be82bf7453957f21d9e6b32abcc9ea3aca8ba92c3a3d7971e6a512ecc73f2cf5" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122539Z-final-solar-tests.json", + "sha256": "8406beaa539351da13d0db2e0d81f8a0c206eb51ff7a0b344805dc289f03a0da" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T115858Z-race.jsonl", + "sha256": "1accc2f434a4d8a87fbc331d99ed38cbb4735ce0f0b7fe1873eca9fe5c08a5ab" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T122613Z-final-canvas-tests.log", + "sha256": "16157df3c3defff9269281f0e305a8a3dd6f4e373160f321adc30160001998b6" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T124235Z-final-canvas-all-lint.log", + "sha256": "a4443afdcfb6d7363adb285762515ccf7cf50473b1a05c20c1a50f6bed4d26b0" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T124503Z-final-canvas-types.log", + "sha256": "7a657fb734f82b9f4569eccbf4a96baf5946830493bf79a3ea42219cd083a5df" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122709Z-release-producers.log", + "sha256": "e961f4c40c0ee2a6909e044bc52c640dd1170f13bec3e771c5bc50c59f12f638" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T123800Z-final-chain-native-producers.log", + "sha256": "e14fabc6e04bf591b285e1d6b9a9e224b9f7e5596cff8df7da1f99d6193c3d28" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122957Z-release-recovery.log", + "sha256": "0cc54c60900f3a1da4547ea5c17309c92eaf87f65920a519db5dce7cc17d9c91" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T123027Z-final-release-installed.log", + "sha256": "333ac90609ad790067f5063dbab32ed36ca24745e1cb5a5bcd1999407ef39ab1" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122857Z-final-npm-install-report.json", + "sha256": "bc6d3284f6f8d729b79f569767fbb8be81464633d2e00160bab5ee13b800894d" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T122857Z-final-npm-install.log", + "sha256": "f91e98cb6d8bef26ca7cfc6db687adeef16ef7d27ed0d21866ffe4449f909ecb" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T121459Z-deployed-canvas-report.json", + "sha256": "10a18cad3be2f3767c711f39f18ec2da93a6e964804d24b9b532c52472add88a" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T123031Z-final-solar-checks.log", + "sha256": "8f3e1f9bf1795e9f4e0026237c794266635cf0e1bc99505797e8240e7e090c60" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T123336Z-final-go-vet.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T123336Z-final-staticcheck.log", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T123336Z-final-go-build-report.json", + "sha256": "bd1ac2b047ca0a8e129bc1b23514de0a0a6ac89abc9e1c0b011ed9cdc62ddb7c" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T123140Z-postgres-boundary.log", + "sha256": "88ccb13be465621ba3df2fcefc024eb880fa8456e46046fdb687edb81d272708" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T123301Z-other-skips-boundary.log", + "sha256": "d3f8a6633792e6cbd8b8f4a990c1580b88f0a66ae10d802b542c646b94bfe839" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T124201Z-validation-adapter-tests.log", + "sha256": "20745a0e8e5eeb96d2b94de2e41996cd1c456da98cb2028578083b83c03c7705" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T124306Z-final-size-guard.log", + "sha256": "169ffe657af12f6bd86de948e7daadbcf2b322e93829e51b6f4f704804bd53d4" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T124307Z-final-size-guard.log", + "sha256": "085f4c7af4a635e0d8f58c8df71b6d2b5c3229214e224d20fb21aa44b0623230" + }, + { + "path": "../../../ZabCanvas/.worktrees/forge-local-20261003-source-delivery/evidence/local-20261005-final-certification/forge/20261005T124235Z-final-size-guard.log", + "sha256": "8c748ee82404d140bdee33c8931c664e203ec176f1706b9554a9a65e73ca69a8" + }, + { + "path": "../../../Orion/.worktrees/eleven-local-20261003-source-delivery/evidence/local-20261005-coordinated-runtime/forge/20261005T104926Z-real-marker.log", + "sha256": "fa4facb83f9df625184784557feb9c93499488b1e21df4ba1e684a5d52a3f192" + }, + { + "path": "evidence/local-20261005-coordinated-runtime/forge/20261005T105635Z-final-control-cef-report.json", + "sha256": "bc1bff25179cefb28a958c5c83eda96fb51b2cd056fd5aa09ca797b1bd6abac3" + }, + { + "path": "evidence/local-20261005-final-certification/forge/20261005T115108Z-baseline.json", + "sha256": "d404010fe0f6eb2cbce87455396ad05b0434f57709cc9d16105e788002548952" + } + ] +} diff --git a/evidence/local-20261005-render-cadence/forge/20261005T165339Z-qualification.md b/evidence/local-20261005-render-cadence/forge/20261005T165339Z-qualification.md new file mode 100644 index 0000000..a79e6e9 --- /dev/null +++ b/evidence/local-20261005-render-cadence/forge/20261005T165339Z-qualification.md @@ -0,0 +1,38 @@ +# Qualification locale du rendu LSDP / Vision + +Verdict **PASS**. Scène réelle complète, caméra physique, serveur LSDP natif et Pulsar CEF. Le candidat Vision est local et intégré au package Solar. Aucun commit, push, merge ni modification Prism. + +512 transactions distinctes sont cadencées sur 2500 ms (charge imposée : 204,8/s). Chacune est reçue séquentiellement et vérifiée dans Solar, dans l’ordre, sans resynchronisation. Les valeurs scalaires peuvent partager une image ; 361 états distincts terminent une présentation sur la passe entière. + +| Fenêtre fixe depuis le début | ACK natifs | Mutations LSML reçues | États distincts terminés côté Solar/Vision | +|---|---:|---:|---:| +| 0–1000 ms | 205 | 204 | 137 | +| 1000–2000 ms | 205 | 204 | 144 | + +Dans les premiers 100 ms : **20 ACK natifs, 18 mutations LSML reçues, 9 états distincts terminés**. La cadence n’est pas uniforme à chaque intervalle de 100 ms ; le critère 120/s est appliqué à chaque fenêtre complète fixe de 1000 ms. + +Le fichier exporté contient **1,000 s, 60 images et 60 valeurs différentes**, toutes lisibles, strictement croissantes et identiques aux 60 images consécutives originales. Fenêtre choisie avant la capture : 1–2 s. Aucun compteur ajouté, aucune interpolation. Le compteur est le champ texte de la scène. Chaque image du fichier exporté a été relue indépendamment par OCR. + +![Extrait réel CEF : 60 images et 60 valeurs](D:\Documents\Zab\Artifacts\2026-10-05\lsdp-512-verified\20261005T165024Z-disjoint-final-paced-512-lsdp-cef-1s-60fps.mp4) + +[Enregistrement complet](D:\Documents\Zab\Artifacts\2026-10-05\lsdp-512-verified\20261005T165024Z-disjoint-final-paced-512-lsdp-cef.mp4). Le dernier état 512 apparaît vers 2520 ms, avec une incertitude d’alignement de ±26,2 ms. Les surfaces grises restent présentes sur toutes les images ; la caméra est conservée. Le LSML et le LSMLZ sources restent inchangés et les mutations RAM sont abandonnées après le test. + +Le coût médian des appels moteur est de 2,3 ms pour l’application Vision et 1,6 ms pour l’encodage/soumission du rendu. La latence médiane du dernier état envoyé au reçu de présentation couvrant vaut 17,72 ms. Ce ne sont ni des traces internes d’application du serveur natif, ni une mesure du scanout physique. + +Vision effectue 3 à 7 contributions de composition par image mesurée, au lieu de retomber à 177 lorsque caméra et texte changent ensemble. 75 images utilisent deux zones distinctes ; les pixels situés entre ces zones restent conservés. 40 comparaisons GPU exactes avec le nouveau cache désactivé dans l’oracle couvrent le résultat et l’invalidation. + +| Critère | Risque | Commande / preuve | +|---|---|---| +| 512 mutations reçues, ordre et hashes | perte ou confusion de reçus | Reproduction native/CEF ; runtime PASS, 512/512 et zéro resync | +| Au moins 120 états rendus/s | compter caméra, LSML ou acquittements fusionnés comme images | analyse-render-cadence.py : 137 puis 144 dans les fenêtres fixes | +| Contenu vidéo à 60 fps | répétition, texte illisible, surfaces absentes | analyse-native-counter.py puis verify-counter-clip.py : 60 valeurs/60 images, zéro trou | +| Pixels du compositeur | changement de blend, clip, bord ou déplacement | GPU requis : 40 sorties exactes comparées au compositeur sans nouveau cache | +| Commandes et cycle de vie | barrières ou médias périmés | Solar : 192 tests / 26 fichiers ; lint, types, build et 35/35 modules accessibles | +| Source Vision et package | WASM périmé, provenance ambiguë | pins source/binaire comparés avec le build Vision et les deux copies Solar | +| Préservation | modification Prism ou source persistée | baselines Git identiques ; LSML/LSMLZ identiques ; processus propres récolés | + +Vision : **144 tests Rust**, **13 tests JavaScript**, fmt/clippy et WASM release réussis. Deux tests opt-in de polices externes sont ignorés comme précédemment ; la vraie scène et ses polices sont cependant exercées par CEF. Les contrôles de build/pins/binaire natif réussissent. + +[Rapport JSON et hashes](D:\Documents\Zab\Solar\.worktrees\forge-local-20261003-source-resolution\evidence\local-20261005-render-cadence\forge\20261005T165339Z-qualification.json). [Preuves propres à Vision](D:\Documents\Lumencast\lumencast-vision\.worktrees\eleven-local-20261004-solar-surface-parity\evidence\local-20261005-render-cadence\eleven\20261005T165339Z-qualification.json). + +La preuve concerne cette scène, ce matériel et ce profil. Le débit natif interne n’est pas déduit de la cadence imposée. La rafale immédiate historique de 512 transactions n’est pas requalifiée ici ; elle avait saturé l’abonnement natif. Les limites de mémoire et les chemins complets du compositeur sont conservés. La documentation de reproduction et des frontières se trouve dans [render-cadence.md](D:\Documents\Zab\Solar\.worktrees\forge-local-20261003-source-resolution\docs\development\render-cadence.md). diff --git a/evidence/local-20261005-solar-audit/forge/20261005T183100Z-historical-certify-chain.py b/evidence/local-20261005-solar-audit/forge/20261005T183100Z-historical-certify-chain.py new file mode 100644 index 0000000..d5e78fb --- /dev/null +++ b/evidence/local-20261005-solar-audit/forge/20261005T183100Z-historical-certify-chain.py @@ -0,0 +1,252 @@ +"""Reconcile existing local proofs; never start, publish or modify a runtime.""" +import argparse +import base64 +import datetime +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess + +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--orion", required=True) +parser.add_argument("--canvas", required=True) +parser.add_argument("--pulsar", required=True) +parser.add_argument("--output", required=True, help="Canonical evidence prefix inside this Solar worktree") +args = parser.parse_args() +solar = Path(__file__).resolve().parent.parent +orion, canvas = Path(args.orion).resolve(), Path(args.canvas).resolve() +prefix = Path(args.output).resolve() +relative = Path("evidence/local-20261005-final-certification/forge") +assert prefix.parent == solar / relative +se, oe, ce = solar / relative, orion / relative, canvas / relative +proofs = [] + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def read(path): + return json.loads(Path(path).read_text(encoding="utf-8-sig")) + + +def latest(root, pattern): + paths = sorted(root.glob(pattern)) + assert paths, "Missing proof: " + pattern + path = paths[-1] + proofs.append(path) + return path + + +def git(root, *arguments): + return subprocess.check_output(["git", "-C", str(root), *arguments], stderr=subprocess.DEVNULL) + + +def snapshot(expected): + root = Path(expected["root"]) + return { + "root": expected["root"], + "head": git(root, "rev-parse", "HEAD").decode().strip(), + "branch": git(root, "branch", "--show-current").decode().strip(), + "status": git(root, "status", "--porcelain=v1").decode(), + "trackedDiffSHA256": hashlib.sha256(git(root, "diff", "--binary", "HEAD")).hexdigest(), + "untracked": {name: sha(root / name) for name in + git(root, "ls-files", "--others", "--exclude-standard", "-z").decode().split("\0") if name}, + } + + +baseline_path = se / "20261005T115108Z-baseline.json" +baseline = read(baseline_path) +preserved = {} +for name in ["Prism", "Lumencast"]: + expected = baseline["repositories"][name] + current = snapshot(expected) + assert current == expected, name + " changed since baseline" + preserved[name] = {"result": "PASS", "head": current["head"], "branch": current["branch"], + "trackedDiffSHA256": current["trackedDiffSHA256"], "untrackedFiles": len(current["untracked"])} +for name, root in [("Solar", solar), ("Orion", orion), ("ZabCanvas", canvas)]: + expected = baseline["repositories"][name] + assert git(root, "rev-parse", "HEAD").decode().strip() == expected["head"] + assert git(root, "branch", "--show-current").decode().strip() == expected["branch"] + assert Path(git(root, "rev-parse", "--show-toplevel").decode().strip()) == root + +cold_path = latest(se, "*-canvas-cold-report.json") +cold = read(cold_path) +assert cold["result"] == "PASS_LOCAL_COLD_START" and cold["sourceUnchanged"] +assert not cold["mutatedLSMLSaved"] and not cold["broadcast"] +assert len(cold["phases"]) == 2 and len(cold["frames"]) == 4 +warm, offline = cold["phases"] +assert warm["disk"] == offline["disk"] and offline["stats"]["upstreamSourceReads"] == 0 +assert all(phase["allProcessesReaped"] for phase in cold["phases"]) +assert all(warm["pids"][name] != offline["pids"][name] for name in warm["pids"]) +producer_path = ce / cold_path.name.replace("-canvas-cold-report.json", "-actual-vision-report.json") +producer = read(producer_path) +proofs.append(producer_path) +assert producer["result"] == "PASS_ACTUAL_CANVAS_VISION_ADMISSION" +assert not producer["seededValidation"] and not producer["renderBundleRequired"] +assert producer["assets"] == 7 and producer["bindingClosure"] +artifacts = Path(producer["producerArtifacts"]) +assert sha(artifacts / "capsule.json") == cold["canvasCapsuleSHA256"] +assert sha(artifacts / "scene.lsmlz") == producer["archiveSHA256"] +capsule = read(artifacts / "capsule.json")["envelope"] +assert capsule["blue_manifest"]["readiness"]["offline_ready"] +assert capsule["blue_manifest"]["scene_version"] == producer["sourceVersion"] +program = json.loads(base64.b64decode(capsule["blue_program"])) +calls = sorted(entry["id"] for entry in program["entrypoints"] if entry["kind"] == "call") +for lane in ["program", "preview"]: + assert sorted(call["entrypoint"] for call in cold["commands"] if call["lane"] == lane) == calls +assert offline["execution"]["dispatches"] == len(cold["commands"]) +assert offline["execution"]["remoteQueryErrors"] >= len(cold["commands"]) +assert not offline["execution"]["externalSuccessClaimed"] +for name, digest in cold["runtimeArtifacts"].items(): + path = {"orion/executable": orion / "build/orion-consolidated.exe", "pulsar/executable": Path(args.pulsar), + "proof/host": solar / "scripts/proof/cold-start-host.mjs", + "proof/driver": solar / "scripts/prove-cold-start-cef.py"}.get(name) + if name.startswith("solar/dist/"): + path = solar / name.removeprefix("solar/") + assert path is not None and sha(path) == digest, "Runtime drift: " + name + +peer_path = latest(se, "*-final-peer-camera-report.json") +peer = read(peer_path) +assert peer["result"] == "PASS_REAL_PEER_CEF" and peer["sourceUnchanged"] and peer["allProcessesReaped"] +assert len(peer["frames"]) == 10 and not peer["mutatedLSMLSaved"] and not peer["broadcast"] +for frame in cold["frames"] + peer["frames"]: + assert sha(frame["path"]) == frame["sha256"] + proofs.append(Path(frame["path"])) + +unit_path = latest(se, "*-final-solar-tests.json") +unit = read(unit_path) +assert unit["success"] and unit["numFailedTests"] == 0 +race_path = latest(oe, "*-race.jsonl") +events = [json.loads(line) for line in race_path.read_text(encoding="utf-8-sig").splitlines() + if line.startswith("{")] +assert not any(event["Action"] == "fail" for event in events) +go_counts = {"testsAndSubtests": sum(event["Action"] == "pass" and "Test" in event for event in events), + "packages": sum(event["Action"] == "pass" and "Test" not in event for event in events), + "skips": [event["Test"] for event in events if event["Action"] == "skip" and "Test" in event]} +assert go_counts["packages"] == 22 +assert any(event["Action"] == "pass" and event.get("Test") == + "TestOperatorExecutionDiscoversEveryCommandAcrossScopes" for event in events) +canvas_tests = latest(ce, "*-final-canvas-tests.log") +test_log = canvas_tests.read_text(encoding="utf-8-sig") +match = re.search(r"(\d+) passed, (\d+) skipped", test_log) +assert match and " failed" not in test_log +for pattern in ["*-final-canvas-all-lint.log", "*-final-canvas-types.log"]: + text = latest(ce, pattern).read_text(encoding="utf-8-sig") + assert "All checks passed!" in text or "Success: no issues found" in text + +native_path = latest(se, "*-release-producers.log") +native_text = native_path.read_text(encoding="utf-8-sig") +native_tests = ["TestRealNativeProducers", "TestRealSharedNative", + "TestRealNativeTransportRecovery", "TestRealNativeBlueStructuralMutation"] +for name in native_tests: + assert "--- PASS: " + name + " " in native_text +assert json.loads(native_text.splitlines()[-1])["originalUnchanged"] +native_owner_path = oe / (prefix.name + "-native-producers.log") +native_owner_path.write_text("\n".join(native_text.splitlines()[:-1]) + "\n", encoding="utf-8") +proofs.append(native_owner_path) +for pattern in ["*-release-recovery.log", "*-final-release-installed.log"]: + receipt = json.loads(latest(se, pattern).read_text(encoding="utf-8-sig").splitlines()[-1]) + assert receipt["result"] == "PASS" +installed = read(latest(se, "*-final-npm-install-report.json")) +assert installed["result"] == "PASS_NPM_INSTALL" +for name, digest in installed["distHashes"].items(): + assert sha(solar / "dist" / name) == digest + assert sha(Path(installed["installedRoot"]) / "dist" / name) == digest +postinstall_path = se / latest(se, "*-final-npm-install-report.json").name.replace("-report.json", ".log") +proofs.append(postinstall_path) +assert "> node scripts/patch-lumencast-protocol.mjs" in postinstall_path.read_text(encoding="utf-8-sig") +manifest = read(solar / "dist/native/manifest.json") +binary = solar / "dist/native" / manifest["binaries"]["win32-x64"]["file"] +assert sha(binary) == manifest["binaries"]["win32-x64"]["sha256"] +assert manifest["source_revision"] == baseline["repositories"]["Lumencast"]["head"] + +remote_path = latest(se, "*-deployed-canvas-report.json") +remote = read(remote_path) +assert remote["reference"]["signatureVerified"] +assert remote["brokerBoundary"]["operatorDereferenceDenied"] +assert remote["boundaries"] == ["CANVAS_SOURCE_HTTP_404"] +latest(se, "*-final-solar-checks.log") +for pattern in ["*-final-go-vet.log", "*-final-staticcheck.log", "*-final-go-build-report.json"]: + latest(oe, pattern) +for pattern in ["*-postgres-boundary.log", "*-other-skips-boundary.log"]: + latest(ce, pattern) +adapter_tests = latest(ce, "*-validation-adapter-tests.log").read_text(encoding="utf-8-sig") +assert " passed" in adapter_tests and " failed" not in adapter_tests +for owner in [solar, orion, canvas]: + guard = latest(owner / relative, "*-final-size-guard.log").read_text(encoding="utf-8-sig") + assert "no growth" in guard +historical_marker = orion / "evidence/local-20261005-coordinated-runtime/forge/20261005T104926Z-real-marker.log" +assert "--- PASS: TestRealMarkerRuleLaunchAndLSMLIntent" in historical_marker.read_text(encoding="utf-8-sig") +proofs.append(historical_marker) +historical_control = solar / "evidence/local-20261005-coordinated-runtime/forge/20261005T105635Z-final-control-cef-report.json" +assert read(historical_control)["result"] == "PASS" +proofs.append(historical_control) +proofs.append(baseline_path) + +candidates = {} +for name, owner in [("Solar", solar), ("Orion", orion), ("ZabCanvas", canvas)]: + names = git(owner, "ls-files", "--cached", "--others", "--exclude-standard", "-z").decode().split("\0") + files = {name: sha(owner / name) for name in names if name and not name.startswith("evidence/") + and (owner / name).is_file()} + candidates[name] = {"head": git(owner, "rev-parse", "HEAD").decode().strip(), + "branch": git(owner, "branch", "--show-current").decode().strip(), "files": files} + +report = { + "stamp": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "result": "PASS_LOCAL_READY_FOR_PRISM_INTEGRATION", + "authority": baseline["authority"], "producerAdmission": producer["result"], + "candidates": candidates, + "bluePreparation": {"sourceAssetsDeclarationsAndClosure": "PASS", "assets": producer["assets"], + "programDigest": producer["programDigest"], "validationAuthority": "ZabCanvas", + "executionAuthority": "Orion", "businessOracleInOrion": False}, + "coldStart": {"result": cold["result"], "commandsDiscovered": len(calls), "dispatches": len(cold["commands"]), + "frames": 4, "immutableCapsulesAndSelection": True, "liveLSMLPersisted": False, + "allProcessesReaped": True, "upstreamSourceReadsWhenOffline": 0}, + "cameras": {"result": peer["result"], "frames": 10, "allProcessesReaped": True, + "transport": "loopback Meet/WebRTC, separate physical camera publisher, two CEF viewers"}, + "tests": {"solar": unit["numPassedTests"], "solarFiles": len(unit["testResults"]), + "orionRace": go_counts, "nativeActualOptIns": native_tests, + "canvasPassed": int(match[1]), "canvasSkipped": int(match[2]), + "canvasWarning": "pre-existing duplicate OpenAPI operation ID on asset GET/HEAD", + "finalValidationAdapterTests": adapter_tests.splitlines()[-1], "sizeGuards": "PASS_ALL_THREE"}, + "installedPackage": {"result": installed["result"], "files": installed["files"], + "nativeBuild": "release Windows x64", "nativeManifest": manifest}, + "preserved": preserved, + "retainedIndependentProofs": { + "Marker": "historical actual process/window/native ACK/persisted intent/off proof; not a fresh rerun", + "sceneCompensation": "historical 11-frame CEF scenario; current runtime tests retained", + }, + "remainingApplicationCodeOwner": "Prism", + "prismIntegration": [ + "project the complete asset inventory including fonts into immutable LSML", + "own the packaged shared receiver lifecycle, origins, readiness, logout and shutdown", + "sync immutable Canvas sources/Blue capsules at startup in stable per-account storage", + "obtain and refresh admitted refs via the existing authorized workload broker flow", + "drive Program/Preview with desired scene-control LSML and observed renderer acknowledgements", + "consume Orion controls/stream-rule status and route viewer/capture capability declarations", + "qualify the complete installed application and upgrade lifecycle", + ], + "externalBoundaries": [ + "local changes uncommitted/unpublished; remote Canvas source route still 404 until deployment", + "operator denial on broker-only locator is expected, not evidence of missing artifacts", + "mTLS workload broker retrieval was not exercised", + "cached sources are immutable; execution leases expire normally, no indefinite offline authority", + "offline command dispatch/error reporting proved; offline database business success not claimed", + "camera proof is local WebRTC, no WAN/TURN or Linux release certification", + "PostgreSQL and configured external integration tests skipped; no schema migration introduced", + "no new long Pulsar soak, remote CI, production deployment or complete installed Prism E2E", + "workspace checks cover new owned evidence and preserved continuations, not historical global layout", + ], +} +for owner in [solar, orion, canvas]: + owned = dict(report) + owned["evidence"] = [{"path": Path(os.path.relpath(path, owner)).as_posix(), "sha256": sha(path)} + for path in dict.fromkeys(proofs)] + destination = owner / relative / (prefix.name + "-certificate.json") + destination.write_text(json.dumps(owned, indent=2) + "\n", encoding="utf-8") +print(json.dumps({"result": report["result"], "solarTests": unit["numPassedTests"], + "orionRace": go_counts["testsAndSubtests"], "canvasTests": int(match[1]), + "preserved": list(preserved)})) diff --git a/evidence/local-20261005-solar-audit/forge/20261005T184644Z-audit.md b/evidence/local-20261005-solar-audit/forge/20261005T184644Z-audit.md new file mode 100644 index 0000000..8cfbff1 --- /dev/null +++ b/evidence/local-20261005-solar-audit/forge/20261005T184644Z-audit.md @@ -0,0 +1,111 @@ +# Audit Solar — maintenance, structure et documentation + +Audit local du 5 octobre 2026, exécuté directement par Eleven, sans délégation ni ADR. +Le candidat Solar reste dans `forge/local-20261003-source-resolution`, HEAD +`537b606f9ecf97b5e0ce0f5bd80e88350f0823d5`. +Les modifications sont locales et non committées. Le HEAD exact fait foi dans +le [snapshot du candidat](20261005T184644Z-candidate.json). + +## Verdict local + +**PASS_SOLAR_LOCAL** : aucun module produit inaccessible, export inutilisé ou dépendance +produit sans usage détecté. Les 35 fichiers de code sont atteints depuis les entrées +publiques/HTML et rattachés à leur propriétaire. La documentation courante est cohérente +avec ces contrats et les commandes actives. Le contrôle physique global du workspace +est consigné séparément de ce verdict de code. + +## Reliquats supprimés + +- L'ancien postinstall de 1 907 lignes et son second patcher ont été remplacés par + l'entrée WebRTC réellement consommée et quatre modules responsables : registry, + viewer, rooms et roster. Les patches du protocole, du montage React et des bundles + compilés ne sont plus appliqués. Les trois fichiers WebRTC utiles sont **identiques + octet pour octet** à ceux de l'installation précédente ; une deuxième passe ne + modifie rien. +- `mutateDocument`, doublon de mutation conservé uniquement par ses tests, a été retiré. + Ses scénarios utilisent maintenant le chemin réel `NativeState.patch`, avec les + mêmes assertions d'atomicité, d'échappement, d'erreur et de préservation de la source. +- `CAM_SLOTS_PREFIX`, son commentaire orphelin et l'assertion portant uniquement sur + cette constante ont été retirés. Les tests d'affectation/libération de slots restent. +- Les options ignorées `MountOptions.scene` et `testSession`, ainsi que leur collecte + dans les entrées HTML, ont été retirées. La sélection effective reste celle du + document natif et de `nativeLSDP.selector`. +- Les quatre générateurs de phase anciens sont archivés dans ce dossier, hors des + outils actifs. Leurs bytes et leur rôle dans les certificats précédents sont conservés. + Le runbook ne les présente plus comme des commandes courantes. + +## Structure et documentation + +Les frontières restent celles des contrats : `scenes`, `internal`, `engine`, `sources`, +`peer-viewer` et `server`. Le [document d'architecture](../../../docs/development/architecture.md) +explique les entrées, l'autorité d'état, les dépendances et les points d'extension ; +le [code map dérivé](../../../docs/development/code-map.md) associe chaque fichier et +symbole à ses imports, consommateurs, tests et README propriétaire. + +Le graphe suit les imports/reexports et l'appel réel du worker. Les tests ne peuvent +pas retenir un module produit orphelin. Les exports publics sont des contrats explicitement +documentés ; les helpers utilisés dans leur propre module conservent cet usage réel. +Les six dépendances produit sont consommées. Les symboles locaux inutilisés, paramètres, +labels et instructions inaccessibles sont également contrôlés par TypeScript/eslint. + +Le catalogue des 43 outils distingue les commandes actives, leurs helpers et les archives. +Les docs API, cache, réception native, caméras, cadence, qualification et release ont été +réconciliées avec le moteur Vision et le format réellement packagé. Les références +obsolètes et l'état de publication Vision ont été corrigés. Les caches Python sont exclus +des fichiers produit. Le formatter préserve également les preuves historiques, +l'index dérivé, le lockfile et les bytes amont qualifiés grâce à `.prettierignore`. + +## Matrice de preuves + +| Critère | Résultat observé | Preuve | +|---|---|---| +| Usage du code | 35/35 fichiers atteints ; zéro module, export ou dépendance inutilisé ; zéro référence locale cassée | [Graphe AST](20261005T184644Z-source-usage.json) | +| Documentation courante | 23 documents, 111 liens locaux, 88 mentions de commandes npm, 113 chemins, 51 références d'outils ; zéro défaut | [Rapport documentaire](20261005T184644Z-documentation.json) | +| Détection des régressions | 5 tests négatifs réussis : orphelins/exports test-only, worker, dépendances, liens/commandes/outils, frontières historiques et externes | [Tests des gardes](20261005T184644Z-architecture-tests.log) | +| Fonctionnement Solar | 191 tests, 26 fichiers, zéro échec ou test en attente | [Résultats unitaires](20261005T184644Z-unit-test-results.json) | +| Typage et qualité | Lint et TypeScript réussis, y compris entrées host/dev ; garde de taille réussie et auto-tests réussis | [Synthèse des validations](20261005T184644Z-validation.json) | +| Build et moteur | Build réussi ; 3 chunks host autonomes ; 5 assets Vision sans doublon ; les 4 pins moteur correspondent | [Contrôle du bundle](20261005T184644Z-bundle.log) | +| Installation WebRTC | Archives amont vérifiées contre le lockfile ; seulement 3 fichiers utiles changés ; sorties qualifiées identiques et idempotentes | [Comparaison des installers](20261005T182900Z-viewer-installer.json) | +| Paquet réel | Tarball extrait dans une installation neuve ; postinstall exécuté deux fois ; API ESM et types présents ; serveur/binaire Windows présents ; cache LSML/LSMLZ assets/Blue restauré | [Paquet et hashes](20261005T184644Z-package.json), [driver exécuté](20261005T184644Z-verify-package.mjs) | +| Périmètre préservé | HEAD, index, diff et status de Solar canonique, Prism, Orion, LSDP natif et Vision canonique identiques au baseline | [Comparatif de préservation](20261005T184644Z-preservation.json) | + +Les workflows existants PR/main et release exécutent maintenant `check:architecture` +et `test:architecture`. Leurs fichiers ont été contrôlés localement ; aucune CI distante +n'est annoncée verte pour ce candidat non publié. + +## Limites et conservation + +Ce contrôle dérive les usages statiques, les entrées publiques, imports dynamiques littéraux +et worker URLs. Il ne promet pas une preuve universelle de tous les comportements opaques +ou des internes des moteurs amont. Les liens et commandes explicitement d'autres dépôts, +les endpoints distants et les ADR/preuves historiques ne sont pas des documents Solar courants. +Les erreurs admises par les gardes sont exercées par leurs tests négatifs. + +Les bytes des quatre assets Vision et des modules WebRTC consommés restent ceux du candidat +déjà qualifié en CEF. Aucun nouveau test physique CEF n'a été revendiqué lors de cet audit ; +les preuves de caméra et de cadence conservent leur frontière initiale. Le cache packagé +est un test de contrat synthétique, pas une nouvelle validation de scène par ZabCanvas. +Le binaire natif Windows repackagé est inchangé ; aucune release Linux n'a été qualifiée ici. + +Le worktree existant et les preuves précédentes sont conservés pour la revue de la migration. +Il n'y a eu ni commit, push, merge, publication, déploiement ni modification de Prism. + + +## Clôture physique du workspace + +Le [garde canonique](../../../../../../agents/_shared/zab-workspace-guard.ps1) a terminé +sans modification de ses règles. Ses contrôles de présence dans HEAD ont été regroupés +et vérifiés avec les vrais objets Git pour 76 worktrees et 70 révisions distinctes ; +les ACL, branches, chemins et contrôles de preuves restent ceux du script canonique. +Tous ces HEAD sont restés identiques après le contrôle. + +Son verdict est **BLOCKED_PREEXISTING_LAYOUT** : 9 251 écarts de classement dans le +workspace, dont 70 anciens chemins de preuves dans ce worktree Solar. Aucun chemin +créé par cet audit n'est en défaut. Exemples globaux : `.pnpm-store` à la racine, +noms de worktrees anciens et preuves conservées sous le rôle `eleven`. +Voir [le résultat et la méthode](20261005T184644Z-workspace-layout.json). + +La clôture administrative globale reste donc bloquée. Déplacer des preuves ou des +worktrees d'autres campagnes toucherait des continuations actives, dont Prism/Orion, +et dépasse l'autorité de cet audit Solar. Ces fichiers et les références historiques +ont été préservés ; aucune correction de règle ou exception n'a masqué ces défauts. diff --git a/evidence/local-20261007-vision-integration/eleven/20261007T201000Z-authoring-report.json b/evidence/local-20261007-vision-integration/eleven/20261007T201000Z-authoring-report.json new file mode 100644 index 0000000..529b7b5 --- /dev/null +++ b/evidence/local-20261007-vision-integration/eleven/20261007T201000Z-authoring-report.json @@ -0,0 +1,48 @@ +{ + "role": "Eleven", + "thread": "01a116c8-c342-7e62-b783-59953e085982", + "workUnit": "local-20261007-vision-integration", + "scope": "Loopback local authoring source verification; immutable base LSML/assets; Vision private geometry and style projection", + "validation": "194 tests / 28 files; typecheck, lint, build and architecture checks PASS", + "integrationProof": { + "result": "PASS", + "scene": "local-authoring-proof", + "sceneVersion": "sha256:3acec2f28b594702bdd81f15d17a54c7ee9de08e08624f3bb16cd8f67d20eec1", + "source": "actual Prism sceneToEditableLsml; isolated authored shape/text fixture", + "orion": "actual rebuilt embedded Orion; source-only open and sequenced HTTP patch", + "native": "actual packaged Rust receiver", + "renderer": "actual Solar host and Vision WASM; headless Chromium SwiftShader", + "sourceReads": 1, + "programUntouched": true, + "immutableSourcePreserved": true, + "errors": [], + "limits": "Isolated authored fixture; no real saved Canvas scene, Blue admission, CEF, Live, recording or active Prism restart.", + "pixelOracle": { + "result": "PASS", + "beforeShapeBounds": [ + 160, + 160, + 360, + 260 + ], + "afterColourBounds": [ + 160, + 160, + 360, + 260 + ], + "afterMoveResizeBounds": [ + 220, + 160, + 480, + 260 + ], + "blackTextPixels": 334, + "textPositionUnchanged": true + } + }, + "aggregateProof": "Prism/evidence/local-20261007-vision-integration/eleven/20261007T200000Z-authoring.json", + "aggregateSha256": "5580a33431fde07e2ea480c70a067297efda3bf7b32b8ce82f28e4243d830a90", + "authority": "Push existing candidate branch; no merge, production activation or Live", + "rollback": "Revert this scoped candidate increment; Prism old running dev remains preserved" +} diff --git a/evidence/local-20261007-vision-integration/eleven/20261007T212000Z-validation.txt b/evidence/local-20261007-vision-integration/eleven/20261007T212000Z-validation.txt new file mode 100644 index 0000000..1cec242 --- /dev/null +++ b/evidence/local-20261007-vision-integration/eleven/20261007T212000Z-validation.txt @@ -0,0 +1,5 @@ +Source-only integration candidate. +Solar: typecheck PASS; 196 tests / 28 files PASS; lint PASS; code map 37 files PASS; production build/layout/asset pins PASS. +Vision engine source: 02714c2; native LSDP source: e695e14f9f664f430ffc2e0e75d083fab88abc7f. +Changes: native geometry/style binding projection, licensed host/private font admission, native capture atlas with fixed pixel surface, cold subscriber prewarm compatible abort, obsolete-media retirement guard. +Limits: isolated frame performance is not actual CEF/Program proof; Prism Preview validation continues. diff --git a/evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.json b/evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.json new file mode 100644 index 0000000..d084830 --- /dev/null +++ b/evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.json @@ -0,0 +1,466 @@ +{ + "candidate": "canonical-once, private verified font snapshots, direct LSML/assets admission", + "hostHash": "51abd8395dffea605647158a440b0ae4482af21af21a761f7e94854aad91a1ed", + "visionPins": { + "ui/mainpresenter.mjs": { + "bytes": 26702, + "sha256": "88de7073b8538a12998139bc499866a06eb03925ff523f7683fb2f721aab3eae" + }, + "ui/fonts.mjs": { + "bytes": 1977, + "sha256": "d49ed45181a8aa714f675aaa24f5439e9547b274d6eba6cd2bb995c981765c5f" + }, + "pkg/lumencast_vision_web.js": { + "bytes": 148483, + "sha256": "f9f61fbf53fee481cf5c26179d853bf34eb94c0fb5cb4c8d386264638c31297a" + }, + "pkg/lumencast_vision_web_bg.wasm": { + "bytes": 10253842, + "sha256": "7626634a8ad14231ee9f7a50b0aaacca999d9f78d30cb4e171f798b3aa2a3796" + } + }, + "nativeHash": "7bd059c56b0daa56b3da24f0ccdb3b6a888709feff9618cd3b8cd2a045bdce60", + "switches": { + "canevas-chat-sponso": { + "total": { + "count": 9, + "mean": 187.01111112700568, + "median": 185.90000009536743, + "p95": 210.70000004768372, + "max": 210.70000004768372, + "sum": 1683.1000001430511 + }, + "stages": { + "worker-delivery": { + "count": 9, + "mean": 2.9777723683251276, + "median": 2.60009765625, + "p95": 4.299902439117432, + "max": 4.299902439117432, + "sum": 26.799951314926147 + }, + "worker-verify": { + "count": 9, + "mean": 17.922222269905937, + "median": 18.09999990463257, + "p95": 22.799999952316284, + "max": 22.799999952316284, + "sum": 161.30000042915344 + }, + "classify": { + "count": 9, + "mean": 0.011111127005683051, + "median": 0, + "p95": 0.10000014305114746, + "max": 0.10000014305114746, + "sum": 0.10000014305114746 + }, + "source": { + "count": 9, + "mean": 23.255555550257366, + "median": 22, + "p95": 35.40000009536743, + "max": 35.40000009536743, + "sum": 209.29999995231628 + }, + "package": { + "count": 9, + "mean": 12.677777767181396, + "median": 12.5, + "p95": 14.799999952316284, + "max": 14.799999952316284, + "sum": 114.09999990463257 + }, + "fonts": { + "count": 9, + "mean": 19.188888920678032, + "median": 18.90000009536743, + "p95": 23.699999809265137, + "max": 23.699999809265137, + "sum": 172.7000002861023 + }, + "vision": { + "count": 9, + "mean": 76.01111110051473, + "median": 76.79999995231628, + "p95": 90.30000019073486, + "max": 90.30000019073486, + "sum": 684.0999999046326 + }, + "images": { + "count": 9, + "mean": 0, + "median": 0, + "p95": 0, + "max": 0, + "sum": 0 + } + }, + "presenter": { + "sceneLoadMs": { + "count": 9, + "mean": 32.388888888888886, + "median": 30.700000047683716, + "p95": 39.60000014305115, + "max": 39.60000014305115, + "sum": 291.5 + }, + "assetDecodeUploadMs": { + "count": 9, + "mean": 17.18888889418708, + "median": 16.700000047683716, + "p95": 31.300000190734863, + "max": 31.300000190734863, + "sum": 154.70000004768372 + }, + "gpuSubmitCallMs": { + "count": 9, + "mean": 23.77777777777778, + "median": 25, + "p95": 27.59999990463257, + "max": 27.59999990463257, + "sum": 214 + }, + "mainThreadReceiptMs": { + "count": 9, + "mean": 0, + "median": 0, + "p95": 0, + "max": 0, + "sum": 0 + }, + "textPreparation": { + "count": 9, + "mean": 0.7666666666666668, + "median": 0.8, + "p95": 1.2, + "max": 1.2, + "sum": 6.900000000000001 + } + } + }, + "Editable scene": { + "total": { + "count": 10, + "mean": 61.77000002861023, + "median": 61.09999990463257, + "p95": 84.29999995231628, + "max": 84.29999995231628, + "sum": 617.7000002861023 + }, + "stages": { + "worker-delivery": { + "count": 10, + "mean": 0.8200146436691285, + "median": 0.89990234375, + "p95": 1.2000489234924316, + "max": 1.2000489234924316, + "sum": 8.200146436691284 + }, + "worker-verify": { + "count": 10, + "mean": 19.500000023841856, + "median": 18, + "p95": 35.700000047683716, + "max": 35.700000047683716, + "sum": 195.00000023841858 + }, + "classify": { + "count": 10, + "mean": 0.019999980926513672, + "median": 0, + "p95": 0.09999990463256836, + "max": 0.09999990463256836, + "sum": 0.19999980926513672 + }, + "source": { + "count": 10, + "mean": 1.1700000047683716, + "median": 1.1000001430511475, + "p95": 2.700000047683716, + "max": 2.700000047683716, + "sum": 11.700000047683716 + }, + "package": { + "count": 10, + "mean": 1.459999966621399, + "median": 1.5, + "p95": 2.5, + "max": 2.5, + "sum": 14.59999966621399 + }, + "fonts": { + "count": 10, + "mean": 6.789999961853027, + "median": 6.599999904632568, + "p95": 8.399999856948853, + "max": 8.399999856948853, + "sum": 67.89999961853027 + }, + "vision": { + "count": 10, + "mean": 15.799999976158142, + "median": 16, + "p95": 23, + "max": 23, + "sum": 157.99999976158142 + }, + "images": { + "count": 10, + "mean": 0, + "median": 0, + "p95": 0, + "max": 0, + "sum": 0 + } + }, + "presenter": { + "sceneLoadMs": { + "count": 10, + "mean": 10.230000042915345, + "median": 10.700000047683716, + "p95": 13.100000143051147, + "max": 13.100000143051147, + "sum": 102.30000042915344 + }, + "assetDecodeUploadMs": { + "count": 10, + "mean": 0.020000004768371583, + "median": 0, + "p95": 0.10000014305114746, + "max": 0.10000014305114746, + "sum": 0.20000004768371582 + }, + "gpuSubmitCallMs": { + "count": 10, + "mean": 4.489999985694885, + "median": 5, + "p95": 8, + "max": 8, + "sum": 44.89999985694885 + }, + "mainThreadReceiptMs": { + "count": 10, + "mean": 0, + "median": 0, + "p95": 0, + "max": 0, + "sum": 0 + }, + "textPreparation": { + "count": 10, + "mean": 1.57, + "median": 1.4, + "p95": 2.1, + "max": 2.1, + "sum": 15.700000000000001 + } + } + } + }, + "firstStandardSwitchMs": 422.2999999523163, + "mutation": { + "name": "persistent-paced-130", + "count": 390, + "distinctFrames": 390, + "elapsedMs": 2995.899999856949, + "rate": 130.17790981628963, + "endToFrame": { + "count": 390, + "mean": 3.0746153874275013, + "median": 2.8000001907348633, + "p95": 4.5, + "max": 25.399999856948853, + "sum": 1199.1000010967255 + }, + "verifiedReceptionToFrame": { + "count": 390, + "mean": 1.2892307672745142, + "median": 1.2000000476837158, + "p95": 2, + "max": 4, + "sum": 502.79999923706055 + }, + "stages": { + "worker-verify": { + "count": 390, + "mean": 0.23384615519107915, + "median": 0.20000004768371582, + "p95": 0.40000009536743164, + "max": 1.1000001430511475, + "sum": 91.20000052452087 + }, + "worker-delivery": { + "count": 390, + "mean": 0.544711578809298, + "median": 0.400146484375, + "p95": 0.9000489711761475, + "max": 17.10019540786743, + "sum": 212.43751573562622 + }, + "classify": { + "count": 390, + "mean": 0.020769227468050443, + "median": 0, + "p95": 0.10000014305114746, + "max": 0.19999980926513672, + "sum": 8.099998712539673 + }, + "images": { + "count": 390, + "mean": 0.010769231502826397, + "median": 0, + "p95": 0.09999990463256836, + "max": 0.10000014305114746, + "sum": 4.200000286102295 + } + }, + "diagnostics": { + "main:json-stringify": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "main:json-parse": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "main:text-encode": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "main:text-decode": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "main:canvas-copy": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "main:response-json": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "worker:text-decode": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "worker:json-parse": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "worker:text-encode": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "worker:json-stringify": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + }, + "worker:worker-postMessage": { + "count": 0, + "totalMs": 0, + "perMutationMs": 0, + "maxInput": 0 + } + }, + "vision": { + "engineApplyCallMs": { + "count": 390, + "mean": 0.3174358930343237, + "median": 0.2999999523162842, + "p95": 0.5, + "max": 0.9000000953674316, + "sum": 123.79999828338623 + }, + "gpuSubmitCallMs": { + "count": 390, + "mean": 0.6341025707049247, + "median": 0.5999999046325684, + "p95": 1, + "max": 1.3000001907348633, + "sum": 247.30000257492065 + } + } + }, + "httpImageProof": { + "elapsedMs": 118.40000009536743, + "detail": { + "transactionId": "a4715db14c2afdaf2f44e8c35830db4f", + "target": "scene", + "sequence": 14, + "stateHash": "tree-sha256:977b5af77b92a3d7d93aae546e1352a60dd0f4859ee7e36e114da426e169e85b", + "render": "patch", + "presentation": { + "throughSequence": 14, + "stateHash": "tree-sha256:977b5af77b92a3d7d93aae546e1352a60dd0f4859ee7e36e114da426e169e85b", + "mutationCount": 1 + } + }, + "writer": { + "receipt": { + "level": "applied", + "target": "scene", + "transactionId": "a4715db14c2afdaf2f44e8c35830db4f" + }, + "value": "http://127.0.0.1:55555/proof-image.png" + }, + "changedPixels": 4095, + "loadsBefore": 10, + "loadsAfter": 10, + "enginePointerBefore": 6464496, + "enginePointerAfter": 6464496, + "engineCreations": 1 + }, + "references": [ + "evidence\\local-20261008-full-profile-switch\\eleven\\20261008T122322Z-analysis.json", + "evidence\\local-20261008-full-profile-mutation\\eleven\\20261008T122352Z-analysis.json", + "evidence\\local-20261008-http-image-lsdp\\eleven\\20261008T122437Z-report.json" + ], + "validation": { + "solarTests": 225, + "solarFiles": 31, + "rustAssetsTests": 10, + "rustRuntimeTests": 9, + "rustRuntimeIgnored": 1, + "presenterTests": 14, + "gpuTestsAfterRejection": 19, + "typecheck": "PASS", + "build": "PASS", + "productLint": "PASS excluding ad hoc .cache scripts", + "unfilteredLint": "FAIL 1598 ad hoc cache script diagnostics", + "architecture": "PASS", + "bundlePins": "PASS" + }, + "remaining": [ + "Rust scene admission/layout/graph and image decode/upload and GPU cold composition", + "typed internal Vision patch/metadata interface still JSON", + "whole verified LSML state still cloned worker -> main per notification" + ], + "rejected": "fixed source-over GPU candidate changed exact pixels; removed; source GPU files restored", + "workspaceGuard": { + "decision": "block", + "reason": "invalid new evidence path in concurrent Pulsar worktree", + "path": "D:/Documents/Zab/Pulsar/.worktrees/eleven-local-20261008-webm-transition/evidence/local-20261008-webm-transition/eleven/session.json", + "action": "preserved foreign active work; local product validations remain green; global closure blocked" + } +} diff --git a/evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.md b/evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.md new file mode 100644 index 0000000..7b1044b --- /dev/null +++ b/evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.md @@ -0,0 +1,25 @@ +# Local preparation optimizations and final evidence + +The retained changes are canonical LSML serialization once (normative zero-version hash then root stamp), owned verified font snapshots/acknowledged bank reuse, and direct canonical LSML + assets into the persistent Rust engine. Plain delivery arrays are defensively copied; private font admission cannot be forged through the JS constructor or prototype. Active structural rebuilds reuse font-only verification; scene changes dispose the active owner and reacquire source. Import/export still uses LSMLZ. + +The canonical bytes and independent normative content address match. Rust direct admission reuses archive path classification, asset hashes, required-local-reference resolution and quotas. Source identity/Blue declarations stay pinned. This is a local, unmerged candidate on the existing worktrees. + +Final host: host-BM_1-tX6.js. All four Vision artifact pins checked. Rust binary unchanged, SHA256 7bd059c56b0daa56b3da24f0ccdb3b6a888709feff9618cd3b8cd2a045bdce60. + +40 switches: first20 without CPU sampling, second20 diagnostic. One GPU engine/pointer, 165 fonts and 8 initial preload batches unchanged, zero scene/runtime errors. Baseline mean (exclude first standard switch only): standard 187.01ms (previous199.41), editable61.77ms (previous62.25; no significant overall gain). First standard422.3ms, separately reported. Source/package/fonts are overlapping; do not add their durations. Submission/front-canvas timing does not measure physical display scan-out. No Prism/Orion/Blue execution and no remote-network timing qualification. + +Standard scene package phase22.67 ->12.68ms; Vision87.01 ->76.01ms. Diagnostic main JSON.stringify10.46 ->3.69ms (includes canonical LSML, not exclusively non-LSML JSON). Raw timings vary locally; this is no guaranteed absolute latency. Some residual costs, notably snapshot verification, have not fallen and mask preparation gains on Editable. + +390 scalar mutations paced at130/s:390distinctframes,130.18frames/s; send->submission3.07ms mean/P954.5ms, verified reception->submission1.29ms/P952ms. Visionapply0.317ms, submit0.634ms. Includes real Rust LSDP and real VisionWASM, with final hash/pixel checks. The slower ephemeral inter-switch mutation HTTP harness is not this application measurement. + +HTTP image proof on canvas-chat-sponso: mutate /defaults/__lit.image.image_100 to a new permitted local HTTP URL using Rust LSDP. Exactly one HTTP GET,4095changedpixels, accepted patch, unchanged engine pointer and unchanged10scene loads. Local HTTP elapsed118.4ms includes acquisition/decode/upload/draw and benchmark writer; it is not a scalar mutation cost. Actual Blue execution is not covered. The already-active URL cache remains; refreshing bytes at an identical URL without changing a binding is not qualified by this test. + +GPU source-over shortcut: rejected after4exactpixel tests failed. Fully removed;19GPUtests pass on native RTX3070Vulkan. It never entered SolarWASM. Existing pixel requirements and antialiasing remain intact. + +225Solar tests/31files;10Rustasset tests;9Rust runtime tests with1existingignored;14presenter tests. Typecheck, full build, productlint(.cache excluded explicitly), architecture and bundle/native pins pass. Unfiltered lint still scans pre-existing ad hoc cache scripts (1598diagnostics); rules were not weakened. Failed benchmark startup due missing generated index.html and initial HTTP harness count assertion are excluded, kept in their reports and corrected before successful replay. + +Open work: internal Vision JSON patch/metadata typing; deeper Rust loading/image/GPU optimization; worker main full-state clone. No claim that all six proposed optimizations are complete. The original legacy benchmark report flag allCorrections describes its previous setup; this result file is the scope of this candidate. + +Final raw evidence: ../local-20261008-full-profile-switch/eleven/20261008T122322Z-{report,profile,analysis}.json and .cpuprofile; ../local-20261008-full-profile-mutation/eleven/20261008T122352Z-{report,profile,analysis}.json and .cpuprofile; ../local-20261008-http-image-lsdp/eleven/20261008T122437Z-report.json and before/after PNGs. Lumencast evidence: evidence/local-20261008-direct-scene-parts/eleven/. + +Workspace guard: BLOCK due to a foreign concurrent Pulsar evidence path (`D:/Documents/Zab/Pulsar/.worktrees/eleven-local-20261008-webm-transition/evidence/local-20261008-webm-transition/eleven/session.json`). Solar/Vision writes are compliant; the foreign active file is preserved. Global workspace closure is blocked, not claimed green. diff --git a/evidence/local-20261008-solar-merge/eleven/20261008T123900Z-candidate.md b/evidence/local-20261008-solar-merge/eleven/20261008T123900Z-candidate.md new file mode 100644 index 0000000..9bed7a2 --- /dev/null +++ b/evidence/local-20261008-solar-merge/eleven/20261008T123900Z-candidate.md @@ -0,0 +1,17 @@ +# Solar merge candidate — 2026-10-08 + +Authority: user explicitly requested push and merge Solar. No ADR, release tag, deployment or Prism change. + +Scope: persistent Vision engine across fresh scene admissions; installation/catalogue and Windows fonts; direct canonical LSML plus addressed assets; received LSDP mutations independent of Orion; active-scene-only ownership; image binding changes continue to acquire HTTP assets. Existing source-store adapters are preserved for a future launcher integration. Standalone Solar performs no catalogue synchronization. + +Validation on staged product candidate: 225 tests/31 files PASS; TypeScript PASS; product ESLint PASS (local .cache explicitly excluded); architecture/source usage/map/documentation/size PASS; 5 architecture tooling tests PASS; runtime layout, host bundle and four Vision artifact pins PASS; pinned native browser client PASS. Full build passed on the identical product source earlier this session; only documentation changed since that build. Unfiltered local ESLint scans ad hoc .cache scripts and is not asserted green. Those scripts and installation font files are not committed. + +Measured isolated local behavior: warm standard switch mean187.01ms, editable61.77ms; first standard422.3ms separately. 390 scalar mutations,390distinctframes,130.18frames/s, mean send-to-submission3.07ms. HTTP image mutation: oneGET,4095changedpixels, same engine and no scene load. These are submission/front-canvas tests, not physical scan-out, production network, Blue execution or Prism/Pulsar qualification. Detailed aggregate is evidence/local-20261008-render-preparation/eleven/20261008T122600Z-results.json. Raw CPU traces, recordings, font installation files and private scene screenshots remain local and unpublished. + +Vision artifacts are vendored and byte-pinned, including WASM SHA2567626634a8ad14231ee9f7a50b0aaacca999d9f78d30cb4e171f798b3aa2a3796. Their source includes local uncommitted Vision font/direct-parts work on base031788f42750f08e00dfa506c0e85d0819da8aa1, truthfully recorded in public/vision/vision-assets.json. This Solar merge does not merge that separate Vision source branch. Native Rust binary remains revisione695e14f9f664f430ffc2e0e75d083fab88abc7f. + +Open performance work: internal Vision patch/metadata JSON, full verified-state worker clone, Rust scene loading/images/GPU composition. Rejected GPU shortcut is absent; exact-pixel requirements unchanged. + +Global workspace guard remains BLOCK due exclusively to invalid new evidence names in concurrent Pulsar worktree eleven-local-20261008-webm-transition (session.json and runtime fixture/result/log). Those active foreign files are preserved. Product checks and authorized Solar integration proceed; global workspace closure is not claimed. + +Rollback: revert Solar PR merge; prior bundled Vision artifacts/pins return with the revert. Preserve active worktrees and untracked evidence/caches. diff --git a/host.html b/host.html index 28ada57..e343b82 100644 --- a/host.html +++ b/host.html @@ -11,13 +11,7 @@ margin: 0; padding: 0; height: 100%; - /* Transparent so Pulsar CEF (OSR) composites the host as a true - alpha overlay: native capture sources anchored BEHIND the - browser_source (x-zab.capture placeholder is transparent) show - through. A #000 here is opaque and hides them (black on-air). - On-air the OBS canvas behind is black, so opaque scenes look - identical; in the Prism preview the scene-server SOLAR_FIT_SHIM - forces its own background, so preview is unaffected. */ + /* Preserve alpha from Vision output for transparent scene pixels. */ background: transparent; } #scene { @@ -30,14 +24,11 @@
- + diff --git a/index.html b/index.html index 7acaa38..0840e51 100644 --- a/index.html +++ b/index.html @@ -11,6 +11,6 @@
- + diff --git a/package-lock.json b/package-lock.json index 2524da5..daee0ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,224 +1,40 @@ { "name": "@zablab/solar", - "version": "2.0.2", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@zablab/solar", - "version": "2.0.2", + "version": "3.0.0", "hasInstallScript": true, "dependencies": { - "@lumencast/runtime": "^0.18.2", - "@preact/signals-react": "^3.2.1", - "framer-motion": "^12.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@lumencast/canonical": "0.18.2", + "@lumencast/protocol": "0.18.2", + "@lumencast/runtime": "0.18.2", + "@noble/hashes": "1.8.0", + "fast-json-patch": "3.1.1", + "fflate": "0.8.3" }, "devDependencies": { - "@lumencast/protocol": "^0.18.1", - "@playwright/test": "^1.49.1", - "@tailwindcss/postcss": "^4.0.7", "@types/node": "^22.13.5", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@typescript-eslint/eslint-plugin": "^8.24.0", - "@typescript-eslint/parser": "^8.24.0", - "@vitejs/plugin-react": "^4.3.4", - "@vitest/coverage-v8": "^4.1.5", + "@vitest/coverage-v8": "4.1.11", + "esbuild": "0.25.12", "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", + "fake-indexeddb": "^6.2.4", "globals": "^17.6.0", "happy-dom": "^20.9.0", - "postcss": "^8.5.2", "prettier": "^3.5.2", - "tailwindcss": "^4.0.7", "typescript": "^5.7.3", "typescript-eslint": "^8.24.0", "vite": "^6.1.0", "vite-plugin-dts": "^4.5.0", - "vitest": "^4.1.5", + "vitest": "4.1.11", "ws": "^8.21.0" }, "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node": ">=22.0.0" } }, "node_modules/@babel/helper-string-parser": { @@ -241,30 +57,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", @@ -281,72 +73,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", @@ -865,9 +591,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -946,9 +672,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1095,28 +821,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1328,20 +1032,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@playwright/test": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", - "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", "engines": { - "node": ">=18" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@preact/signals-core": { @@ -1371,13 +1071,6 @@ "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" - }, "node_modules/@rollup/pluginutils": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", @@ -1951,455 +1644,55 @@ "dev": true, "license": "MIT" }, - "node_modules/@tailwindcss/node": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.4.tgz", - "integrity": "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==", + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.4" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.4.tgz", - "integrity": "sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-arm64": "4.2.4", - "@tailwindcss/oxide-darwin-x64": "4.2.4", - "@tailwindcss/oxide-freebsd-x64": "4.2.4", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.4", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.4", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.4", - "@tailwindcss/oxide-linux-x64-musl": "4.2.4", - "@tailwindcss/oxide-wasm32-wasi": "4.2.4", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.4", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.4" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.4.tgz", - "integrity": "sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==", - "cpu": [ - "arm64" - ], + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } + "license": "MIT" }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.4.tgz", - "integrity": "sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==", - "cpu": [ - "arm64" - ], + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } + "license": "MIT" }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.4.tgz", - "integrity": "sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.4.tgz", - "integrity": "sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.4.tgz", - "integrity": "sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.4.tgz", - "integrity": "sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.4.tgz", - "integrity": "sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.4.tgz", - "integrity": "sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.4.tgz", - "integrity": "sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.4.tgz", - "integrity": "sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.4.tgz", - "integrity": "sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.4.tgz", - "integrity": "sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/postcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.4.tgz", - "integrity": "sha512-wgAVj6nUWAolAu8YFvzT2cTBIElWHkjZwFYovF+xsqKsW2ADxM/X2opxj5NsF/qVccAOjRNe8X2IdPzMsWyHTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.4", - "@tailwindcss/oxide": "4.2.4", - "postcss": "^8.5.6", - "tailwindcss": "4.2.4" - } - }, - "node_modules/@types/argparse": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", - "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", - "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -2650,36 +1943,15 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.5.tgz", - "integrity": "sha512-38C0/Ddb7HcRG0Z4/DUem8x57d2p9jYgp18mkaYswEOQBGsI1CG4f/hjm0ZCeaJfWhSZ4k7jgs29V1Zom7Ki9A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -2693,8 +1965,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.5", - "vitest": "4.1.5" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2703,16 +1975,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2721,13 +1993,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.5", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2758,9 +2030,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -2771,13 +2043,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -2785,14 +2057,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2801,9 +2073,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -2811,13 +2083,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2923,9 +2195,9 @@ "license": "MIT" }, "node_modules/@vue/language-core/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -3067,152 +2339,14 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" + "node": ">=12" } }, "node_modules/ast-v8-to-istanbul": { @@ -3244,32 +2378,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3280,23 +2388,10 @@ "node": "18 || 20 || >=22" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", - "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3306,90 +2401,6 @@ "node": "20 || >=22" } }, - "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3400,31 +2411,10 @@ "node": ">=6" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001809", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", - "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -3511,67 +2501,6 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", @@ -3604,308 +2533,46 @@ "dev": true, "license": "MIT" }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.3.1" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">= 0.4" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.407", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", - "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/enhanced-resolve": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", - "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", - "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -3948,16 +2615,6 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4047,93 +2704,6 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -4172,9 +2742,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -4303,9 +2873,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4319,6 +2889,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fake-indexeddb": { + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.4.tgz", + "integrity": "sha512-INKeIKEtSViN4yVtEWEUqbsqmaIy7Ls+MfU0yxQVXg67pOJ/sH1ZxcVrP8XrKULUFohcPD9gnmym+qBfEybACw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4326,6 +2906,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4341,9 +2927,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -4375,6 +2961,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4426,22 +3018,6 @@ "dev": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/framer-motion": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", @@ -4509,206 +3085,55 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10.13.0" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "license": "ISC" }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/happy-dom": { + "version": "20.9.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz", + "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", "dev": true, "license": "MIT", + "dependencies": { + "@types/node": ">=20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "@types/ws": "^8.18.1", + "entities": "^7.0.1", + "whatwg-mimetype": "^3.0.0", + "ws": "^8.18.3" + }, "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/happy-dom": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.9.0.tgz", - "integrity": "sha512-GZZ9mKe8r646NUAf/zemnGbjYh4Bt8/MqASJY+pSm5ZDtc3YQox+4gsLI7yi1hba6o+eCsGxpHn5+iEVn31/FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": ">=20.0.0", - "@types/whatwg-mimetype": "^3.0.2", - "@types/ws": "^8.18.1", - "entities": "^7.0.1", - "whatwg-mimetype": "^3.0.0", - "ws": "^8.18.3" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=20.0.0" } }, "node_modules/has-flag": { @@ -4721,64 +3146,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", @@ -4856,31 +3223,14 @@ "node": ">=0.8.19" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4889,854 +3239,168 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-async-function": { + "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "is-extglob": "^2.1.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "ISC" }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kolorist": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=10" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=8" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 0.8.0" } }, "node_modules/local-pkg": { @@ -5780,29 +3444,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5841,16 +3482,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -5926,191 +3557,47 @@ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/optionator": { "version": "0.9.4", @@ -6130,24 +3617,6 @@ "node": ">= 0.8.0" } }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6266,48 +3735,6 @@ "pathe": "^2.0.3" } }, - "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.59.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", @@ -6363,18 +3790,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6423,67 +3838,6 @@ "react": "^19.2.5" } }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -6494,30 +3848,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -6573,226 +3903,46 @@ "fsevents": "~2.3.2" } }, - "node_modules/safe-array-concat": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, "node_modules/siginfo": { @@ -6837,26 +3987,12 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -6867,104 +4003,6 @@ "node": ">=0.6.19" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -7004,27 +4042,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tailwindcss": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.4.tgz", - "integrity": "sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", - "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -7033,9 +4050,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", - "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -7060,9 +4077,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz", + "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==", "dev": true, "license": "MIT", "engines": { @@ -7101,84 +4118,6 @@ "node": ">= 0.8.0" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7224,25 +4163,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -7260,37 +4180,6 @@ "node": ">= 10.0.0" } }, - "node_modules/update-browserslist-db": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", - "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7428,19 +4317,19 @@ } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -7468,12 +4357,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -7550,95 +4439,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -7687,13 +4487,6 @@ } } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/package.json b/package.json index 9373476..5588994 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@zablab/solar", - "version": "2.0.2", - "description": "Solar — scene runtime bundle for the Zablab broadcast platform (Pulsar CEF + Prism webview + editor preview)", + "version": "3.0.0", + "description": "Solar — LSDP scene client and Vision renderer for the Zablab broadcast platform", "type": "module", "private": true, "main": "./dist/solar.js", @@ -12,65 +12,78 @@ "types": "./dist/index.d.ts", "import": "./dist/solar.js" }, - "./style.css": "./dist/solar.css" + "./server": { + "types": "./dist/server/types/server/index.d.ts", + "import": "./dist/server/index.mjs" + } }, "files": [ - "dist" + "dist", + "scripts/patch-peer-viewer.mjs", + "scripts/peer-viewer" ], "author": { "name": "Zablab", "email": "wellplayedprincipale@gmail.com" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" }, "scripts": { "dev": "vite", - "build": "npm run build:lib && npm run build:host && node scripts/build-host-html.mjs", + "build": "npm run build:lib && npm run build:host && node scripts/build-host-html.mjs && npm run build:server && npm run check:layout", + "build:server": "node scripts/build-server.mjs", + "package:native": "node scripts/package-native-server.mjs", + "check:native-package": "node scripts/check-native-package.mjs", "build:lib": "vite build", "build:host": "vite build --config vite.config.host.ts", + "prebuild:host": "node scripts/check-vision-assets.mjs", "lint": "eslint --max-warnings 0 .", "typecheck": "tsc -b --pretty", "format": "prettier --write .", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest", - "test:e2e": "playwright test", - "postinstall": "node scripts/patch-lumencast-protocol.mjs", - "check:bundle": "node scripts/check-bundle-size.mjs && node scripts/check-host-bundle.mjs" + "postinstall": "node scripts/patch-peer-viewer.mjs", + "check:layout": "node scripts/check-runtime-layout.mjs", + "check:source-usage": "node scripts/check-source-usage.mjs", + "check:bundle": "npm run check:layout && node scripts/check-host-bundle.mjs && node scripts/check-vision-assets.mjs", + "prepack": "npm run check:layout", + "native:start": "node scripts/native-lsdp-local.mjs", + "test:native": "node scripts/test-native-lsdp.mjs", + "check:native-client": "node scripts/check-native-client.mjs", + "code:map": "node scripts/code/cli.mjs map", + "code:check": "node scripts/code/cli.mjs check", + "code:read": "node scripts/code/cli.mjs read", + "code:find": "node scripts/code/cli.mjs find", + "check:docs": "node scripts/check-documentation.mjs", + "check:architecture": "npm run check:source-usage && npm run code:check && npm run check:docs && python scripts/check_file_sizes.py", + "test:peer-install": "node scripts/test-peer-install.mjs", + "test:architecture": "node --test tests/tooling/*.test.mjs" }, "dependencies": { - "@lumencast/runtime": "^0.18.2", - "@preact/signals-react": "^3.2.1", - "framer-motion": "^12.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "@lumencast/canonical": "0.18.2", + "@lumencast/protocol": "0.18.2", + "@lumencast/runtime": "0.18.2", + "@noble/hashes": "1.8.0", + "fast-json-patch": "3.1.1", + "fflate": "0.8.3" }, "devDependencies": { - "@lumencast/protocol": "^0.18.1", - "@playwright/test": "^1.49.1", - "@tailwindcss/postcss": "^4.0.7", "@types/node": "^22.13.5", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@typescript-eslint/eslint-plugin": "^8.24.0", - "@typescript-eslint/parser": "^8.24.0", - "@vitejs/plugin-react": "^4.3.4", - "@vitest/coverage-v8": "^4.1.5", + "@vitest/coverage-v8": "4.1.11", + "esbuild": "0.25.12", "eslint": "^9.20.1", "eslint-config-prettier": "^10.0.1", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", + "fake-indexeddb": "^6.2.4", "globals": "^17.6.0", "happy-dom": "^20.9.0", - "postcss": "^8.5.2", "prettier": "^3.5.2", - "tailwindcss": "^4.0.7", "typescript": "^5.7.3", "typescript-eslint": "^8.24.0", "vite": "^6.1.0", "vite-plugin-dts": "^4.5.0", - "vitest": "^4.1.5", + "vitest": "4.1.11", "ws": "^8.21.0" } } diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index 7222755..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -const VITE_PORT = Number(process.env.SOLAR_DEV_PORT ?? 5173); - -export default defineConfig({ - testDir: "./tests/e2e", - testMatch: /.*\.spec\.ts$/, - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 1 : 0, - workers: 1, - reporter: process.env.CI ? "github" : "list", - timeout: 30_000, - expect: { timeout: 5_000 }, - // ADR 007 sub-chantier B : the bespoke mock-orion global setup was - // removed with Solar's home-grown protocol. The LSDP/1.1 E2E harness - // (a stub Lumencast server + the runtime's server kit) is Probe's - // follow-up — acceptance (b)/(c)/(e). - // - // ADR 001 (dual-build) adds the served-host-bundle smoke test - // (tests/e2e/host-bundle.spec.ts). That spec serves the BUILT - // dist/host/ over its own static HTTP server (no Vite, no import map) to - // reproduce the Pulsar CEF / Orion static-serve conditions, so it does - // NOT need the Vite dev webServer. The dev `webServer` is therefore - // wired only when a spec opts into `baseURL` — left off by default so - // the host smoke test stays self-contained and CI-stable. - use: { - headless: true, - // Local PowerShell: $env:SOLAR_E2E_BROWSER='system-chrome'; npm run test:e2e - channel: - process.env.SOLAR_E2E_BROWSER === "system-chrome" ? "chrome" : undefined, - trace: "retain-on-failure", - }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], -}); - -// VITE_PORT is retained for a future dev-server-backed harness; reference -// it so the unused-var lint stays green until that harness lands. -void VITE_PORT; diff --git a/postcss.config.js b/postcss.config.js deleted file mode 100644 index c2ddf74..0000000 --- a/postcss.config.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; diff --git a/public/fonts/LICENSE-AlexBrush.txt b/public/fonts/LICENSE-AlexBrush.txt new file mode 100644 index 0000000..19b5092 --- /dev/null +++ b/public/fonts/LICENSE-AlexBrush.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011, Robert Leuschke (robert.leuschke@gmail.com), +with Reserved Font Name "Alex Brush". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/LICENSE-Figtree.txt b/public/fonts/LICENSE-Figtree.txt new file mode 100644 index 0000000..80b12bb --- /dev/null +++ b/public/fonts/LICENSE-Figtree.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/assets/fonts/LICENSE-Geist.txt b/public/fonts/LICENSE-Geist.txt similarity index 100% rename from src/assets/fonts/LICENSE-Geist.txt rename to public/fonts/LICENSE-Geist.txt diff --git a/public/fonts/LICENSE-IBMPlexMono.txt b/public/fonts/LICENSE-IBMPlexMono.txt new file mode 100644 index 0000000..83eac82 --- /dev/null +++ b/public/fonts/LICENSE-IBMPlexMono.txt @@ -0,0 +1,93 @@ +Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/LICENSE-SpaceGrotesk.txt b/public/fonts/LICENSE-SpaceGrotesk.txt new file mode 100644 index 0000000..d5666d7 --- /dev/null +++ b/public/fonts/LICENSE-SpaceGrotesk.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/alex-brush-400.woff2 b/public/fonts/alex-brush-400.woff2 new file mode 100644 index 0000000..46fc0b2 Binary files /dev/null and b/public/fonts/alex-brush-400.woff2 differ diff --git a/public/fonts/figtree-variable.ttf b/public/fonts/figtree-variable.ttf new file mode 100644 index 0000000..579e2ab Binary files /dev/null and b/public/fonts/figtree-variable.ttf differ diff --git a/src/assets/fonts/geist-latin.woff2 b/public/fonts/geist-latin.woff2 similarity index 100% rename from src/assets/fonts/geist-latin.woff2 rename to public/fonts/geist-latin.woff2 diff --git a/src/assets/fonts/geist-mono-latin.woff2 b/public/fonts/geist-mono-latin.woff2 similarity index 100% rename from src/assets/fonts/geist-mono-latin.woff2 rename to public/fonts/geist-mono-latin.woff2 diff --git a/public/fonts/ibm-plex-mono-400.woff2 b/public/fonts/ibm-plex-mono-400.woff2 new file mode 100644 index 0000000..0804aaf Binary files /dev/null and b/public/fonts/ibm-plex-mono-400.woff2 differ diff --git a/public/fonts/ibm-plex-mono-500.woff2 b/public/fonts/ibm-plex-mono-500.woff2 new file mode 100644 index 0000000..090f82f Binary files /dev/null and b/public/fonts/ibm-plex-mono-500.woff2 differ diff --git a/public/fonts/ibm-plex-mono-600.woff2 b/public/fonts/ibm-plex-mono-600.woff2 new file mode 100644 index 0000000..67aeeb0 Binary files /dev/null and b/public/fonts/ibm-plex-mono-600.woff2 differ diff --git a/public/fonts/space-grotesk-400.woff2 b/public/fonts/space-grotesk-400.woff2 new file mode 100644 index 0000000..0e63471 Binary files /dev/null and b/public/fonts/space-grotesk-400.woff2 differ diff --git a/public/fonts/space-grotesk-500.woff2 b/public/fonts/space-grotesk-500.woff2 new file mode 100644 index 0000000..0db251f Binary files /dev/null and b/public/fonts/space-grotesk-500.woff2 differ diff --git a/public/fonts/space-grotesk-600.woff2 b/public/fonts/space-grotesk-600.woff2 new file mode 100644 index 0000000..a1db41a Binary files /dev/null and b/public/fonts/space-grotesk-600.woff2 differ diff --git a/public/fonts/space-grotesk-700.woff2 b/public/fonts/space-grotesk-700.woff2 new file mode 100644 index 0000000..44604a0 Binary files /dev/null and b/public/fonts/space-grotesk-700.woff2 differ diff --git a/public/vision/pkg/lumencast_vision_web.js b/public/vision/pkg/lumencast_vision_web.js new file mode 100644 index 0000000..626f3db --- /dev/null +++ b/public/vision/pkg/lumencast_vision_web.js @@ -0,0 +1,3118 @@ +/* @ts-self-types="./lumencast_vision_web.d.ts" */ + +/** + * Browser-owned wrapper around the same fixed Rust GPU runtime. + */ +export class VisionGpuEngine { + static __wrap(ptr) { + const obj = Object.create(VisionGpuEngine.prototype); + obj.__wbg_ptr = ptr; + VisionGpuEngineFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + VisionGpuEngineFinalization.unregister(this); + return ptr; + } + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_visiongpuengine_free(ptr, 0); + } + /** + * Apply a path-to-value patch atomically and return committed ChangeSet metadata. + * @param {string} patch_json + * @returns {string} + */ + apply_patch(patch_json) { + let deferred3_0; + let deferred3_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(patch_json, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_apply_patch(retptr, this.__wbg_ptr, ptr0, len0); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr2 = r0; + var len2 = r1; + if (r3) { + ptr2 = 0; len2 = 0; + throw takeObject(r2); + } + deferred3_0 = ptr2; + deferred3_1 = len2; + return getStringFromWasm0(ptr2, len2); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred3_0, deferred3_1, 1); + } + } + /** + * Return encoded package bytes for one currently required image. + * @param {string} path + * @returns {Uint8Array} + */ + asset_bytes(path) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(path, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_asset_bytes(retptr, this.__wbg_ptr, ptr0, len0); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + if (r3) { + throw takeObject(r2); + } + var v2 = getArrayU8FromWasm0(r0, r1).slice(); + wasm.__wbindgen_export5(r0, r1 * 1, 1); + return v2; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Clear one disconnected camera or peer-video texture. + * @param {string} path + * @returns {boolean} + */ + clear_live_image(path) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(path, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_clear_live_image(retptr, this.__wbg_ptr, ptr0, len0); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + if (r2) { + throw takeObject(r1); + } + return r0 !== 0; + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Release the active scene while keeping the GPU engine ready. + */ + clear_scene() { + wasm.visiongpuengine_clear_scene(this.__wbg_ptr); + } + /** + * Validate LSML/LSMLZ, create the persistent surface and request its GPU device. + * @param {Uint8Array} package_bytes + * @param {Uint8Array} font_bytes + * @param {OffscreenCanvas} canvas + * @param {number} width + * @param {number} height + * @returns {Promise} + */ + static create(package_bytes, font_bytes, canvas, width, height) { + const ptr0 = passArray8ToWasm0(package_bytes, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(font_bytes, wasm.__wbindgen_export); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.visiongpuengine_create(ptr0, len0, ptr1, len1, addHeapObject(canvas), width, height); + return takeObject(ret); + } + /** + * Initialize one WebGL engine independently of any scene source. + * @param {HTMLCanvasElement} canvas + * @param {number} width + * @param {number} height + * @returns {Promise} + */ + static create_empty_html_canvas(canvas, width, height) { + const ret = wasm.visiongpuengine_create_empty_html_canvas(addHeapObject(canvas), width, height); + return takeObject(ret); + } + /** + * Create the explicit WebGL2 presenter from a caller-preconfigured HTML canvas. + * + * The caller must create its `webgl2` context with desynchronized presentation + * and confirm the attribute before calling this method. This path only requests + * the GL backend; it never falls back to browser WebGPU. + * @param {Uint8Array} package_bytes + * @param {Uint8Array} font_bytes + * @param {HTMLCanvasElement} canvas + * @param {number} width + * @param {number} height + * @returns {Promise} + */ + static create_html_canvas(package_bytes, font_bytes, canvas, width, height) { + const ptr0 = passArray8ToWasm0(package_bytes, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(font_bytes, wasm.__wbindgen_export); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.visiongpuengine_create_html_canvas(ptr0, len0, ptr1, len1, addHeapObject(canvas), width, height); + return takeObject(ret); + } + /** + * Create the WebGL presenter with every explicit font asset available at admission. + * @param {Uint8Array} package_bytes + * @param {Array} fonts + * @param {HTMLCanvasElement} canvas + * @param {number} width + * @param {number} height + * @returns {Promise} + */ + static create_html_canvas_with_fonts(package_bytes, fonts, canvas, width, height) { + const ptr0 = passArray8ToWasm0(package_bytes, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.visiongpuengine_create_html_canvas_with_fonts(ptr0, len0, addHeapObject(fonts), addHeapObject(canvas), width, height); + return takeObject(ret); + } + /** + * Create the WebGPU presenter with every explicit font asset available at admission. + * @param {Uint8Array} package_bytes + * @param {Array} fonts + * @param {OffscreenCanvas} canvas + * @param {number} width + * @param {number} height + * @returns {Promise} + */ + static create_with_fonts(package_bytes, fonts, canvas, width, height) { + const ptr0 = passArray8ToWasm0(package_bytes, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.visiongpuengine_create_with_fonts(ptr0, len0, addHeapObject(fonts), addHeapObject(canvas), width, height); + return takeObject(ret); + } + /** + * Return preload counters without retaining scene-dependent text caches. + * @returns {string} + */ + font_registry_info_json() { + let deferred1_0; + let deferred1_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.visiongpuengine_font_registry_info_json(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + deferred1_0 = r0; + deferred1_1 = r1; + return getStringFromWasm0(r0, r1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred1_0, deferred1_1, 1); + } + } + /** + * Return source identity, authored dimensions, node count and backend metadata. + * @returns {string} + */ + info_json() { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.visiongpuengine_info_json(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr1 = r0; + var len1 = r1; + if (r3) { + ptr1 = 0; len1 = 0; + throw takeObject(r2); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred2_0, deferred2_1, 1); + } + } + /** + * Return the host-owned live capture and peer-video sources. + * @returns {string} + */ + live_media_sources_json() { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.visiongpuengine_live_media_sources_json(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr1 = r0; + var len1 = r1; + if (r3) { + ptr1 = 0; len1 = 0; + throw takeObject(r2); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred2_0, deferred2_1, 1); + } + } + /** + * Load a fresh scene without creating a device, surface or renderer. + * @param {Uint8Array} package_bytes + * @param {Array} fonts + */ + load_scene(package_bytes, fonts) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(package_bytes, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_load_scene(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(fonts)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Load canonical LSML and explicit assets without a temporary ZIP container. + * @param {Uint8Array} document_bytes + * @param {Array} asset_paths + * @param {Array} asset_bytes + */ + load_scene_parts(document_bytes, asset_paths, asset_bytes) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passArray8ToWasm0(document_bytes, wasm.__wbindgen_export); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_load_scene_parts(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(asset_paths), addHeapObject(asset_bytes)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Preload installation fonts once; repeated content is never decoded/registered again. + * @param {Array} fonts + * @returns {string} + */ + preload_fonts(fonts) { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.visiongpuengine_preload_fonts(retptr, this.__wbg_ptr, addHeapObject(fonts)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr1 = r0; + var len1 = r1; + if (r3) { + ptr1 = 0; len1 = 0; + throw takeObject(r2); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred2_0, deferred2_1, 1); + } + } + /** + * Register one browser-decoded bitmap once; its pixels stay on the GPU path. + * @param {string} path + * @param {ImageBitmap} bitmap + */ + register_image(path, bitmap) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(path, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_register_image(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(bitmap)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Admit bounded content-addressed bytes and upload their decoded image. + * @param {string} path + * @param {Uint8Array} bytes + * @param {ImageBitmap} bitmap + */ + register_image_asset(path, bytes, bitmap) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(path, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(bytes, wasm.__wbindgen_export); + const len1 = WASM_VECTOR_LEN; + wasm.visiongpuengine_register_image_asset(retptr, this.__wbg_ptr, ptr0, len0, ptr1, len1, addHeapObject(bitmap)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } + /** + * Submit the retained graph to the persistent browser surface. + * @param {number} width + * @param {number} height + * @returns {string} + */ + render(width, height) { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.visiongpuengine_render(retptr, this.__wbg_ptr, width, height); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr1 = r0; + var len1 = r1; + if (r3) { + ptr1 = 0; len1 = 0; + throw takeObject(r2); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred2_0, deferred2_1, 1); + } + } + /** + * Return required image package paths as a JSON array. + * @returns {string} + */ + required_images_json() { + let deferred2_0; + let deferred2_1; + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.visiongpuengine_required_images_json(retptr, this.__wbg_ptr); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true); + var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true); + var ptr1 = r0; + var len1 = r1; + if (r3) { + ptr1 = 0; len1 = 0; + throw takeObject(r2); + } + deferred2_0 = ptr1; + deferred2_1 = len1; + return getStringFromWasm0(ptr1, len1); + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + wasm.__wbindgen_export5(deferred2_0, deferred2_1, 1); + } + } + /** + * Recreate GPU resources after an HTML WebGL context restoration. + * + * The admitted LSML, latest accepted state, layout, text caches and retained + * graph remain owned by this instance. Only device/surface resources are + * replaced; the host must upload the required package images again. + * @param {HTMLCanvasElement} canvas + * @param {number} width + * @param {number} height + * @returns {Promise} + */ + restore_html_canvas(canvas, width, height) { + const ret = wasm.visiongpuengine_restore_html_canvas(this.__wbg_ptr, addHeapObject(canvas), width, height); + return takeObject(ret); + } + /** + * Replace one host-owned live camera or peer-video texture. + * @param {string} path + * @param {ImageBitmap} bitmap + */ + update_live_image(path, bitmap) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + const ptr0 = passStringToWasm0(path, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len0 = WASM_VECTOR_LEN; + wasm.visiongpuengine_update_live_image(retptr, this.__wbg_ptr, ptr0, len0, addHeapObject(bitmap)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } + } +} +if (Symbol.dispose) VisionGpuEngine.prototype[Symbol.dispose] = VisionGpuEngine.prototype.free; +function __wbg_get_imports() { + const import0 = { + __proto__: null, + __wbg_Error_30c8987f7c2ed4e2: function(arg0, arg1) { + const ret = Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); + }, + __wbg_Window_a2a6c4d665047b14: function(arg0) { + const ret = getObject(arg0).Window; + return addHeapObject(ret); + }, + __wbg_WorkerGlobalScope_2664448a7c667d67: function(arg0) { + const ret = getObject(arg0).WorkerGlobalScope; + return addHeapObject(ret); + }, + __wbg___wbindgen_boolean_get_5b446f51afd21013: function(arg0) { + const v = getObject(arg0); + const ret = typeof(v) === 'boolean' ? v : undefined; + return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; + }, + __wbg___wbindgen_debug_string_4687d8d8c2017d52: function(arg0, arg1) { + const ret = debugString(getObject(arg1)); + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_is_function_1f9d30630b8b1d3d: function(arg0) { + const ret = typeof(getObject(arg0)) === 'function'; + return ret; + }, + __wbg___wbindgen_is_null_e343b7d08827ba72: function(arg0) { + const ret = getObject(arg0) === null; + return ret; + }, + __wbg___wbindgen_is_string_90b56bc79aad6f6c: function(arg0) { + const ret = typeof(getObject(arg0)) === 'string'; + return ret; + }, + __wbg___wbindgen_is_undefined_8865fb403f8fe9d8: function(arg0) { + const ret = getObject(arg0) === undefined; + return ret; + }, + __wbg___wbindgen_number_get_2e0e7dee9f701a71: function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'number' ? obj : undefined; + getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); + }, + __wbg___wbindgen_string_get_0380ccaa2f57f0d9: function(arg0, arg1) { + const obj = getObject(arg1); + const ret = typeof(obj) === 'string' ? obj : undefined; + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg___wbindgen_throw_41e9ee4f547fc59a: function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + __wbg__wbg_cb_unref_dcc1a90847f04c41: function(arg0) { + getObject(arg0)._wbg_cb_unref(); + }, + __wbg_activeTexture_11079673eac5c921: function(arg0, arg1) { + getObject(arg0).activeTexture(arg1 >>> 0); + }, + __wbg_activeTexture_2fad46c01cafe6b1: function(arg0, arg1) { + getObject(arg0).activeTexture(arg1 >>> 0); + }, + __wbg_attachShader_8920a5beea54aba7: function(arg0, arg1, arg2) { + getObject(arg0).attachShader(getObject(arg1), getObject(arg2)); + }, + __wbg_attachShader_9567ae57af6746d5: function(arg0, arg1, arg2) { + getObject(arg0).attachShader(getObject(arg1), getObject(arg2)); + }, + __wbg_beginQuery_eaee271e7ccd67d1: function(arg0, arg1, arg2) { + getObject(arg0).beginQuery(arg1 >>> 0, getObject(arg2)); + }, + __wbg_beginRenderPass_3c53642423af50dc: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).beginRenderPass(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_bindAttribLocation_6a6f66cbe96ef24e: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).bindAttribLocation(getObject(arg1), arg2 >>> 0, getStringFromWasm0(arg3, arg4)); + }, + __wbg_bindAttribLocation_ade0ee986a734cdf: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).bindAttribLocation(getObject(arg1), arg2 >>> 0, getStringFromWasm0(arg3, arg4)); + }, + __wbg_bindBufferRange_a5aef3642afdabc8: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).bindBufferRange(arg1 >>> 0, arg2 >>> 0, getObject(arg3), arg4, arg5); + }, + __wbg_bindBuffer_0324f62b876c4403: function(arg0, arg1, arg2) { + getObject(arg0).bindBuffer(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindBuffer_2195e89b585ce0c3: function(arg0, arg1, arg2) { + getObject(arg0).bindBuffer(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindFramebuffer_22e4b8e6e6d8d0e8: function(arg0, arg1, arg2) { + getObject(arg0).bindFramebuffer(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindFramebuffer_378eb18ef219d37a: function(arg0, arg1, arg2) { + getObject(arg0).bindFramebuffer(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindRenderbuffer_db090531fb28de34: function(arg0, arg1, arg2) { + getObject(arg0).bindRenderbuffer(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindRenderbuffer_e0c5aeef245a913a: function(arg0, arg1, arg2) { + getObject(arg0).bindRenderbuffer(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindSampler_9b9743706aee71d1: function(arg0, arg1, arg2) { + getObject(arg0).bindSampler(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindTexture_3cf008f256f3ed99: function(arg0, arg1, arg2) { + getObject(arg0).bindTexture(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindTexture_613a47b8917a0ccd: function(arg0, arg1, arg2) { + getObject(arg0).bindTexture(arg1 >>> 0, getObject(arg2)); + }, + __wbg_bindVertexArrayOES_3141815cb8234a6e: function(arg0, arg1) { + getObject(arg0).bindVertexArrayOES(getObject(arg1)); + }, + __wbg_bindVertexArray_f58af0a346185ec5: function(arg0, arg1) { + getObject(arg0).bindVertexArray(getObject(arg1)); + }, + __wbg_blendColor_68fc6d6454f2128d: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).blendColor(arg1, arg2, arg3, arg4); + }, + __wbg_blendColor_a6256f41d1925899: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).blendColor(arg1, arg2, arg3, arg4); + }, + __wbg_blendEquationSeparate_0a17fa4ea863d445: function(arg0, arg1, arg2) { + getObject(arg0).blendEquationSeparate(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_blendEquationSeparate_fa2d4e5dde5ad8b3: function(arg0, arg1, arg2) { + getObject(arg0).blendEquationSeparate(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_blendEquation_5fb8aee6a55cf900: function(arg0, arg1) { + getObject(arg0).blendEquation(arg1 >>> 0); + }, + __wbg_blendEquation_d21914e937b14896: function(arg0, arg1) { + getObject(arg0).blendEquation(arg1 >>> 0); + }, + __wbg_blendFuncSeparate_28abe082febb1a26: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).blendFuncSeparate(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); + }, + __wbg_blendFuncSeparate_2af9d47f7efc0452: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).blendFuncSeparate(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); + }, + __wbg_blendFunc_491ee1105079ce76: function(arg0, arg1, arg2) { + getObject(arg0).blendFunc(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_blendFunc_589e4252bb396484: function(arg0, arg1, arg2) { + getObject(arg0).blendFunc(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_blitFramebuffer_7b39bfec6cfece56: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { + getObject(arg0).blitFramebuffer(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0); + }, + __wbg_bufferData_50f5eb06469330e4: function(arg0, arg1, arg2, arg3) { + getObject(arg0).bufferData(arg1 >>> 0, arg2, arg3 >>> 0); + }, + __wbg_bufferData_53e0f7a06b58fe81: function(arg0, arg1, arg2, arg3) { + getObject(arg0).bufferData(arg1 >>> 0, getObject(arg2), arg3 >>> 0); + }, + __wbg_bufferData_5fa44d94fe7a3878: function(arg0, arg1, arg2, arg3) { + getObject(arg0).bufferData(arg1 >>> 0, getObject(arg2), arg3 >>> 0); + }, + __wbg_bufferData_bad7ce562b6a9eac: function(arg0, arg1, arg2, arg3) { + getObject(arg0).bufferData(arg1 >>> 0, arg2, arg3 >>> 0); + }, + __wbg_bufferSubData_22c4c420e0f67cda: function(arg0, arg1, arg2, arg3) { + getObject(arg0).bufferSubData(arg1 >>> 0, arg2, getObject(arg3)); + }, + __wbg_bufferSubData_8609cd8a2c013181: function(arg0, arg1, arg2, arg3) { + getObject(arg0).bufferSubData(arg1 >>> 0, arg2, getObject(arg3)); + }, + __wbg_call_187d372bd5fdd4aa: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, arguments); }, + __wbg_clearBufferfv_05f7fc7252e623eb: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).clearBufferfv(arg1 >>> 0, arg2, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_clearBufferiv_e8e740accbd0f2df: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).clearBufferiv(arg1 >>> 0, arg2, getArrayI32FromWasm0(arg3, arg4)); + }, + __wbg_clearBufferuiv_e7f50f91f2195e38: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).clearBufferuiv(arg1 >>> 0, arg2, getArrayU32FromWasm0(arg3, arg4)); + }, + __wbg_clearDepth_4840a5497ae51286: function(arg0, arg1) { + getObject(arg0).clearDepth(arg1); + }, + __wbg_clearDepth_5a119c24c5d193bc: function(arg0, arg1) { + getObject(arg0).clearDepth(arg1); + }, + __wbg_clearStencil_1b8b8c727f598eec: function(arg0, arg1) { + getObject(arg0).clearStencil(arg1); + }, + __wbg_clearStencil_e4383e4a543e7997: function(arg0, arg1) { + getObject(arg0).clearStencil(arg1); + }, + __wbg_clear_175cf3cb6d98d72b: function(arg0, arg1) { + getObject(arg0).clear(arg1 >>> 0); + }, + __wbg_clear_cb96796cc568b7c8: function(arg0, arg1) { + getObject(arg0).clear(arg1 >>> 0); + }, + __wbg_clientWaitSync_73122aa040ce343d: function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).clientWaitSync(getObject(arg1), arg2 >>> 0, arg3 >>> 0); + return ret; + }, + __wbg_colorMask_4d6afbbbc3022fbe: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).colorMask(arg1 !== 0, arg2 !== 0, arg3 !== 0, arg4 !== 0); + }, + __wbg_colorMask_8c903995f6f873be: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).colorMask(arg1 !== 0, arg2 !== 0, arg3 !== 0, arg4 !== 0); + }, + __wbg_compileShader_073a75409aaf96d2: function(arg0, arg1) { + getObject(arg0).compileShader(getObject(arg1)); + }, + __wbg_compileShader_c2b53ff3a8851f60: function(arg0, arg1) { + getObject(arg0).compileShader(getObject(arg1)); + }, + __wbg_compressedTexSubImage2D_4942a783e20221c2: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) { + getObject(arg0).compressedTexSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, getObject(arg8)); + }, + __wbg_compressedTexSubImage2D_4c408678ff5f3056: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).compressedTexSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8, arg9); + }, + __wbg_compressedTexSubImage2D_80961cdbbd2b3aea: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) { + getObject(arg0).compressedTexSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, getObject(arg8)); + }, + __wbg_compressedTexSubImage3D_1e0f86237d82f8b9: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { + getObject(arg0).compressedTexSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, getObject(arg10)); + }, + __wbg_compressedTexSubImage3D_682e402a0ac201ba: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).compressedTexSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10, arg11); + }, + __wbg_configure_1e2c1c9edad07d26: function() { return handleError(function (arg0, arg1) { + getObject(arg0).configure(getObject(arg1)); + }, arguments); }, + __wbg_copyBufferSubData_e82219c4834bd9ae: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).copyBufferSubData(arg1 >>> 0, arg2 >>> 0, arg3, arg4, arg5); + }, + __wbg_copyExternalImageToTexture_116560f3be856776: function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).copyExternalImageToTexture(getObject(arg1), getObject(arg2), getObject(arg3)); + }, arguments); }, + __wbg_copyTexSubImage2D_455fba12cf4a776c: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) { + getObject(arg0).copyTexSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8); + }, + __wbg_copyTexSubImage2D_fe71e36efa520f37: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) { + getObject(arg0).copyTexSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8); + }, + __wbg_copyTexSubImage3D_717cd6c97934f2e6: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).copyTexSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9); + }, + __wbg_copyTextureToTexture_d2e6a1eb3254b828: function() { return handleError(function (arg0, arg1, arg2, arg3) { + getObject(arg0).copyTextureToTexture(getObject(arg1), getObject(arg2), getObject(arg3)); + }, arguments); }, + __wbg_createBindGroupLayout_b1bd63b4e88459d8: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).createBindGroupLayout(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_createBindGroup_f539b26ca341308f: function(arg0, arg1) { + const ret = getObject(arg0).createBindGroup(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_createBuffer_d1dd23bf8704564c: function(arg0) { + const ret = getObject(arg0).createBuffer(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createBuffer_d800e9b1d41b2ee5: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).createBuffer(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_createBuffer_ffe613371d42cfd3: function(arg0) { + const ret = getObject(arg0).createBuffer(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createCommandEncoder_3352d1ffc36c6fc0: function(arg0, arg1) { + const ret = getObject(arg0).createCommandEncoder(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_createFramebuffer_2c6b5b0f49c88e5d: function(arg0) { + const ret = getObject(arg0).createFramebuffer(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createFramebuffer_d0969af4e9886297: function(arg0) { + const ret = getObject(arg0).createFramebuffer(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createPipelineLayout_6eab52c327118937: function(arg0, arg1) { + const ret = getObject(arg0).createPipelineLayout(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_createProgram_cbf448e6f18a9402: function(arg0) { + const ret = getObject(arg0).createProgram(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createProgram_e0c81b0d2b8d0fd0: function(arg0) { + const ret = getObject(arg0).createProgram(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createQuery_ec241083d0b36197: function(arg0) { + const ret = getObject(arg0).createQuery(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createRenderPipeline_0ebb7ebc653e9207: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).createRenderPipeline(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_createRenderbuffer_2b17e9fe2e6fb4be: function(arg0) { + const ret = getObject(arg0).createRenderbuffer(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createRenderbuffer_c864095d6ebd60d0: function(arg0) { + const ret = getObject(arg0).createRenderbuffer(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createSampler_1b330d9da0c6523f: function(arg0) { + const ret = getObject(arg0).createSampler(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createSampler_9bd91d7e928c0060: function(arg0, arg1) { + const ret = getObject(arg0).createSampler(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_createShaderModule_cefa51336cb288ae: function(arg0, arg1) { + const ret = getObject(arg0).createShaderModule(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_createShader_6c1a2295587878a5: function(arg0, arg1) { + const ret = getObject(arg0).createShader(arg1 >>> 0); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createShader_7c9d99a21ecb08c0: function(arg0, arg1) { + const ret = getObject(arg0).createShader(arg1 >>> 0); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createTexture_01e8e591b64237de: function(arg0) { + const ret = getObject(arg0).createTexture(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createTexture_5f2583508ebe379b: function(arg0) { + const ret = getObject(arg0).createTexture(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createTexture_ed7e9fc04dd54d84: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).createTexture(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_createVertexArrayOES_a8dc8498b56df937: function(arg0) { + const ret = getObject(arg0).createVertexArrayOES(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createVertexArray_b223ea69dd08fcab: function(arg0) { + const ret = getObject(arg0).createVertexArray(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_createView_da41c2d2cb212715: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).createView(getObject(arg1)); + return addHeapObject(ret); + }, arguments); }, + __wbg_cullFace_12123c1c97f1ba2c: function(arg0, arg1) { + getObject(arg0).cullFace(arg1 >>> 0); + }, + __wbg_cullFace_1a01f0b6d5aeb687: function(arg0, arg1) { + getObject(arg0).cullFace(arg1 >>> 0); + }, + __wbg_deleteBuffer_070e58aaaf2a7a8b: function(arg0, arg1) { + getObject(arg0).deleteBuffer(getObject(arg1)); + }, + __wbg_deleteBuffer_7775ee9e248c8468: function(arg0, arg1) { + getObject(arg0).deleteBuffer(getObject(arg1)); + }, + __wbg_deleteFramebuffer_3f8ab330098bf8e6: function(arg0, arg1) { + getObject(arg0).deleteFramebuffer(getObject(arg1)); + }, + __wbg_deleteFramebuffer_570d3b214a509fa2: function(arg0, arg1) { + getObject(arg0).deleteFramebuffer(getObject(arg1)); + }, + __wbg_deleteProgram_822906f0368175d0: function(arg0, arg1) { + getObject(arg0).deleteProgram(getObject(arg1)); + }, + __wbg_deleteProgram_b524b30ad176cdb4: function(arg0, arg1) { + getObject(arg0).deleteProgram(getObject(arg1)); + }, + __wbg_deleteQuery_266895df9763e0b3: function(arg0, arg1) { + getObject(arg0).deleteQuery(getObject(arg1)); + }, + __wbg_deleteRenderbuffer_826c9b43b7ea68f5: function(arg0, arg1) { + getObject(arg0).deleteRenderbuffer(getObject(arg1)); + }, + __wbg_deleteRenderbuffer_f4e121a3ab21c605: function(arg0, arg1) { + getObject(arg0).deleteRenderbuffer(getObject(arg1)); + }, + __wbg_deleteSampler_f8afded8bf215d53: function(arg0, arg1) { + getObject(arg0).deleteSampler(getObject(arg1)); + }, + __wbg_deleteShader_a70f6cc1918d67eb: function(arg0, arg1) { + getObject(arg0).deleteShader(getObject(arg1)); + }, + __wbg_deleteShader_da18a294158c2d15: function(arg0, arg1) { + getObject(arg0).deleteShader(getObject(arg1)); + }, + __wbg_deleteSync_4f0be28b03f50495: function(arg0, arg1) { + getObject(arg0).deleteSync(getObject(arg1)); + }, + __wbg_deleteTexture_78c170241204a466: function(arg0, arg1) { + getObject(arg0).deleteTexture(getObject(arg1)); + }, + __wbg_deleteTexture_9f42750adb092e4f: function(arg0, arg1) { + getObject(arg0).deleteTexture(getObject(arg1)); + }, + __wbg_deleteVertexArrayOES_e01f31634d466176: function(arg0, arg1) { + getObject(arg0).deleteVertexArrayOES(getObject(arg1)); + }, + __wbg_deleteVertexArray_ca9768805d051d49: function(arg0, arg1) { + getObject(arg0).deleteVertexArray(getObject(arg1)); + }, + __wbg_depthFunc_92e96d8f46a23300: function(arg0, arg1) { + getObject(arg0).depthFunc(arg1 >>> 0); + }, + __wbg_depthFunc_a41fb5adacb22e09: function(arg0, arg1) { + getObject(arg0).depthFunc(arg1 >>> 0); + }, + __wbg_depthMask_06ecff4d4514c38f: function(arg0, arg1) { + getObject(arg0).depthMask(arg1 !== 0); + }, + __wbg_depthMask_0d7c3dec3b8405ee: function(arg0, arg1) { + getObject(arg0).depthMask(arg1 !== 0); + }, + __wbg_depthRange_24c7969249461e8e: function(arg0, arg1, arg2) { + getObject(arg0).depthRange(arg1, arg2); + }, + __wbg_depthRange_6a4e169ccf314148: function(arg0, arg1, arg2) { + getObject(arg0).depthRange(arg1, arg2); + }, + __wbg_disableVertexAttribArray_11c9f11bee30c9d8: function(arg0, arg1) { + getObject(arg0).disableVertexAttribArray(arg1 >>> 0); + }, + __wbg_disableVertexAttribArray_c4c8af1f9e4120d4: function(arg0, arg1) { + getObject(arg0).disableVertexAttribArray(arg1 >>> 0); + }, + __wbg_disable_19fe787eccdf0272: function(arg0, arg1) { + getObject(arg0).disable(arg1 >>> 0); + }, + __wbg_disable_81b2cb5f8cea15ae: function(arg0, arg1) { + getObject(arg0).disable(arg1 >>> 0); + }, + __wbg_displayHeight_903e670be87f29ab: function(arg0) { + const ret = getObject(arg0).displayHeight; + return ret; + }, + __wbg_displayWidth_5da2bf55e335368a: function(arg0) { + const ret = getObject(arg0).displayWidth; + return ret; + }, + __wbg_document_9854e03c05fc8834: function(arg0) { + const ret = getObject(arg0).document; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_drawArraysInstancedANGLE_b76512b5abba6668: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).drawArraysInstancedANGLE(arg1 >>> 0, arg2, arg3, arg4); + }, + __wbg_drawArraysInstanced_9b44aa78a087154d: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).drawArraysInstanced(arg1 >>> 0, arg2, arg3, arg4); + }, + __wbg_drawArrays_08872e02fb57b847: function(arg0, arg1, arg2, arg3) { + getObject(arg0).drawArrays(arg1 >>> 0, arg2, arg3); + }, + __wbg_drawArrays_741af5c3f8db636f: function(arg0, arg1, arg2, arg3) { + getObject(arg0).drawArrays(arg1 >>> 0, arg2, arg3); + }, + __wbg_drawBuffersWEBGL_c1bc0bbfd584c9b2: function(arg0, arg1) { + getObject(arg0).drawBuffersWEBGL(getObject(arg1)); + }, + __wbg_drawBuffers_7a58bd4729f7a1fc: function(arg0, arg1) { + getObject(arg0).drawBuffers(getObject(arg1)); + }, + __wbg_drawElementsInstancedANGLE_c3fc0cfa9e29a1bc: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).drawElementsInstancedANGLE(arg1 >>> 0, arg2, arg3 >>> 0, arg4, arg5); + }, + __wbg_drawElementsInstanced_d7c396aa034159b6: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).drawElementsInstanced(arg1 >>> 0, arg2, arg3 >>> 0, arg4, arg5); + }, + __wbg_drawIndexed_638959aae942557c: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).drawIndexed(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4, arg5 >>> 0); + }, + __wbg_draw_086a9578fc9898c2: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).draw(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); + }, + __wbg_enableVertexAttribArray_1496b78a8dbab9fd: function(arg0, arg1) { + getObject(arg0).enableVertexAttribArray(arg1 >>> 0); + }, + __wbg_enableVertexAttribArray_d52693866bc8b8dd: function(arg0, arg1) { + getObject(arg0).enableVertexAttribArray(arg1 >>> 0); + }, + __wbg_enable_6eeb9c0e9b483cee: function(arg0, arg1) { + getObject(arg0).enable(arg1 >>> 0); + }, + __wbg_enable_cb4fc19b51148c37: function(arg0, arg1) { + getObject(arg0).enable(arg1 >>> 0); + }, + __wbg_endQuery_73863d04a795d520: function(arg0, arg1) { + getObject(arg0).endQuery(arg1 >>> 0); + }, + __wbg_end_b57473834b877409: function(arg0) { + getObject(arg0).end(); + }, + __wbg_fenceSync_330293024adb66bc: function(arg0, arg1, arg2) { + const ret = getObject(arg0).fenceSync(arg1 >>> 0, arg2 >>> 0); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_finish_09ec094c10f41e7b: function(arg0) { + const ret = getObject(arg0).finish(); + return addHeapObject(ret); + }, + __wbg_finish_173a156040775de3: function(arg0) { + getObject(arg0).finish(); + }, + __wbg_finish_31ffb964be7e9749: function(arg0) { + getObject(arg0).finish(); + }, + __wbg_finish_ec1c191f66a895b1: function(arg0, arg1) { + const ret = getObject(arg0).finish(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_flush_13b162660ad850d2: function(arg0) { + getObject(arg0).flush(); + }, + __wbg_flush_a5ad38200881ac35: function(arg0) { + getObject(arg0).flush(); + }, + __wbg_framebufferRenderbuffer_1c6337e0030411f3: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).framebufferRenderbuffer(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, getObject(arg4)); + }, + __wbg_framebufferRenderbuffer_f019e9231b142c0d: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).framebufferRenderbuffer(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, getObject(arg4)); + }, + __wbg_framebufferTexture2D_73c52f23ec33aa22: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).framebufferTexture2D(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, getObject(arg4), arg5); + }, + __wbg_framebufferTexture2D_caddefa1f767646e: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).framebufferTexture2D(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, getObject(arg4), arg5); + }, + __wbg_framebufferTextureLayer_1d5c0f0e3234e521: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).framebufferTextureLayer(arg1 >>> 0, arg2 >>> 0, getObject(arg3), arg4, arg5); + }, + __wbg_framebufferTextureMultiviewOVR_77f19224ff7be6ef: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + getObject(arg0).framebufferTextureMultiviewOVR(arg1 >>> 0, arg2 >>> 0, getObject(arg3), arg4, arg5, arg6); + }, + __wbg_frontFace_6751b4b97f997aeb: function(arg0, arg1) { + getObject(arg0).frontFace(arg1 >>> 0); + }, + __wbg_frontFace_b064adb15b6f3772: function(arg0, arg1) { + getObject(arg0).frontFace(arg1 >>> 0); + }, + __wbg_getBindGroupLayout_fb40c3b93d37f466: function(arg0, arg1) { + const ret = getObject(arg0).getBindGroupLayout(arg1 >>> 0); + return addHeapObject(ret); + }, + __wbg_getBufferSubData_dd23246099b166d1: function(arg0, arg1, arg2, arg3) { + getObject(arg0).getBufferSubData(arg1 >>> 0, arg2, getObject(arg3)); + }, + __wbg_getContext_051324720aa5b7e8: function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2), getObject(arg3)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_getContext_189ce278aa985704: function() { return handleError(function (arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2), getObject(arg3)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_getContext_635e36719cad2623: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_getContext_e567868594d2e0c6: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).getContext(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_getCurrentTexture_9f3b84d0eaa6cd95: function() { return handleError(function (arg0) { + const ret = getObject(arg0).getCurrentTexture(); + return addHeapObject(ret); + }, arguments); }, + __wbg_getExtension_abfda2c5db286b07: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).getExtension(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_getIndexedParameter_0f49864473e337fa: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).getIndexedParameter(arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); + }, arguments); }, + __wbg_getParameter_974595a6624c6c10: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).getParameter(arg1 >>> 0); + return addHeapObject(ret); + }, arguments); }, + __wbg_getParameter_a97f491753b05668: function() { return handleError(function (arg0, arg1) { + const ret = getObject(arg0).getParameter(arg1 >>> 0); + return addHeapObject(ret); + }, arguments); }, + __wbg_getPreferredCanvasFormat_0ef5034c8902201b: function(arg0) { + const ret = getObject(arg0).getPreferredCanvasFormat(); + return (__wbindgen_enum_GpuTextureFormat.indexOf(ret) + 1 || 102) - 1; + }, + __wbg_getProgramInfoLog_208fb1ff31006495: function(arg0, arg1, arg2) { + const ret = getObject(arg1).getProgramInfoLog(getObject(arg2)); + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_getProgramInfoLog_3276b294d6d42fe2: function(arg0, arg1, arg2) { + const ret = getObject(arg1).getProgramInfoLog(getObject(arg2)); + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_getProgramParameter_50bafab01cf88652: function(arg0, arg1, arg2) { + const ret = getObject(arg0).getProgramParameter(getObject(arg1), arg2 >>> 0); + return addHeapObject(ret); + }, + __wbg_getProgramParameter_fc7124bb9fb7c675: function(arg0, arg1, arg2) { + const ret = getObject(arg0).getProgramParameter(getObject(arg1), arg2 >>> 0); + return addHeapObject(ret); + }, + __wbg_getQueryParameter_937ba13f77db778f: function(arg0, arg1, arg2) { + const ret = getObject(arg0).getQueryParameter(getObject(arg1), arg2 >>> 0); + return addHeapObject(ret); + }, + __wbg_getShaderInfoLog_7e0cbba353ca95da: function(arg0, arg1, arg2) { + const ret = getObject(arg1).getShaderInfoLog(getObject(arg2)); + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_getShaderInfoLog_bb64c5a52df0b7a3: function(arg0, arg1, arg2) { + const ret = getObject(arg1).getShaderInfoLog(getObject(arg2)); + var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + var len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_getShaderParameter_745acf10b8e45e31: function(arg0, arg1, arg2) { + const ret = getObject(arg0).getShaderParameter(getObject(arg1), arg2 >>> 0); + return addHeapObject(ret); + }, + __wbg_getShaderParameter_e55ea9c456df5484: function(arg0, arg1, arg2) { + const ret = getObject(arg0).getShaderParameter(getObject(arg1), arg2 >>> 0); + return addHeapObject(ret); + }, + __wbg_getSupportedExtensions_1459bad37860c0c3: function(arg0) { + const ret = getObject(arg0).getSupportedExtensions(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_getSupportedProfiles_7b2acfa66b565d24: function(arg0) { + const ret = getObject(arg0).getSupportedProfiles(); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_getSyncParameter_bb4be47b67bfa2f8: function(arg0, arg1, arg2) { + const ret = getObject(arg0).getSyncParameter(getObject(arg1), arg2 >>> 0); + return addHeapObject(ret); + }, + __wbg_getUniformBlockIndex_5e2252ceb34531a5: function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).getUniformBlockIndex(getObject(arg1), getStringFromWasm0(arg2, arg3)); + return ret; + }, + __wbg_getUniformLocation_50ba45dade572703: function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).getUniformLocation(getObject(arg1), getStringFromWasm0(arg2, arg3)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_getUniformLocation_cb4e3e121235f91a: function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).getUniformLocation(getObject(arg1), getStringFromWasm0(arg2, arg3)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_get_6c896e0571ddae51: function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }, + __wbg_get_fd12a9100976c296: function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_get_unchecked_288889d017702237: function(arg0, arg1) { + const ret = getObject(arg0)[arg1 >>> 0]; + return addHeapObject(ret); + }, + __wbg_gpu_afdd4387c7afe5f9: function(arg0) { + const ret = getObject(arg0).gpu; + return addHeapObject(ret); + }, + __wbg_height_50d2b2bc8c6ec859: function(arg0) { + const ret = getObject(arg0).height; + return ret; + }, + __wbg_height_816341c9bbd4e9e3: function(arg0) { + const ret = getObject(arg0).height; + return ret; + }, + __wbg_height_9dca4d43c3e4b84b: function(arg0) { + const ret = getObject(arg0).height; + return ret; + }, + __wbg_height_aa756cf29b6d629c: function(arg0) { + const ret = getObject(arg0).height; + return ret; + }, + __wbg_height_fc97e1a0c2e7331f: function(arg0) { + const ret = getObject(arg0).height; + return ret; + }, + __wbg_includes_9b0dfa946c313621: function(arg0, arg1, arg2) { + const ret = getObject(arg0).includes(getObject(arg1), arg2); + return ret; + }, + __wbg_instanceof_GpuOutOfMemoryError_5c3b8a2499f59adb: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof GPUOutOfMemoryError; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_GpuValidationError_7659e07b12d4e184: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof GPUValidationError; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_HtmlCanvasElement_0a30c11fbbf41841: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof HTMLCanvasElement; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Uint8Array_828cef2aaacafc31: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Uint8Array; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_WebGl2RenderingContext_64c994c14c0790fb: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof WebGL2RenderingContext; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_instanceof_Window_82d71df4eddf88bc: function(arg0) { + let result; + try { + result = getObject(arg0) instanceof Window; + } catch (_) { + result = false; + } + const ret = result; + return ret; + }, + __wbg_invalidateFramebuffer_4f576db65bb37d6a: function() { return handleError(function (arg0, arg1, arg2) { + getObject(arg0).invalidateFramebuffer(arg1 >>> 0, getObject(arg2)); + }, arguments); }, + __wbg_is_4b278c0bd3caba97: function(arg0, arg1) { + const ret = Object.is(getObject(arg0), getObject(arg1)); + return ret; + }, + __wbg_label_7add8cb37a6ef98f: function(arg0, arg1) { + const ret = getObject(arg1).label; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_length_7f3c00c40364105e: function(arg0) { + const ret = getObject(arg0).length; + return ret; + }, + __wbg_length_d4bdea10311bd9cf: function(arg0) { + const ret = getObject(arg0).length; + return ret; + }, + __wbg_limits_06bcb36c8409843b: function(arg0) { + const ret = getObject(arg0).limits; + return addHeapObject(ret); + }, + __wbg_limits_601ad2e086ef8141: function(arg0) { + const ret = getObject(arg0).limits; + return addHeapObject(ret); + }, + __wbg_linkProgram_571a9ea00d38ff83: function(arg0, arg1) { + getObject(arg0).linkProgram(getObject(arg1)); + }, + __wbg_linkProgram_acdc299c43d734a0: function(arg0, arg1) { + getObject(arg0).linkProgram(getObject(arg1)); + }, + __wbg_mapAsync_b0597127f5037286: function(arg0, arg1, arg2, arg3) { + const ret = getObject(arg0).mapAsync(arg1 >>> 0, arg2, arg3); + return addHeapObject(ret); + }, + __wbg_matchMedia_8a4857f947f11f82: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).matchMedia(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_matches_778d0873b064b1bf: function(arg0) { + const ret = getObject(arg0).matches; + return ret; + }, + __wbg_maxBindGroupsPlusVertexBuffers_52369f089736ef9d: function(arg0) { + const ret = getObject(arg0).maxBindGroupsPlusVertexBuffers; + return ret; + }, + __wbg_maxBindGroups_4e424afe6ce86ca2: function(arg0) { + const ret = getObject(arg0).maxBindGroups; + return ret; + }, + __wbg_maxBindingsPerBindGroup_7d035da36821c44f: function(arg0) { + const ret = getObject(arg0).maxBindingsPerBindGroup; + return ret; + }, + __wbg_maxBufferSize_423f4a084e32a195: function(arg0) { + const ret = getObject(arg0).maxBufferSize; + return ret; + }, + __wbg_maxColorAttachmentBytesPerSample_c4cd9126f6d287c6: function(arg0) { + const ret = getObject(arg0).maxColorAttachmentBytesPerSample; + return ret; + }, + __wbg_maxColorAttachments_d924670762b9e250: function(arg0) { + const ret = getObject(arg0).maxColorAttachments; + return ret; + }, + __wbg_maxComputeInvocationsPerWorkgroup_707a3868f7cebb59: function(arg0) { + const ret = getObject(arg0).maxComputeInvocationsPerWorkgroup; + return ret; + }, + __wbg_maxComputeWorkgroupSizeX_0a4d99463cbd6e5e: function(arg0) { + const ret = getObject(arg0).maxComputeWorkgroupSizeX; + return ret; + }, + __wbg_maxComputeWorkgroupSizeY_85123ea0587f7558: function(arg0) { + const ret = getObject(arg0).maxComputeWorkgroupSizeY; + return ret; + }, + __wbg_maxComputeWorkgroupSizeZ_a3186b4c5267d44f: function(arg0) { + const ret = getObject(arg0).maxComputeWorkgroupSizeZ; + return ret; + }, + __wbg_maxComputeWorkgroupStorageSize_57b297355cfb6204: function(arg0) { + const ret = getObject(arg0).maxComputeWorkgroupStorageSize; + return ret; + }, + __wbg_maxComputeWorkgroupsPerDimension_4158f95e673d54c4: function(arg0) { + const ret = getObject(arg0).maxComputeWorkgroupsPerDimension; + return ret; + }, + __wbg_maxDynamicStorageBuffersPerPipelineLayout_226b0b70910aa16c: function(arg0) { + const ret = getObject(arg0).maxDynamicStorageBuffersPerPipelineLayout; + return ret; + }, + __wbg_maxDynamicUniformBuffersPerPipelineLayout_0e835fda711fc7e6: function(arg0) { + const ret = getObject(arg0).maxDynamicUniformBuffersPerPipelineLayout; + return ret; + }, + __wbg_maxInterStageShaderVariables_8c4a1d727e2aa35a: function(arg0) { + const ret = getObject(arg0).maxInterStageShaderVariables; + return ret; + }, + __wbg_maxSampledTexturesPerShaderStage_6675f5e91d9a728a: function(arg0) { + const ret = getObject(arg0).maxSampledTexturesPerShaderStage; + return ret; + }, + __wbg_maxSamplersPerShaderStage_1910fa38a6ed1e1f: function(arg0) { + const ret = getObject(arg0).maxSamplersPerShaderStage; + return ret; + }, + __wbg_maxStorageBufferBindingSize_2e244bded070b18d: function(arg0) { + const ret = getObject(arg0).maxStorageBufferBindingSize; + return ret; + }, + __wbg_maxStorageBuffersPerShaderStage_a285f3ebca51ca0d: function(arg0) { + const ret = getObject(arg0).maxStorageBuffersPerShaderStage; + return ret; + }, + __wbg_maxStorageTexturesPerShaderStage_7aa946f0fc322a2b: function(arg0) { + const ret = getObject(arg0).maxStorageTexturesPerShaderStage; + return ret; + }, + __wbg_maxTextureArrayLayers_0e699147ad00502d: function(arg0) { + const ret = getObject(arg0).maxTextureArrayLayers; + return ret; + }, + __wbg_maxTextureDimension1D_aabf6add54decfe2: function(arg0) { + const ret = getObject(arg0).maxTextureDimension1D; + return ret; + }, + __wbg_maxTextureDimension2D_dd598b27e9c0c1c4: function(arg0) { + const ret = getObject(arg0).maxTextureDimension2D; + return ret; + }, + __wbg_maxTextureDimension3D_f944266c65dfd1a9: function(arg0) { + const ret = getObject(arg0).maxTextureDimension3D; + return ret; + }, + __wbg_maxUniformBufferBindingSize_59fa6be7cfbeeb53: function(arg0) { + const ret = getObject(arg0).maxUniformBufferBindingSize; + return ret; + }, + __wbg_maxUniformBuffersPerShaderStage_bee5f00a4d706c7f: function(arg0) { + const ret = getObject(arg0).maxUniformBuffersPerShaderStage; + return ret; + }, + __wbg_maxVertexAttributes_5cf6392c4e9033fe: function(arg0) { + const ret = getObject(arg0).maxVertexAttributes; + return ret; + }, + __wbg_maxVertexBufferArrayStride_548baa887375d865: function(arg0) { + const ret = getObject(arg0).maxVertexBufferArrayStride; + return ret; + }, + __wbg_maxVertexBuffers_75d881156591f5da: function(arg0) { + const ret = getObject(arg0).maxVertexBuffers; + return ret; + }, + __wbg_message_2aad50368e15e8f1: function(arg0, arg1) { + const ret = getObject(arg1).message; + const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2); + const len1 = WASM_VECTOR_LEN; + getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); + getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); + }, + __wbg_minStorageBufferOffsetAlignment_5ba9b77792bdadb3: function(arg0) { + const ret = getObject(arg0).minStorageBufferOffsetAlignment; + return ret; + }, + __wbg_minUniformBufferOffsetAlignment_ab7d52a5293b22bd: function(arg0) { + const ret = getObject(arg0).minUniformBufferOffsetAlignment; + return ret; + }, + __wbg_navigator_2156486643462a87: function(arg0) { + const ret = getObject(arg0).navigator; + return addHeapObject(ret); + }, + __wbg_navigator_c9bce48d4b578c9c: function(arg0) { + const ret = getObject(arg0).navigator; + return addHeapObject(ret); + }, + __wbg_new_1dbf7428bba60a42: function(arg0) { + const ret = new Uint8Array(getObject(arg0)); + return addHeapObject(ret); + }, + __wbg_new_617a8cdb8bb1130e: function() { + const ret = new Object(); + return addHeapObject(ret); + }, + __wbg_new_a217df4351db4b9b: function() { return handleError(function (arg0, arg1) { + const ret = new OffscreenCanvas(arg0 >>> 0, arg1 >>> 0); + return addHeapObject(ret); + }, arguments); }, + __wbg_new_ee2291f50781bf1d: function() { + const ret = new Array(); + return addHeapObject(ret); + }, + __wbg_new_typed_689a3a281a8d4da6: function() { + const ret = new Object(); + return addHeapObject(ret); + }, + __wbg_new_typed_b01cb72a8af741a3: function(arg0, arg1) { + try { + var state0 = {a: arg0, b: arg1}; + var cb0 = (arg0, arg1) => { + const a = state0.a; + state0.a = 0; + try { + return __wasm_bindgen_func_elem_9927(a, state0.b, arg0, arg1); + } finally { + state0.a = a; + } + }; + const ret = new Promise(cb0); + return addHeapObject(ret); + } finally { + state0.a = 0; + } + }, + __wbg_now_e7c6795a7f81e10f: function(arg0) { + const ret = getObject(arg0).now(); + return ret; + }, + __wbg_of_d5dbecdc59e02e34: function(arg0) { + const ret = Array.of(getObject(arg0)); + return addHeapObject(ret); + }, + __wbg_onSubmittedWorkDone_1190213cee1ecf7e: function(arg0) { + const ret = getObject(arg0).onSubmittedWorkDone(); + return addHeapObject(ret); + }, + __wbg_performance_3fcf6e32a7e1ed0a: function(arg0) { + const ret = getObject(arg0).performance; + return addHeapObject(ret); + }, + __wbg_pixelStorei_af18e636b3e7b725: function(arg0, arg1, arg2) { + getObject(arg0).pixelStorei(arg1 >>> 0, arg2); + }, + __wbg_pixelStorei_cb4e43466f5b8703: function(arg0, arg1, arg2) { + getObject(arg0).pixelStorei(arg1 >>> 0, arg2); + }, + __wbg_polygonOffset_81dc8ca84e29572b: function(arg0, arg1, arg2) { + getObject(arg0).polygonOffset(arg1, arg2); + }, + __wbg_polygonOffset_8f922d9173d8849d: function(arg0, arg1, arg2) { + getObject(arg0).polygonOffset(arg1, arg2); + }, + __wbg_popErrorScope_182b8e03671d81ef: function(arg0) { + const ret = getObject(arg0).popErrorScope(); + return addHeapObject(ret); + }, + __wbg_prototypesetcall_bc27214492979395: function(arg0, arg1, arg2) { + Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), getObject(arg2)); + }, + __wbg_pushErrorScope_fa7df784c43bdb51: function(arg0, arg1) { + getObject(arg0).pushErrorScope(__wbindgen_enum_GpuErrorFilter[arg1]); + }, + __wbg_push_2baf45db356cf468: function(arg0, arg1) { + const ret = getObject(arg0).push(getObject(arg1)); + return ret; + }, + __wbg_queryCounterEXT_9016a6ffa350b4b2: function(arg0, arg1, arg2) { + getObject(arg0).queryCounterEXT(getObject(arg1), arg2 >>> 0); + }, + __wbg_querySelectorAll_6aebfe3df1fb2014: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).querySelectorAll(getStringFromWasm0(arg1, arg2)); + return addHeapObject(ret); + }, arguments); }, + __wbg_querySelector_ecbc2550705b235f: function() { return handleError(function (arg0, arg1, arg2) { + const ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2)); + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, arguments); }, + __wbg_queueMicrotask_9833f9a49df95a49: function(arg0) { + const ret = getObject(arg0).queueMicrotask; + return addHeapObject(ret); + }, + __wbg_queueMicrotask_a72f977e97f23c5f: function(arg0) { + queueMicrotask(getObject(arg0)); + }, + __wbg_queue_7b62c28143d44293: function(arg0) { + const ret = getObject(arg0).queue; + return addHeapObject(ret); + }, + __wbg_readBuffer_b5d9a34a2f3d20d5: function(arg0, arg1) { + getObject(arg0).readBuffer(arg1 >>> 0); + }, + __wbg_readPixels_31e44e70558ac590: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + getObject(arg0).readPixels(arg1, arg2, arg3, arg4, arg5 >>> 0, arg6 >>> 0, getObject(arg7)); + }, arguments); }, + __wbg_readPixels_3321086a7818a739: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + getObject(arg0).readPixels(arg1, arg2, arg3, arg4, arg5 >>> 0, arg6 >>> 0, getObject(arg7)); + }, arguments); }, + __wbg_readPixels_eb030cd9976eeca5: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7) { + getObject(arg0).readPixels(arg1, arg2, arg3, arg4, arg5 >>> 0, arg6 >>> 0, arg7); + }, arguments); }, + __wbg_renderbufferStorageMultisample_ed16f3ef446e3bc7: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).renderbufferStorageMultisample(arg1 >>> 0, arg2, arg3 >>> 0, arg4, arg5); + }, + __wbg_renderbufferStorage_52b131bbf3112b3a: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).renderbufferStorage(arg1 >>> 0, arg2 >>> 0, arg3, arg4); + }, + __wbg_renderbufferStorage_bca487b0dea425b8: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).renderbufferStorage(arg1 >>> 0, arg2 >>> 0, arg3, arg4); + }, + __wbg_requestAdapter_a539af006419f2e9: function(arg0, arg1) { + const ret = getObject(arg0).requestAdapter(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_requestDevice_5cb8a582e55d08cb: function(arg0, arg1) { + const ret = getObject(arg0).requestDevice(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_resolve_0076e10020304ede: function(arg0) { + const ret = Promise.resolve(getObject(arg0)); + return addHeapObject(ret); + }, + __wbg_samplerParameterf_2d62cb907392e2d6: function(arg0, arg1, arg2, arg3) { + getObject(arg0).samplerParameterf(getObject(arg1), arg2 >>> 0, arg3); + }, + __wbg_samplerParameteri_a5615e67ed0b654b: function(arg0, arg1, arg2, arg3) { + getObject(arg0).samplerParameteri(getObject(arg1), arg2 >>> 0, arg3); + }, + __wbg_scissor_12ddc324094acd50: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).scissor(arg1, arg2, arg3, arg4); + }, + __wbg_scissor_a9468482697c2bdb: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).scissor(arg1, arg2, arg3, arg4); + }, + __wbg_setBindGroup_11bdbb60cc8b54b9: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2), getArrayU32FromWasm0(arg3, arg4), arg5, arg6 >>> 0); + }, arguments); }, + __wbg_setBindGroup_418c3e0eb6943ce0: function(arg0, arg1, arg2) { + getObject(arg0).setBindGroup(arg1 >>> 0, getObject(arg2)); + }, + __wbg_setIndexBuffer_01327df91742b73e: function(arg0, arg1, arg2, arg3) { + getObject(arg0).setIndexBuffer(getObject(arg1), __wbindgen_enum_GpuIndexFormat[arg2], arg3); + }, + __wbg_setIndexBuffer_241097e303986c14: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setIndexBuffer(getObject(arg1), __wbindgen_enum_GpuIndexFormat[arg2], arg3, arg4); + }, + __wbg_setPipeline_b6f981027e02cd16: function(arg0, arg1) { + getObject(arg0).setPipeline(getObject(arg1)); + }, + __wbg_setScissorRect_889235eeb784732b: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setScissorRect(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); + }, + __wbg_setVertexBuffer_6db3b60e99280744: function(arg0, arg1, arg2, arg3) { + getObject(arg0).setVertexBuffer(arg1 >>> 0, getObject(arg2), arg3); + }, + __wbg_setVertexBuffer_cbf4ca1627c02f4c: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).setVertexBuffer(arg1 >>> 0, getObject(arg2), arg3, arg4); + }, + __wbg_set_145a351398b48c65: function() { return handleError(function (arg0, arg1, arg2) { + const ret = Reflect.set(getObject(arg0), getObject(arg1), getObject(arg2)); + return ret; + }, arguments); }, + __wbg_set_a_82818effc94f6256: function(arg0, arg1) { + getObject(arg0).a = arg1; + }, + __wbg_set_access_a099cfbbeec9b96f: function(arg0, arg1) { + getObject(arg0).access = __wbindgen_enum_GpuStorageTextureAccess[arg1]; + }, + __wbg_set_address_mode_u_a68737cf5d288f95: function(arg0, arg1) { + getObject(arg0).addressModeU = __wbindgen_enum_GpuAddressMode[arg1]; + }, + __wbg_set_address_mode_v_b1c3c45933f540d1: function(arg0, arg1) { + getObject(arg0).addressModeV = __wbindgen_enum_GpuAddressMode[arg1]; + }, + __wbg_set_address_mode_w_889c31cf7022c764: function(arg0, arg1) { + getObject(arg0).addressModeW = __wbindgen_enum_GpuAddressMode[arg1]; + }, + __wbg_set_alpha_106f21a936a85eba: function(arg0, arg1) { + getObject(arg0).alpha = getObject(arg1); + }, + __wbg_set_alpha_mode_5544568dbac50280: function(arg0, arg1) { + getObject(arg0).alphaMode = __wbindgen_enum_GpuCanvasAlphaMode[arg1]; + }, + __wbg_set_alpha_to_coverage_enabled_3372ce329447b8f1: function(arg0, arg1) { + getObject(arg0).alphaToCoverageEnabled = arg1 !== 0; + }, + __wbg_set_array_layer_count_22afa0a979e4ad55: function(arg0, arg1) { + getObject(arg0).arrayLayerCount = arg1 >>> 0; + }, + __wbg_set_array_stride_f64_6816040e5e7598c3: function(arg0, arg1) { + getObject(arg0).arrayStride = arg1; + }, + __wbg_set_aspect_9de5253d4c460895: function(arg0, arg1) { + getObject(arg0).aspect = __wbindgen_enum_GpuTextureAspect[arg1]; + }, + __wbg_set_aspect_a48d046965270281: function(arg0, arg1) { + getObject(arg0).aspect = __wbindgen_enum_GpuTextureAspect[arg1]; + }, + __wbg_set_aspect_b1a9909bf315433f: function(arg0, arg1) { + getObject(arg0).aspect = __wbindgen_enum_GpuTextureAspect[arg1]; + }, + __wbg_set_attributes_9e38cb1dde387a5b: function(arg0, arg1, arg2) { + getObject(arg0).attributes = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_b_a3297ee7e7cac3a8: function(arg0, arg1) { + getObject(arg0).b = arg1; + }, + __wbg_set_base_array_layer_2435ba92c80346ae: function(arg0, arg1) { + getObject(arg0).baseArrayLayer = arg1 >>> 0; + }, + __wbg_set_base_mip_level_8b6093e875e7c65d: function(arg0, arg1) { + getObject(arg0).baseMipLevel = arg1 >>> 0; + }, + __wbg_set_beginning_of_pass_write_index_e552c5e8b8bbf52f: function(arg0, arg1) { + getObject(arg0).beginningOfPassWriteIndex = arg1 >>> 0; + }, + __wbg_set_bind_group_layouts_458c44ba55100b82: function(arg0, arg1, arg2) { + getObject(arg0).bindGroupLayouts = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_binding_81b3fac7f7acaf8d: function(arg0, arg1) { + getObject(arg0).binding = arg1 >>> 0; + }, + __wbg_set_binding_b6cee57f35ac5190: function(arg0, arg1) { + getObject(arg0).binding = arg1 >>> 0; + }, + __wbg_set_blend_1a801617945f7945: function(arg0, arg1) { + getObject(arg0).blend = getObject(arg1); + }, + __wbg_set_buffer_1548ae88a9188037: function(arg0, arg1) { + getObject(arg0).buffer = getObject(arg1); + }, + __wbg_set_buffer_8d0ac64ad20dfc84: function(arg0, arg1) { + getObject(arg0).buffer = getObject(arg1); + }, + __wbg_set_buffers_5d0e0c50791f710e: function(arg0, arg1, arg2) { + getObject(arg0).buffers = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_bytes_per_row_c28583f0063160f1: function(arg0, arg1) { + getObject(arg0).bytesPerRow = arg1 >>> 0; + }, + __wbg_set_clear_value_gpu_color_dict_a9f763e8372ac1de: function(arg0, arg1) { + getObject(arg0).clearValue = getObject(arg1); + }, + __wbg_set_code_5d5b0b9e2fd0dca7: function(arg0, arg1, arg2) { + getObject(arg0).code = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_color_8ecace4011f47d2e: function(arg0, arg1) { + getObject(arg0).color = getObject(arg1); + }, + __wbg_set_color_attachments_622fe2d5997fda7a: function(arg0, arg1, arg2) { + getObject(arg0).colorAttachments = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_compare_080c9e492ff36990: function(arg0, arg1) { + getObject(arg0).compare = __wbindgen_enum_GpuCompareFunction[arg1]; + }, + __wbg_set_compare_817cf3695599eaa6: function(arg0, arg1) { + getObject(arg0).compare = __wbindgen_enum_GpuCompareFunction[arg1]; + }, + __wbg_set_count_8ff0c9474e39a849: function(arg0, arg1) { + getObject(arg0).count = arg1 >>> 0; + }, + __wbg_set_cull_mode_85d2b4ab0ce3a564: function(arg0, arg1) { + getObject(arg0).cullMode = __wbindgen_enum_GpuCullMode[arg1]; + }, + __wbg_set_depth_bias_95abf479cae3f3cd: function(arg0, arg1) { + getObject(arg0).depthBias = arg1; + }, + __wbg_set_depth_bias_clamp_ba3d0b8348151350: function(arg0, arg1) { + getObject(arg0).depthBiasClamp = arg1; + }, + __wbg_set_depth_bias_slope_scale_6b2584d93f5b9cd2: function(arg0, arg1) { + getObject(arg0).depthBiasSlopeScale = arg1; + }, + __wbg_set_depth_clear_value_e30a4c754c6b3b26: function(arg0, arg1) { + getObject(arg0).depthClearValue = arg1; + }, + __wbg_set_depth_compare_a90de4e3714397ab: function(arg0, arg1) { + getObject(arg0).depthCompare = __wbindgen_enum_GpuCompareFunction[arg1]; + }, + __wbg_set_depth_fail_op_b5c64541d1b6b482: function(arg0, arg1) { + getObject(arg0).depthFailOp = __wbindgen_enum_GpuStencilOperation[arg1]; + }, + __wbg_set_depth_load_op_932888016d762d3e: function(arg0, arg1) { + getObject(arg0).depthLoadOp = __wbindgen_enum_GpuLoadOp[arg1]; + }, + __wbg_set_depth_or_array_layers_e2f074a0284e4806: function(arg0, arg1) { + getObject(arg0).depthOrArrayLayers = arg1 >>> 0; + }, + __wbg_set_depth_read_only_be790175a1c2db9a: function(arg0, arg1) { + getObject(arg0).depthReadOnly = arg1 !== 0; + }, + __wbg_set_depth_stencil_attachment_54a8922f5fbe08bf: function(arg0, arg1) { + getObject(arg0).depthStencilAttachment = getObject(arg1); + }, + __wbg_set_depth_stencil_b7cffc59ad4da529: function(arg0, arg1) { + getObject(arg0).depthStencil = getObject(arg1); + }, + __wbg_set_depth_store_op_9054814f164ab55d: function(arg0, arg1) { + getObject(arg0).depthStoreOp = __wbindgen_enum_GpuStoreOp[arg1]; + }, + __wbg_set_depth_write_enabled_31a821ee1fb3b0b3: function(arg0, arg1) { + getObject(arg0).depthWriteEnabled = arg1 !== 0; + }, + __wbg_set_device_210484a77b675c9c: function(arg0, arg1) { + getObject(arg0).device = getObject(arg1); + }, + __wbg_set_dimension_3da9d03131a9f446: function(arg0, arg1) { + getObject(arg0).dimension = __wbindgen_enum_GpuTextureDimension[arg1]; + }, + __wbg_set_dimension_56332450afa3e0c0: function(arg0, arg1) { + getObject(arg0).dimension = __wbindgen_enum_GpuTextureViewDimension[arg1]; + }, + __wbg_set_dst_factor_865ba9aaf187890c: function(arg0, arg1) { + getObject(arg0).dstFactor = __wbindgen_enum_GpuBlendFactor[arg1]; + }, + __wbg_set_end_of_pass_write_index_8f164f9e60d4ad16: function(arg0, arg1) { + getObject(arg0).endOfPassWriteIndex = arg1 >>> 0; + }, + __wbg_set_entries_6f866302103b81e9: function(arg0, arg1, arg2) { + getObject(arg0).entries = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_entries_f26b77ab9548e906: function(arg0, arg1, arg2) { + getObject(arg0).entries = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_entry_point_71cef95c137b5774: function(arg0, arg1, arg2) { + getObject(arg0).entryPoint = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_entry_point_b70f98f5025a114d: function(arg0, arg1, arg2) { + getObject(arg0).entryPoint = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_external_texture_7f966c604c4f8098: function(arg0, arg1) { + getObject(arg0).externalTexture = getObject(arg1); + }, + __wbg_set_fail_op_d59d0187e4111dfe: function(arg0, arg1) { + getObject(arg0).failOp = __wbindgen_enum_GpuStencilOperation[arg1]; + }, + __wbg_set_flip_y_5a9a7c3fd5a2314e: function(arg0, arg1) { + getObject(arg0).flipY = arg1 !== 0; + }, + __wbg_set_format_23f7f32549751d43: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuTextureFormat[arg1]; + }, + __wbg_set_format_283dca56552f07a3: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuTextureFormat[arg1]; + }, + __wbg_set_format_5080a858117ad2c1: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuVertexFormat[arg1]; + }, + __wbg_set_format_66735b94bd868ba2: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuTextureFormat[arg1]; + }, + __wbg_set_format_7f2bdbfb101b1ae1: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuTextureFormat[arg1]; + }, + __wbg_set_format_92732ea75d3b79f5: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuTextureFormat[arg1]; + }, + __wbg_set_format_f009e603f7d4c28e: function(arg0, arg1) { + getObject(arg0).format = __wbindgen_enum_GpuTextureFormat[arg1]; + }, + __wbg_set_fragment_d2b0ec97d7cf8d47: function(arg0, arg1) { + getObject(arg0).fragment = getObject(arg1); + }, + __wbg_set_front_face_d3f8a2e07e7b25dd: function(arg0, arg1) { + getObject(arg0).frontFace = __wbindgen_enum_GpuFrontFace[arg1]; + }, + __wbg_set_g_b527ee8a9bed553d: function(arg0, arg1) { + getObject(arg0).g = arg1; + }, + __wbg_set_has_dynamic_offset_0c72ffa900c5a269: function(arg0, arg1) { + getObject(arg0).hasDynamicOffset = arg1 !== 0; + }, + __wbg_set_height_c9789c1c77eaedff: function(arg0, arg1) { + getObject(arg0).height = arg1 >>> 0; + }, + __wbg_set_height_f6619158e5735877: function(arg0, arg1) { + getObject(arg0).height = arg1 >>> 0; + }, + __wbg_set_height_fde391767df1ce27: function(arg0, arg1) { + getObject(arg0).height = arg1 >>> 0; + }, + __wbg_set_label_17202740051e9722: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_2fefb39c0e0dbbe8: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_3cb2322e6f6db14c: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_3f2ccaafef5ff7c9: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_612add98a4398f92: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_70a09ee68d6b1b26: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_92cd3811e96b487c: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_9c2a186152427ee0: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_c3eaf136aa464cba: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_c7987704d29f284b: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_cfe64bca8945ee30: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_e02179cf97e95763: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_label_ee172cd5f6a96961: function(arg0, arg1, arg2) { + getObject(arg0).label = getStringFromWasm0(arg1, arg2); + }, + __wbg_set_layout_454e3a091b390cd4: function(arg0, arg1) { + getObject(arg0).layout = getObject(arg1); + }, + __wbg_set_layout_75dc1ca3f2421cff: function(arg0, arg1) { + getObject(arg0).layout = getObject(arg1); + }, + __wbg_set_layout_gpu_auto_layout_mode_06a2b95af1043098: function(arg0, arg1) { + getObject(arg0).layout = __wbindgen_enum_GpuAutoLayoutMode[arg1]; + }, + __wbg_set_load_op_c56b1269acc2d51f: function(arg0, arg1) { + getObject(arg0).loadOp = __wbindgen_enum_GpuLoadOp[arg1]; + }, + __wbg_set_lod_max_clamp_db24179f67f3aa31: function(arg0, arg1) { + getObject(arg0).lodMaxClamp = arg1; + }, + __wbg_set_lod_min_clamp_2bbce566e9fefa04: function(arg0, arg1) { + getObject(arg0).lodMinClamp = arg1; + }, + __wbg_set_mag_filter_db8e6b42d4f8846d: function(arg0, arg1) { + getObject(arg0).magFilter = __wbindgen_enum_GpuFilterMode[arg1]; + }, + __wbg_set_mapped_at_creation_3f320fef6761b02c: function(arg0, arg1) { + getObject(arg0).mappedAtCreation = arg1 !== 0; + }, + __wbg_set_mask_c1079e551ec360dc: function(arg0, arg1) { + getObject(arg0).mask = arg1 >>> 0; + }, + __wbg_set_max_anisotropy_84749fdcec362dc4: function(arg0, arg1) { + getObject(arg0).maxAnisotropy = arg1; + }, + __wbg_set_min_binding_size_f64_897e3cd4496ddec9: function(arg0, arg1) { + getObject(arg0).minBindingSize = arg1; + }, + __wbg_set_min_filter_d435bbfc5a637757: function(arg0, arg1) { + getObject(arg0).minFilter = __wbindgen_enum_GpuFilterMode[arg1]; + }, + __wbg_set_mip_level_03ed7bbd0ded367f: function(arg0, arg1) { + getObject(arg0).mipLevel = arg1 >>> 0; + }, + __wbg_set_mip_level_count_047936c630acee7b: function(arg0, arg1) { + getObject(arg0).mipLevelCount = arg1 >>> 0; + }, + __wbg_set_mip_level_count_44bc46a1ae6f6daa: function(arg0, arg1) { + getObject(arg0).mipLevelCount = arg1 >>> 0; + }, + __wbg_set_mip_level_f3745730372683d5: function(arg0, arg1) { + getObject(arg0).mipLevel = arg1 >>> 0; + }, + __wbg_set_mipmap_filter_62fb49a84b0747ff: function(arg0, arg1) { + getObject(arg0).mipmapFilter = __wbindgen_enum_GpuMipmapFilterMode[arg1]; + }, + __wbg_set_mode_7edfbc344ef9c650: function(arg0, arg1) { + getObject(arg0).mode = __wbindgen_enum_GpuCanvasToneMappingMode[arg1]; + }, + __wbg_set_module_392eeaa269f203b0: function(arg0, arg1) { + getObject(arg0).module = getObject(arg1); + }, + __wbg_set_module_715d37652c4998ec: function(arg0, arg1) { + getObject(arg0).module = getObject(arg1); + }, + __wbg_set_multisample_ff72a7a5456cbeb7: function(arg0, arg1) { + getObject(arg0).multisample = getObject(arg1); + }, + __wbg_set_multisampled_039f032dc4b67367: function(arg0, arg1) { + getObject(arg0).multisampled = arg1 !== 0; + }, + __wbg_set_offset_f64_127e8a0aa5c5485a: function(arg0, arg1) { + getObject(arg0).offset = arg1; + }, + __wbg_set_offset_f64_457756429ede426d: function(arg0, arg1) { + getObject(arg0).offset = arg1; + }, + __wbg_set_offset_f64_a903425d5a8e5815: function(arg0, arg1) { + getObject(arg0).offset = arg1; + }, + __wbg_set_operation_00a77386523b88f9: function(arg0, arg1) { + getObject(arg0).operation = __wbindgen_enum_GpuBlendOperation[arg1]; + }, + __wbg_set_origin_gpu_origin_2d_dict_8156da81552cd590: function(arg0, arg1) { + getObject(arg0).origin = getObject(arg1); + }, + __wbg_set_origin_gpu_origin_3d_dict_0619d4860adb4eb6: function(arg0, arg1) { + getObject(arg0).origin = getObject(arg1); + }, + __wbg_set_origin_gpu_origin_3d_dict_abd0dec737979786: function(arg0, arg1) { + getObject(arg0).origin = getObject(arg1); + }, + __wbg_set_pass_op_3cf10feb3d76ab97: function(arg0, arg1) { + getObject(arg0).passOp = __wbindgen_enum_GpuStencilOperation[arg1]; + }, + __wbg_set_power_preference_b42d00a8facfbade: function(arg0, arg1) { + getObject(arg0).powerPreference = __wbindgen_enum_GpuPowerPreference[arg1]; + }, + __wbg_set_premultiplied_alpha_6d23532454ce9a12: function(arg0, arg1) { + getObject(arg0).premultipliedAlpha = arg1 !== 0; + }, + __wbg_set_primitive_e796cf76f0ff89f3: function(arg0, arg1) { + getObject(arg0).primitive = getObject(arg1); + }, + __wbg_set_query_set_f030702f1b69199f: function(arg0, arg1) { + getObject(arg0).querySet = getObject(arg1); + }, + __wbg_set_r_6ece4d74af63364f: function(arg0, arg1) { + getObject(arg0).r = arg1; + }, + __wbg_set_required_features_bbab71414c45e621: function(arg0, arg1, arg2) { + getObject(arg0).requiredFeatures = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_required_limits_837f62d865e7cfac: function(arg0, arg1) { + getObject(arg0).requiredLimits = getObject(arg1); + }, + __wbg_set_resolve_target_gpu_texture_view_e4c1e3bbb8c27d87: function(arg0, arg1) { + getObject(arg0).resolveTarget = getObject(arg1); + }, + __wbg_set_resource_8fd8658b30d86ecf: function(arg0, arg1) { + getObject(arg0).resource = getObject(arg1); + }, + __wbg_set_resource_gpu_buffer_binding_33099b25da65b610: function(arg0, arg1) { + getObject(arg0).resource = getObject(arg1); + }, + __wbg_set_resource_gpu_texture_view_4cffe7bc7c8e5cbe: function(arg0, arg1) { + getObject(arg0).resource = getObject(arg1); + }, + __wbg_set_rows_per_image_c6d50d227e634379: function(arg0, arg1) { + getObject(arg0).rowsPerImage = arg1 >>> 0; + }, + __wbg_set_sample_count_481c255a12054e1d: function(arg0, arg1) { + getObject(arg0).sampleCount = arg1 >>> 0; + }, + __wbg_set_sample_type_ebc5fcd029513bda: function(arg0, arg1) { + getObject(arg0).sampleType = __wbindgen_enum_GpuTextureSampleType[arg1]; + }, + __wbg_set_sampler_89cb4a7efcfc6005: function(arg0, arg1) { + getObject(arg0).sampler = getObject(arg1); + }, + __wbg_set_shader_location_3fb9f6a012eba494: function(arg0, arg1) { + getObject(arg0).shaderLocation = arg1 >>> 0; + }, + __wbg_set_size_f64_2f591b0654540477: function(arg0, arg1) { + getObject(arg0).size = arg1; + }, + __wbg_set_size_f64_e844c985b8f95261: function(arg0, arg1) { + getObject(arg0).size = arg1; + }, + __wbg_set_size_gpu_extent_3d_dict_adf57388ab1d4f18: function(arg0, arg1) { + getObject(arg0).size = getObject(arg1); + }, + __wbg_set_source_211aa06496d82b73: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_source_html_canvas_element_5577621adc0f8ff6: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_source_html_image_element_23fa06e99ca07cc0: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_source_html_video_element_e57b5cacb7b02736: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_source_image_data_c80600573a809775: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_source_offscreen_canvas_7d49271754887b4e: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_source_video_frame_e4faebda27c393e4: function(arg0, arg1) { + getObject(arg0).source = getObject(arg1); + }, + __wbg_set_src_factor_6f2c9ec8e4d3d979: function(arg0, arg1) { + getObject(arg0).srcFactor = __wbindgen_enum_GpuBlendFactor[arg1]; + }, + __wbg_set_stencil_back_c54d0443b8b6a957: function(arg0, arg1) { + getObject(arg0).stencilBack = getObject(arg1); + }, + __wbg_set_stencil_clear_value_a321b0e045bfd8c2: function(arg0, arg1) { + getObject(arg0).stencilClearValue = arg1 >>> 0; + }, + __wbg_set_stencil_front_3ff3f8385852efff: function(arg0, arg1) { + getObject(arg0).stencilFront = getObject(arg1); + }, + __wbg_set_stencil_load_op_37d20deccb26a0f1: function(arg0, arg1) { + getObject(arg0).stencilLoadOp = __wbindgen_enum_GpuLoadOp[arg1]; + }, + __wbg_set_stencil_read_mask_021ef4271b24352c: function(arg0, arg1) { + getObject(arg0).stencilReadMask = arg1 >>> 0; + }, + __wbg_set_stencil_read_only_75fe66a2356d6e92: function(arg0, arg1) { + getObject(arg0).stencilReadOnly = arg1 !== 0; + }, + __wbg_set_stencil_store_op_501f91638dd386e6: function(arg0, arg1) { + getObject(arg0).stencilStoreOp = __wbindgen_enum_GpuStoreOp[arg1]; + }, + __wbg_set_stencil_write_mask_ec1c12237e094bdd: function(arg0, arg1) { + getObject(arg0).stencilWriteMask = arg1 >>> 0; + }, + __wbg_set_step_mode_3cbbdeba1e5dfd62: function(arg0, arg1) { + getObject(arg0).stepMode = __wbindgen_enum_GpuVertexStepMode[arg1]; + }, + __wbg_set_storage_texture_786aea7c5773b6c1: function(arg0, arg1) { + getObject(arg0).storageTexture = getObject(arg1); + }, + __wbg_set_store_op_678f33376d741711: function(arg0, arg1) { + getObject(arg0).storeOp = __wbindgen_enum_GpuStoreOp[arg1]; + }, + __wbg_set_strip_index_format_70313df755145d5e: function(arg0, arg1) { + getObject(arg0).stripIndexFormat = __wbindgen_enum_GpuIndexFormat[arg1]; + }, + __wbg_set_targets_674b33931e512fb1: function(arg0, arg1, arg2) { + getObject(arg0).targets = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_texture_4e0ee4c98ac25b81: function(arg0, arg1) { + getObject(arg0).texture = getObject(arg1); + }, + __wbg_set_texture_95f2bfdf7767e76f: function(arg0, arg1) { + getObject(arg0).texture = getObject(arg1); + }, + __wbg_set_texture_a33be3fe02ac6264: function(arg0, arg1) { + getObject(arg0).texture = getObject(arg1); + }, + __wbg_set_timestamp_writes_de6a09f299b71b76: function(arg0, arg1) { + getObject(arg0).timestampWrites = getObject(arg1); + }, + __wbg_set_tone_mapping_320c1aad31db2e7f: function(arg0, arg1) { + getObject(arg0).toneMapping = getObject(arg1); + }, + __wbg_set_topology_b92cfe523bd9653b: function(arg0, arg1) { + getObject(arg0).topology = __wbindgen_enum_GpuPrimitiveTopology[arg1]; + }, + __wbg_set_type_43e0092f16775979: function(arg0, arg1) { + getObject(arg0).type = __wbindgen_enum_GpuSamplerBindingType[arg1]; + }, + __wbg_set_type_79cec55caf4cdb6d: function(arg0, arg1) { + getObject(arg0).type = __wbindgen_enum_GpuBufferBindingType[arg1]; + }, + __wbg_set_unclipped_depth_32b7caf29fa5633d: function(arg0, arg1) { + getObject(arg0).unclippedDepth = arg1 !== 0; + }, + __wbg_set_usage_1ee33d98267e787d: function(arg0, arg1) { + getObject(arg0).usage = arg1 >>> 0; + }, + __wbg_set_usage_2365e2704b1fdb10: function(arg0, arg1) { + getObject(arg0).usage = arg1 >>> 0; + }, + __wbg_set_usage_d53ee6f0c7aedbfa: function(arg0, arg1) { + getObject(arg0).usage = arg1 >>> 0; + }, + __wbg_set_usage_f3e34822998d2147: function(arg0, arg1) { + getObject(arg0).usage = arg1 >>> 0; + }, + __wbg_set_vertex_77ed7a1229239b5a: function(arg0, arg1) { + getObject(arg0).vertex = getObject(arg1); + }, + __wbg_set_view_dimension_893e2d16561e56e8: function(arg0, arg1) { + getObject(arg0).viewDimension = __wbindgen_enum_GpuTextureViewDimension[arg1]; + }, + __wbg_set_view_dimension_f2c5fe4bf927c3fe: function(arg0, arg1) { + getObject(arg0).viewDimension = __wbindgen_enum_GpuTextureViewDimension[arg1]; + }, + __wbg_set_view_formats_427069064d8b7139: function(arg0, arg1, arg2) { + getObject(arg0).viewFormats = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_view_formats_9c2f01a6f3b365c7: function(arg0, arg1, arg2) { + getObject(arg0).viewFormats = getArrayJsValueViewFromWasm0(arg1, arg2); + }, + __wbg_set_view_gpu_texture_view_35f4655788535c4d: function(arg0, arg1) { + getObject(arg0).view = getObject(arg1); + }, + __wbg_set_view_gpu_texture_view_a532c825c52042c0: function(arg0, arg1) { + getObject(arg0).view = getObject(arg1); + }, + __wbg_set_visibility_d8a6821789538c25: function(arg0, arg1) { + getObject(arg0).visibility = arg1 >>> 0; + }, + __wbg_set_width_0ca908d167690992: function(arg0, arg1) { + getObject(arg0).width = arg1 >>> 0; + }, + __wbg_set_width_b0e1267db4b196b5: function(arg0, arg1) { + getObject(arg0).width = arg1 >>> 0; + }, + __wbg_set_width_b20525f5f4df4eb8: function(arg0, arg1) { + getObject(arg0).width = arg1 >>> 0; + }, + __wbg_set_write_mask_42d89f182ade6b2d: function(arg0, arg1) { + getObject(arg0).writeMask = arg1 >>> 0; + }, + __wbg_set_x_58965df5af1ad27d: function(arg0, arg1) { + getObject(arg0).x = arg1 >>> 0; + }, + __wbg_set_x_f470b03dd54724cd: function(arg0, arg1) { + getObject(arg0).x = arg1 >>> 0; + }, + __wbg_set_y_4c44eb40ebca5bfc: function(arg0, arg1) { + getObject(arg0).y = arg1 >>> 0; + }, + __wbg_set_y_b79cba22b145de99: function(arg0, arg1) { + getObject(arg0).y = arg1 >>> 0; + }, + __wbg_set_z_2e6820ef0f5821ed: function(arg0, arg1) { + getObject(arg0).z = arg1 >>> 0; + }, + __wbg_shaderSource_06379dac249994dd: function(arg0, arg1, arg2, arg3) { + getObject(arg0).shaderSource(getObject(arg1), getStringFromWasm0(arg2, arg3)); + }, + __wbg_shaderSource_c8812639b47d87b9: function(arg0, arg1, arg2, arg3) { + getObject(arg0).shaderSource(getObject(arg1), getStringFromWasm0(arg2, arg3)); + }, + __wbg_static_accessor_GLOBAL_266715b9d96ba635: function() { + const ret = typeof global === 'undefined' ? null : global; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_static_accessor_GLOBAL_THIS_10fb7dc1ae063179: function() { + const ret = typeof globalThis === 'undefined' ? null : globalThis; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_static_accessor_SELF_0b583911f537483a: function() { + const ret = typeof self === 'undefined' ? null : self; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_static_accessor_WINDOW_d7f903d1508cbdc4: function() { + const ret = typeof window === 'undefined' ? null : window; + return isLikeNone(ret) ? 0 : addHeapObject(ret); + }, + __wbg_stencilFuncSeparate_3f31dcd12a0d4785: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).stencilFuncSeparate(arg1 >>> 0, arg2 >>> 0, arg3, arg4 >>> 0); + }, + __wbg_stencilFuncSeparate_f168b66c597aa85f: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).stencilFuncSeparate(arg1 >>> 0, arg2 >>> 0, arg3, arg4 >>> 0); + }, + __wbg_stencilMaskSeparate_b30c097ea6b98504: function(arg0, arg1, arg2) { + getObject(arg0).stencilMaskSeparate(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_stencilMaskSeparate_ff739f8711e9fd42: function(arg0, arg1, arg2) { + getObject(arg0).stencilMaskSeparate(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_stencilMask_9e18cc018897c747: function(arg0, arg1) { + getObject(arg0).stencilMask(arg1 >>> 0); + }, + __wbg_stencilMask_a22be64c66abd825: function(arg0, arg1) { + getObject(arg0).stencilMask(arg1 >>> 0); + }, + __wbg_stencilOpSeparate_7d01185cbdb89d99: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).stencilOpSeparate(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); + }, + __wbg_stencilOpSeparate_c5af5f5dbd237bea: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).stencilOpSeparate(arg1 >>> 0, arg2 >>> 0, arg3 >>> 0, arg4 >>> 0); + }, + __wbg_submit_077c85cc28e36892: function(arg0, arg1, arg2) { + getObject(arg0).submit(getArrayJsValueViewFromWasm0(arg1, arg2)); + }, + __wbg_texImage2D_82bf2b4c8aa9a1db: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, arg9); + }, arguments); }, + __wbg_texImage2D_a856af552acdc9d9: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texImage2D_ca3d0fe24620ec85: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texImage3D_29a847f33877ed12: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { + getObject(arg0).texImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8 >>> 0, arg9 >>> 0, getObject(arg10)); + }, arguments); }, + __wbg_texImage3D_78fb39719c620063: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) { + getObject(arg0).texImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8 >>> 0, arg9 >>> 0, arg10); + }, arguments); }, + __wbg_texParameteri_647fdb1426809cb5: function(arg0, arg1, arg2, arg3) { + getObject(arg0).texParameteri(arg1 >>> 0, arg2 >>> 0, arg3); + }, + __wbg_texParameteri_7864076abcdb03e0: function(arg0, arg1, arg2, arg3) { + getObject(arg0).texParameteri(arg1 >>> 0, arg2 >>> 0, arg3); + }, + __wbg_texStorage2D_cf2bd991072d9efb: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).texStorage2D(arg1 >>> 0, arg2, arg3 >>> 0, arg4, arg5); + }, + __wbg_texStorage3D_74b701c2b176380c: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + getObject(arg0).texStorage3D(arg1 >>> 0, arg2, arg3 >>> 0, arg4, arg5, arg6); + }, + __wbg_texSubImage2D_07337d0974e99c39: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage2D_196472d4111ce5b6: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage2D_24cf5097ebaa84cc: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage2D_31a0c2797aa04f32: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, arg9); + }, arguments); }, + __wbg_texSubImage2D_5e90c7a719d40256: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage2D_64e36398b8f046ac: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage2D_bb322aae90f7657f: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage2D_d7bffff06a3b8315: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) { + getObject(arg0).texSubImage2D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7 >>> 0, arg8 >>> 0, getObject(arg9)); + }, arguments); }, + __wbg_texSubImage3D_001f2e62ed945b72: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, getObject(arg11)); + }, arguments); }, + __wbg_texSubImage3D_5143d5af7e182ddc: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, getObject(arg11)); + }, arguments); }, + __wbg_texSubImage3D_53c6014e2d129891: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, getObject(arg11)); + }, arguments); }, + __wbg_texSubImage3D_5d773fc7a8cd4d08: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, getObject(arg11)); + }, arguments); }, + __wbg_texSubImage3D_adb96e37e6949ec9: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, getObject(arg11)); + }, arguments); }, + __wbg_texSubImage3D_d4e74595e01f7f71: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, getObject(arg11)); + }, arguments); }, + __wbg_texSubImage3D_e0d113b7047a5130: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) { + getObject(arg0).texSubImage3D(arg1 >>> 0, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9 >>> 0, arg10 >>> 0, arg11); + }, arguments); }, + __wbg_then_c949d5a25a4e78f8: function(arg0, arg1, arg2) { + const ret = getObject(arg0).then(getObject(arg1), getObject(arg2)); + return addHeapObject(ret); + }, + __wbg_then_e71170d78fcf8954: function(arg0, arg1) { + const ret = getObject(arg0).then(getObject(arg1)); + return addHeapObject(ret); + }, + __wbg_unconfigure_835307f58dc68d80: function(arg0) { + getObject(arg0).unconfigure(); + }, + __wbg_uniform1f_9cfecfaeb8cf43c1: function(arg0, arg1, arg2) { + getObject(arg0).uniform1f(getObject(arg1), arg2); + }, + __wbg_uniform1f_b5856e35a43b65a1: function(arg0, arg1, arg2) { + getObject(arg0).uniform1f(getObject(arg1), arg2); + }, + __wbg_uniform1i_531b98c4a7177366: function(arg0, arg1, arg2) { + getObject(arg0).uniform1i(getObject(arg1), arg2); + }, + __wbg_uniform1i_fc2dc8cadc49d8ec: function(arg0, arg1, arg2) { + getObject(arg0).uniform1i(getObject(arg1), arg2); + }, + __wbg_uniform1ui_353c67ec681b540c: function(arg0, arg1, arg2) { + getObject(arg0).uniform1ui(getObject(arg1), arg2 >>> 0); + }, + __wbg_uniform2fv_319c49f0ce340f26: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform2fv(getObject(arg1), getArrayF32FromWasm0(arg2, arg3)); + }, + __wbg_uniform2fv_a7d3c1830d18c1b2: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform2fv(getObject(arg1), getArrayF32FromWasm0(arg2, arg3)); + }, + __wbg_uniform2iv_65f36fc9777697d7: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform2iv(getObject(arg1), getArrayI32FromWasm0(arg2, arg3)); + }, + __wbg_uniform2iv_ca9cb507409b9ad1: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform2iv(getObject(arg1), getArrayI32FromWasm0(arg2, arg3)); + }, + __wbg_uniform2uiv_c95af5fb653ccb86: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform2uiv(getObject(arg1), getArrayU32FromWasm0(arg2, arg3)); + }, + __wbg_uniform3fv_055ea5ac38af4fce: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform3fv(getObject(arg1), getArrayF32FromWasm0(arg2, arg3)); + }, + __wbg_uniform3fv_58520ffe22a6e27c: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform3fv(getObject(arg1), getArrayF32FromWasm0(arg2, arg3)); + }, + __wbg_uniform3iv_0590e8b7eff2a791: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform3iv(getObject(arg1), getArrayI32FromWasm0(arg2, arg3)); + }, + __wbg_uniform3iv_e91b1410dc299f50: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform3iv(getObject(arg1), getArrayI32FromWasm0(arg2, arg3)); + }, + __wbg_uniform3uiv_9992491000463a52: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform3uiv(getObject(arg1), getArrayU32FromWasm0(arg2, arg3)); + }, + __wbg_uniform4f_22bfec882308eec2: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).uniform4f(getObject(arg1), arg2, arg3, arg4, arg5); + }, + __wbg_uniform4f_8dd8f311e656ca0d: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).uniform4f(getObject(arg1), arg2, arg3, arg4, arg5); + }, + __wbg_uniform4fv_3ef2f5121fcb78ae: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform4fv(getObject(arg1), getArrayF32FromWasm0(arg2, arg3)); + }, + __wbg_uniform4fv_cf8ff646c36232c8: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform4fv(getObject(arg1), getArrayF32FromWasm0(arg2, arg3)); + }, + __wbg_uniform4iv_6fc5436b4f876039: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform4iv(getObject(arg1), getArrayI32FromWasm0(arg2, arg3)); + }, + __wbg_uniform4iv_99c2b07787d362d6: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform4iv(getObject(arg1), getArrayI32FromWasm0(arg2, arg3)); + }, + __wbg_uniform4uiv_a6fb3237d9df7ed4: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniform4uiv(getObject(arg1), getArrayU32FromWasm0(arg2, arg3)); + }, + __wbg_uniformBlockBinding_07116582301fb019: function(arg0, arg1, arg2, arg3) { + getObject(arg0).uniformBlockBinding(getObject(arg1), arg2 >>> 0, arg3 >>> 0); + }, + __wbg_uniformMatrix2fv_34b2245136106a2f: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix2fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix2fv_611c7fcb378f057e: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix2fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix2x3fv_6b33a76d09b6c00f: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix2x3fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix2x4fv_66c787e2d0a5dd0a: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix2x4fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix3fv_472e7824735acce1: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix3fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix3fv_83fdc9c3c0d43204: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix3fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix3x2fv_33df849b78b8556e: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix3x2fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix3x4fv_8914dc5121a813b4: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix3x4fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix4fv_dff9738d995fd0b1: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix4fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix4fv_e691d7f14d3609f7: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix4fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix4x2fv_1c9ccf4427b82548: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix4x2fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_uniformMatrix4x3fv_a84b16e0a7abee2c: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).uniformMatrix4x3fv(getObject(arg1), arg2 !== 0, getArrayF32FromWasm0(arg3, arg4)); + }, + __wbg_useProgram_5bdae58fd9f9826f: function(arg0, arg1) { + getObject(arg0).useProgram(getObject(arg1)); + }, + __wbg_useProgram_e075c2902e420761: function(arg0, arg1) { + getObject(arg0).useProgram(getObject(arg1)); + }, + __wbg_vertexAttribDivisorANGLE_659fb027198a47c3: function(arg0, arg1, arg2) { + getObject(arg0).vertexAttribDivisorANGLE(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_vertexAttribDivisor_7575d382215322f7: function(arg0, arg1, arg2) { + getObject(arg0).vertexAttribDivisor(arg1 >>> 0, arg2 >>> 0); + }, + __wbg_vertexAttribIPointer_fc8057c62cfcc63c: function(arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).vertexAttribIPointer(arg1 >>> 0, arg2, arg3 >>> 0, arg4, arg5); + }, + __wbg_vertexAttribPointer_24b495e18df325dc: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + getObject(arg0).vertexAttribPointer(arg1 >>> 0, arg2, arg3 >>> 0, arg4 !== 0, arg5, arg6); + }, + __wbg_vertexAttribPointer_54d56b18f71a946d: function(arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + getObject(arg0).vertexAttribPointer(arg1 >>> 0, arg2, arg3 >>> 0, arg4 !== 0, arg5, arg6); + }, + __wbg_videoHeight_4070b88110b1ac0d: function(arg0) { + const ret = getObject(arg0).videoHeight; + return ret; + }, + __wbg_videoWidth_ea74d0fb1493f025: function(arg0) { + const ret = getObject(arg0).videoWidth; + return ret; + }, + __wbg_viewport_16936692526e2ce6: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).viewport(arg1, arg2, arg3, arg4); + }, + __wbg_viewport_e35d16c542d2ce70: function(arg0, arg1, arg2, arg3, arg4) { + getObject(arg0).viewport(arg1, arg2, arg3, arg4); + }, + __wbg_visiongpuengine_new: function(arg0) { + const ret = VisionGpuEngine.__wrap(arg0); + return addHeapObject(ret); + }, + __wbg_width_3b5d66c5eac1eb28: function(arg0) { + const ret = getObject(arg0).width; + return ret; + }, + __wbg_width_3d0dce3d9892e35e: function(arg0) { + const ret = getObject(arg0).width; + return ret; + }, + __wbg_width_7a1b335e4a553dc3: function(arg0) { + const ret = getObject(arg0).width; + return ret; + }, + __wbg_width_8a9cc26363f10d5f: function(arg0) { + const ret = getObject(arg0).width; + return ret; + }, + __wbg_width_c9bfe09197f3b26e: function(arg0) { + const ret = getObject(arg0).width; + return ret; + }, + __wbg_writeBuffer_f4bb3f54adfe1330: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) { + getObject(arg0).writeBuffer(getObject(arg1), arg2, getArrayU8FromWasm0(arg3, arg4), arg5, arg6); + }, arguments); }, + __wbg_writeTexture_30e592e8c061c3d9: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) { + getObject(arg0).writeTexture(getObject(arg1), getArrayU8FromWasm0(arg2, arg3), getObject(arg4), getObject(arg5)); + }, arguments); }, + __wbindgen_generic_0000000000000001: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 2763, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_9909); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000002: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("GPUDevice")], shim_idx: 663, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_3779); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000003: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("any")], shim_idx: 663, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_3779_24); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000004: function(arg0, arg1) { + // Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("undefined")], shim_idx: 663, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`. + const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_3779_25); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000005: function(arg0) { + // Cast intrinsic for `F64 -> Externref`. + const ret = arg0; + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000006: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(F32)) -> NamedExternref("Float32Array")`. + const ret = getArrayF32FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000007: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(I16)) -> NamedExternref("Int16Array")`. + const ret = getArrayI16FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000008: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(I32)) -> NamedExternref("Int32Array")`. + const ret = getArrayI32FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_0000000000000009: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(I8)) -> NamedExternref("Int8Array")`. + const ret = getArrayI8FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_000000000000000a: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(U16)) -> NamedExternref("Uint16Array")`. + const ret = getArrayU16FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_000000000000000b: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(U32)) -> NamedExternref("Uint32Array")`. + const ret = getArrayU32FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_000000000000000c: function(arg0, arg1) { + // Cast intrinsic for `Ref(Slice(U8)) -> NamedExternref("Uint8Array")`. + const ret = getArrayU8FromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_generic_000000000000000d: function(arg0, arg1) { + // Cast intrinsic for `Ref(String) -> Externref`. + const ret = getStringFromWasm0(arg0, arg1); + return addHeapObject(ret); + }, + __wbindgen_object_clone_ref: function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }, + __wbindgen_object_drop_ref: function(arg0) { + takeObject(arg0); + }, + }; + return { + __proto__: null, + "./lumencast_vision_web_bg.js": import0, + }; +} + +function __wasm_bindgen_func_elem_9927(arg0, arg1, arg2, arg3) { + wasm.__wasm_bindgen_func_elem_9927(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); +} + +function __wasm_bindgen_func_elem_9909(arg0, arg1, arg2) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.__wasm_bindgen_func_elem_9909(retptr, arg0, arg1, addHeapObject(arg2)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function __wasm_bindgen_func_elem_3779(arg0, arg1, arg2) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.__wasm_bindgen_func_elem_3779(retptr, arg0, arg1, addHeapObject(arg2)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function __wasm_bindgen_func_elem_3779_24(arg0, arg1, arg2) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.__wasm_bindgen_func_elem_3779_24(retptr, arg0, arg1, addHeapObject(arg2)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + +function __wasm_bindgen_func_elem_3779_25(arg0, arg1, arg2) { + try { + const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); + wasm.__wasm_bindgen_func_elem_3779_25(retptr, arg0, arg1, addHeapObject(arg2)); + var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true); + var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true); + if (r1) { + throw takeObject(r0); + } + } finally { + wasm.__wbindgen_add_to_stack_pointer(16); + } +} + + +const __wbindgen_enum_GpuAddressMode = ["clamp-to-edge", "repeat", "mirror-repeat"]; + + +const __wbindgen_enum_GpuAutoLayoutMode = ["auto"]; + + +const __wbindgen_enum_GpuBlendFactor = ["zero", "one", "src", "one-minus-src", "src-alpha", "one-minus-src-alpha", "dst", "one-minus-dst", "dst-alpha", "one-minus-dst-alpha", "src-alpha-saturated", "constant", "one-minus-constant", "src1", "one-minus-src1", "src1-alpha", "one-minus-src1-alpha"]; + + +const __wbindgen_enum_GpuBlendOperation = ["add", "subtract", "reverse-subtract", "min", "max"]; + + +const __wbindgen_enum_GpuBufferBindingType = ["uniform", "storage", "read-only-storage"]; + + +const __wbindgen_enum_GpuCanvasAlphaMode = ["opaque", "premultiplied"]; + + +const __wbindgen_enum_GpuCanvasToneMappingMode = ["standard", "extended"]; + + +const __wbindgen_enum_GpuCompareFunction = ["never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always"]; + + +const __wbindgen_enum_GpuCullMode = ["none", "front", "back"]; + + +const __wbindgen_enum_GpuErrorFilter = ["validation", "out-of-memory", "internal"]; + + +const __wbindgen_enum_GpuFilterMode = ["nearest", "linear"]; + + +const __wbindgen_enum_GpuFrontFace = ["ccw", "cw"]; + + +const __wbindgen_enum_GpuIndexFormat = ["uint16", "uint32"]; + + +const __wbindgen_enum_GpuLoadOp = ["load", "clear"]; + + +const __wbindgen_enum_GpuMipmapFilterMode = ["nearest", "linear"]; + + +const __wbindgen_enum_GpuPowerPreference = ["low-power", "high-performance"]; + + +const __wbindgen_enum_GpuPrimitiveTopology = ["point-list", "line-list", "line-strip", "triangle-list", "triangle-strip"]; + + +const __wbindgen_enum_GpuSamplerBindingType = ["filtering", "non-filtering", "comparison"]; + + +const __wbindgen_enum_GpuStencilOperation = ["keep", "zero", "replace", "invert", "increment-clamp", "decrement-clamp", "increment-wrap", "decrement-wrap"]; + + +const __wbindgen_enum_GpuStorageTextureAccess = ["write-only", "read-only", "read-write"]; + + +const __wbindgen_enum_GpuStoreOp = ["store", "discard"]; + + +const __wbindgen_enum_GpuTextureAspect = ["all", "stencil-only", "depth-only"]; + + +const __wbindgen_enum_GpuTextureDimension = ["1d", "2d", "3d"]; + + +const __wbindgen_enum_GpuTextureFormat = ["r8unorm", "r8snorm", "r8uint", "r8sint", "r16unorm", "r16snorm", "r16uint", "r16sint", "r16float", "rg8unorm", "rg8snorm", "rg8uint", "rg8sint", "r32uint", "r32sint", "r32float", "rg16unorm", "rg16snorm", "rg16uint", "rg16sint", "rg16float", "rgba8unorm", "rgba8unorm-srgb", "rgba8snorm", "rgba8uint", "rgba8sint", "bgra8unorm", "bgra8unorm-srgb", "rgb9e5ufloat", "rgb10a2uint", "rgb10a2unorm", "rg11b10ufloat", "rg32uint", "rg32sint", "rg32float", "rgba16unorm", "rgba16snorm", "rgba16uint", "rgba16sint", "rgba16float", "rgba32uint", "rgba32sint", "rgba32float", "stencil8", "depth16unorm", "depth24plus", "depth24plus-stencil8", "depth32float", "depth32float-stencil8", "bc1-rgba-unorm", "bc1-rgba-unorm-srgb", "bc2-rgba-unorm", "bc2-rgba-unorm-srgb", "bc3-rgba-unorm", "bc3-rgba-unorm-srgb", "bc4-r-unorm", "bc4-r-snorm", "bc5-rg-unorm", "bc5-rg-snorm", "bc6h-rgb-ufloat", "bc6h-rgb-float", "bc7-rgba-unorm", "bc7-rgba-unorm-srgb", "etc2-rgb8unorm", "etc2-rgb8unorm-srgb", "etc2-rgb8a1unorm", "etc2-rgb8a1unorm-srgb", "etc2-rgba8unorm", "etc2-rgba8unorm-srgb", "eac-r11unorm", "eac-r11snorm", "eac-rg11unorm", "eac-rg11snorm", "astc-4x4-unorm", "astc-4x4-unorm-srgb", "astc-5x4-unorm", "astc-5x4-unorm-srgb", "astc-5x5-unorm", "astc-5x5-unorm-srgb", "astc-6x5-unorm", "astc-6x5-unorm-srgb", "astc-6x6-unorm", "astc-6x6-unorm-srgb", "astc-8x5-unorm", "astc-8x5-unorm-srgb", "astc-8x6-unorm", "astc-8x6-unorm-srgb", "astc-8x8-unorm", "astc-8x8-unorm-srgb", "astc-10x5-unorm", "astc-10x5-unorm-srgb", "astc-10x6-unorm", "astc-10x6-unorm-srgb", "astc-10x8-unorm", "astc-10x8-unorm-srgb", "astc-10x10-unorm", "astc-10x10-unorm-srgb", "astc-12x10-unorm", "astc-12x10-unorm-srgb", "astc-12x12-unorm", "astc-12x12-unorm-srgb"]; + + +const __wbindgen_enum_GpuTextureSampleType = ["float", "unfilterable-float", "depth", "sint", "uint"]; + + +const __wbindgen_enum_GpuTextureViewDimension = ["1d", "2d", "2d-array", "cube", "cube-array", "3d"]; + + +const __wbindgen_enum_GpuVertexFormat = ["uint8", "uint8x2", "uint8x4", "sint8", "sint8x2", "sint8x4", "unorm8", "unorm8x2", "unorm8x4", "snorm8", "snorm8x2", "snorm8x4", "uint16", "uint16x2", "uint16x4", "sint16", "sint16x2", "sint16x4", "unorm16", "unorm16x2", "unorm16x4", "snorm16", "snorm16x2", "snorm16x4", "float16", "float16x2", "float16x4", "float32", "float32x2", "float32x3", "float32x4", "uint32", "uint32x2", "uint32x3", "uint32x4", "sint32", "sint32x2", "sint32x3", "sint32x4", "unorm10-10-10-2", "unorm8x4-bgra"]; + + +const __wbindgen_enum_GpuVertexStepMode = ["vertex", "instance"]; +const VisionGpuEngineFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_visiongpuengine_free(ptr, 1)); + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + heap[idx] = obj; + return idx; +} + +const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(state => wasm.__wbindgen_export4(state.a, state.b)); + +function debugString(val) { + // primitive types + const type = typeof val; + if (type == 'number' || type == 'boolean' || val == null) { + return `${val}`; + } + if (type == 'string') { + return `"${val}"`; + } + if (type == 'symbol') { + const description = val.description; + if (description == null) { + return 'Symbol'; + } else { + return `Symbol(${description})`; + } + } + if (type == 'function') { + const name = val.name; + if (typeof name == 'string' && name.length > 0) { + return `Function(${name})`; + } else { + return 'Function'; + } + } + // objects + if (Array.isArray(val)) { + const length = val.length; + let debug = '['; + if (length > 0) { + debug += debugString(val[0]); + } + for(let i = 1; i < length; i++) { + debug += ', ' + debugString(val[i]); + } + debug += ']'; + return debug; + } + // Test for built-in + const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); + let className; + if (builtInMatches && builtInMatches.length > 1) { + className = builtInMatches[1]; + } else { + // Failed to match the standard '[object ClassName]' + return toString.call(val); + } + if (className == 'Object') { + // we're a user defined class or Object + // JSON.stringify avoids problems with cycles, and is generally much + // easier than looping through ownProperties of `val`. + try { + return 'Object(' + JSON.stringify(val) + ')'; + } catch (_) { + return 'Object'; + } + } + // errors + if (val instanceof Error) { + return `${val.name}: ${val.message}\n${val.stack}`; + } + // TODO we could test for more things here, like `Set`s and `Map`s. + return className; +} + +function dropObject(idx) { + if (idx < 1028) return; + heap[idx] = heap_next; + heap_next = idx; +} + +function getArrayF32FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len); +} + +function getArrayI16FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getInt16ArrayMemory0().subarray(ptr / 2, ptr / 2 + len); +} + +function getArrayI32FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getInt32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len); +} + +function getArrayI8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getInt8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +function getArrayJsValueViewFromWasm0(ptr, len) { + ptr = ptr >>> 0; + const mem = getDataViewMemory0(); + const result = []; + for (let i = ptr; i < ptr + 4 * len; i += 4) { + result.push(getObject(mem.getUint32(i, true))); + } + return result; +} + +function getArrayU16FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint16ArrayMemory0().subarray(ptr / 2, ptr / 2 + len); +} + +function getArrayU32FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len); +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} + +let cachedDataViewMemory0 = null; +function getDataViewMemory0() { + if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { + cachedDataViewMemory0 = new DataView(wasm.memory.buffer); + } + return cachedDataViewMemory0; +} + +let cachedFloat32ArrayMemory0 = null; +function getFloat32ArrayMemory0() { + if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) { + cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer); + } + return cachedFloat32ArrayMemory0; +} + +let cachedInt16ArrayMemory0 = null; +function getInt16ArrayMemory0() { + if (cachedInt16ArrayMemory0 === null || cachedInt16ArrayMemory0.byteLength === 0) { + cachedInt16ArrayMemory0 = new Int16Array(wasm.memory.buffer); + } + return cachedInt16ArrayMemory0; +} + +let cachedInt32ArrayMemory0 = null; +function getInt32ArrayMemory0() { + if (cachedInt32ArrayMemory0 === null || cachedInt32ArrayMemory0.byteLength === 0) { + cachedInt32ArrayMemory0 = new Int32Array(wasm.memory.buffer); + } + return cachedInt32ArrayMemory0; +} + +let cachedInt8ArrayMemory0 = null; +function getInt8ArrayMemory0() { + if (cachedInt8ArrayMemory0 === null || cachedInt8ArrayMemory0.byteLength === 0) { + cachedInt8ArrayMemory0 = new Int8Array(wasm.memory.buffer); + } + return cachedInt8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + return decodeText(ptr >>> 0, len); +} + +let cachedUint16ArrayMemory0 = null; +function getUint16ArrayMemory0() { + if (cachedUint16ArrayMemory0 === null || cachedUint16ArrayMemory0.byteLength === 0) { + cachedUint16ArrayMemory0 = new Uint16Array(wasm.memory.buffer); + } + return cachedUint16ArrayMemory0; +} + +let cachedUint32ArrayMemory0 = null; +function getUint32ArrayMemory0() { + if (cachedUint32ArrayMemory0 === null || cachedUint32ArrayMemory0.byteLength === 0) { + cachedUint32ArrayMemory0 = new Uint32Array(wasm.memory.buffer); + } + return cachedUint32ArrayMemory0; +} + +let cachedUint8ArrayMemory0 = null; +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getObject(idx) { return heap[idx]; } + +function handleError(f, args) { + try { + return f.apply(this, args); + } catch (e) { + wasm.__wbindgen_export3(addHeapObject(e)); + } +} + +let heap = new Array(1024).fill(undefined); +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function isLikeNone(x) { + return x === undefined || x === null; +} + +function makeMutClosure(arg0, arg1, f) { + const state = { a: arg0, b: arg1, cnt: 1 }; + const real = (...args) => { + + // First up with a closure we increment the internal reference + // count. This ensures that the Rust closure environment won't + // be deallocated while we're invoking it. + state.cnt++; + const a = state.a; + state.a = 0; + try { + return f(a, state.b, ...args); + } finally { + state.a = a; + real._wbg_cb_unref(); + } + }; + real._wbg_cb_unref = () => { + if (--state.cnt === 0) { + wasm.__wbindgen_export4(state.a, state.b); + state.a = 0; + CLOSURE_DTORS.unregister(state); + } + }; + CLOSURE_DTORS.register(real, state, state); + return real; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} + +function passStringToWasm0(arg, malloc, realloc) { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = cachedTextEncoder.encodeInto(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +function takeObject(idx) { + const ret = getObject(idx); + dropObject(idx); + return ret; +} + +let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); +cachedTextDecoder.decode(); +const MAX_SAFARI_DECODE_BYTES = 2146435072; +let numBytesDecoded = 0; +function decodeText(ptr, len) { + numBytesDecoded += len; + if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { + cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + cachedTextDecoder.decode(); + numBytesDecoded = len; + } + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +const cachedTextEncoder = new TextEncoder(); + +if (!('encodeInto' in cachedTextEncoder)) { + cachedTextEncoder.encodeInto = function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; + }; +} + +let WASM_VECTOR_LEN = 0; + +let wasmModule, wasmInstance, wasm; +function __wbg_finalize_init(instance, module) { + wasmInstance = instance; + wasm = instance.exports; + wasmModule = module; + cachedDataViewMemory0 = null; + cachedFloat32ArrayMemory0 = null; + cachedInt16ArrayMemory0 = null; + cachedInt32ArrayMemory0 = null; + cachedInt8ArrayMemory0 = null; + cachedUint16ArrayMemory0 = null; + cachedUint32ArrayMemory0 = null; + cachedUint8ArrayMemory0 = null; + return wasm; +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (!module.ok) { + throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`); + } + + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + } catch (e) { + const validResponse = expectedResponseType(module.type); + + if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { throw e; } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + } else { + return instance; + } + } + + function expectedResponseType(type) { + switch (type) { + case 'basic': case 'cors': case 'default': return true; + } + return false; + } +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (module !== undefined) { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + const instance = new WebAssembly.Instance(module, imports); + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (module_or_path !== undefined) { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (module_or_path === undefined) { + module_or_path = new URL('lumencast_vision_web_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync, __wbg_init as default }; diff --git a/public/vision/pkg/lumencast_vision_web_bg.wasm b/public/vision/pkg/lumencast_vision_web_bg.wasm new file mode 100644 index 0000000..0150dc2 Binary files /dev/null and b/public/vision/pkg/lumencast_vision_web_bg.wasm differ diff --git a/public/vision/ui/fonts.mjs b/public/vision/ui/fonts.mjs new file mode 100644 index 0000000..8f78df0 --- /dev/null +++ b/public/vision/ui/fonts.mjs @@ -0,0 +1,44 @@ +const MAX_FONTS = 64; +const MAX_FONT_BYTES = 64 * 1024 * 1024; + +// Font assets are loaded once, before scene admission, never during mutation. +export async function fetchFonts(fetcher) { + const manifest = await fetcher("/fonts", { cache: "no-store" }); + if (manifest.status === 404) { + const response = await fetcher("/font", { cache: "no-store" }); + if (response.status === 204) return []; + return [await readFont(response, MAX_FONT_BYTES)]; + } + if (!manifest.ok) throw new Error(`Font manifest request failed with HTTP ${manifest.status}.`); + const paths = await manifest.json(); + if (!Array.isArray(paths) || paths.length > MAX_FONTS || paths.some((path,index) => path !== `/fonts/${index}`)) { + throw new Error("Invalid same-origin font asset manifest."); + } + const fonts = []; + let total = 0; + for (const path of paths) { + const response = await fetcher(path, { cache: "no-store" }); + const bytes = await readFont(response, MAX_FONT_BYTES - total); + total += bytes.byteLength; + fonts.push(bytes); + } + return fonts; +} + +async function readFont(response, remaining) { + if (!response.ok) throw new Error(`Font request failed with HTTP ${response.status}.`); + const declared = response.headers?.get?.("content-length"); + if (declared !== null && declared !== undefined && Number(declared) > remaining) { + throw new Error("Font assets exceed 64 MiB."); + } + const bytes = new Uint8Array(await response.arrayBuffer()); + if (bytes.byteLength > remaining) throw new Error("Font assets exceed 64 MiB."); + return bytes; +} + +export function fontFactory(Engine, method, fonts) { + const multiple = `${method}_with_fonts`; + if (typeof Engine[multiple] === "function") return (...args) => Engine[multiple](args[0], fonts, ...args.slice(1)); + if (fonts.length > 1) throw new Error("The fixed WASM engine cannot admit multiple supplied fonts."); + return (...args) => Engine[method](args[0], fonts[0] ?? new Uint8Array(), ...args.slice(1)); +} diff --git a/public/vision/ui/mainpresenter.mjs b/public/vision/ui/mainpresenter.mjs new file mode 100644 index 0000000..56d4339 --- /dev/null +++ b/public/vision/ui/mainpresenter.mjs @@ -0,0 +1,657 @@ +import { fetchFonts, fontFactory } from "./fonts.mjs"; + +const IMAGE_BITMAP_OPTIONS = Object.freeze({ + premultiplyAlpha: "none", + colorSpaceConversion: "default", +}); + +const WEBGL2_CONTEXT_ATTRIBUTES = Object.freeze({ + desynchronized: true, + antialias: false, + alpha: true, + premultipliedAlpha: true, +}); + +const STOPPED = Symbol("presenter stopped"); + +export function selectPresenterProfile(search = "") { + return new URLSearchParams(search).get("presenter") === "webgl" ? "webgl" : "webgpu"; +} + +export function createMainThreadPresenter(canvas, dependencies = {}) { + const fetchResource = dependencies.fetch ?? ((...args) => globalThis.fetch(...args)); + const loadWasm = dependencies.loadWasm ?? (() => import("/pkg/lumencast_vision_web.js")); + const createBitmap = dependencies.createImageBitmap ?? ((...args) => globalThis.createImageBitmap(...args)); + const BlobType = dependencies.Blob ?? globalThis.Blob; + const clock = dependencies.performance ?? globalThis.performance; + + const listeners = new Map(); + let engine = null; + let latestSize = null; + let lastSequence = -1; + let queue = Promise.resolve(); + let hasSubmittedFrame = false; + let sceneLoaded = false; + let disposed = false; + let generation = 0; + let context = null; + let contextLost = false; + let restoring = false; + let restoreEpoch = 0; + let initialRequest = null; + let packageBytes = null; + let fontBytes = null; + let lastAppliedSequence = -1; + let pendingRestore = false; + const uploadedAssets = new Set(); + + // State mutations stay in Rust while its GPU projection is unavailable. + // There is no replay journal and no scene parse on the recovery path. + function onContextLost(event) { + if (disposed) return; + event.preventDefault(); + contextLost = true; + restoreEpoch += 1; + emit({ type: "context-lost", seq: Math.max(0, lastSequence), phase: "context", message: "GPU context lost; accepted state is retained." }); + } + + function onContextRestored() { + if (disposed || !initialRequest) return; + if (restoring) { pendingRestore = true; return; } + const ticket = generation; + const epoch = restoreEpoch; + restoring = true; + queue = queue.then(async () => { + ensureActive(ticket); + emit({ type: "context-restoring", seq: Math.max(0, lastSequence) }); + if (engine) { + if (typeof engine.restore_html_canvas !== "function") throw new Error("Rust GPU engine does not support retained context restoration."); + const size = latestSize ?? authoredSurfaceSize(readEngineInfo(engine)); + await engine.restore_html_canvas(canvas, size.width, size.height); + ensureActive(ticket); + if (epoch !== restoreEpoch || context?.isContextLost?.()) throw new Error("GPU context was lost again during restoration."); + if (sceneLoaded) await registerRequiredImages(engine, { Blob: BlobType, createImageBitmap: createBitmap }, () => { + ensureActive(ticket); + if (epoch !== restoreEpoch || context?.isContextLost?.()) throw new Error("GPU context was lost again during image restoration."); + }); + uploadedAssets.clear(); + if (sceneLoaded) for (const path of parseJson(engine.required_images_json(), "required_images_json()")) uploadedAssets.add(path); + ensureActive(ticket); + if (epoch !== restoreEpoch || context?.isContextLost?.()) throw new Error("GPU context was lost again during image restoration."); + contextLost = false; + if (sceneLoaded) await submit(Math.max(0, lastAppliedSequence), size, {}, ticket); + } else { + contextLost = false; + await initialize(initialRequest, {}, ticket); + } + ensureActive(ticket); + if (contextLost) throw new Error("GPU context unavailable after restoration."); + emit({ type: "context-restored", seq: Math.max(0, lastAppliedSequence) }); + }).catch(error => postError(Math.max(0, lastSequence), "context", error, {})).finally(() => { + restoring = false; + if (pendingRestore && !disposed) { pendingRestore = false; onContextRestored(); } + }); + } + + canvas?.addEventListener?.("webglcontextlost", onContextLost); + canvas?.addEventListener?.("webglcontextrestored", onContextRestored); + + function emit(message) { + if (disposed) return; + for (const listener of listeners.get("message") ?? []) listener({ data: message }); + } + + function postError(seq, phase, error, timing) { + if (error === STOPPED || disposed) return; + emit({ + type: "error", + seq, + phase: typeof phase === "string" ? phase : "presenter", + message: error instanceof Error ? error.message : String(error), + timing, + }); + } + + function receiptFor(request, receivedAt) { + return { + mainThreadReceiptMs: Number.isFinite(request.sentAt) && Number.isFinite(clock.timeOrigin) + ? Math.max(0, clock.timeOrigin + receivedAt - request.sentAt) + : null, + }; + } + + function elapsedSince(startedAt) { + return Math.max(0, clock.now() - startedAt); + } + + function isActive(ticket) { + return !disposed && ticket === generation; + } + + function ensureActive(ticket) { + if (!isActive(ticket)) throw STOPPED; + } + + function postMessage(request) { + if (disposed || !request || !Number.isSafeInteger(request.seq) || request.seq < 0) return; + + const receivedAt = clock.now(); + if (request.seq <= lastSequence) return; + lastSequence = request.seq; + if (request.type === "patch") clock.mark?.(`vision-main-received-${request.seq}`); + + const receipt = receiptFor(request, receivedAt); + if (request.type === "patch" && !hasSubmittedFrame) { + postError(request.seq, "patch", withContext(new Error("State patches are unavailable until the first GPU submission."), "patch", receipt), receipt); + return; + } + + const ticket = generation; + if (request.type === "init" || request.type === "init-empty") initialRequest = request; + queue = queue.then(() => handle(request, receipt, ticket)).catch((error) => { + postError(request.seq, error?.phase ?? request.type, error, error?.timing ?? receipt); + }); + } + + async function handle(request, receipt, ticket) { + ensureActive(ticket); + if (request.type === "init" || request.type === "init-empty") { + await initialize(request, receipt, ticket); + return; + } + + if (!engine) throw withContext(new Error("The Rust GPU engine is not ready."), request.type, receipt); + + if (request.type === "preload-fonts") { + if (!Array.isArray(request.fonts) || request.fonts.some(f => !(f instanceof Uint8Array))) throw new Error("Font preload requires byte arrays."); + if (typeof engine.preload_fonts !== "function") throw new Error("Rust engine does not support font preloading."); + const info = parseJsonRecord(await engine.preload_fonts(request.fonts), "preload_fonts()"); + ensureActive(ticket); + emit({type: "fonts-ready", seq: request.seq, fontRegistry: info}); + return; + } + + if (request.type === "clear-scene") { + if (typeof engine.clear_scene !== "function") throw new Error("Rust engine cannot release its scene."); + engine.clear_scene(); + sceneLoaded = false; + uploadedAssets.clear(); + hasSubmittedFrame = false; + emit({ type: "scene-cleared", seq: request.seq }); + return; + } + + if (request.type === "load") { + await loadScene(request, receipt, ticket); + return; + } + + if (request.type === "resize") { + await resize(request, receipt, ticket); + return; + } + + if (request.type === "patch") { + await patch(request, receipt, ticket); + return; + } + + if (request.type === "media-frames") { + await updateLiveFrames(request, receipt, ticket); + return; + } + + if (request.type === "media-clear") { + await clearLiveFrames(request, receipt, ticket); + return; + } + + throw withContext(new Error(`Unknown presenter request: ${String(request.type)}`), "presenter", receipt); + } + + async function initialize(request, receipt, ticket) { + if (engine) throw withContext(new Error("The Rust GPU engine is already initialized for this canvas."), "init", receipt); + if (request.canvas !== canvas || !canvas || typeof canvas.getContext !== "function") { + throw withContext(new Error("The HTML canvas presenter did not receive its owned HTMLCanvasElement."), "init", receipt); + } + + checkedSize(request.width, request.height); + context = canvas.getContext("webgl2", WEBGL2_CONTEXT_ATTRIBUTES); + if (!context) { + throw withContext(new Error("This browser cannot create the requested WebGL2 canvas context."), "init", receipt); + } + + const actualAttributes = typeof context.getContextAttributes === "function" + ? context.getContextAttributes() + : null; + if (!actualAttributes || actualAttributes.desynchronized !== true) { + throw withContext(new Error("The WebGL2 canvas context did not confirm desynchronized:true; the low-latency presenter cannot start."), "init", receipt); + } + + emit({ + type: "presenter-info", + seq: request.seq, + presenterInfo: { name: "HTMLCanvas · WebGL2", desynchronized: true }, + }); + + if (request.type !== "init-empty" && !packageBytes) { + const sceneResponse = await fetchRequired("/scene", "Scene package", fetchResource); + ensureActive(ticket); + packageBytes = new Uint8Array(await sceneResponse.arrayBuffer()); + } + if (request.type !== "init-empty" && !fontBytes) fontBytes = await fetchFonts(fetchResource); + ensureActive(ticket); + + const wasm = await loadWasm(); + ensureActive(ticket); + if (typeof wasm.default !== "function" || typeof wasm.VisionGpuEngine?.create_html_canvas !== "function") { + throw withContext(new Error("The WASM package does not export init() and VisionGpuEngine.create_html_canvas()."), "init", receipt); + } + await wasm.default(); + ensureActive(ticket); + + const initialSize = checkedSize(request.width, request.height); + if (request.type === "init-empty") { + if (typeof wasm.VisionGpuEngine.create_empty_html_canvas !== "function") throw new Error("Rust engine cannot initialize independently of a scene."); + const candidate = await wasm.VisionGpuEngine.create_empty_html_canvas(canvas, initialSize.width, initialSize.height); + if (!isActive(ticket)) { candidate?.free?.(); throw STOPPED; } + engine = candidate; + validateEngine(engine); + latestSize = initialSize; + emit({ type: "engine-ready", seq: request.seq }); + return; + } + const sceneByteLength = packageBytes.byteLength; + const candidate = await fontFactory(wasm.VisionGpuEngine, "create_html_canvas", fontBytes)( + packageBytes, + canvas, + initialSize.width, + initialSize.height, + ); + if (!isActive(ticket)) { + candidate?.free?.(); + throw STOPPED; + } + engine = candidate; + validateEngine(engine); + packageBytes = null; + fontBytes = null; + lastAppliedSequence = request.seq; + sceneLoaded = true; + + const info = readEngineInfo(engine); + const assetStartedAt = clock.now(); + await registerRequiredImages(engine, { Blob: BlobType, createImageBitmap: createBitmap }); + ensureActive(ticket); + const assetDecodeUploadMs = elapsedSince(assetStartedAt); + + latestSize = authoredSurfaceSize(info); + emit({ + type: "ready", + seq: request.seq, + info, + mediaSources: readMediaSources(engine), + width: latestSize.width, + height: latestSize.height, + sceneBytes: sceneByteLength, + presenterInfo: { name: "HTMLCanvas · WebGL2", desynchronized: true }, + timing: { ...receipt, assetDecodeUploadMs }, + }); + + try { + await submit(request.seq, latestSize, receipt, ticket); + } catch (error) { + throw withContext(error, "init", error?.timing ?? receipt); + } + } + + async function loadScene(request, receipt, ticket) { + if (!(request.packageBytes instanceof Uint8Array) || !Array.isArray(request.fonts) + || request.fonts.some(font => !(font instanceof Uint8Array))) { + throw new Error("Scene load requires fresh package bytes and explicit fonts."); + } + if (contextLost || context?.isContextLost?.()) throw new Error("GPU context unavailable during scene load."); + if (typeof engine.load_scene !== "function") throw new Error("Rust engine does not support persistent scene loading."); + const startedAt = clock.now(); + if (request.assets !== undefined) { + if (!Array.isArray(request.assets) || request.assets.some(asset => + typeof asset.path !== "string" || !(asset.bytes instanceof Uint8Array)) + || request.fonts.length !== 0 || typeof engine.load_scene_parts !== "function") { + throw new Error("Scene parts require bounded assets and an admitted font bank."); + } + await engine.load_scene_parts(request.packageBytes, + request.assets.map(asset => asset.path), request.assets.map(asset => asset.bytes)); + } else { + await engine.load_scene(request.packageBytes, request.fonts); + } + sceneLoaded = true; + ensureActive(ticket); + hasSubmittedFrame = false; + uploadedAssets.clear(); + const sceneLoadMs = elapsedSince(startedAt); + const assetStartedAt = clock.now(); + await registerRequiredImages(engine, { Blob: BlobType, createImageBitmap: createBitmap }, () => ensureActive(ticket)); + const info = readEngineInfo(engine); + latestSize = authoredSurfaceSize(info); + lastAppliedSequence = request.seq; + const timing = { ...receipt, sceneLoadMs, assetDecodeUploadMs: elapsedSince(assetStartedAt) }; + emit({ type: "ready", seq: request.seq, info, mediaSources: readMediaSources(engine), + width: latestSize.width, height: latestSize.height, sceneBytes: request.packageBytes.byteLength, timing }); + await submit(request.seq, latestSize, timing, ticket); + } + + async function registerRequiredImages(candidate, { Blob: BlobClass, createImageBitmap: decodeBitmap }, checkContext = () => {}) { + if (typeof BlobClass !== "function" || typeof decodeBitmap !== "function") { + throw new Error("This browser cannot decode the scene's required image assets."); + } + + const paths = parseJson(candidate.required_images_json(), "required_images_json()"); + if (!Array.isArray(paths) || paths.some((path) => typeof path !== "string" || path.length === 0)) { + throw new Error("The Rust engine returned an invalid required image path list."); + } + + const unique = [...new Set(paths)]; + const decoded = await Promise.allSettled(unique.map(async (path) => { + checkContext(); + const encoded = candidate.asset_bytes(path); + if (!(encoded instanceof Uint8Array)) throw new Error(`Image asset ${path} was not returned as Uint8Array.`); + const bitmap = await decodeBitmap(new BlobClass([encoded]), IMAGE_BITMAP_OPTIONS); + if (!bitmap || typeof bitmap.close !== "function") throw new Error(`Image asset ${path} did not decode to an ImageBitmap.`); + return { path, bitmap }; + })); + try { + const failed = decoded.find(result => result.status === "rejected"); + if (failed) throw failed.reason; + for (const result of decoded) { + const { path, bitmap } = result.value; + checkContext(); + await candidate.register_image(path, bitmap); + checkContext(); + uploadedAssets.add(path); + } + } finally { + for (const result of decoded) if (result.status === "fulfilled") result.value.bitmap.close(); + } + } + + async function registerPatchImages(images, ticket) { + if (images === undefined) return; + if (!Array.isArray(images) || images.length > 128 || images.some(image => + !image || typeof image.path !== "string" || !(image.bytes instanceof Uint8Array) + || image.bytes.length === 0 || image.bytes.length > 8 * 1024 * 1024)) { + throw new Error("Invalid bounded image asset patch."); + } + const unique = new Map(images.filter(image => !uploadedAssets.has(image.path)).map(image => [image.path, image])); + if (unique.size === 0) return; + if (typeof engine.register_image_asset !== "function") throw new Error("Rust GPU engine does not support retained image admission."); + const decoded = await Promise.allSettled([...unique.values()].map(async image => { + ensureActive(ticket); + const bitmap = await createBitmap(new BlobType([image.bytes]), IMAGE_BITMAP_OPTIONS); + if (!bitmap || typeof bitmap.close !== "function") throw new Error(`Image asset ${image.path} did not decode to an ImageBitmap.`); + return { ...image, bitmap }; + })); + try { + const failed = decoded.find(result => result.status === "rejected"); + if (failed) throw failed.reason; + for (const result of decoded) { + ensureActive(ticket); + if (contextLost || context?.isContextLost?.()) throw new Error("GPU context unavailable during image admission."); + const { path, bytes, bitmap } = result.value; + await engine.register_image_asset(path, bytes, bitmap); + uploadedAssets.add(path); + } + } finally { + for (const result of decoded) if (result.status === "fulfilled") result.value.bitmap.close(); + } + } + + async function resize(request, receipt, ticket) { + const size = checkedSize(request.width, request.height); + if (latestSize && latestSize.width === size.width && latestSize.height === size.height) { + emit({ type: "resize-unchanged", seq: request.seq, width: size.width, height: size.height }); + return; + } + + try { + await submit(request.seq, size, receipt, ticket); + latestSize = size; + } catch (error) { + throw withContext(error, "resize", error?.timing ?? receipt); + } + } + + async function patch(request, receipt, ticket) { + if (typeof request.patchJson !== "string") { + throw withContext(new Error("A state patch must be serialized JSON."), "patch", receipt); + } + + const applyStartedAt = clock.now(); + let rawMetadata; + try { + await registerPatchImages(request.images, ticket); + rawMetadata = await engine.apply_patch(request.patchJson); + ensureActive(ticket); + } catch (error) { + throw withContext(error, "patch", { ...receipt, engineApplyCallMs: elapsedSince(applyStartedAt) }); + } + + const engineApplyCallMs = elapsedSince(applyStartedAt); + let metadata; + try { + metadata = parseJsonRecord(rawMetadata, "apply_patch()"); + } catch (error) { + throw withContext(error, "patch", { ...receipt, engineApplyCallMs }); + } + + const patchTiming = { ...receipt, engineApplyCallMs }; + lastAppliedSequence = request.seq; + emit({ + type: "patch-applied", + seq: request.seq, + metadata: { ...metadata, seq: request.seq, engineApplyCallMs }, + mediaSources: readMediaSources(engine), + timing: patchTiming, + }); + + if (!latestSize) return; + try { + await submit(request.seq, latestSize, patchTiming, ticket); + } catch (error) { + throw withContext(error, "patch", error?.timing ?? patchTiming); + } + } + + async function updateLiveFrames(request, receipt, ticket) { + if (!hasSubmittedFrame) { + throw withContext(new Error("Live media updates are unavailable until the first GPU submission."), "media-frames", receipt); + } + if (!Array.isArray(request.frames) || request.frames.length === 0) { + throw withContext(new Error("A live media update must contain at least one frame."), "media-frames", receipt); + } + try { + for (const frame of request.frames) { + if (!frame || typeof frame.path !== "string" || !frame.bitmap || typeof frame.bitmap.close !== "function") { + throw new Error("A live media frame needs a path and an owned ImageBitmap."); + } + await engine.update_live_image(frame.path, frame.bitmap); + ensureActive(ticket); + } + if (latestSize) await submit(request.seq, latestSize, receipt, ticket); + lastAppliedSequence = request.seq; + emit({ type: "media-applied", seq: request.seq, mediaSources: readMediaSources(engine), timing: receipt }); + } catch (error) { + throw withContext(error, "media-frames", receipt); + } finally { + for (const frame of request.frames) frame?.bitmap?.close?.(); + } + } + + async function clearLiveFrames(request, receipt, ticket) { + if (!Array.isArray(request.paths) || request.paths.some((path) => typeof path !== "string" || path.length === 0)) { + throw withContext(new Error("A live media clear must contain valid image paths."), "media-clear", receipt); + } + try { + let changed = false; + for (const path of new Set(request.paths)) { + // Rust knows whether this active texture actually held live pixels. + // Legacy engines return void; keep their conservative render behavior. + changed = (await engine.clear_live_image(path)) !== false || changed; + ensureActive(ticket); + } + if (changed && latestSize) await submit(request.seq, latestSize, receipt, ticket); + lastAppliedSequence = request.seq; + emit({ type: "media-cleared", seq: request.seq, paths: [...new Set(request.paths)], timing: receipt }); + } catch (error) { + throw withContext(error, "media-clear", receipt); + } + } + + async function submit(seq, size, timing, ticket) { + if (contextLost || context?.isContextLost?.()) { + contextLost = true; + emit({ type: "frame-deferred", seq, phase: "context", timing, message: "State accepted; rendering awaits GPU context restoration." }); + return; + } + const info = readEngineInfo(engine); + const startedAt = clock.now(); + let rawStats; + try { + rawStats = await engine.render(size.width, size.height); + ensureActive(ticket); + } catch (error) { + throw withContext(error, "render", { ...timing, gpuSubmitCallMs: elapsedSince(startedAt) }); + } + + const gpuSubmitCallMs = elapsedSince(startedAt); + if (contextLost || context?.isContextLost?.()) { + contextLost = true; + emit({ type: "frame-deferred", seq, phase: "context", timing, message: "GPU context interrupted the submission." }); + return; + } + clock.mark?.(`vision-main-submitted-${seq}`); + + let stats; + try { + stats = parseJsonRecord(rawStats, "render()"); + } catch (error) { + throw withContext(error, "render", { ...timing, gpuSubmitCallMs }); + } + + emit({ + type: "frame-submitted", + seq, + width: size.width, + height: size.height, + stats, + info, + presenterInfo: { name: "HTMLCanvas · WebGL2", desynchronized: true }, + timing: { ...timing, gpuSubmitCallMs }, + }); + hasSubmittedFrame = true; + } + + function addEventListener(type, listener) { + if (typeof listener !== "function") return; + const entries = listeners.get(type) ?? new Set(); + entries.add(listener); + listeners.set(type, entries); + } + + function terminate() { + if (disposed) return; + disposed = true; + generation += 1; + const ownedEngine = engine; + engine = null; + latestSize = null; + hasSubmittedFrame = false; + // wasm-bindgen holds a mutable Rust borrow across asynchronous restoration. + // Release the engine only after that call and all queued work have settled. + queue = queue.catch(() => {}).then(() => ownedEngine?.free?.()); + canvas?.removeEventListener?.("webglcontextlost", onContextLost); + canvas?.removeEventListener?.("webglcontextrestored", onContextRestored); + packageBytes = null; + fontBytes = null; + initialRequest = null; + } + + return { + addEventListener, + postMessage, + terminate, + flush: () => queue, + }; +} + +async function fetchRequired(url, label, fetcher) { + const response = await fetcher(url, { cache: "no-store" }); + if (!response.ok) throw new Error(`${label} request failed with HTTP ${response.status}.`); + return response; +} + +function validateEngine(candidate) { + const methods = [ + "info_json", + "required_images_json", + "asset_bytes", + "live_media_sources_json", + "register_image", + "update_live_image", + "clear_live_image", + "render", + "apply_patch", + ]; + if (!candidate || methods.some((method) => typeof candidate[method] !== "function")) { + throw new Error("VisionGpuEngine is missing a required typed WASM method."); + } +} + +function readMediaSources(instance) { + const sources = parseJson(instance.live_media_sources_json(), "live_media_sources_json()"); + if (!Array.isArray(sources)) throw new Error("The Rust GPU engine returned an invalid live media source list."); + return sources; +} + +function readEngineInfo(instance) { + const info = parseJsonRecord(instance.info_json(), "info_json()"); + if (!Number.isFinite(info.width) || info.width <= 0 || !Number.isFinite(info.height) || info.height <= 0) { + throw new Error("The Rust GPU engine did not report valid authored scene dimensions."); + } + return info; +} + +function authoredSurfaceSize(info) { + return checkedSize(Math.ceil(info.width), Math.ceil(info.height)); +} + +function checkedSize(width, height) { + if (!Number.isSafeInteger(width) || width < 1 || !Number.isSafeInteger(height) || height < 1) { + throw new Error("Requested GPU surface dimensions are invalid."); + } + if (!Number.isSafeInteger(width * height)) throw new Error("Requested GPU surface dimensions exceed the supported range."); + return { width, height }; +} + +function parseJson(value, label) { + if (typeof value !== "string") throw new Error(`Rust ${label} did not return JSON text.`); + try { + return JSON.parse(value); + } catch { + throw new Error(`Rust ${label} returned invalid JSON.`); + } +} + +function parseJsonRecord(value, label) { + const result = parseJson(value, label); + if (!result || typeof result !== "object" || Array.isArray(result)) { + throw new Error(`Rust ${label} did not return a JSON object.`); + } + return result; +} + +function withContext(value, phase, timing) { + const error = value instanceof Error ? value : new Error(String(value)); + error.phase = phase; + error.timing = timing; + return error; +} diff --git a/public/vision/vision-assets.json b/public/vision/vision-assets.json new file mode 100644 index 0000000..e13aba0 --- /dev/null +++ b/public/vision/vision-assets.json @@ -0,0 +1,33 @@ +{ + "schema": "solar.vision-assets.v1", + "upstream": "Lumencast/lumencast-vision", + "base_revision": "031788f42750f08e00dfa506c0e85d0819da8aa1", + "source_state": "Local uncommitted direct LSML/assets transfer, canonical serialization and verified font candidate; not merged or deployed.", + "files": { + "ui/mainpresenter.mjs": { + "bytes": 26702, + "sha256": "88de7073b8538a12998139bc499866a06eb03925ff523f7683fb2f721aab3eae" + }, + "ui/fonts.mjs": { + "bytes": 1977, + "sha256": "d49ed45181a8aa714f675aaa24f5439e9547b274d6eba6cd2bb995c981765c5f" + }, + "pkg/lumencast_vision_web.js": { + "bytes": 148483, + "sha256": "f9f61fbf53fee481cf5c26179d853bf34eb94c0fb5cb4c8d386264638c31297a" + }, + "pkg/lumencast_vision_web_bg.wasm": { + "bytes": 10253842, + "sha256": "7626634a8ad14231ee9f7a50b0aaacca999d9f78d30cb4e171f798b3aa2a3796" + } + }, + "source_provenance": { + "repository": "Lumencast/lumencast-vision", + "engine_base": "031788f42750f08e00dfa506c0e85d0819da8aa1", + "engine_branch": "eleven/local-20261008-font-preload", + "engine_evidence": "evidence/local-20261008-direct-scene-parts/eleven/", + "working_diff_sha256": "a99e5020b608d4a391f059be30d780f25dad9fee22854b30b30e411e4bd66d5a" + }, + "source_revision": "031788f42750f08e00dfa506c0e85d0819da8aa1", + "merged_revision": "031788f42750f08e00dfa506c0e85d0819da8aa1" +} diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..7200109 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,72 @@ +# Solar maintenance and qualification tools + +Owner: Solar build/installation/qualification. Package commands and the following CLI tools +are active entrypoints. Helpers belong to their nearest README. They are not renderer code. +All generated output must be inside this worktree's `build/`, `dist/` or canonical `evidence/`. +No tool implicitly publishes, deploys or edits Prism. Host/camera probes launch only owned processes. + +## Build, guards and installation + +| File | Invocation / responsibility | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `build-host-html.mjs` | Build's versioned relative CEF HTML entry | +| `build-server.mjs` | `npm run build:server`; Node reception package and declarations | +| `check-host-bundle.mjs` | `npm run check:bundle`; actual emitted source maps and self-contained imports | +| `check-runtime-layout.mjs` | `npm run check:layout`; unique assets and installed-root collisions | +| `check-vision-assets.mjs` | Build and bundle; immutable Vision file SHA-256/size pins | +| `check-native-client.mjs` | `npm run check:native-client`; upstream client/license/vector pins | +| `check-native-package.mjs` | `npm run check:native-package`; packaged native manifest/binary hashes | +| `check-source-usage.mjs` | `npm run check:source-usage`; product module/export/dependency graph | +| `check-documentation.mjs` | `npm run check:docs`; local current links/paths/commands and tool ownership | +| `check_file_sizes.py` | `python scripts/check_file_sizes.py`; per-file size policy and self-tests | +| `package-native-server.mjs` | `npm run package:native -- --binary --platform --revision ` | +| `create-runtime-manifest.mjs` | Release only; archive/version/repository/tag/output arguments, both native platforms required | +| `patch-peer-viewer.mjs` | Package postinstall; [receive-only compatibility](peer-viewer/README.md) | +| `test-peer-install.mjs` | `npm run test:peer-install -- `; fresh lock-pinned hoisted deps, exact three targets and idempotence | +| `code/` | [Derived discovery and docs guard](code/README.md); `npm run code:read -- ` | + +The `.github/workflows/release.yml` source declares the actual release checks. A configured +runner or binary path does not prove provisioning, publication, deployment or application startup. + +## Real native and process-level checks + +| File | Invocation / responsibility | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `native-lsdp-local.mjs` | `npm run native:start -- --help`; same product receiver, RAM-only mutation host | +| `native-lsdp-tools.mjs` | Helper used by local/proof producers; runtime bundling/native reads | +| `test-native-lsdp.mjs` | `npm run test:native -- `; actual binary snapshots/operations | +| `test-reception-server.mjs` | `node scripts/test-reception-server.mjs ` | +| `test-reception-install.mjs` | `node scripts/test-reception-install.mjs `; installed dist and existing adapter loaded in memory | +| `test-reception-recovery.mjs` | `node scripts/test-reception-recovery.mjs `; child loss and same-port recovery | +| `test-reception-burst.mjs` | `node scripts/test-reception-burst.mjs `; native burst/recovery boundary, not pixel cadence | +| `test-packaged-cache.mjs` | `node scripts/test-packaged-cache.mjs`; built immutable store/export boundary | + +The integration adapter test's Prism argument is read-only; the full installed application +lifecycle remains a separate Prism task. See [qualification](../docs/runbooks/qualification.md). + +## Scene/camera/producer qualification + +| File | Responsibility | +| -------------------------------- | -------------------------------------------------------------------------------------- | +| `prove-native-cef.py` | Native LSML mutations plus real owned Pulsar CEF scene | +| `prove-scene-control-cef.py` | Two lanes, coordinated transitions and compensation | +| `prove-cold-start-cef.py` | Complete shutdown/restart, immutable capsules, explicit real-admission or fixture mode | +| `prove-peer-camera-cef.py` | Actual Meet, physical publisher and receive-only CEF viewers | +| `prove-authenticated-canvas.mjs` | Identity/source route/broker-locator probe with separate boundary verdicts | +| `prove-read-query-gateway.mjs` | Authoritative query/gateway diagnostic; operator denial is not broker artifact absence | +| `proof/` | [Capture, producer and analysis helpers](proof/README.md) | + +Pass `--help` to Python drivers; Node diagnostic headers document their arguments. Each run +records its specific admitted source, receipt/pixel boundary, errors and owned-process cleanup. +External success is never inferred from a unit test or a connection ACK. + +## Historical evidence + +The old React/bundle postinstall is removed. Session-specific `certify-chain.py`, +`consolidate-maturity.py`, `consolidate-canvas-offline.py` and the early fixture-only +`verify-canvas-acquisition.mjs` are archived byte-for-byte under +[`evidence/local-20261005-solar-audit/forge/`](../evidence/local-20261005-solar-audit/forge/). +They are retained to explain immutable phase certificates and hashes, not to rewrite current +source or docs. Owner: the corresponding qualification phase; review condition: deletion +requires retiring the linked historical certificate. The dated burst reproduction script +remains with its captured baseline, as documented in the cadence runbook. diff --git a/scripts/bench-editable-preview.mjs b/scripts/bench-editable-preview.mjs deleted file mode 100644 index 2f982cd..0000000 --- a/scripts/bench-editable-preview.mjs +++ /dev/null @@ -1,318 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { createHash } from "node:crypto"; -import { mkdir, readFile, writeFile } from "node:fs/promises"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parseArgs } from "node:util"; -import { chromium } from "@playwright/test"; -import ts from "typescript"; - -const { values } = parseArgs({ - options: { - baseline: { type: "string" }, - output: { type: "string", default: "test-results/editable-preview-bench" }, - }, -}); -assert( - values.baseline, - "Pass --baseline for the unoptimized source.", -); -const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); -const sourceDir = "src/internal/editable-preview"; -const git = (...args) => - execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim(); -const sha256 = (data) => createHash("sha256").update(data).digest("hex"); -const baselineRevision = git( - "rev-parse", - "--verify", - `${values.baseline}^{commit}`, -); -const output = resolve(root, values.output); -await mkdir(output, { recursive: true }); - -// Compile the actual entrypoint and its relative imports, without substituting -// a hand-written optimized implementation for either side of the comparison. -async function bundle(readSource) { - const modules = new Map(); - const sources = new Map(); - async function visit(id) { - if (modules.has(id)) return; - assert.match(id, /^\.\/[a-z-]+$/); - const source = await readSource(`${sourceDir}/${id.slice(2)}.ts`); - sources.set(id, source); - const compiled = ts.transpileModule(source, { - compilerOptions: { - target: ts.ScriptTarget.ES2022, - module: ts.ModuleKind.CommonJS, - }, - }).outputText; - modules.set(id, compiled); - for (const match of compiled.matchAll(/require\("(\.\/[a-z-]+)"\)/g)) { - await visit(match[1]); - } - } - await visit("./sideband"); - return { - hash: sha256(JSON.stringify([...sources].sort())), - code: `(() => { - const factories = {${[...modules].map(([id, code]) => `${JSON.stringify(id)}: (module, exports, require) => {${code}\n}`).join(",")}}; - const loaded = new Map(); - const require = (id) => { - if (!loaded.has(id)) { - const module = { exports: {} }; - loaded.set(id, module); - factories[id](module, module.exports, require); - } - return loaded.get(id).exports; - }; - globalThis.installBenchmarkSideband = require("./sideband").installEditablePreviewSideband; - })();`, - }; -} - -const baseline = await bundle((path) => - git("show", `${baselineRevision}:${path}`), -); -const candidate = await bundle((path) => readFile(resolve(root, path), "utf8")); -const browser = await chromium.launch({ - headless: true, - channel: - process.env.SOLAR_E2E_BROWSER === "system-chrome" ? "chrome" : undefined, -}); -const scenarios = [ - { wrappers: 100, patches: 4 }, - { wrappers: 500, patches: 4 }, - { wrappers: 1000, patches: 1 }, - { wrappers: 1000, patches: 4 }, - { wrappers: 1000, patches: 8 }, - { wrappers: 1000, patches: 4, replaceEvery: 15 }, -]; -const report = { - baselineRevision, - candidateHead: git("rev-parse", "HEAD"), - sourceHashes: { baseline: baseline.hash, candidate: candidate.hash }, - browser: browser.version(), - frames: 75, - samples: 7, - method: - "Synthetic DOM with decoded images and SVG. Real sideband/MutationObserver; controlled RAF and WebSocket. CPU timings include microtasks, exclude layout/paint and network.", - results: [], -}; - -async function measure(page, scenario) { - return page.evaluate( - async ({ scenario, frames }) => { - const { document, performance, EventTarget, MessageEvent, TextEncoder } = - globalThis; - const image = (colour) => - `data:image/svg+xml,${encodeURIComponent(``)}`; - const scene = document.getElementById("scene"); - scene.innerHTML = Array.from( - { length: scenario.wrappers }, - (_, i) => - `
Item ${i}
`, - ).join(""); - await Promise.all( - [...scene.querySelectorAll("img")].map((img) => img.decode()), - ); - await document.fonts.ready; - const definitions = [ - ["opacity", 0.5], - ["width", 64], - ["fontSize", 12], - ["fill", "#ff0066"], - ["translate", [4, 2]], - ["value", "Updated"], - ["src", image("#26a")], - ["background", "#804"], - ]; - const indices = Array.from({ length: scenario.patches }, (_, i) => - Math.floor(((i + 1) * scenario.wrappers) / (scenario.patches + 1)), - ); - const patches = indices.map((index, i) => ({ - path: `__editable.${Array.from(new TextEncoder().encode(`component-${index}`), (byte) => byte.toString(16).padStart(2, "0")).join("")}.${definitions[i][0]}`, - value: definitions[i][1], - })); - - let id = 0; - let bindingQueries = 0; - const callbacks = new Map(); - const nativeRAF = globalThis.requestAnimationFrame; - const nativeCancelRAF = globalThis.cancelAnimationFrame; - const nativeQueryAll = document.querySelectorAll; - globalThis.requestAnimationFrame = (callback) => { - callbacks.set(++id, callback); - return id; - }; - globalThis.cancelAnimationFrame = (key) => callbacks.delete(key); - document.querySelectorAll = function (selector) { - if (selector === "[data-lumencast-bind-animate]") bindingQueries++; - return nativeQueryAll.call(this, selector); - }; - const sockets = []; - class Socket extends EventTarget { - constructor() { - super(); - sockets.push(this); - } - close() { - this.closed = true; - } - } - const stop = globalThis.installBenchmarkSideband( - "ws://127.0.0.1/benchmark", - document, - Socket, - ); - try { - const initialStart = performance.now(); - sockets[0].dispatchEvent( - new MessageEvent("message", { - data: JSON.stringify({ type: "accepted_patch", patches }), - }), - ); - await Promise.resolve(); - const initialMs = performance.now() - initialStart; - let frameMs = 0; - for (let frame = 0; frame < frames; frame++) { - // Runtime writes are deliberately restored on every frame. A cache - // must still reapply accepted state instead of merely doing no work. - const opacityNode = scene.children[indices[0]]; - opacityNode.style.opacity = "1"; - if ( - scenario.replaceEvery && - frame > 0 && - frame % scenario.replaceEvery === 0 - ) { - const index = indices[1]; - const replacement = scene.children[index].cloneNode(true); - replacement.style.width = "1px"; - for (const element of replacement.querySelectorAll( - "div,svg,img,video,canvas", - )) { - element.style.width = "1px"; - } - scene.children[index].replaceWith(replacement); - } - if (callbacks.size !== 1) - throw new Error("Convergence lease expired during benchmark"); - const start = performance.now(); - const pending = [...callbacks.values()]; - callbacks.clear(); - for (const callback of pending) callback(start); - await Promise.resolve(); - frameMs += performance.now() - start; - if (opacityNode.style.opacity !== "0.5") - throw new Error("Accepted opacity was not restored"); - } - await Promise.all( - [...scene.querySelectorAll("img")].map((img) => img.decode()), - ); - return { - initialMs, - frameMs, - totalMs: initialMs + frameMs, - bindingQueries, - html: scene.innerHTML, - }; - } finally { - stop(); - globalThis.requestAnimationFrame = nativeRAF; - globalThis.cancelAnimationFrame = nativeCancelRAF; - document.querySelectorAll = nativeQueryAll; - } - }, - { scenario, frames: report.frames }, - ); -} - -try { - const pages = {}; - for (const [name, source] of Object.entries({ baseline, candidate })) { - const page = await browser.newPage({ - viewport: { width: 1920, height: 1080 }, - deviceScaleFactor: 1, - }); - await page.setContent( - `
`, - ); - await page.addScriptTag({ content: source.code }); - pages[name] = page; - } - for (const scenario of scenarios) { - const samples = { baseline: [], candidate: [] }; - // One warm-up, then alternate order to reduce fixed-order/JIT bias. - for (const page of Object.values(pages)) await measure(page, scenario); - for (let round = 0; round < report.samples; round++) { - for (const name of round % 2 - ? ["candidate", "baseline"] - : ["baseline", "candidate"]) { - samples[name].push(await measure(pages[name], scenario)); - } - } - const last = Object.fromEntries( - Object.entries(samples).map(([key, entries]) => [key, entries.at(-1)]), - ); - assert.equal( - last.candidate.html, - last.baseline.html, - "DOM differs between baseline and candidate", - ); - const name = `${scenario.wrappers}-${scenario.patches}-${scenario.replaceEvery ?? 0}`; - const screenshots = {}; - for (const key of ["baseline", "candidate"]) { - screenshots[key] = await pages[key].screenshot({ - path: resolve(output, `${name}-${key}.png`), - }); - } - assert( - screenshots.baseline.equals(screenshots.candidate), - `PNG differs for ${name}`, - ); - const stats = (entries) => { - const median = (key) => - entries.map((entry) => entry[key]).sort((a, b) => a - b)[ - Math.floor(entries.length / 2) - ]; - return { - initialMs: median("initialMs"), - frameMs: median("frameMs"), - totalMs: median("totalMs"), - msPerFrame: median("frameMs") / report.frames, - bindingQueries: entries.map((entry) => entry.bindingQueries), - totalMsSamples: entries.map((entry) => entry.totalMs), - }; - }; - const expectedQueries = - 1 + - (scenario.replaceEvery - ? Math.floor((report.frames - 1) / scenario.replaceEvery) - : 0); - assert( - samples.candidate.every( - (entry) => entry.bindingQueries === expectedQueries, - ), - "Unexpected repeated binding scan", - ); - const result = { - ...scenario, - baseline: stats(samples.baseline), - candidate: stats(samples.candidate), - identicalDom: true, - identicalPng: true, - pngSHA256: sha256(screenshots.candidate), - }; - result.cpuReductionPercent = - 100 * (1 - result.candidate.totalMs / result.baseline.totalMs); - report.results.push(result); - console.log(JSON.stringify(result)); - } - await writeFile( - resolve(output, "report.json"), - `${JSON.stringify(report, null, 2)}\n`, - ); - console.log(`Report: ${resolve(output, "report.json")}`); -} finally { - await browser.close(); -} diff --git a/scripts/build-host-html.mjs b/scripts/build-host-html.mjs index 352221d..c43046b 100644 --- a/scripts/build-host-html.mjs +++ b/scripts/build-host-html.mjs @@ -1,29 +1,17 @@ #!/usr/bin/env node /** - * Reconciles the host build's emitted HTML into the served contract - * (ADR 001 §3.3) — runs as the third step of `npm run build`, after the - * library build and the host build (`vite build --config vite.config.host.ts`). + * Reconciles the standalone Vision host HTML after its Vite build. * - * The host Vite target (app mode, no externals) emits a self-contained - * bundle under `dist/host/`: a hashed JS chunk that inlines every runtime - * dep (react / react-dom / @preact/signals-react / framer-motion) and an - * HTML file that imports it via a RELATIVE module URL (`./assets/host-*.js`). + * The host Vite target emits a self-contained bundle under `dist/host/` + * and an HTML file that imports it via a relative module URL. * Because Vite names the HTML after its input (`host.html`), this step: * - * 1. renames `dist/host/host.html` → `dist/host/index.html` so the served - * contract stays `/static/solar/v{N}/index.html` (D3, unchanged shape); - * 2. stamps the real package version into `` - * (resolution criterion 5: generator reflects 0.2.1); - * 3. asserts the served HTML's entry script is a relative `./` URL — a - * cheap guard so the served form can never regress to a bare/absolute - * module specifier the CEF can't resolve (D1). The deep - * anti-bare-specifier scan of the JS lives in - * scripts/check-host-bundle.mjs (CI gate). + * 1. renames `host.html` to the served `index.html`; + * 2. stamps the package version into the generator meta; + * 3. verifies the entry script remains relative for CEF. * - * This is intentionally a thin reconcile, not a hand-rolled bootstrap: the - * bootstrap now lives in src/host-entry.tsx and is compiled into the - * hashed chunk, so there is no inlined `import "./solar.js"` to keep in - * sync with the build's filenames (R3 mitigation). + * The bootstrap lives in src/host-entry.ts and is compiled into the host + * chunk, so no renderer entry is hand-maintained here. */ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"; @@ -84,6 +72,23 @@ if (!entrySrc.startsWith("./") && !entrySrc.startsWith("../")) { writeFileSync(served, html); +writeFileSync( + resolve(hostDir, "solar-host-contract.json"), + JSON.stringify( + { + schema_version: "solar.host.contract.v1", + protocol_version: "solar.host.v2", + capabilities: [ + "solar.vision.lsml.v1", + "solar.native-lsdp.v1", + "solar.source-provider.v1", + ], + }, + null, + 2, + ) + "\n", +); + const bytes = Buffer.byteLength(html, "utf8"); console.log( `solar host html : ${bytes} B raw at ${served} (entry ${entrySrc}, generator @zablab/solar ${pkg.version})`, diff --git a/scripts/build-server.mjs b/scripts/build-server.mjs new file mode 100644 index 0000000..f607f9d --- /dev/null +++ b/scripts/build-server.mjs @@ -0,0 +1,16 @@ +import { build } from "esbuild"; +import { execFileSync } from "node:child_process"; +execFileSync( + process.execPath, + ["node_modules/typescript/bin/tsc", "-p", "tsconfig.server.json"], + { stdio: "inherit" }, +); +await build({ + entryPoints: ["src/server/index.ts"], + outfile: "dist/server/index.mjs", + bundle: true, + platform: "node", + target: "node22", + format: "esm", + sourcemap: true, +}); diff --git a/scripts/check-bundle-size.mjs b/scripts/check-bundle-size.mjs deleted file mode 100644 index 3aebe8d..0000000 --- a/scripts/check-bundle-size.mjs +++ /dev/null @@ -1,183 +0,0 @@ -// Verifies that Solar's per-mode chunks honour the size budget -// declared in chantier-solar.md and that the broadcast chunk does -// not pull in any overlay (control / test / status-pill) code. -// -// Run after `npm run build`. Exits non-zero on any violation — -// designed to be wired into CI as a separate step. - -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; -import { gzipSync } from "node:zlib"; -import { resolve, join } from "node:path"; - -const DIST = resolve(import.meta.dirname, "..", "dist"); -const HOST_DIR = resolve(DIST, "host"); - -// Per-mode budgets, gzipped, in bytes. These target the LIBRARY/runtime -// chunks (externals; react/framer NOT inlined) consumed by Prism. -const BROADCAST_BUDGET = 200 * 1024; -const CONTROL_BUDGET = 280 * 1024; -const TEST_BUDGET = 360 * 1024; - -// Dedicated budget for the self-contained HOST bundle (ADR 001 §4): the -// served artefact inlines react + react-dom + @preact/signals-react + -// framer-motion, so it is necessarily much larger than the library -// chunks and is NOT held to the library budgets. Observed ~120 KiB gz -// (the inlined main chunk dominates at ~112 KiB); 400 KiB gz leaves -// headroom for React/Framer growth while still catching a regression -// that doubles the served weight (e.g. a duplicate runtime). -const HOST_BUDGET = 400 * 1024; - -// Since ADR 007 (Solar = thin adapter over @lumencast/runtime), the -// per-mode chunks come from the runtime and may carry TWO hash segments -// (the runtime's own chunk hash + Vite's re-bundle hash), e.g. -// `broadcast-BqOhSNsY-8nj7XQpl.js`. Match against the known source names -// rather than trying to guess where the hash boundary is. -const KNOWN_PREFIXES = [ - "solar", - "index", - "tree", - "broadcast", - "control", - "test", - "status-pill", -]; - -const files = readdirSync(DIST).filter((f) => f.endsWith(".js")); -const fileMap = new Map(); // source name → { full, raw, gzip } -for (const f of files) { - const buf = readFileSync(join(DIST, f)); - const gzip = gzipSync(buf).length; - // A chunk maps to a known prefix when its name is exactly the prefix - // (`solar.js`) or the prefix followed by a hash segment - // (`broadcast-XXXX[-YYYY].js`). Longest match wins so `status-pill` - // isn't shadowed by a shorter prefix. - const match = KNOWN_PREFIXES.filter( - (p) => f === `${p}.js` || f.startsWith(`${p}-`), - ).sort((a, b) => b.length - a.length)[0]; - if (match) fileMap.set(match, { full: f, raw: buf.length, gzip }); -} - -function need(prefix) { - const entry = fileMap.get(prefix); - if (!entry) { - fail(`expected dist chunk for "${prefix}" — not found`); - } - return entry; -} - -const errors = []; -function fail(msg) { - errors.push(msg); -} - -const solar = need("solar"); -const indexShared = need("index"); -const tree = need("tree"); -const broadcast = need("broadcast"); -const control = need("control"); -const test = need("test"); -const statusPill = need("status-pill"); - -// --- Per-mode bundle weights --------------------------------------- - -const broadcastBundle = - solar.gzip + indexShared.gzip + tree.gzip + broadcast.gzip; -const controlBundle = broadcastBundle + control.gzip + statusPill.gzip; -const testBundle = controlBundle + test.gzip; - -console.log("solar runtime — chunk sizes"); -console.log( - " broadcast : %d B raw / %d B gz (sum)", - solar.raw + indexShared.raw + tree.raw + broadcast.raw, - broadcastBundle, -); -console.log( - " control : %d B raw / %d B gz (sum)", - solar.raw + - indexShared.raw + - tree.raw + - broadcast.raw + - control.raw + - statusPill.raw, - controlBundle, -); -console.log( - " test : %d B raw / %d B gz (sum)", - solar.raw + - indexShared.raw + - tree.raw + - broadcast.raw + - control.raw + - statusPill.raw + - test.raw, - testBundle, -); - -if (broadcastBundle > BROADCAST_BUDGET) { - fail( - `broadcast bundle ${broadcastBundle} B gz > ${BROADCAST_BUDGET} B budget`, - ); -} -if (controlBundle > CONTROL_BUDGET) { - fail(`control bundle ${controlBundle} B gz > ${CONTROL_BUDGET} B budget`); -} -if (testBundle > TEST_BUDGET) { - fail(`test bundle ${testBundle} B gz > ${TEST_BUDGET} B budget`); -} - -// --- Broadcast chunk does not import overlay chunks --------------- - -const broadcastSource = readFileSync(join(DIST, broadcast.full), "utf8"); - -// The chunk's import statements reference the chunks it depends on -// by relative URL. We can grep for the basename of the forbidden -// chunks. If broadcast imports any of them, it's a tree-shake leak. -const FORBIDDEN_FOR_BROADCAST = ["control", "test", "status-pill"]; -for (const forbidden of FORBIDDEN_FOR_BROADCAST) { - const ref = fileMap.get(forbidden); - if (!ref) continue; - // Looking for an import like `from"./control-XXXXX.js"` or similar. - if (broadcastSource.includes(ref.full)) { - fail( - `broadcast chunk references "${ref.full}" — overlay code leaked into broadcast mode`, - ); - } -} - -// --- Host bundle budget (self-contained, deps inlined) ------------ - -if (existsSync(HOST_DIR) && statSync(HOST_DIR).isDirectory()) { - let hostRaw = 0; - let hostGzip = 0; - const walk = (dir) => { - for (const name of readdirSync(dir)) { - const full = join(dir, name); - if (statSync(full).isDirectory()) walk(full); - else if (name.endsWith(".js")) { - const buf = readFileSync(full); - hostRaw += buf.length; - hostGzip += gzipSync(buf).length; - } - } - }; - walk(HOST_DIR); - - console.log("\nsolar host bundle — served artefact (deps inlined)"); - console.log(" host (all js) : %d B raw / %d B gz (sum)", hostRaw, hostGzip); - - if (hostGzip > HOST_BUDGET) { - fail(`host bundle ${hostGzip} B gz > ${HOST_BUDGET} B budget`); - } -} else { - fail(`host bundle dir ${HOST_DIR} missing — did the host build run?`); -} - -// --- Done --------------------------------------------------------- - -if (errors.length > 0) { - console.error("\nbundle-size check FAILED :"); - for (const e of errors) console.error(" -", e); - process.exit(1); -} - -console.log("\nbundle-size check OK"); diff --git a/scripts/check-documentation.mjs b/scripts/check-documentation.mjs new file mode 100644 index 0000000..2bcef8c --- /dev/null +++ b/scripts/check-documentation.mjs @@ -0,0 +1,5 @@ +import { resolve } from "node:path"; +import { documentationReport } from "./code/documentation.mjs"; +const report = documentationReport(resolve(import.meta.dirname, "..")); +console.log(JSON.stringify(report, null, 2)); +if (report.failures.length) process.exitCode = 1; diff --git a/scripts/check-host-bundle.mjs b/scripts/check-host-bundle.mjs index d5f4fdc..b603943 100644 --- a/scripts/check-host-bundle.mjs +++ b/scripts/check-host-bundle.mjs @@ -1,184 +1,73 @@ -#!/usr/bin/env node -/** - * Anti-bare-specifier gate for the served host bundle — ADR 001 §6.2, - * the core guard against the B4 regression. - * - * The Pulsar CEF (and Orion's static serve) load `dist/host/index.html` - * in a bare browser: no bundler, no ``; + data = Buffer.from( + data.toString("utf8").replace("", `${bootstrap}`), + ); + } + response.writeHead(200, { + "Content-Type": mime[extname(file)] ?? "application/octet-stream", + "Content-Length": data.length, + "Cache-Control": "no-store", + }); + response.end(data); + } catch (error) { + json(response, error.code === "ENOENT" ? 404 : 400, { + error: error.message, + }); + } +}); +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", resolve); + }); +} catch (error) { + await native.stop(); + throw error; +} +let closing = false; +async function stop() { + if (closing) return; + closing = true; + server.closeAllConnections(); + server.close(); + await native.stop(); +} +process.once("SIGINT", () => { + void stop(); +}); +process.once("SIGTERM", () => { + void stop(); +}); +process.once("exit", () => { + void native.stop(); +}); +console.log( + JSON.stringify({ + solar: origin + "/", + native: native.url, + nativePid: native.pid, + sceneId, + archiveDigest, + persistence: "memory-only", + }), +); diff --git a/scripts/native-lsdp-tools.mjs b/scripts/native-lsdp-tools.mjs new file mode 100644 index 0000000..bf76aec --- /dev/null +++ b/scripts/native-lsdp-tools.mjs @@ -0,0 +1,89 @@ +import { SolarReceptionServer } from "../dist/server/index.mjs"; +import { build } from "esbuild"; +import { resolve } from "node:path"; +import { BrowserLSDP } from "../vendor/lsdp-native-browser/src/browser.js"; + +// Bundle the exact browser hash implementation in memory for Node >=20 tools. +const built = await build({ + stdin: { + contents: + 'export { nativeTreeHash } from "./src/internal/native-tree.ts"; export { NativeState } from "./src/internal/native-state.ts";', + resolveDir: resolve(import.meta.dirname, ".."), + }, + bundle: true, + format: "esm", + platform: "node", + write: false, +}); +export const { nativeTreeHash, NativeState } = await import( + `data:text/javascript;base64,${Buffer.from(built.outputFiles[0].text).toString("base64")}` +); + +export async function startNative(binary, document, origin, onFailure) { + const reception = new SolarReceptionServer({ + binaryPath: binary, + origins: [origin], + resources: { + scene: { type: "solar.lsml/1", initial: document }, + "orion/state": { type: "orion.state/1", initial: {} }, + "solar/program": { type: "solar.lsml/1", initial: null }, + "solar/preview": { type: "solar.lsml/1", initial: null }, + "solar/generations": { type: "orion.state/1", initial: {} }, + "solar/sessions": { type: "orion.state/1", initial: {} }, + }, + onFailure, + }); + const connection = await reception.start(); + return { + reception, + url: connection.websocketUrl, + get pid() { + return reception.status().pid; + }, + address: connection.address, + stop: () => reception.stop(), + }; +} + +export async function readNative(peer, target = "scene") { + const previous = peer.options.onTransaction; + let resolve, reject; + const result = new Promise((yes, no) => { + resolve = yes; + reject = no; + }); + const timer = setTimeout( + () => reject(new Error("Native snapshot timed out.")), + 30_000, + ); + void result.catch(() => {}); + peer.options.onTransaction = async (value, context) => { + if ( + context.metadata.profile !== "lsdp.state.read/1" || + context.metadata.target !== target + ) { + if (previous) return previous(value, context); + throw new Error("Unexpected native download."); + } + const stateHash = nativeTreeHash(value); + resolve({ state: value, stateHash }); + return { level: "applied", stateHash }; + }; + try { + await peer.transaction({ kind: "state.read", target }); + return await result; + } finally { + clearTimeout(timer); + peer.options.onTransaction = previous; + } +} + +export async function withNative(url, callback) { + const peer = new BrowserLSDP(url); + try { + await peer.ready; + return await callback(peer); + } finally { + peer.close(); + } +} diff --git a/scripts/package-native-server.mjs b/scripts/package-native-server.mjs new file mode 100644 index 0000000..6b2e166 --- /dev/null +++ b/scripts/package-native-server.mjs @@ -0,0 +1,58 @@ +import { createHash } from "node:crypto"; +import { copyFile, chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { parseArgs } from "node:util"; +const { values } = parseArgs({ + options: { + binary: { type: "string" }, + platform: { type: "string" }, + revision: { type: "string" }, + }, +}); +if ( + !values.binary || + !/^(win32|linux|darwin)-(x64|arm64)$/.test(values.platform ?? "") || + !/^[a-f0-9]{40}$/.test(values.revision ?? "") +) + throw new Error( + "Usage: package-native-server --binary --platform --revision ", + ); +const key = values.platform, + name = key.startsWith("win32-") ? "lsdpd.exe" : "lsdpd"; +const target = resolve("dist/native", key, name); +await mkdir(dirname(target), { recursive: true }); +await copyFile(resolve(values.binary), target); +await chmod(target, 0o755); +const manifestPath = resolve("dist/native/manifest.json"); +let previous; +try { + previous = JSON.parse(await readFile(manifestPath, "utf8")); +} catch (error) { + if (error.code !== "ENOENT") throw error; +} +if (previous && previous.source_revision !== values.revision) + throw new Error( + "All packaged platforms must use the same native source revision.", + ); +const manifest = { + schema: "solar.native-server.v1", + wire: "LSDP-TCP/2.0-draft2", + source_revision: values.revision, + binaries: { + ...previous?.binaries, + [key]: { + file: `${key}/${name}`, + sha256: createHash("sha256") + .update(await readFile(target)) + .digest("hex"), + }, + }, +}; +await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n"); +console.log( + JSON.stringify({ + platform: key, + revision: values.revision, + sha256: manifest.binaries[key].sha256, + }), +); diff --git a/scripts/patch-known-peer-roster.mjs b/scripts/patch-known-peer-roster.mjs deleted file mode 100644 index 2880771..0000000 --- a/scripts/patch-known-peer-roster.mjs +++ /dev/null @@ -1,100 +0,0 @@ -import { readFile, writeFile } from "node:fs/promises"; - -// Keep the authoritative peer roster available when signaling overtakes -// peer-joined, across both readable runtime modules and bundled distributions. -export async function patchKnownPeerRoster(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - if (!source.includes("knownPeers =")) { - source = source.replace( - /(\n {2}remotes = [^\n]+;\n)/, - "$1 knownPeers = /* @__PURE__ */ new Map();\n", - ); - } - source = source.replace( - /for \(const ([A-Za-z_$][\w$]*) of ([A-Za-z_$][\w$]*)\.peers\) this\.ensureRemote\(\1\);/g, - "for (const $1 of $2.peers) this.knownPeers.set($1.id, $1), this.ensureRemote($1);", - ); - source = source.replace( - /this\.emit\("peer-joined", ([A-Za-z_$][\w$]*)\.peer\), this\.ensureRemote\(\1\.peer\);/g, - 'this.knownPeers.set($1.peer.id, $1.peer), this.emit("peer-joined", $1.peer), this.ensureRemote($1.peer);', - ); - source = source.replace( - /(case "peer-left": \{\n\s+const [A-Za-z_$][\w$]* = this\.remotes\.get\(([A-Za-z_$][\w$]*)\.peerId\);)/g, - "$1\n this.knownPeers.delete($2.peerId);", - ); - source = source.replace( - /this\.ensureRemote\(\{ id: ([A-Za-z_$][\w$]*), name: \1\.slice\(0, 8\), role: "publisher" \}\)/g, - 'this.ensureRemote(this.knownPeers.get($1) ?? { id: $1, name: $1.slice(0, 8), role: "publisher" })', - ); - source = source.replace( - /(tearDown\(\) \{\n\s+for \(const [^\n]+\n\s+this\.remotes\.clear\(\);)(?!\n\s+this\.knownPeers\.clear)/, - "$1\n this.knownPeers.clear();", - ); - source = source - .replace( - /(this\.knownPeers\.set\(([A-Za-z_$][\w$]*)\.peer\.id, \2\.peer\),\s*)+/g, - "$1", - ) - .replace( - /((?: {8})this\.knownPeers\.delete\(([A-Za-z_$][\w$]*)\.peerId\);\n)(?:\1)+/g, - "$1", - ); - } else { - if (!source.includes("knownPeers =")) { - source = source.replace( - /(private remotes = new Map\(\);|remotes = new Map\(\);)/, - "$1\n private knownPeers = new Map();", - ); - source = source.replace( - " private knownPeers = new Map();", - path.endsWith(".js") - ? " knownPeers = new Map();" - : " private knownPeers = new Map();", - ); - } - source = source.replace( - /for \(const peer of msg\.peers\) this\.ensureRemote\(peer\);/g, - "for (const peer of msg.peers) {\n this.knownPeers.set(peer.id, peer);\n this.ensureRemote(peer);\n }", - ); - source = source.replace( - /(case "peer-joined": \{\n)(\s*)this\.emit\("peer-joined", msg\.peer\);/g, - '$1$2this.knownPeers.set(msg.peer.id, msg.peer);\n$2this.emit("peer-joined", msg.peer);', - ); - source = source.replace( - /(case "peer-left": \{\n)(\s*)const remote = this\.remotes\.get\(msg\.peerId\);/g, - "$1$2this.knownPeers.delete(msg.peerId);\n$2const remote = this.remotes.get(msg.peerId);", - ); - source = source.replace( - /this\.ensureRemote\(\{ id: from, name: from\.slice\(0, 8\), role: "publisher" \}\)/g, - 'this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: "publisher" })', - ); - source = source.replace( - /(this\.remotes\.clear\(\);)(?!\r?\n\s*this\.knownPeers\.clear)/, - "$1\n this.knownPeers.clear();", - ); - source = source - .replace( - /((?:\s*)this\.knownPeers\.set\(msg\.peer\.id, msg\.peer\);\r?\n)(?:\1)+/g, - "$1", - ) - .replace( - /((?:\s*)this\.knownPeers\.delete\(msg\.peerId\);\r?\n)(?:\1)+/g, - "$1", - ); - } - - if ( - !source.includes("knownPeers") || - !source.includes("this.knownPeers.get(") || - !source.includes("this.knownPeers.set(") - ) { - throw new Error( - `unsupported @lumencast/runtime known-peer contract: ${path}`, - ); - } - if (source !== before) await writeFile(path, source); -} diff --git a/scripts/patch-lumencast-protocol.mjs b/scripts/patch-lumencast-protocol.mjs deleted file mode 100644 index 523ded9..0000000 --- a/scripts/patch-lumencast-protocol.mjs +++ /dev/null @@ -1,1907 +0,0 @@ -import { readdir, readFile, writeFile } from "node:fs/promises"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { patchKnownPeerRoster } from "./patch-known-peer-roster.mjs"; - -const root = fileURLToPath(new URL("..", import.meta.url)); -const packageRoot = join(root, "node_modules", "@lumencast", "protocol"); -const targets = [ - join(packageRoot, "src", "host-allow.ts"), - join(packageRoot, "dist", "host-allow.js"), -]; -const supportedCurrentLimits = [ - "const MAX_URL_LEN = 8192;", - "const MAX_URL_LEN = 262144;", -]; -// Prism resolves authenticated Canvas images to bounded inline raster URLs. -// A full-HD authored PNG legitimately exceeds the old 256 KiB thumbnail cap -// once base64 encoded (Launch is ~4.9 MiB). Keep the no-network data:image -// contract, but give production scene artwork a still-bounded 16 MiB ceiling. -const patched = "const MAX_URL_LEN = 16777216;"; - -for (const path of targets) { - let source; - try { - source = await readFile(path, "utf8"); - } catch (error) { - throw new Error(`@lumencast/protocol target missing: ${path}`, { cause: error }); - } - const current = supportedCurrentLimits.find((value) => source.includes(value)); - if (current !== undefined) { - await writeFile(path, source.replace(current, patched)); - continue; - } - if (source.includes(patched)) continue; - throw new Error(`unsupported @lumencast/protocol host-allow contract: ${path}`); -} - -console.log("[solar] @lumencast/protocol local render image limit=16777216"); - -const runtimeRoot = join(root, "node_modules", "@lumencast", "runtime"); -const runtimeSource = join(runtimeRoot, "src", "mount.ts"); -const runtimeSourceNeedle = ` token: options.token,\n ...(options.scene`; -const runtimeSourceReplacement = - ` token: options.token,\n ...(options.webSocketImpl !== undefined\n` + - ` ? { webSocketImpl: options.webSocketImpl }\n` + - ` : {}),\n ...(options.scene`; - -async function patchRuntimeFile(path, needle, replacement) { - const source = await readFile(path, "utf8"); - if (source.includes(needle)) { - await writeFile(path, source.replace(needle, replacement)); - return "patched"; - } - if (source.includes(replacement)) return "already-patched"; - if ( - replacement.includes("webSocketImpl") && - (source.includes("...(options.webSocketImpl !== undefined ? { webSocketImpl: options.webSocketImpl } : {})") || - source.includes("...t.webSocketImpl !== void 0 ? { webSocketImpl: t.webSocketImpl } : {}")) - ) { - return "already-patched"; - } - if ( - replacement.includes("const previousName = existing.info.name;") && - source.includes("const previousName = existing.info.name;") && - source.includes("existing.info.name = peer.name;") && - source.includes("existing.info.role = peer.role;") - ) { - return "already-patched"; - } - throw new Error(`unsupported @lumencast/runtime mount contract: ${path}`); -} - -await patchRuntimeFile(runtimeSource, runtimeSourceNeedle, runtimeSourceReplacement); -await patchRuntimeFile( - join(runtimeRoot, "dist", "mount.js"), - ` token: options.token,\n ...(options.scene`, - ` token: options.token,\n ...(options.webSocketImpl !== undefined\n` + - ` ? { webSocketImpl: options.webSocketImpl }\n` + - ` : {}),\n ...(options.scene`, -); - -const runtimeDistFiles = await readdir(join(runtimeRoot, "dist")); -const runtimeIndexFile = runtimeDistFiles.find( - (name) => /^index-.*\.js$/.test(name), -); -if (!runtimeIndexFile) { - throw new Error("@lumencast/runtime bundled index missing"); -} -await patchRuntimeFile( - join(runtimeRoot, "dist", runtimeIndexFile), - ` token: t.token,\n ...t.scene`, - ` token: t.token,\n ...t.webSocketImpl !== void 0 ? { webSocketImpl: t.webSocketImpl } : {},\n ...t.scene`, -); - -console.log("[solar] @lumencast/runtime forwards webSocketImpl to WsClient"); - -// MeetViewer builds one aggregate MediaStream per authored peer. Chromium can -// deliver that stream's audio track before its video track, so the same object -// is intentionally published more than once while it becomes usable. The -// original registry treated same-object writes as no-ops and the LIVE -// primitive could stay attached to an audio-only stream until the next room -// handoff. Keep the re-emit behaviour in both readable modules and every -// bundled runtime variant shipped with the standalone Solar host. -async function patchPeerStreamRegistry(path) { - let source = await readFile(path, "utf8"); - const original = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - const needle = "if (t.get(i) === c) return;\n const o = !t.has(i);"; - const replacement = - "/* @prism-peer-stream-reemit */\n const o = !t.has(i);"; - if (source.includes(needle)) source = source.replace(needle, replacement); - if (source !== original) await writeFile(path, source); - return; - } - - if (source.includes("Re-publish the same object")) return; - const needles = [ - " if (streams.get(peerLabel) === stream) return; // idempotent re-emit guard\n", - " if (streams.get(peerLabel) === stream) return;\n", - " if (streams.get(peerLabel) === stream)\n return; // idempotent re-emit guard\n", - " if (streams.get(peerLabel) === stream)\n return;\n", - ]; - const needle = needles.find((value) => source.includes(value)); - if (needle === undefined) { - throw new Error(`unsupported @lumencast/runtime peer-stream registry contract: ${path}`); - } - const replacement = - " // MeetViewer fills one aggregate MediaStream incrementally (audio first, video second).\n" + - " // Re-publish the same object so consumers waiting for video observe the second edge.\n"; - source = source.replace(needle, replacement); - await writeFile(path, source); -} - -await patchPeerStreamRegistry(join(runtimeRoot, "src", "webrtc", "peer-stream-registry.ts")); -await patchPeerStreamRegistry(join(runtimeRoot, "dist", "webrtc", "peer-stream-registry.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchPeerStreamRegistry(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime republishes aggregate peer streams as video arrives"); - -// A signaling `signal` can overtake `peer-joined`. Runtime 0.18.2 then creates -// the RTCPeerConnection with an UUID-derived fallback name and never replaces -// it when the authoritative PeerInfo arrives. The media flows, but the registry -// publishes it under the fallback key, leaving the authored camera slot empty. -// Reconcile that temporary identity in-place and re-key an already-received -// stream without recreating the peer connection or changing the wire protocol. -const identitySourceNeedle = ` const existing = this.remotes.get(peer.id); - if (existing) return existing;`; -const identitySourceReplacement = ` const existing = this.remotes.get(peer.id); - if (existing) { - const previousName = existing.info.name; - existing.info.name = peer.name; - existing.info.role = peer.role; - if (previousName !== peer.name && existing.stream.getTracks().length > 0) { - this.emit("peer-left", { peerId: peer.id, peerName: previousName }); - this.emit("remote-track", { - peerId: peer.id, - peerName: peer.name, - stream: existing.stream, - }); - } - return existing; - }`; - -const identityDistNeedle = ` const existing = this.remotes.get(peer.id); - if (existing) - return existing;`; -const identityDistReplacement = ` const existing = this.remotes.get(peer.id); - if (existing) { - const previousName = existing.info.name; - existing.info.name = peer.name; - existing.info.role = peer.role; - if (previousName !== peer.name && existing.stream.getTracks().length > 0) { - this.emit("peer-left", { peerId: peer.id, peerName: previousName }); - this.emit("remote-track", { - peerId: peer.id, - peerName: peer.name, - stream: existing.stream, - }); - } - return existing; - }`; - -async function upgradeIdentityMutation(path) { - const source = await readFile(path, "utf8"); - const upgraded = source - .replace( - /^([ \t]*)existing\.info = peer;$/m, - "$1existing.info.name = peer.name;\n$1existing.info.role = peer.role;", - ) - .replace( - /^([ \t]*)existing\.info\.name = peer\.name;\r?\n[ \t]*existing\.info\.role = peer\.role;$/m, - "$1existing.info.name = peer.name;\n$1existing.info.role = peer.role;", - ); - if (upgraded !== source) await writeFile(path, upgraded); -} - -await upgradeIdentityMutation(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await upgradeIdentityMutation(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -await patchRuntimeFile( - join(runtimeRoot, "src", "webrtc", "meet-viewer.ts"), - identitySourceNeedle, - identitySourceReplacement, -); -await patchRuntimeFile( - join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), - identityDistNeedle, - identityDistReplacement, -); - -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - const path = join(runtimeRoot, "dist", name); - const original = await readFile(path, "utf8"); - const source = original.replace( - /([A-Za-z_$][\w$]*)\.info = ([A-Za-z_$][\w$]*);/, - "$1.info.name = $2.name;\n $1.info.role = $2.role;", - ); - if (source !== original) await writeFile(path, source); - if (source.includes("previousName =") && source.includes('this.emit("remote-track"')) continue; - const match = source.match( - /(^[ \t]*)const ([A-Za-z_$][\w$]*) = this\.remotes\.get\(([A-Za-z_$][\w$]*)\.id\);\n\1if \(\2\) return \2;/m, - ); - if (!match) { - throw new Error(`unsupported @lumencast/runtime MeetViewer contract: ${path}`); - } - const [, indent, existing, peer] = match; - const replacement = - `${indent}const ${existing} = this.remotes.get(${peer}.id);\n` + - `${indent}if (${existing}) {\n` + - `${indent} const previousName = ${existing}.info.name;\n` + - `${indent} ${existing}.info.name = ${peer}.name;\n` + - `${indent} ${existing}.info.role = ${peer}.role;\n` + - `${indent} if (previousName !== ${peer}.name && ${existing}.stream.getTracks().length > 0) {\n` + - `${indent} this.emit("peer-left", { peerId: ${peer}.id, peerName: previousName });\n` + - `${indent} this.emit("remote-track", { peerId: ${peer}.id, peerName: ${peer}.name, stream: ${existing}.stream });\n` + - `${indent} }\n` + - `${indent} return ${existing};\n` + - `${indent}}`; - await writeFile(path, source.replace(match[0], replacement)); -} - -console.log("[solar] @lumencast/runtime reconciles early signaling peer identity"); - -await patchKnownPeerRoster(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchKnownPeerRoster(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchKnownPeerRoster(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime retains authoritative peer roster across retries"); - -// A superseded RTCPeerConnection can report `failed` / `closed` after a retry -// has already installed a fresh RemoteState for the same peer. Its late event -// must not delete the fresh state nor withdraw that peer's live stream from the -// registry. Only the RemoteState that is still current owns removal. -async function patchRemoteGenerationGuard(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - source = source.replace( - /\((\w+)\.connectionState === "failed" \|\| \1\.connectionState === "closed"\) && \(this\.remotes\.delete\((\w+)\.id\), this\.emit\("peer-left", \{ peerId: \2\.id, peerName: \2\.name \}\)\)/g, - '($1.connectionState === "failed" || $1.connectionState === "closed") && this.remotes.get($2.id)?.pc === $1 && (this.remotes.delete($2.id), this.emit("peer-left", { peerId: $2.id, peerName: $2.name }))', - ); - } else { - source = source.replace( - /if \(pc\.connectionState === "failed" \|\| pc\.connectionState === "closed"\) \{\r?\n(\s*)this\.remotes\.delete\(peer\.id\);\r?\n\s*this\.emit\("peer-left", \{ peerId: peer\.id, peerName: peer\.name \}\);\r?\n\s*\}/g, - 'if (\n$1 (pc.connectionState === "failed" || pc.connectionState === "closed") &&\n$1 this.remotes.get(peer.id)?.pc === pc\n$1) {\n$1 this.remotes.delete(peer.id);\n$1 this.emit("peer-left", { peerId: peer.id, peerName: peer.name });\n$1}', - ); - } - - if (!source.includes("this.remotes.get(peer.id)?.pc === pc") && !bundled) { - throw new Error(`unsupported @lumencast/runtime remote generation contract: ${path}`); - } - if (bundled && !/this\.remotes\.get\([A-Za-z_$][\w$]*\.id\)\?\.pc === [A-Za-z_$][\w$]*/.test(source)) { - throw new Error(`unsupported @lumencast/runtime bundled generation contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchRemoteGenerationGuard(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchRemoteGenerationGuard(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchRemoteGenerationGuard(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime ignores stale peer-connection terminal events"); - -// Chromium can expose a connected receiver with decoded frames after the -// initial `track` callback raced an identity reconciliation or registry -// subscription. Re-publishing the already-owned aggregate stream when the -// current peer connection reaches `connected` is idempotent (`registry.set` -// ignores the same stream) and restores the authored slot without creating a -// second connection, renderer, or cache. -async function patchConnectedStreamRepublish(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - const connectedBlock = ` const current = this.remotes.get(peer.id);\n if (\n pc.connectionState === "connected" &&\n current?.pc === pc &&\n current.stream.getTracks().length > 0\n ) {\n this.emit("remote-track", {\n peerId: peer.id,\n peerName: current.info.name,\n stream: current.stream,\n });\n }`; - - while (source.includes(`${connectedBlock}\n${connectedBlock}`)) { - source = source.replace(`${connectedBlock}\n${connectedBlock}`, connectedBlock); - } - - if (bundled) { - source = source.replace( - /this\.emit\("connection-state", \{ peerId: ([A-Za-z_$][\w$]*)\.id, state: ([A-Za-z_$][\w$]*)\.connectionState \}\), \(\2\.connectionState === "failed"/g, - 'this.emit("connection-state", { peerId: $1.id, state: $2.connectionState }), $2.connectionState === "connected" && this.remotes.get($1.id)?.pc === $2 && this.remotes.get($1.id).stream.getTracks().length > 0 && this.emit("remote-track", { peerId: $1.id, peerName: this.remotes.get($1.id).info.name, stream: this.remotes.get($1.id).stream }), ($2.connectionState === "failed"', - ); - } else { - if (!source.includes('pc.connectionState === "connected"')) { - source = source.replace( - ` this.emit("connection-state", { peerId: peer.id, state: pc.connectionState });`, - ` this.emit("connection-state", { peerId: peer.id, state: pc.connectionState });\n${connectedBlock}`, - ); - } - } - - const marker = bundled - ? 'connectionState === "connected" && this.remotes.get(' - : 'pc.connectionState === "connected"'; - if (!source.includes(marker)) { - throw new Error(`unsupported @lumencast/runtime connected stream contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchConnectedStreamRepublish(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchConnectedStreamRepublish(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchConnectedStreamRepublish(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime republishes connected aggregate streams"); - -// The signaling server may announce a stale `peer-left` while the current -// RTCPeerConnection is still connected and decoding frames (for example while -// the publisher is replaced under the same authored label). Do not tear down a -// live aggregate on that transient control message; the connection-state -// terminal event remains the authoritative cleanup path. A replacement peer -// can take the label through the registry generation guard below. -async function patchStalePeerLeave(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - source = source.replace( - /case"peer-left":\{const ([A-Za-z_$][\w$]*)=this\.remotes\.get\(([A-Za-z_$][\w$]*)\.peerId\);this\.knownPeers\.delete\(\2\.peerId\),\1&&\(\1\.pc\.close\(\),this\.remotes\.delete\(\2\.peerId\),this\.emit\("peer-left",\{peerId:\2\.peerId,peerName:\1\.info\.name\}\)\);break\}/g, - 'case"peer-left":{const $1=this.remotes.get($2.peerId);this.knownPeers.delete($2.peerId),$1&&($1.pc.connectionState==="connected"&&$1.stream.getTracks().length>0||($1.pc.close(),this.remotes.delete($2.peerId),this.emit("peer-left",{peerId:$2.peerId,peerName:$1.info.name})));break}', - ); - } else if (path.endsWith("meet-viewer.js")) { - source = source.replace( - ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // The pc owns the tracks — closing it ends them. The registry/consumer\n // are notified via the peer-left event (label-keyed).\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n break;\n }`, - ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // Keep a still-connected aggregate alive. The terminal connection\n // event performs authoritative cleanup after a real disconnect.\n if (remote.pc.connectionState !== "connected" || remote.stream.getTracks().length === 0) {\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n }\n break;\n }`, - ); - } else { - const sourceNeedle = ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // The pc owns the tracks — closing it ends them. The registry/consumer\n // are notified via the peer-left event (label-keyed).\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n break;\n }`; - const sourceReplacement = ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // Keep a still-connected aggregate alive. The terminal connection\n // event performs authoritative cleanup after a real disconnect.\n if (remote.pc.connectionState !== "connected" || remote.stream.getTracks().length === 0) {\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n }\n break;\n }`; - source = source.replace(sourceNeedle, sourceReplacement); - } - - if (!source.includes("connectionState") || !source.includes("stream.getTracks().length")) { - throw new Error(`unsupported @lumencast/runtime stale peer-leave contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchStalePeerLeave(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchStalePeerLeave(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchStalePeerLeave(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime preserves connected streams across stale peer leaves"); - -// A publisher can reconnect under the same stable peer label before the -// signaling server delivers the old peer's `peer-left`. The shared registry is -// label-keyed, so the stale leave used to remove the replacement stream even -// though its new RTCPeerConnection was connected and receiving frames. Track -// the peer id that most recently published each label and let only that -// generation withdraw it. -async function patchRegistryPeerGeneration(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - source = source.replace( - /function ([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*), ([A-Za-z_$][\w$]*), ([A-Za-z_$][\w$]*)\) \{\n {2}\2\.on\("remote-track", \(([A-Za-z_$][\w$]*)\) => \{\n {4}const ([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\(\5\.peerName\);\n {4}\4\.acquire\(\6, \2\) && \3\.set\(\6, \5\.stream\);\n {2}\}\), \2\.on\("peer-left", \(([A-Za-z_$][\w$]*)\) => \{\n {4}const ([A-Za-z_$][\w$]*) = \7\(\8\.peerName\);\n {4}\4\.acquire\(\9, \2\) && \(\3\.remove\(\9\), \4\.release\(\9, \2\)\);\n {2}\}\);\n\}/, - (_match, fn, viewer, registry, claim, trackEvent, trackKey, labelFn, leftEvent, leftKey) => - `function ${fn}(${viewer}, ${registry}, ${claim}) {\n` + - ` const activePeerIds = /* @__PURE__ */ new Map();\n` + - ` ${viewer}.on("remote-track", (${trackEvent}) => {\n` + - ` const ${trackKey} = ${labelFn}(${trackEvent}.peerName);\n` + - ` ${claim}.acquire(${trackKey}, ${viewer}) && (activePeerIds.set(${trackKey}, ${trackEvent}.peerId), ${registry}.set(${trackKey}, ${trackEvent}.stream));\n` + - ` }), ${viewer}.on("peer-left", (${leftEvent}) => {\n` + - ` const ${leftKey} = ${labelFn}(${leftEvent}.peerName);\n` + - ` activePeerIds.get(${leftKey}) === ${leftEvent}.peerId && ${claim}.acquire(${leftKey}, ${viewer}) && (activePeerIds.delete(${leftKey}), ${registry}.remove(${leftKey}), ${claim}.release(${leftKey}, ${viewer}));\n` + - ` });\n` + - `}`, - ); - } else { - source = source.replace( - `): void {\n // Index the registry by the NORMALISED label`, - `): void {\n const activePeerIds = new Map();\n // Index the registry by the NORMALISED label`, - ); - source = source.replace( - ` if (claim.acquire(key, viewer)) registry.set(key, e.stream);`, - ` if (claim.acquire(key, viewer)) {\n activePeerIds.set(key, e.peerId);\n registry.set(key, e.stream);\n }`, - ); - source = source.replace( - ` if (claim.acquire(key, viewer)) {\n registry.remove(key);\n claim.release(key, viewer);\n }`, - ` if (\n activePeerIds.get(key) === e.peerId &&\n claim.acquire(key, viewer)\n ) {\n activePeerIds.delete(key);\n registry.remove(key);\n claim.release(key, viewer);\n }`, - ); - // The emitted JS has no type parameters and braces the one-line set. - source = source.replace( - `function wireViewer(viewer, registry, claim) {\n // Index`, - `function wireViewer(viewer, registry, claim) {\n const activePeerIds = new Map();\n // Index`, - ); - source = source.replace( - ` if (claim.acquire(key, viewer))\n registry.set(key, e.stream);`, - ` if (claim.acquire(key, viewer)) {\n activePeerIds.set(key, e.peerId);\n registry.set(key, e.stream);\n }`, - ); - source = source.replace( - ` if (claim.acquire(key, viewer)) {\n registry.remove(key);\n claim.release(key, viewer);\n }`, - ` if (activePeerIds.get(key) === e.peerId && claim.acquire(key, viewer)) {\n activePeerIds.delete(key);\n registry.remove(key);\n claim.release(key, viewer);\n }`, - ); - } - - if ( - !source.includes("activePeerIds") || - (!bundled && !source.includes("e.peerId")) || - (bundled && !source.includes("activePeerIds.get(")) - ) { - throw new Error(`unsupported @lumencast/runtime registry generation contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchRegistryPeerGeneration(join(runtimeRoot, "src", "webrtc", "index.ts")); -await patchRegistryPeerGeneration(join(runtimeRoot, "dist", "webrtc", "index.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchRegistryPeerGeneration(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime ignores stale same-label peer leaves"); - -// Recover a viewer leg after a relay/ICE failure. Late trickle candidates are -// ignored until a fresh offer creates the next generation; otherwise an orphan -// ICE packet leaves a `new` RTCPeerConnection that can never carry a camera. -async function patchViewerRecovery(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - const recovered = bundled - ? source.includes("this.clearRemoteRetry(e.id)") && - source.includes("this.scheduleRemoteRetry(r?.info ?? e)") - : source.includes("this.clearRemoteRetry(peer.id);") && - source.includes("this.scheduleRemoteRetry(current?.info ?? peer);"); - if (recovered && source.includes("retryTimers = ")) return; - - if (bundled) { - source = source.replace( - "knownPeers = /* @__PURE__ */ new Map();", - "knownPeers = /* @__PURE__ */ new Map();\n retryTimers = /* @__PURE__ */ new Map();\n retryAttempts = /* @__PURE__ */ new Map();", - ); - source = source.replace( - " tearDown() {\n for (const e of this.remotes.values()) e.pc.close();\n this.remotes.clear();\n this.knownPeers.clear();\n }", - " tearDown() {\n for (const e of this.retryTimers.values()) clearTimeout(e);\n this.retryTimers.clear();\n this.retryAttempts.clear();\n for (const e of this.remotes.values()) e.pc.close();\n this.remotes.clear();\n this.knownPeers.clear();\n }", - ); - source = source.replace( - " n || (n = this.ensureRemote(this.knownPeers.get(e) ?? { id: e, name: e.slice(0, 8), role: \"publisher\" }));", - " if (!n && s.kind === \"ice\") return;\n n || (n = this.ensureRemote(this.knownPeers.get(e) ?? { id: e, name: e.slice(0, 8), role: \"publisher\" }));", - ); - source = source.replace( - " this.emit(\"connection-state\", { peerId: e.id, state: n.connectionState }), n.connectionState === \"connected\" && this.remotes.get(e.id)?.pc === n && this.remotes.get(e.id).stream.getTracks().length > 0 && this.emit(\"remote-track\", { peerId: e.id, peerName: this.remotes.get(e.id).info.name, stream: this.remotes.get(e.id).stream }), (n.connectionState === \"failed\" || n.connectionState === \"closed\") && this.remotes.get(e.id)?.pc === n && (this.remotes.delete(e.id), this.emit(\"peer-left\", { peerId: e.id, peerName: e.name }));", - " this.emit(\"connection-state\", { peerId: e.id, state: n.connectionState }), n.connectionState === \"connected\" && this.remotes.get(e.id)?.pc === n && this.remotes.get(e.id).stream.getTracks().length > 0 && (this.clearRemoteRetry(e.id), this.emit(\"remote-track\", { peerId: e.id, peerName: this.remotes.get(e.id).info.name, stream: this.remotes.get(e.id).stream })), (n.connectionState === \"failed\" || n.connectionState === \"closed\") && this.remotes.get(e.id)?.pc === n && (() => { const r = this.remotes.get(e.id); this.remotes.delete(e.id), n.close(), this.emit(\"peer-left\", { peerId: e.id, peerName: r?.info.name ?? e.name }), this.scheduleRemoteRetry(r?.info ?? e); })();", - ); - source = source.replace( - " /* ---- Helpers ------------------------------------------------------ */", - " scheduleRemoteRetry(e) {\n if (!this.knownPeers.has(e.id) || this.retryTimers.has(e.id)) return;\n const s = (this.retryAttempts.get(e.id) ?? 0) + 1;\n if (s > 3) return;\n const n = Math.min(1e3, 250 * 2 ** (s - 1));\n const r = setTimeout(() => {\n this.retryTimers.delete(e.id);\n if (!this.knownPeers.has(e.id) || this.remotes.has(e.id) || this.ws?.readyState !== this.deps.WebSocket.OPEN) return;\n this.retryAttempts.set(e.id, s);\n const i = this.ensureRemote(this.knownPeers.get(e.id) ?? e);\n void this.dialRemote(e.id, i);\n }, n);\n this.retryTimers.set(e.id, r);\n }\n clearRemoteRetry(e) {\n const s = this.retryTimers.get(e);\n s !== void 0 && (clearTimeout(s), this.retryTimers.delete(e));\n this.retryAttempts.delete(e);\n }\n async dialRemote(e, s) {\n const n = this.remotes.get(e);\n if (!n || s !== void 0 && n !== s || n.makingOffer || n.pc.signalingState !== \"stable\") return;\n try {\n n.makingOffer = !0, await n.pc.setLocalDescription(), n.pc.localDescription && this.sendSignal(e, { kind: \"sdp\", description: { type: n.pc.localDescription.type, sdp: n.pc.localDescription.sdp } });\n } catch {\n } finally {\n n.makingOffer = !1;\n }\n }\n /* ---- Helpers ------------------------------------------------------ */", - ); - } else { - const js = path.endsWith(".js"); - const field = js ? " knownPeers = new Map();" : " private knownPeers = new Map();"; - source = source.replace( - field, - js - ? `${field}\n retryTimers = new Map();\n retryAttempts = new Map();` - : `${field}\n private retryTimers = new Map>();\n private retryAttempts = new Map();`, - ); - const tearDown = js - ? " tearDown() {\n for (const r of this.remotes.values())\n r.pc.close();" - : " private tearDown(): void {\n for (const r of this.remotes.values()) r.pc.close();"; - source = source.replace( - tearDown, - js - ? " tearDown() {\n for (const timer of this.retryTimers.values()) clearTimeout(timer);\n this.retryTimers.clear();\n this.retryAttempts.clear();\n for (const r of this.remotes.values())\n r.pc.close();" - : " private tearDown(): void {\n for (const timer of this.retryTimers.values()) clearTimeout(timer);\n this.retryTimers.clear();\n this.retryAttempts.clear();\n for (const r of this.remotes.values()) r.pc.close();", - ); - const signalNeedle = js - ? " if (!remote) {\n remote = this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: \"publisher\" });" - : " if (!remote) {\n remote = this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: \"publisher\" });"; - source = source.replace( - signalNeedle, - js - ? " if (!remote) {\n if (payload.kind === \"ice\") return;\n remote = this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: \"publisher\" });" - : " if (!remote) {\n if (payload.kind === \"ice\") return;\n remote = this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: \"publisher\" });", - ); - const helper = js - ? " scheduleRemoteRetry(peer) {\n if (!this.knownPeers.has(peer.id) || this.retryTimers.has(peer.id)) return;\n const attempt = (this.retryAttempts.get(peer.id) ?? 0) + 1;\n if (attempt > 3) return;\n const delay = Math.min(1000, 250 * 2 ** (attempt - 1));\n const timer = setTimeout(() => {\n this.retryTimers.delete(peer.id);\n if (!this.knownPeers.has(peer.id) || this.remotes.has(peer.id) || this.ws?.readyState !== this.deps.WebSocket.OPEN) return;\n this.retryAttempts.set(peer.id, attempt);\n const remote = this.ensureRemote(this.knownPeers.get(peer.id) ?? peer);\n void this.dialRemote(peer.id, remote);\n }, delay);\n this.retryTimers.set(peer.id, timer);\n }\n clearRemoteRetry(peerId) {\n const timer = this.retryTimers.get(peerId);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.retryTimers.delete(peerId);\n }\n this.retryAttempts.delete(peerId);\n }\n async dialRemote(peerId, expected) {\n const remote = this.remotes.get(peerId);\n if (!remote || expected !== undefined && remote !== expected || remote.makingOffer || remote.pc.signalingState !== \"stable\") return;\n try {\n remote.makingOffer = true;\n await remote.pc.setLocalDescription();\n if (remote.pc.localDescription) this.sendSignal(peerId, { kind: \"sdp\", description: { type: remote.pc.localDescription.type, sdp: remote.pc.localDescription.sdp } });\n } catch {\n } finally {\n remote.makingOffer = false;\n }\n }\n" - : " private scheduleRemoteRetry(peer: PeerInfo): void {\n if (!this.knownPeers.has(peer.id) || this.retryTimers.has(peer.id)) return;\n const attempt = (this.retryAttempts.get(peer.id) ?? 0) + 1;\n if (attempt > 3) return;\n const delay = Math.min(1_000, 250 * 2 ** (attempt - 1));\n const timer = setTimeout(() => {\n this.retryTimers.delete(peer.id);\n if (!this.knownPeers.has(peer.id) || this.remotes.has(peer.id) || this.ws?.readyState !== this.deps.WebSocket.OPEN) return;\n this.retryAttempts.set(peer.id, attempt);\n const remote = this.ensureRemote(this.knownPeers.get(peer.id) ?? peer);\n void this.dialRemote(peer.id, remote);\n }, delay);\n this.retryTimers.set(peer.id, timer);\n }\n\n private clearRemoteRetry(peerId: string): void {\n const timer = this.retryTimers.get(peerId);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.retryTimers.delete(peerId);\n }\n this.retryAttempts.delete(peerId);\n }\n\n private async dialRemote(peerId: string, expected?: RemoteState): Promise {\n const remote = this.remotes.get(peerId);\n if (!remote || (expected !== undefined && remote !== expected) || remote.makingOffer || remote.pc.signalingState !== \"stable\") return;\n try {\n remote.makingOffer = true;\n await remote.pc.setLocalDescription();\n if (remote.pc.localDescription) this.sendSignal(peerId, { kind: \"sdp\", description: { type: remote.pc.localDescription.type as \"offer\" | \"answer\" | \"pranswer\" | \"rollback\", sdp: remote.pc.localDescription.sdp } });\n } catch {\n } finally {\n remote.makingOffer = false;\n }\n }\n\n"; - const helperNeedle = js ? " /* ---- Helpers ------------------------------------------------------ */" : " /* ---- Helpers ------------------------------------------------------ */"; - source = source.replace(helperNeedle, `${helper}${helperNeedle}`); - } - - // Earlier patches reshape the connection-state handler before recovery runs. - // Patch and verify its terminal edge independently of the retry helper: a - // helper present without a call made clean installs pass but never redialed. - if (!bundled) { - if (!source.includes("this.clearRemoteRetry(peer.id);")) { - const connected = /(current\.stream\.getTracks\(\)\.length > 0\s*\) \{\s*)(this\.emit\("remote-track")/; - if (!connected.test(source)) { - throw new Error(`unsupported @lumencast/runtime connected recovery contract: ${path}`); - } - source = source.replace(connected, "$1this.clearRemoteRetry(peer.id);\n $2"); - } - if (!source.includes("this.scheduleRemoteRetry(current?.info ?? peer);")) { - const terminal = /(^[ \t]*)this\.remotes\.delete\(peer\.id\);\r?\n[ \t]*this\.emit\("peer-left", \{ peerId: peer\.id, peerName: peer\.name \}\);/m; - if (!terminal.test(source)) { - throw new Error(`unsupported @lumencast/runtime terminal recovery contract: ${path}`); - } - source = source.replace( - terminal, - (_match, indent) => [ - `${indent}const current = this.remotes.get(peer.id);`, - `${indent}this.remotes.delete(peer.id);`, - `${indent}pc.close();`, - `${indent}this.emit("peer-left", { peerId: peer.id, peerName: current?.info.name ?? peer.name });`, - `${indent}this.scheduleRemoteRetry(current?.info ?? peer);`, - ].join("\n"), - ); - } - } - - if (!source.includes("scheduleRemoteRetry") || - (!bundled && (!source.includes("this.clearRemoteRetry(peer.id);") || - !source.includes("this.scheduleRemoteRetry(current?.info ?? peer);")))) { - throw new Error(`unsupported @lumencast/runtime viewer recovery contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchViewerRecovery(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchViewerRecovery(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchViewerRecovery(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime recovers failed viewer camera legs"); - -// A recovered Solar receiver cannot rely on the runtime's automatic -// `negotiationneeded` offer: the Prism publisher owns the first offer and the -// preview injection deliberately suppresses that automatic viewer offer to -// avoid glare. Once a receiver has actually failed, however, the retry path -// is the only party that can restart the leg if the publisher did not observe -// its terminal state. Create an explicit offer for that retry. The initial -// negotiation handler still calls the no-argument form and remains suppressed -// by the preview injection, so this does not change the steady-state wire. -async function patchViewerRetryOffer(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - source = source.replace( - /await ([A-Za-z_$][\w$]*)\.pc\.setLocalDescription\(\), ([A-Za-z_$][\w$]*)\.pc\.localDescription/g, - (_match, pcOwner, localOwner) => - `await ${pcOwner}.pc.setLocalDescription(await ${pcOwner}.pc.createOffer()), ${localOwner}.pc.localDescription`, - ); - } else { - source = source.replace( - /await remote\.pc\.setLocalDescription\(\);/g, - "const offer = await remote.pc.createOffer();\n await remote.pc.setLocalDescription(offer);", - ); - } - - if ( - !source.includes("pc.createOffer()") || - (!bundled && !source.includes("setLocalDescription(offer)")) - ) { - throw new Error(`unsupported @lumencast/runtime viewer retry-offer contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchViewerRetryOffer(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchViewerRetryOffer(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchViewerRetryOffer(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime emits explicit offers for failed viewer retries"); - -// A retry offer can cross a publisher-owned retry offer in flight. MeetViewer -// is receive-only in production, so the publisher's offer is authoritative: -// make the viewer polite and explicitly roll back its own retry offer before -// accepting that fresh publisher offer. This keeps a failed slot from being -// stranded in `have-local-offer` while preserving the initial one-way dial. -async function patchViewerRetryCollision(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - source = source.replace( - 'if(r.ignoreOffer=!this.isPolite(a)&&d,r.ignoreOffer)return;await c.setRemoteDescription(f);', - 'if(r.ignoreOffer=!this.isPolite(a)&&d,r.ignoreOffer)return;d&&f.type==="offer"&&await c.setLocalDescription({type:"rollback"}).catch(()=>{});await c.setRemoteDescription(f);', - ); - source = source.replace( - /if \(([A-Za-z_$][\w$]*)\.ignoreOffer = !this\.isPolite\(([A-Za-z_$][\w$]*)\) && ([A-Za-z_$][\w$]*), \1\.ignoreOffer\) return;\s*await ([A-Za-z_$][\w$]*)\.setRemoteDescription\(([A-Za-z_$][\w$]*)\);/g, - (_match, remote, from, collision, pc, desc) => - `if (${remote}.ignoreOffer = !this.isPolite(${from}) && ${collision}, ${remote}.ignoreOffer) return; ${collision} && ${desc}.type === "offer" && await ${pc}.setLocalDescription({type:"rollback"}).catch(()=>{}); await ${pc}.setRemoteDescription(${desc});`, - ); - source = source.replace( - 'isPolite(a){return this.selfId?this.selfId>a:!1}', - 'isPolite(){return!0}', - ); - source = source.replace( - /isPolite\(([A-Za-z_$][\w$]*)\)\s*\{\s*return this\.selfId\s*\?\s*this\.selfId\s*>\s*\1\s*:\s*!1\s*\}/g, - "isPolite(){return!0}", - ); - // Vite's published runtime chunks use the readable class-method form - // (`isPolite(e) { ... }`) even though they are consumed as bundled - // dependencies. Keep the receive-only viewer polite in those chunks too; - // otherwise a retry offer can be ignored by id ordering and strand a slot - // in `have-local-offer`. - source = source.replace( - /isPolite\(([A-Za-z_$][\w$]*)\)\s*\{\s*return this\.selfId\s*\?\s*this\.selfId\s*>\s*\1\s*:\s*!1;?\s*\}/g, - "isPolite(){return!0}", - ); - } else { - source = source.replace( - ` if (remote.ignoreOffer) return;\n\n await pc.setRemoteDescription(desc);`, - ` if (remote.ignoreOffer) return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n\n await pc.setRemoteDescription(desc);`, - ); - source = source.replace( - ` private isPolite(otherId: string): boolean {\n if (!this.selfId) return false;\n return this.selfId > otherId;\n }`, - ` private isPolite(_otherId: string): boolean {\n // MeetViewer is receive-only; a publisher offer always wins a retry glare.\n return true;\n }`, - ); - source = source.replace( - ` if (remote.ignoreOffer) return;\n\n await pc.setRemoteDescription(desc);`, - ` if (remote.ignoreOffer) return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n\n await pc.setRemoteDescription(desc);`, - ); - source = source.replace( - ` if (remote.ignoreOffer)\n return;\n await pc.setRemoteDescription(desc);`, - ` if (remote.ignoreOffer)\n return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n await pc.setRemoteDescription(desc);`, - ); - source = source.replace( - ` if (remote.ignoreOffer)\n return;\n await pc.setRemoteDescription(desc);`, - ` if (remote.ignoreOffer)\n return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n await pc.setRemoteDescription(desc);`, - ); - source = source.replace( - ` isPolite(otherId) {\n if (!this.selfId)\n return false;\n return this.selfId > otherId;\n }`, - ` isPolite(_otherId) {\n return true;\n }`, - ); - source = source.replace( - ` isPolite(otherId) {\n if (!this.selfId)\n return false;\n return this.selfId > otherId;\n }`, - ` isPolite(_otherId) {\n return true;\n }`, - ); - } - - if ( - !source.includes("setLocalDescription({ type: \"rollback\" })") && - !source.includes('setLocalDescription({type:"rollback"})') - ) { - throw new Error(`unsupported @lumencast/runtime viewer retry-collision contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchViewerRetryCollision(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchViewerRetryCollision(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchViewerRetryCollision(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime rolls back viewer retry glare in favour of publisher offers"); - -// WebSocket frames arrive in order, but MeetViewer's SDP/ICE handlers are -// asynchronous. Without a small protocol queue, the offer for one camera can -// enter `handleSignal` while the previous peer's `peer-joined` is still -// allocating its transceivers; the resulting generation stays `new` and the -// corresponding authored slot never receives a track. Serialize only the -// signaling state machine (not media delivery) so three camera slots can join -// deterministically without changing the wire protocol. -async function patchViewerMessageSerialization(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - if (!source.includes("messageQueue =")) { - source = source.replace( - /(^\s*retryAttempts = [^;]+;\r?\n)/m, - "$1 messageQueue = Promise.resolve();\n", - ); - } - source = source.replace( - /([A-Za-z_$][\w$]*)\.addEventListener\("message", \(\w+\) => void this\.onMessage\(\w+\.data\)\)/g, - (_match, socket) => - `${socket}.addEventListener("message", (event) => { this.messageQueue = this.messageQueue.then(() => this.onMessage(event.data)).catch(() => undefined); })`, - ); - } else { - const js = path.endsWith(".js"); - const hasMessageQueueField = js - ? /^\s+messageQueue = Promise\.resolve\(\);/m.test(source) - : source.includes("private messageQueue"); - if (!hasMessageQueueField) { - const field = js - ? " retryAttempts = new Map();" - : " private retryAttempts = new Map();"; - const replacement = - js - ? `${field}\n messageQueue = Promise.resolve();` - : `${field}\n private messageQueue: Promise = Promise.resolve();`; - source = source.replace(field, replacement); - } - source = source.replace( - /ws\.addEventListener\("message", \(ev\) => void this\.onMessage\(\(ev as MessageEvent\)\.data\)\);/g, - `ws.addEventListener("message", (ev) => {\n this.messageQueue = this.messageQueue\n .then(() => this.onMessage((ev as MessageEvent).data))\n .catch(() => undefined);\n });`, - ); - source = source.replace( - /ws\.addEventListener\("message", \(ev\) => void this\.onMessage\(ev\.data\)\);/g, - `ws.addEventListener("message", (ev) => {\n this.messageQueue = this.messageQueue\n .then(() => this.onMessage(ev.data))\n .catch(() => undefined);\n });`, - ); - } - - if (!source.includes("messageQueue")) { - throw new Error(`unsupported @lumencast/runtime viewer message contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchViewerMessageSerialization(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchViewerMessageSerialization(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchViewerMessageSerialization(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime serializes MeetViewer signaling"); - -// A publisher retry can deliver a fresh SDP offer while Solar still holds the -// previous failed/closed receiver generation. Passing that offer to the -// terminal RTCPeerConnection is rejected by Chromium, leaving the authored -// camera slot empty. Drop only that stale generation before accepting a new -// offer; the receive-only viewer remains the answerer and the wire contract is -// unchanged. -async function patchViewerStaleOfferGeneration(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (bundled) { - if (!source.includes("= void 0;")) { - source = source.replace( - /async handleSignal\(([A-Za-z_$][\w$]*),\s*([A-Za-z_$][\w$]*)\)\s*\{\s*let\s+([A-Za-z_$][\w$]*)\s*=\s*this\.remotes\.get\(\1\);\s*if\s*\(!\3\s*&&\s*\2\.kind\s*===\s*"ice"\)\s*return;/, - (_match, from, payload, remote) => - `async handleSignal(${from}, ${payload}) {\n` + - ` let ${remote} = this.remotes.get(${from});\n` + - ` if (${remote} && (${remote}.pc.connectionState === "failed" || ${remote}.pc.connectionState === "closed")) {\n` + - ` ${remote}.pc.close();\n` + - ` this.remotes.delete(${from});\n` + - ` ${remote} = void 0;\n` + - ` }\n` + - ` if (!${remote} && ${payload}.kind === "ice") return;`, - ); - } - } else { - const signalPattern = /(\n[ \t]*)let remote = this\.remotes\.get\(from\);\r?\n([ \t]*)if \(!remote\) \{/; - if (!source.includes("remote = undefined;") && signalPattern.test(source)) { - source = source.replace( - signalPattern, - (_match, lineIndent, blockIndent) => - `${lineIndent}let remote = this.remotes.get(from);\n` + - `${blockIndent}if (remote && (remote.pc.connectionState === "failed" || remote.pc.connectionState === "closed")) {\n` + - `${blockIndent} remote.pc.close();\n` + - `${blockIndent} this.remotes.delete(from);\n` + - `${blockIndent} remote = undefined;\n` + - `${blockIndent}}\n` + - `${blockIndent}if (!remote) {`, - ); - } - } - - if (!source.includes("connectionState === \"failed\"") || - (!bundled && !source.includes("remote = undefined;")) || - (bundled && !source.includes("= void 0;"))) { - throw new Error(`unsupported @lumencast/runtime stale viewer offer contract: ${path}`); - } - if (source !== before) await writeFile(path, source); -} - -await patchViewerStaleOfferGeneration(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchViewerStaleOfferGeneration(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchViewerStaleOfferGeneration(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime replaces stale viewer generations before fresh offers"); - -async function patchUnbundledViewerRecoveryTerminal(path) { - let source = await readFile(path, "utf8"); - const before = source; - if (source.includes("this.scheduleRemoteRetry(current?.info ?? peer)")) return; - const js = path.endsWith(".js"); - const connectedNeedle = js - ? " this.emit(\"remote-track\", {\\n peerId: peer.id,\\n peerName: current.info.name,\\n stream: current.stream,\\n });" - : " this.emit(\"remote-track\", {\\n peerId: peer.id,\\n peerName: current.info.name,\\n stream: current.stream,\\n });"; - const connectedReplacement = js - ? " this.clearRemoteRetry(peer.id);\\n this.emit(\"remote-track\", {\\n peerId: peer.id,\\n peerName: current.info.name,\\n stream: current.stream,\\n });" - : " this.clearRemoteRetry(peer.id);\\n this.emit(\"remote-track\", {\\n peerId: peer.id,\\n peerName: current.info.name,\\n stream: current.stream,\\n });"; - source = source.replace(connectedNeedle, connectedReplacement); - const terminalNeedle = js - ? " this.remotes.delete(peer.id);\\n this.emit(\"peer-left\", { peerId: peer.id, peerName: peer.name });" - : " this.remotes.delete(peer.id);\\n this.emit(\"peer-left\", { peerId: peer.id, peerName: peer.name });"; - const terminalReplacement = js - ? " const current = this.remotes.get(peer.id);\\n this.remotes.delete(peer.id);\\n pc.close();\\n this.emit(\"peer-left\", { peerId: peer.id, peerName: current?.info.name ?? peer.name });\\n this.scheduleRemoteRetry(current?.info ?? peer);" - : " const current = this.remotes.get(peer.id);\\n this.remotes.delete(peer.id);\\n pc.close();\\n this.emit(\"peer-left\", { peerId: peer.id, peerName: current?.info.name ?? peer.name });\\n this.scheduleRemoteRetry(current?.info ?? peer);"; - source = source.replace(terminalNeedle, terminalReplacement); - if (source !== before) await writeFile(path, source); -} - -await patchUnbundledViewerRecoveryTerminal(join(runtimeRoot, "src", "webrtc", "meet-viewer.ts")); -await patchUnbundledViewerRecoveryTerminal(join(runtimeRoot, "dist", "webrtc", "meet-viewer.js")); - -// Keep a connected aggregate alive when an old signaling generation announces -// `peer-left`; the RTCPeerConnection terminal event is the authoritative -// cleanup signal. This variant is needed for Vite's minified host chunks. -async function patchBundledConnectedPeerLeave(path) { - let source = await readFile(path, "utf8"); - const before = source; - source = source.replace( - /case\s+"peer-left":\s*\{\s*const ([A-Za-z_$][\w$]*) = this\.remotes\.get\(([A-Za-z_$][\w$]*)\.peerId\);\s*this\.knownPeers\.delete\(\2\.peerId\);\s*\1 && \(\1\.pc\.close\(\), this\.remotes\.delete\(\2\.peerId\), this\.emit\("peer-left", \{ peerId: \2\.peerId, peerName: \1\.info\.name \}\)\);\s*break;\s*\}/g, - (_match, remote, message) => - `case "peer-left": {\n const ${remote} = this.remotes.get(${message}.peerId);\n this.knownPeers.delete(${message}.peerId);\n const retryTimer = this.retryTimers.get(${message}.peerId);\n if (retryTimer !== undefined) {\n clearTimeout(retryTimer);\n this.retryTimers.delete(${message}.peerId);\n }\n this.retryAttempts.delete(${message}.peerId);\n ${remote} && (${remote}.pc.connectionState === "connected" && ${remote}.stream.getTracks().length > 0 || (${remote}.pc.close(), this.remotes.delete(${message}.peerId), this.emit("peer-left", { peerId: ${message}.peerId, peerName: ${remote}.info.name })));\n break;\n }`, - ); - source = source.replace( - /case"peer-left":\{const ([A-Za-z_$][\w$]*)=this\.remotes\.get\(([A-Za-z_$][\w$]*)\.peerId\);this\.knownPeers\.delete\(\2\.peerId\),\1&&\(\1\.pc\.close\(\),this\.remotes\.delete\(\2\.peerId\),this\.emit\("peer-left",\{peerId:\2\.peerId,peerName:\1\.info\.name\}\)\);break\}/g, - (_match, remote, message) => - `case"peer-left":{const ${remote}=this.remotes.get(${message}.peerId);this.knownPeers.delete(${message}.peerId);const retryTimer=this.retryTimers.get(${message}.peerId);retryTimer!==void 0&&(clearTimeout(retryTimer),this.retryTimers.delete(${message}.peerId)),this.retryAttempts.delete(${message}.peerId),${remote}&&(${remote}.pc.connectionState==="connected"&&${remote}.stream.getTracks().length>0||(${remote}.pc.close(),this.remotes.delete(${message}.peerId),this.emit("peer-left",{peerId:${message}.peerId,peerName:${remote}.info.name})));break}`, - ); - if (source !== before) await writeFile(path, source); -} - -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchBundledConnectedPeerLeave(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime preserves connected bundled camera peers"); - -// A persistent ZabCam room can be re-minted after Meet restarts. The two -// local Solar documents (Preview and the live browser_source) must not blank -// while their receive-only viewer moves from the stale room to the replacement. -// Runtime 0.18.2 closes removed meshes before it opens the new ones and keeps a -// first-connected-wins registry, which creates exactly that visible gap. Open -// replacement meshes first and let their first track take ownership; close the -// old generation only after the new WebSocket joins. This is a receive-side -// handoff only: no Pulsar/Program transport is changed. -async function patchSeamlessRoomHandoff(path) { - let source = await readFile(path, "utf8"); - const before = source; - if (source.includes("preferredViewers") || source.includes("h.has(l)")) return; - const portablePath = path.replaceAll("\\", "/"); - - if (portablePath.endsWith("webrtc/index.ts")) { - source = source - .replace( - " const owners = new Map();\n", - " const owners = new Map();\n const preferredViewers = new Set();\n", - ) - .replace( - " if (owner === undefined) {\n owners.set(label, viewer);\n return true;\n }\n return owner === viewer; // only the owning room may publish/withdraw", - " if (owner === undefined || owner === viewer || preferredViewers.has(viewer)) {\n owners.set(label, viewer);\n return true;\n }\n return false; // only the owner or a preferred handoff may publish/withdraw", - ) - .replace(" mesh.viewer.leave();\n", " preferredViewers.delete(mesh.viewer);\n mesh.viewer.leave();\n") - .replace( - ` const next = new Set(rooms.map((r) => r.roomId)); - // Close rooms no longer present. - for (const roomId of [...meshes.keys()]) { - if (!next.has(roomId)) closeRoom(roomId); - } - // Open + join rooms newly added. - const added: MeetViewer[] = []; - for (const room of rooms) { - if (!meshes.has(room.roomId)) { - openRoom(room); - const m = meshes.get(room.roomId); - if (m) added.push(m.viewer); - } - } - await Promise.all(added.map((v) => v.join()));`, - ` const next = new Set(rooms.map((r) => r.roomId)); - const hadMeshes = meshes.size > 0; - const added: MeetViewer[] = []; - for (const room of rooms) { - if (!meshes.has(room.roomId)) { - openRoom(room); - const m = meshes.get(room.roomId); - if (m) { - if (hadMeshes) preferredViewers.add(m.viewer); - added.push(m.viewer); - } - } - } - await Promise.all(added.map((v) => v.join())); - for (const roomId of [...meshes.keys()]) { - if (!next.has(roomId)) closeRoom(roomId); - }`, - ); - } else if (portablePath.endsWith("webrtc/index.js")) { - source = source - .replace( - " const owners = new Map();\n", - " const owners = new Map();\n const preferredViewers = new Set();\n", - ) - .replace( - " if (owner === undefined) {\n owners.set(label, viewer);\n return true;\n }\n return owner === viewer; // only the owning room may publish/withdraw", - " if (owner === undefined || owner === viewer || preferredViewers.has(viewer)) {\n owners.set(label, viewer);\n return true;\n }\n return false; // only the owner or a preferred handoff may publish/withdraw", - ) - .replace(" mesh.viewer.leave();\n", " preferredViewers.delete(mesh.viewer);\n mesh.viewer.leave();\n") - .replace( - ` const next = new Set(rooms.map((r) => r.roomId)); - // Close rooms no longer present. - for (const roomId of [...meshes.keys()]) { - if (!next.has(roomId)) - closeRoom(roomId); - } - // Open + join rooms newly added. - const added = []; - for (const room of rooms) { - if (!meshes.has(room.roomId)) { - openRoom(room); - const m = meshes.get(room.roomId); - if (m) - added.push(m.viewer); - } - } - await Promise.all(added.map((v) => v.join()));`, - ` const next = new Set(rooms.map((r) => r.roomId)); - const hadMeshes = meshes.size > 0; - const added = []; - for (const room of rooms) { - if (!meshes.has(room.roomId)) { - openRoom(room); - const m = meshes.get(room.roomId); - if (m) { - if (hadMeshes) - preferredViewers.add(m.viewer); - added.push(m.viewer); - } - } - } - await Promise.all(added.map((v) => v.join())); - for (const roomId of [...meshes.keys()]) { - if (!next.has(roomId)) - closeRoom(roomId); - }`, - ); - } else { - // Vite's published runtime chunk is minified but keeps the same compact - // shape. The current dependency is patched below; other stale chunks are - // harmless and are not selected by dist/lumencast.js. - if (path.match(/index-.*\.js$/)) { - source = source - .replace( - "return u === void 0 ? (n.set(o, l), !0) : u === l;", - "return u === void 0 || u === l || h.has(l) ? (n.set(o, l), !0) : !1;", - ) - .replace( - " };\n function i(o) {", - " }, h = /* @__PURE__ */ new Set();\n function i(o) {", - ) - .replace("l.viewer.leave(), s.delete(o);", "h.delete(l.viewer), l.viewer.leave(), s.delete(o);") - .replace( - ` for (const d of [...s.keys()]) - l.has(d) || c(d); - const u = [];`, - ` const f = s.size > 0; - const u = [];`, - ) - .replace("g && u.push(g.viewer);", "g && (f && h.add(g.viewer), u.push(g.viewer));") - .replace( - " await Promise.all(u.map((d) => d.join()));\n },", - " await Promise.all(u.map((d) => d.join()));\n for (const d of [...s.keys()]) l.has(d) || c(d);\n },", - ); - } - } - - if (source === before) return; - await writeFile(path, source); -} - -await patchSeamlessRoomHandoff(join(runtimeRoot, "src", "webrtc", "index.ts")); -await patchSeamlessRoomHandoff(join(runtimeRoot, "dist", "webrtc", "index.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchSeamlessRoomHandoff(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime keeps both room generations visible during handoff"); - -// Preserve the old peer owner until a preferred replacement emits video. -async function patchHandoffPeerLeave(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - - if (!bundled) { - if (!source.includes("claim.preserve?.(key, viewer)")) { - const readableJs = path.endsWith(".js"); - const needle = readableJs - ? ' if (activePeerIds.get(key) === e.peerId && claim.acquire(key, viewer)) {' - : [ - ' const key = labelKey(e.peerName);', - ' if (', - ' activePeerIds.get(key) === e.peerId &&', - ' claim.acquire(key, viewer)', - ' ) {', - ].join("\n"); - const replacement = readableJs - ? [ - ' if (activePeerIds.get(key) === e.peerId && claim.preserve?.(key, viewer) === true) return;', - needle, - ].join("\n") - : [ - ' const key = labelKey(e.peerName);', - ' if (', - ' activePeerIds.get(key) === e.peerId &&', - ' claim.preserve?.(key, viewer) === true', - ' ) {', - ' return;', - ' }', - ' if (', - ' activePeerIds.get(key) === e.peerId &&', - ' claim.acquire(key, viewer)', - ' ) {', - ].join("\n"); - if (!source.includes(needle)) { - throw new Error(`unsupported @lumencast/runtime peer handoff contract: ${path}`); - } - source = source.replace(needle, replacement); - } - const readableJs = path.endsWith(".js"); - const claimNeedle = readableJs - ? [ - ' release: (label, viewer) => {', - ' if (owners.get(label) === viewer)', - ' owners.delete(label);', - ' },', - ].join("\n") - : [ - ' release: (label: string, viewer: MeetViewer): void => {', - ' if (owners.get(label) === viewer) owners.delete(label);', - ' },', - ].join("\n"); - const claimReplacement = [ - claimNeedle, - readableJs - ? ' preserve: (_label, viewer) => [...preferredViewers].some((candidate) => candidate !== viewer),' - : ' preserve: (_label: string, viewer: MeetViewer): boolean =>', - ...(readableJs ? [] : [' [...preferredViewers].some((candidate) => candidate !== viewer),']), - ].join("\n"); - if (!source.includes("preserve: (_label")) { - if (!source.includes(claimNeedle)) { - throw new Error(`unsupported @lumencast/runtime peer claim contract: ${path}`); - } - source = source.replace(claimNeedle, claimReplacement); - } - if (source !== before) await writeFile(path, source); - return; - } - - if (!source.includes("s.preserve?.(r, t)")) { - source = source.replace( - "activePeerIds.get(r) === n.peerId && s.acquire(r, t)", - "activePeerIds.get(r) === n.peerId && s.preserve?.(r, t) !== true && s.acquire(r, t)", - ); - source = source.replace( - "const i = activePeerIds.get(r), c = i === n.peerId && s.acquire(r, t);", - "const i = activePeerIds.get(r), c = i === n.peerId && s.preserve?.(r, t) !== true && s.acquire(r, t);", - ); - } - if (!source.includes("preserve: (_label, viewer)")) { - source = source.replace( - /(release: \(o, l\) => \{\n\s*n\.get\(o\) === l && n\.delete\(o\);\n\s*\})(\n\s*\}, h = \/\* @__PURE__ \*\/ new Set\(\);)/, - "$1,\n preserve: (_label, viewer) => [...h].some((candidate) => candidate !== viewer)$2", - ); - } - if (source !== before) await writeFile(path, source); -} - -await patchHandoffPeerLeave(join(runtimeRoot, "src", "webrtc", "index.ts")); -await patchHandoffPeerLeave(join(runtimeRoot, "dist", "webrtc", "index.js")); -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - await patchHandoffPeerLeave(join(runtimeRoot, "dist", name)); -} - -console.log("[solar] @lumencast/runtime keeps the old peer owner until replacement video arrives"); - -// Credential rotation can keep the same opaque room id. The first handoff -// patch above fixes room-id replacement, but the published runtime map is also -// keyed by room id, so the full receive-side generation swap is applied here -// for both source forms and for the already-bundled Vite chunk. This remains a -// viewer-only change: old meshes stay visible until replacement labels arrive. -async function patchCredentialHandoff(path) { - let source = await readFile(path, "utf8"); - const before = source; - const bundled = /index-.*\.js$/.test(path); - if (bundled) return; - const portablePath = path.replaceAll("\\", "/"); - - const tsManagement = String.raw` // roomId → { viewer, fingerprint } - const meshes = new Map(); - // peer_label → owning viewer (first-connected-wins). - const owners = new Map(); - const preferredViewers = new Set(); - - const claim = { - acquire: (label: string, viewer: MeetViewer): boolean => { - const owner = owners.get(label); - if (owner === undefined || owner === viewer || preferredViewers.has(viewer)) { - owners.set(label, viewer); - return true; - } - return false; - }, - release: (label: string, viewer: MeetViewer): void => { - if (owners.get(label) === viewer) owners.delete(label); - }, - preserve: (_label: string, viewer: MeetViewer): boolean => - [...preferredViewers].some((candidate) => candidate !== viewer), - }; - - const roomFingerprint = (room: RoomOptions): string => - JSON.stringify([room.roomId, room.signalingUrl, room.token]); - - function createRoomMesh(room: RoomOptions): { viewer: MeetViewer; fingerprint: string } { - const viewer = new MeetViewer({ - name: room.name ?? "solar-viewer", - ...room, - ...(options.deps !== undefined && room.deps === undefined ? { deps: options.deps } : {}), - }); - wireViewer(viewer, registry, claim); - return { viewer, fingerprint: roomFingerprint(room) }; - } - - function openRoom(room: RoomOptions): void { - if (meshes.has(room.roomId)) return; - meshes.set(room.roomId, createRoomMesh(room)); - } - - function closeMesh(mesh: { viewer: MeetViewer }): void { - for (const [label, owner] of [...owners.entries()]) { - if (owner === mesh.viewer) { - registry.remove(label); - owners.delete(label); - } - } - preferredViewers.delete(mesh.viewer); - mesh.viewer.leave(); - } - - function closeRoom(roomId: string): void { - const mesh = meshes.get(roomId); - if (mesh === undefined) return; - closeMesh(mesh); - meshes.delete(roomId); - } - - function scheduleHandoffClose(oldMesh: { viewer: MeetViewer }, replacements: MeetViewer[]): void { - const oldLabels = [...owners.entries()] - .filter(([, owner]) => owner === oldMesh.viewer) - .map(([label]) => label); - if (oldLabels.length === 0 || replacements.length === 0) { - closeMesh(oldMesh); - return; - } - const deadline = Date.now() + 5_000; - const poll = (): void => { - const ready = oldLabels.every((label) => { - const owner = owners.get(label); - return owner !== oldMesh.viewer && owner !== undefined && replacements.includes(owner); - }); - if (ready || Date.now() >= deadline) { - closeMesh(oldMesh); - return; - } - setTimeout(poll, 25); - }; - poll(); - } -`; - const jsManagement = String.raw` // roomId → { viewer, fingerprint } - const meshes = new Map(); - // peer_label → owning viewer (first-connected-wins). - const owners = new Map(); - const preferredViewers = new Set(); - const claim = { - acquire: (label, viewer) => { - const owner = owners.get(label); - if (owner === undefined || owner === viewer || preferredViewers.has(viewer)) { - owners.set(label, viewer); - return true; - } - return false; - }, - release: (label, viewer) => { - if (owners.get(label) === viewer) - owners.delete(label); - }, - preserve: (_label, viewer) => [...preferredViewers].some((candidate) => candidate !== viewer), - }; - const roomFingerprint = (room) => JSON.stringify([room.roomId, room.signalingUrl, room.token]); - function createRoomMesh(room) { - const viewer = new MeetViewer({ - name: room.name ?? "solar-viewer", - ...room, - ...(options.deps !== undefined && room.deps === undefined ? { deps: options.deps } : {}), - }); - wireViewer(viewer, registry, claim); - return { viewer, fingerprint: roomFingerprint(room) }; - } - function openRoom(room) { - if (meshes.has(room.roomId)) - return; - meshes.set(room.roomId, createRoomMesh(room)); - } - function closeMesh(mesh) { - for (const [label, owner] of [...owners.entries()]) { - if (owner === mesh.viewer) { - registry.remove(label); - owners.delete(label); - } - } - preferredViewers.delete(mesh.viewer); - mesh.viewer.leave(); - } - function closeRoom(roomId) { - const mesh = meshes.get(roomId); - if (mesh === undefined) - return; - closeMesh(mesh); - meshes.delete(roomId); - } - function scheduleHandoffClose(oldMesh, replacements) { - const oldLabels = [...owners.entries()] - .filter(([, owner]) => owner === oldMesh.viewer) - .map(([label]) => label); - if (oldLabels.length === 0 || replacements.length === 0) { - closeMesh(oldMesh); - return; - } - const deadline = Date.now() + 5_000; - const poll = () => { - const ready = oldLabels.every((label) => { - const owner = owners.get(label); - return owner !== oldMesh.viewer && owner !== undefined && replacements.includes(owner); - }); - if (ready || Date.now() >= deadline) { - closeMesh(oldMesh); - return; - } - setTimeout(poll, 25); - }; - poll(); - } -`; - - const managementPattern = portablePath.endsWith("webrtc/index.ts") - ? / {2}\/\/ roomId → \{ viewer, joined \}[\s\S]*?\r?\n {2}\}\r?\n(?= {2}for \(const room of options\.rooms\)\s+openRoom\(room\);)/ - : / {4}\/\/ roomId → \{ viewer, joined \}[\s\S]*?\r?\n {4}\}\r?\n(?= {4}for \(const room of options\.rooms\)\s+openRoom\(room\);)/; - source = source.replace(managementPattern, portablePath.endsWith("webrtc/index.ts") ? tsManagement : jsManagement); - - const tsSetRooms = String.raw` setRooms: async (rooms) => { - const next = new Set(rooms.map((r) => r.roomId)); - const hadMeshes = meshes.size > 0; - const added: MeetViewer[] = []; - const addedRoomIds: string[] = []; - const replaced: Array<{ - roomId: string; - old: { viewer: MeetViewer; fingerprint: string }; - next: { viewer: MeetViewer; fingerprint: string }; - }> = []; - for (const room of rooms) { - const current = meshes.get(room.roomId); - if (current === undefined) { - const nextMesh = createRoomMesh(room); - meshes.set(room.roomId, nextMesh); - if (hadMeshes) preferredViewers.add(nextMesh.viewer); - added.push(nextMesh.viewer); - addedRoomIds.push(room.roomId); - } else if (current.fingerprint !== roomFingerprint(room)) { - const nextMesh = createRoomMesh(room); - meshes.set(room.roomId, nextMesh); - preferredViewers.add(nextMesh.viewer); - added.push(nextMesh.viewer); - replaced.push({ roomId: room.roomId, old: current, next: nextMesh }); - } - } - try { - await Promise.all(added.map((v) => v.join())); - } catch (error) { - for (const item of replaced) { - if (meshes.get(item.roomId)?.viewer === item.next.viewer) { - closeMesh(item.next); - meshes.set(item.roomId, item.old); - } - } - for (const roomId of addedRoomIds) { - const mesh = meshes.get(roomId); - if (mesh && added.includes(mesh.viewer)) closeRoom(roomId); - } - throw error; - } - for (const item of replaced) scheduleHandoffClose(item.old, [item.next.viewer]); - for (const roomId of [...meshes.keys()]) { - if (!next.has(roomId)) { - const old = meshes.get(roomId); - meshes.delete(roomId); - if (old) scheduleHandoffClose(old, added); - } - } - }, -`; - const jsSetRooms = String.raw` setRooms: async (rooms) => { - const next = new Set(rooms.map((r) => r.roomId)); - const hadMeshes = meshes.size > 0; - const added = []; - const addedRoomIds = []; - const replaced = []; - for (const room of rooms) { - const current = meshes.get(room.roomId); - if (current === undefined) { - const nextMesh = createRoomMesh(room); - meshes.set(room.roomId, nextMesh); - if (hadMeshes) - preferredViewers.add(nextMesh.viewer); - added.push(nextMesh.viewer); - addedRoomIds.push(room.roomId); - } - else if (current.fingerprint !== roomFingerprint(room)) { - const nextMesh = createRoomMesh(room); - meshes.set(room.roomId, nextMesh); - preferredViewers.add(nextMesh.viewer); - added.push(nextMesh.viewer); - replaced.push({ roomId: room.roomId, old: current, next: nextMesh }); - } - } - try { - await Promise.all(added.map((v) => v.join())); - } - catch (error) { - for (const item of replaced) { - if (meshes.get(item.roomId)?.viewer === item.next.viewer) { - closeMesh(item.next); - meshes.set(item.roomId, item.old); - } - } - for (const roomId of addedRoomIds) { - const mesh = meshes.get(roomId); - if (mesh && added.includes(mesh.viewer)) - closeRoom(roomId); - } - throw error; - } - for (const item of replaced) - scheduleHandoffClose(item.old, [item.next.viewer]); - for (const roomId of [...meshes.keys()]) { - if (!next.has(roomId)) { - const old = meshes.get(roomId); - meshes.delete(roomId); - if (old) - scheduleHandoffClose(old, added); - } - } - }, -`; - const setRoomsPattern = portablePath.endsWith("webrtc/index.ts") - ? / {4}setRooms: async \(rooms\) => \{[\s\S]*?\r?\n {4}\},\r?\n(?= {4}resolvePeerStream)/ - : / {4}setRooms: async \(rooms\) => \{[\s\S]*?\r?\n {4}\},\r?\n(?= {4}resolvePeerStream)/; - source = source.replace(setRoomsPattern, portablePath.endsWith("webrtc/index.ts") ? tsSetRooms : jsSetRooms); - if (source !== before) await writeFile(path, source); -} - -await patchCredentialHandoff(join(runtimeRoot, "src", "webrtc", "index.ts")); -await patchCredentialHandoff(join(runtimeRoot, "dist", "webrtc", "index.js")); - -const bundledHandoff = String.raw`function se(t) { - const e = de(), s = /* @__PURE__ */ new Map(), n = /* @__PURE__ */ new Map(), r = { - acquire: (o, l) => { - const u = n.get(o); - return u === void 0 || u === l || h.has(l) ? (n.set(o, l), !0) : !1; - }, - release: (o, l) => { - n.get(o) === l && n.delete(o); - }, - preserve: (_label, viewer) => [...h].some((candidate) => candidate !== viewer) - }, h = /* @__PURE__ */ new Set(); - const roomFingerprint = (o) => JSON.stringify([o.roomId, o.signalingUrl, o.token]); - function createRoomMesh(o) { - const l = new ue({ - name: o.name ?? "solar-viewer", - ...o, - ...t.deps !== void 0 && o.deps === void 0 ? { deps: t.deps } : {} - }); - fe(l, e, r); - return { viewer: l, fingerprint: roomFingerprint(o) }; - } - function i(o) { - if (s.has(o.roomId)) return; - s.set(o.roomId, createRoomMesh(o)); - } - function closeMesh(o) { - for (const [l, u] of [...n.entries()]) - u === o.viewer && (e.remove(l), n.delete(l)); - h.delete(o.viewer), o.viewer.leave(); - } - function c(o) { - const l = s.get(o); - l !== void 0 && (closeMesh(l), s.delete(o)); - } - function scheduleHandoffClose(o, l) { - const u = [...n.entries()].filter(([, d]) => d === o.viewer).map(([d]) => d); - if (u.length === 0 || l.length === 0) { - closeMesh(o); - return; - } - const deadline = Date.now() + 5e3; - const poll = () => { - const ready = u.every((d) => { - const f = n.get(d); - return f !== o.viewer && f !== void 0 && l.includes(f); - }); - if (ready || Date.now() >= deadline) { - closeMesh(o); - return; - } - setTimeout(poll, 25); - }; - poll(); - } - for (const o of t.rooms) i(o); - return { - join: async () => { - await Promise.all([...s.values()].map((o) => o.viewer.join())); - }, - leave: () => { - for (const o of [...s.keys()]) c(o); - e.clear(); - }, - setRooms: async (o) => { - const l = new Set(o.map((d) => d.roomId)); - const f = s.size > 0; - const u = [], d = [], g = []; - for (const p of o) { - const m = s.get(p.roomId); - if (m === void 0) { - const y = createRoomMesh(p); - s.set(p.roomId, y), f && h.add(y.viewer), u.push(y.viewer), d.push(p.roomId); - } else if (m.fingerprint !== roomFingerprint(p)) { - const y = createRoomMesh(p); - s.set(p.roomId, y), h.add(y.viewer), u.push(y.viewer), g.push({ roomId: p.roomId, old: m, next: y }); - } - } - try { - await Promise.all(u.map((p) => p.join())); - } catch (p) { - for (const m of g) - s.get(m.roomId)?.viewer === m.next.viewer && (closeMesh(m.next), s.set(m.roomId, m.old)); - for (const m of d) { - const y = s.get(m); - y && u.includes(y.viewer) && c(m); - } - throw p; - } - for (const m of g) scheduleHandoffClose(m.old, [m.next.viewer]); - for (const p of [...s.keys()]) { - if (!l.has(p)) { - const m = s.get(p); - s.delete(p); - if (m) scheduleHandoffClose(m, u); - } - } - }, - resolvePeerStream: (o) => e.resolve(T(o)), - subscribePeerStream: (o, l) => e.subscribe(T(o), l), - registry: e - }; -} -`; -for (const name of runtimeDistFiles.filter((entry) => /^index-.*\.js$/.test(entry))) { - const path = join(runtimeRoot, "dist", name); - let source = await readFile(path, "utf8"); - if (!source.includes("const roomFingerprint") && /function se\(t\) \{/.test(source)) { - source = source.replace(/function se\(t\) \{[\s\S]*?\n\}\nfunction qt\(t\) \{/, () => `${bundledHandoff}function qt(t) {`); - await writeFile(path, source); - } -} - -console.log("[solar] @lumencast/runtime keeps credential-rotated rooms visible during handoff"); - -// A replacement MediaStream is observable before Chromium has decoded its -// first frame. Swapping the visible video at that point creates a black gap on -// both Prism Preview and the local live browser_source. Keep the old stream -// attached while a detached muted element prewarms the replacement, then make -// one receive-side presentation swap once current data is available. This is -// deliberately scoped to Solar's renderer; no Pulsar/Program or room wire is -// changed. -async function patchLivePeerVideo(path) { - let source = await readFile(path, "utf8"); - if (source.includes("const absenceTimer")) return; - const bundled = path.endsWith("live-peer-video.js"); - const needle = bundled - ? / {4}const videoRef = useRef\(null\);[\s\S]*? {4}\}, \[peerLabel, resolvePeerStream, subscribePeerStream\]\);/ - : / {2}const videoRef = useRef\(null\);[\s\S]*? {2}\}, \[peerLabel, resolvePeerStream, subscribePeerStream\]\);/; - const replacement = bundled - ? String.raw` const videoRef = useRef(null); - const [stream, setStream] = useState(null); - const displayedStream = useRef(null); - const handoff = useRef(null); - const absenceTimer = useRef(null); - const cancelAbsence = () => { - if (absenceTimer.current !== null) { - clearTimeout(absenceTimer.current); - absenceTimer.current = null; - } - }; - const cancelHandoff = () => { - const pending = handoff.current; - if (pending === null) - return; - if (pending.timer !== null) - clearTimeout(pending.timer); - pending.element.pause(); - pending.element.srcObject = null; - pending.element.remove(); - handoff.current = null; - }; - const publishStream = (next) => { - cancelAbsence(); - cancelHandoff(); - displayedStream.current = next; - setStream(next); - }; - const handoffStream = (next) => { - const current = displayedStream.current; - if (next === current) - return; - if (next === null) { - if (current !== null) { - cancelHandoff(); - cancelAbsence(); - absenceTimer.current = setTimeout(() => { - absenceTimer.current = null; - if (displayedStream.current === current) - publishStream(null); - }, 3e3); - return; - } - publishStream(next); - return; - } - if (next.getVideoTracks().length === 0) - return; - cancelAbsence(); - if (current === null) { - publishStream(next); - return; - } - cancelHandoff(); - const element = document.createElement("video"); - element.muted = true; - element.autoplay = true; - element.playsInline = true; - element.preload = "auto"; - element.style.cssText = "position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;opacity:0;pointer-events:none"; - document.body.appendChild(element); - element.srcObject = next; - const pending = { element, timer: null }; - handoff.current = pending; - const deadline = Date.now() + 3e3; - const finish = () => { - if (handoff.current !== pending) - return; - if (pending.timer !== null) - clearTimeout(pending.timer); - pending.element.pause(); - pending.element.srcObject = null; - pending.element.remove(); - handoff.current = null; - displayedStream.current = next; - setStream(next); - }; - const poll = () => { - if (handoff.current !== pending) - return; - if (element.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && element.videoWidth > 0) { - finish(); - return; - } - if (Date.now() >= deadline) { - finish(); - return; - } - pending.timer = setTimeout(poll, 16); - }; - void element.play().catch(() => undefined); - poll(); - }; - useEffect(() => { - if (subscribePeerStream !== undefined) { - return subscribePeerStream(peerLabel, handoffStream); - } - if (resolvePeerStream !== undefined) { - handoffStream(resolvePeerStream(peerLabel)); - return; - } - publishStream(null); - }, [peerLabel, resolvePeerStream, subscribePeerStream]); - useEffect(() => cancelHandoff, []); - useEffect(() => () => { - cancelAbsence(); - }, []);` - : String.raw` const videoRef = useRef(null); - const [stream, setStream] = useState(null); - - const displayedStream = useRef(null); - const handoff = useRef<{ - element: HTMLVideoElement; - timer: ReturnType | null; - } | null>(null); - const absenceTimer = useRef | null>(null); - - const cancelAbsence = (): void => { - if (absenceTimer.current !== null) { - clearTimeout(absenceTimer.current); - absenceTimer.current = null; - } - }; - - const cancelHandoff = (): void => { - const pending = handoff.current; - if (pending === null) return; - if (pending.timer !== null) clearTimeout(pending.timer); - pending.element.pause(); - pending.element.srcObject = null; - pending.element.remove(); - handoff.current = null; - }; - - const publishStream = (next: MediaStream | null): void => { - cancelAbsence(); - cancelHandoff(); - displayedStream.current = next; - setStream(next); - }; - - const handoffStream = (next: MediaStream | null): void => { - const current = displayedStream.current; - if (next === current) return; - if (next === null) { - if (current !== null) { - cancelHandoff(); - cancelAbsence(); - absenceTimer.current = setTimeout(() => { - absenceTimer.current = null; - if (displayedStream.current === current) publishStream(null); - }, 3_000); - return; - } - publishStream(next); - return; - } - if (next.getVideoTracks().length === 0) return; - cancelAbsence(); - if (current === null) { - publishStream(next); - return; - } - - cancelHandoff(); - const element = document.createElement("video"); - element.muted = true; - element.autoplay = true; - element.playsInline = true; - element.preload = "auto"; - element.style.cssText = - "position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;opacity:0;pointer-events:none"; - document.body.appendChild(element); - element.srcObject = next; - const pending = { - element, - timer: null as ReturnType | null, - }; - handoff.current = pending; - const deadline = Date.now() + 3_000; - - const finish = (): void => { - if (handoff.current !== pending) return; - if (pending.timer !== null) clearTimeout(pending.timer); - pending.element.pause(); - pending.element.srcObject = null; - pending.element.remove(); - handoff.current = null; - displayedStream.current = next; - setStream(next); - }; - const poll = (): void => { - if (handoff.current !== pending) return; - if (element.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && element.videoWidth > 0) { - finish(); - return; - } - if (Date.now() >= deadline) { - finish(); - return; - } - pending.timer = setTimeout(poll, 16); - }; - void element.play().catch(() => undefined); - poll(); - }; - - useEffect(() => { - if (subscribePeerStream !== undefined) { - return subscribePeerStream(peerLabel, handoffStream); - } - if (resolvePeerStream !== undefined) { - handoffStream(resolvePeerStream(peerLabel)); - return; - } - publishStream(null); - }, [peerLabel, resolvePeerStream, subscribePeerStream]); - - useEffect(() => cancelHandoff, []); - - useEffect( - () => () => { - cancelAbsence(); - }, - [], - );`; - const patched = source.replace(needle, replacement); - if (patched === source) { - throw new Error(`unsupported @lumencast/runtime live-peer-video contract: ${path}`); - } - await writeFile(path, patched); -} - -await patchLivePeerVideo(join(runtimeRoot, "src", "render", "primitives", "live-peer-video.tsx")); -await patchLivePeerVideo(join(runtimeRoot, "dist", "render", "primitives", "live-peer-video.js")); -console.log("[solar] @lumencast/runtime prewarms replacement peer video before visible swap"); - -// The published runtime also carries the tree primitive in hashed chunks. Vite -// resolves those chunks directly for the standalone host, so patch their -// inlined LivePeerVideo implementation as well as the readable source module. -const bundledLivePeerVideo = String.raw`function Lt({ - peerLabel: t, - objectFit: e, - muted: n -}) { - const r = Pt(), i = r?.resolvePeerStream, o = r?.subscribePeerStream, s = n ?? !r?.liveAudio, a = at(null), [l, c] = Nt(null), u = at(null), f = at(null); - const absenceTimer = at(null); - const cancelAbsence = () => { - if (absenceTimer.current !== null) { - clearTimeout(absenceTimer.current); - absenceTimer.current = null; - } - }; - const cancel = () => { - const p = u.current; - if (p === null) return; - if (p.timer !== null) clearTimeout(p.timer); - p.element.pause(); - p.element.srcObject = null; - p.element.remove(); - u.current = null; - }; - const m = (p) => { - cancelAbsence(); - cancel(); - f.current = p; - c(p); - }; - const g = (p) => { - const y = f.current; - if (p === y) return; - if (p === null) { - if (y !== null) { - cancel(); - cancelAbsence(); - absenceTimer.current = setTimeout(() => { - absenceTimer.current = null; - if (f.current === y) - m(null); - }, 3e3); - return; - } - m(p); - return; - } - if (p.getVideoTracks().length === 0) - return; - cancelAbsence(); - if (y === null) { - m(p); - return; - } - cancel(); - const E = document.createElement("video"); - E.muted = true; - E.autoplay = true; - E.playsInline = true; - E.preload = "auto"; - E.style.cssText = "position:fixed;left:-10000px;top:-10000px;width:1px;height:1px;opacity:0;pointer-events:none"; - document.body.appendChild(E); - E.srcObject = p; - const A = { element: E, timer: null }; - u.current = A; - const R = Date.now() + 3e3; - const N = () => { - if (u.current !== A) return; - if (A.timer !== null) clearTimeout(A.timer); - A.element.pause(); - A.element.srcObject = null; - A.element.remove(); - u.current = null; - f.current = p; - c(p); - }; - const w = () => { - if (u.current !== A) return; - if (E.readyState >= 2 && E.videoWidth > 0 || Date.now() >= R) { - N(); - return; - } - A.timer = setTimeout(w, 16); - }; - void E.play().catch(() => void 0); - w(); - }; - return et(() => { - if (o !== void 0) return o(t, g); - if (i !== void 0) { - g(i(t)); - return; - } - m(null); - }, [t, i, o]), et(() => () => { - cancel(); - cancelAbsence(); - }, []), et(() => { - const p = a.current; - if (p !== null) return p.srcObject = l, () => { - p !== null && (p.srcObject = null); - }; - }, [l]), et(() => { - const p = a.current; - p !== null && (p.muted = s); - }, [s, l]), l === null ? /* @__PURE__ */ d("div", { - "aria-hidden": !0, - "data-lumencast-media-live": !0, - style: { width: "100%", height: "100%", opacity: 0, pointerEvents: "none" } - }) : /* @__PURE__ */ d("video", { - ref: a, - "data-lumencast-media-live": !0, - autoPlay: !0, - muted: !0, - playsInline: !0, - style: { width: "100%", height: "100%", objectFit: e, pointerEvents: "none" } - }); -}`; - -async function patchBundledLivePeerVideo(path) { - let source = await readFile(path, "utf8"); - if (source.includes("const absenceTimer")) return; - // Older retained runtime chunks may use a different minifier symbol (or - // contain no peer primitive at all). They are not selected by the current - // package entry and must remain untouched rather than making postinstall - // fail for an orphaned cache chunk. - if (!source.includes("data-lumencast-media-live") || !source.includes("function Lt(")) return; - const pattern = /function Lt\(\{[\s\S]*?\n\}\nfunction Dn\(/; - const match = source.match(pattern); - if (!match) return; - const replacement = `${bundledLivePeerVideo}\nfunction Dn(`; - await writeFile(path, source.replace(pattern, replacement)); -} - -for (const name of (await readdir(join(runtimeRoot, "dist"))).filter((entry) => /^tree-.*\.js$/.test(entry))) { - await patchBundledLivePeerVideo(join(runtimeRoot, "dist", name)); -} -console.log("[solar] @lumencast/runtime patches bundled LivePeerVideo handoffs"); diff --git a/scripts/patch-peer-viewer.mjs b/scripts/patch-peer-viewer.mjs new file mode 100644 index 0000000..f393c40 --- /dev/null +++ b/scripts/patch-peer-viewer.mjs @@ -0,0 +1,15 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { patchRegistry } from "./peer-viewer/registry.mjs"; +import { patchViewer } from "./peer-viewer/viewer.mjs"; +import { patchRooms } from "./peer-viewer/rooms.mjs"; + +// Resolve hoisted npm dependencies. Only the receive-only entry is consumed by Solar. +const runtimeRoot = resolve( + dirname(fileURLToPath(import.meta.resolve("@lumencast/runtime"))), + "..", +); +await patchRegistry(runtimeRoot); +await patchViewer(runtimeRoot); +await patchRooms(runtimeRoot); +console.log("[solar] receive-only WebRTC compatibility patches applied"); diff --git a/scripts/peer-viewer/README.md b/scripts/peer-viewer/README.md new file mode 100644 index 0000000..74108eb --- /dev/null +++ b/scripts/peer-viewer/README.md @@ -0,0 +1,21 @@ +# Receive-only dependency compatibility + +Owner: Solar installation adapter. Entry: `../patch-peer-viewer.mjs` via package postinstall. +The package ships this folder, so hoisted dependencies work in an installed application. +Only the three consumed `@lumencast/runtime/dist/webrtc/` files are written. Source, +compiled scene bundles, React primitives and protocol image limits are outside this adapter. + +- `registry.mjs`: stream re-emission and peer-generation ownership. +- `viewer.mjs`: identity/roster, stale generations, retry offers/glare and serialized signaling. +- `roster.mjs`: the viewer's authoritative room roster across signaling races. +- `rooms.mjs`: room/credential handoff while retaining the old video until the replacement owns it. + +Ordered patches assert the supported upstream text contract and remain idempotent. They +preserve the viewer's useful behavior; they do not launch a publisher or add a renderer. +On an upstream version change, compare pristine and patched files, rerun the viewer tests +and installed-package checks, then retire patches that the upstream has absorbed. + +Checks: meet-viewer-identity-race, publisher-offer-policy, injection, slot-binding and +Antenne suites. The audit compares fresh tarballs with lockfile SHA-512 verification, +requires exactly those three changed files, verifies byte equivalence to the qualified +viewer, and runs the installer twice. See the repository audit report for those hashes. diff --git a/scripts/peer-viewer/registry.mjs b/scripts/peer-viewer/registry.mjs new file mode 100644 index 0000000..75e98a6 --- /dev/null +++ b/scripts/peer-viewer/registry.mjs @@ -0,0 +1,84 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +// MeetViewer builds one aggregate MediaStream per authored peer. Chromium can +// deliver that stream's audio track before its video track, so the same object +// is intentionally published more than once while it becomes usable. The +// original registry treated same-object writes as no-ops and the LIVE +// primitive could stay attached to an audio-only stream until the next room +// handoff. Keep the re-emit behaviour in the consumed dist/webrtc entry. +async function patchPeerStreamRegistry(path) { + let source = await readFile(path, "utf8"); + if (source.includes("Re-publish the same object")) return; + const needles = [ + " if (streams.get(peerLabel) === stream) return; // idempotent re-emit guard\n", + " if (streams.get(peerLabel) === stream) return;\n", + " if (streams.get(peerLabel) === stream)\n return; // idempotent re-emit guard\n", + " if (streams.get(peerLabel) === stream)\n return;\n", + ]; + const needle = needles.find((value) => source.includes(value)); + if (needle === undefined) { + throw new Error( + `unsupported @lumencast/runtime peer-stream registry contract: ${path}`, + ); + } + const replacement = + " // MeetViewer fills one aggregate MediaStream incrementally (audio first, video second).\n" + + " // Re-publish the same object so consumers waiting for video observe the second edge.\n"; + source = source.replace(needle, replacement); + await writeFile(path, source); +} +// A publisher can reconnect under the same stable peer label before the +// signaling server delivers the old peer's `peer-left`. The shared registry is +// label-keyed, so the stale leave used to remove the replacement stream even +// though its new RTCPeerConnection was connected and receiving frames. Track +// the peer id that most recently published each label and let only that +// generation withdraw it. +async function patchRegistryPeerGeneration(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + source = source.replace( + `): void {\n // Index the registry by the NORMALISED label`, + `): void {\n const activePeerIds = new Map();\n // Index the registry by the NORMALISED label`, + ); + source = source.replace( + ` if (claim.acquire(key, viewer)) registry.set(key, e.stream);`, + ` if (claim.acquire(key, viewer)) {\n activePeerIds.set(key, e.peerId);\n registry.set(key, e.stream);\n }`, + ); + source = source.replace( + ` if (claim.acquire(key, viewer)) {\n registry.remove(key);\n claim.release(key, viewer);\n }`, + ` if (\n activePeerIds.get(key) === e.peerId &&\n claim.acquire(key, viewer)\n ) {\n activePeerIds.delete(key);\n registry.remove(key);\n claim.release(key, viewer);\n }`, + ); + // The emitted JS has no type parameters and braces the one-line set. + source = source.replace( + `function wireViewer(viewer, registry, claim) {\n // Index`, + `function wireViewer(viewer, registry, claim) {\n const activePeerIds = new Map();\n // Index`, + ); + source = source.replace( + ` if (claim.acquire(key, viewer))\n registry.set(key, e.stream);`, + ` if (claim.acquire(key, viewer)) {\n activePeerIds.set(key, e.peerId);\n registry.set(key, e.stream);\n }`, + ); + source = source.replace( + ` if (claim.acquire(key, viewer)) {\n registry.remove(key);\n claim.release(key, viewer);\n }`, + ` if (activePeerIds.get(key) === e.peerId && claim.acquire(key, viewer)) {\n activePeerIds.delete(key);\n registry.remove(key);\n claim.release(key, viewer);\n }`, + ); + } + if ( + !source.includes("activePeerIds") || + !source.includes("e.peerId") || + false + ) { + throw new Error( + `unsupported @lumencast/runtime registry generation contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +export async function patchRegistry(runtimeRoot) { + await patchPeerStreamRegistry( + join(runtimeRoot, "dist", "webrtc", "peer-stream-registry.js"), + ); + await patchRegistryPeerGeneration( + join(runtimeRoot, "dist", "webrtc", "index.js"), + ); +} diff --git a/scripts/peer-viewer/rooms.mjs b/scripts/peer-viewer/rooms.mjs new file mode 100644 index 0000000..ff2b727 --- /dev/null +++ b/scripts/peer-viewer/rooms.mjs @@ -0,0 +1,264 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +// A persistent ZabCam room can be re-minted after Meet restarts. The two +// local Solar documents (Preview and the live browser_source) must not blank +// while their receive-only viewer moves from the stale room to the replacement. +// Runtime 0.18.2 closes removed meshes before it opens the new ones and keeps a +// first-connected-wins registry, which creates exactly that visible gap. Open +// replacement meshes first and let their first track take ownership; close the +// old generation only after the new WebSocket joins. This is a receive-side +// handoff only: no Pulsar/Program transport is changed. +async function patchSeamlessRoomHandoff(path) { + let source = await readFile(path, "utf8"); + const before = source; + if (source.includes("preferredViewers") || source.includes("h.has(l)")) + return; + { + source = source + .replace( + " const owners = new Map();\n", + " const owners = new Map();\n const preferredViewers = new Set();\n", + ) + .replace( + " if (owner === undefined) {\n owners.set(label, viewer);\n return true;\n }\n return owner === viewer; // only the owning room may publish/withdraw", + " if (owner === undefined || owner === viewer || preferredViewers.has(viewer)) {\n owners.set(label, viewer);\n return true;\n }\n return false; // only the owner or a preferred handoff may publish/withdraw", + ) + .replace( + " mesh.viewer.leave();\n", + " preferredViewers.delete(mesh.viewer);\n mesh.viewer.leave();\n", + ) + .replace( + ` const next = new Set(rooms.map((r) => r.roomId)); + // Close rooms no longer present. + for (const roomId of [...meshes.keys()]) { + if (!next.has(roomId)) + closeRoom(roomId); + } + // Open + join rooms newly added. + const added = []; + for (const room of rooms) { + if (!meshes.has(room.roomId)) { + openRoom(room); + const m = meshes.get(room.roomId); + if (m) + added.push(m.viewer); + } + } + await Promise.all(added.map((v) => v.join()));`, + ` const next = new Set(rooms.map((r) => r.roomId)); + const hadMeshes = meshes.size > 0; + const added = []; + for (const room of rooms) { + if (!meshes.has(room.roomId)) { + openRoom(room); + const m = meshes.get(room.roomId); + if (m) { + if (hadMeshes) + preferredViewers.add(m.viewer); + added.push(m.viewer); + } + } + } + await Promise.all(added.map((v) => v.join())); + for (const roomId of [...meshes.keys()]) { + if (!next.has(roomId)) + closeRoom(roomId); + }`, + ); + } + if (source === before) return; + await writeFile(path, source); +} +// Preserve the old peer owner until a preferred replacement emits video. +async function patchHandoffPeerLeave(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + if (!source.includes("claim.preserve?.(key, viewer)")) { + const needle = + " if (activePeerIds.get(key) === e.peerId && claim.acquire(key, viewer)) {"; + const replacement = [ + " if (activePeerIds.get(key) === e.peerId && claim.preserve?.(key, viewer) === true) return;", + needle, + ].join("\n"); + if (!source.includes(needle)) { + throw new Error( + `unsupported @lumencast/runtime peer handoff contract: ${path}`, + ); + } + source = source.replace(needle, replacement); + } + const claimNeedle = [ + " release: (label, viewer) => {", + " if (owners.get(label) === viewer)", + " owners.delete(label);", + " },", + ].join("\n"); + const claimReplacement = [ + claimNeedle, + " preserve: (_label, viewer) => [...preferredViewers].some((candidate) => candidate !== viewer),", + ...[], + ].join("\n"); + if (!source.includes("preserve: (_label")) { + if (!source.includes(claimNeedle)) { + throw new Error( + `unsupported @lumencast/runtime peer claim contract: ${path}`, + ); + } + source = source.replace(claimNeedle, claimReplacement); + } + if (source !== before) await writeFile(path, source); + return; + } +} +// Credential rotation can keep the same opaque room id. The first handoff +// patch above fixes room-id replacement, but the published runtime map is also +// keyed by room id, so the full receive-side generation swap is applied here +// for the consumed receive-only entry. This remains a +// viewer-only change: old meshes stay visible until replacement labels arrive. +async function patchCredentialHandoff(path) { + let source = await readFile(path, "utf8"); + const before = source; + const jsManagement = String.raw` // roomId → { viewer, fingerprint } + const meshes = new Map(); + // peer_label → owning viewer (first-connected-wins). + const owners = new Map(); + const preferredViewers = new Set(); + const claim = { + acquire: (label, viewer) => { + const owner = owners.get(label); + if (owner === undefined || owner === viewer || preferredViewers.has(viewer)) { + owners.set(label, viewer); + return true; + } + return false; + }, + release: (label, viewer) => { + if (owners.get(label) === viewer) + owners.delete(label); + }, + preserve: (_label, viewer) => [...preferredViewers].some((candidate) => candidate !== viewer), + }; + const roomFingerprint = (room) => JSON.stringify([room.roomId, room.signalingUrl, room.token]); + function createRoomMesh(room) { + const viewer = new MeetViewer({ + name: room.name ?? "solar-viewer", + ...room, + ...(options.deps !== undefined && room.deps === undefined ? { deps: options.deps } : {}), + }); + wireViewer(viewer, registry, claim); + return { viewer, fingerprint: roomFingerprint(room) }; + } + function openRoom(room) { + if (meshes.has(room.roomId)) + return; + meshes.set(room.roomId, createRoomMesh(room)); + } + function closeMesh(mesh) { + for (const [label, owner] of [...owners.entries()]) { + if (owner === mesh.viewer) { + registry.remove(label); + owners.delete(label); + } + } + preferredViewers.delete(mesh.viewer); + mesh.viewer.leave(); + } + function closeRoom(roomId) { + const mesh = meshes.get(roomId); + if (mesh === undefined) + return; + closeMesh(mesh); + meshes.delete(roomId); + } + function scheduleHandoffClose(oldMesh, replacements) { + const oldLabels = [...owners.entries()] + .filter(([, owner]) => owner === oldMesh.viewer) + .map(([label]) => label); + if (oldLabels.length === 0 || replacements.length === 0) { + closeMesh(oldMesh); + return; + } + const deadline = Date.now() + 5_000; + const poll = () => { + const ready = oldLabels.every((label) => { + const owner = owners.get(label); + return owner !== oldMesh.viewer && owner !== undefined && replacements.includes(owner); + }); + if (ready || Date.now() >= deadline) { + closeMesh(oldMesh); + return; + } + setTimeout(poll, 25); + }; + poll(); + } +`; + const managementPattern = + / {4}\/\/ roomId → \{ viewer, joined \}[\s\S]*?\r?\n {4}\}\r?\n(?= {4}for \(const room of options\.rooms\)\s+openRoom\(room\);)/; + source = source.replace(managementPattern, jsManagement); + const jsSetRooms = String.raw` setRooms: async (rooms) => { + const next = new Set(rooms.map((r) => r.roomId)); + const hadMeshes = meshes.size > 0; + const added = []; + const addedRoomIds = []; + const replaced = []; + for (const room of rooms) { + const current = meshes.get(room.roomId); + if (current === undefined) { + const nextMesh = createRoomMesh(room); + meshes.set(room.roomId, nextMesh); + if (hadMeshes) + preferredViewers.add(nextMesh.viewer); + added.push(nextMesh.viewer); + addedRoomIds.push(room.roomId); + } + else if (current.fingerprint !== roomFingerprint(room)) { + const nextMesh = createRoomMesh(room); + meshes.set(room.roomId, nextMesh); + preferredViewers.add(nextMesh.viewer); + added.push(nextMesh.viewer); + replaced.push({ roomId: room.roomId, old: current, next: nextMesh }); + } + } + try { + await Promise.all(added.map((v) => v.join())); + } + catch (error) { + for (const item of replaced) { + if (meshes.get(item.roomId)?.viewer === item.next.viewer) { + closeMesh(item.next); + meshes.set(item.roomId, item.old); + } + } + for (const roomId of addedRoomIds) { + const mesh = meshes.get(roomId); + if (mesh && added.includes(mesh.viewer)) + closeRoom(roomId); + } + throw error; + } + for (const item of replaced) + scheduleHandoffClose(item.old, [item.next.viewer]); + for (const roomId of [...meshes.keys()]) { + if (!next.has(roomId)) { + const old = meshes.get(roomId); + meshes.delete(roomId); + if (old) + scheduleHandoffClose(old, added); + } + } + }, +`; + const setRoomsPattern = + / {4}setRooms: async \(rooms\) => \{[\s\S]*?\r?\n {4}\},\r?\n(?= {4}resolvePeerStream)/; + source = source.replace(setRoomsPattern, jsSetRooms); + if (source !== before) await writeFile(path, source); +} +export async function patchRooms(runtimeRoot) { + await patchSeamlessRoomHandoff( + join(runtimeRoot, "dist", "webrtc", "index.js"), + ); + await patchHandoffPeerLeave(join(runtimeRoot, "dist", "webrtc", "index.js")); + await patchCredentialHandoff(join(runtimeRoot, "dist", "webrtc", "index.js")); +} diff --git a/scripts/peer-viewer/roster.mjs b/scripts/peer-viewer/roster.mjs new file mode 100644 index 0000000..d5e7c0a --- /dev/null +++ b/scripts/peer-viewer/roster.mjs @@ -0,0 +1,52 @@ +import { readFile, writeFile } from "node:fs/promises"; +// Keep the authoritative peer roster available when signaling overtakes +// peer-joined, in the consumed receive-only entry. +export async function patchKnownPeerRoster(path) { + let source = await readFile(path, "utf8"); + const before = source; + if (!source.includes("knownPeers =")) { + source = source.replace( + /(remotes = new Map\(\);)/, + "$1\n knownPeers = new Map();", + ); + } + source = source.replace( + /for \(const peer of msg\.peers\) this\.ensureRemote\(peer\);/g, + "for (const peer of msg.peers) {\n this.knownPeers.set(peer.id, peer);\n this.ensureRemote(peer);\n }", + ); + source = source.replace( + /(case "peer-joined": \{\n)(\s*)this\.emit\("peer-joined", msg\.peer\);/g, + '$1$2this.knownPeers.set(msg.peer.id, msg.peer);\n$2this.emit("peer-joined", msg.peer);', + ); + source = source.replace( + /(case "peer-left": \{\n)(\s*)const remote = this\.remotes\.get\(msg\.peerId\);/g, + "$1$2this.knownPeers.delete(msg.peerId);\n$2const remote = this.remotes.get(msg.peerId);", + ); + source = source.replace( + /this\.ensureRemote\(\{ id: from, name: from\.slice\(0, 8\), role: "publisher" \}\)/g, + 'this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: "publisher" })', + ); + source = source.replace( + /(this\.remotes\.clear\(\);)(?!\r?\n\s*this\.knownPeers\.clear)/, + "$1\n this.knownPeers.clear();", + ); + source = source + .replace( + /((?:\s*)this\.knownPeers\.set\(msg\.peer\.id, msg\.peer\);\r?\n)(?:\1)+/g, + "$1", + ) + .replace( + /((?:\s*)this\.knownPeers\.delete\(msg\.peerId\);\r?\n)(?:\1)+/g, + "$1", + ); + if ( + !source.includes("knownPeers") || + !source.includes("this.knownPeers.get(") || + !source.includes("this.knownPeers.set(") + ) { + throw new Error( + `unsupported @lumencast/runtime known-peer contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} diff --git a/scripts/peer-viewer/viewer.mjs b/scripts/peer-viewer/viewer.mjs new file mode 100644 index 0000000..f97a287 --- /dev/null +++ b/scripts/peer-viewer/viewer.mjs @@ -0,0 +1,413 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { patchKnownPeerRoster } from "./roster.mjs"; +const identityDistNeedle = ` const existing = this.remotes.get(peer.id); + if (existing) + return existing;`; +const identityDistReplacement = ` const existing = this.remotes.get(peer.id); + if (existing) { + const previousName = existing.info.name; + existing.info.name = peer.name; + existing.info.role = peer.role; + if (previousName !== peer.name && existing.stream.getTracks().length > 0) { + this.emit("peer-left", { peerId: peer.id, peerName: previousName }); + this.emit("remote-track", { + peerId: peer.id, + peerName: peer.name, + stream: existing.stream, + }); + } + return existing; + }`; +async function patchRuntimeFile(path, needle, replacement) { + const source = await readFile(path, "utf8"); + if (source.includes(needle)) { + await writeFile(path, source.replace(needle, replacement)); + return "patched"; + } + if (source.includes(replacement)) return "already-patched"; + if ( + replacement.includes("const previousName = existing.info.name;") && + source.includes("const previousName = existing.info.name;") && + source.includes("existing.info.name = peer.name;") && + source.includes("existing.info.role = peer.role;") + ) { + return "already-patched"; + } + throw new Error(`unsupported @lumencast/runtime mount contract: ${path}`); +} +async function upgradeIdentityMutation(path) { + const source = await readFile(path, "utf8"); + const upgraded = source + .replace( + /^([ \t]*)existing\.info = peer;$/m, + "$1existing.info.name = peer.name;\n$1existing.info.role = peer.role;", + ) + .replace( + /^([ \t]*)existing\.info\.name = peer\.name;\r?\n[ \t]*existing\.info\.role = peer\.role;$/m, + "$1existing.info.name = peer.name;\n$1existing.info.role = peer.role;", + ); + if (upgraded !== source) await writeFile(path, upgraded); +} +// A superseded RTCPeerConnection can report `failed` / `closed` after a retry +// has already installed a fresh RemoteState for the same peer. Its late event +// must not delete the fresh state nor withdraw that peer's live stream from the +// registry. Only the RemoteState that is still current owns removal. +async function patchRemoteGenerationGuard(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + source = source.replace( + /if \(pc\.connectionState === "failed" \|\| pc\.connectionState === "closed"\) \{\r?\n(\s*)this\.remotes\.delete\(peer\.id\);\r?\n\s*this\.emit\("peer-left", \{ peerId: peer\.id, peerName: peer\.name \}\);\r?\n\s*\}/g, + 'if (\n$1 (pc.connectionState === "failed" || pc.connectionState === "closed") &&\n$1 this.remotes.get(peer.id)?.pc === pc\n$1) {\n$1 this.remotes.delete(peer.id);\n$1 this.emit("peer-left", { peerId: peer.id, peerName: peer.name });\n$1}', + ); + } + if (!source.includes("this.remotes.get(peer.id)?.pc === pc")) { + throw new Error( + `unsupported @lumencast/runtime remote generation contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// Chromium can expose a connected receiver with decoded frames after the +// initial `track` callback raced an identity reconciliation or registry +// subscription. Re-publishing the already-owned aggregate stream when the +// current peer connection reaches `connected` is idempotent (`registry.set` +// ignores the same stream) and restores the authored slot without creating a +// second connection, renderer, or cache. +async function patchConnectedStreamRepublish(path) { + let source = await readFile(path, "utf8"); + const before = source; + const connectedBlock = ` const current = this.remotes.get(peer.id);\n if (\n pc.connectionState === "connected" &&\n current?.pc === pc &&\n current.stream.getTracks().length > 0\n ) {\n this.emit("remote-track", {\n peerId: peer.id,\n peerName: current.info.name,\n stream: current.stream,\n });\n }`; + while (source.includes(`${connectedBlock}\n${connectedBlock}`)) { + source = source.replace( + `${connectedBlock}\n${connectedBlock}`, + connectedBlock, + ); + } + { + if (!source.includes('pc.connectionState === "connected"')) { + source = source.replace( + ` this.emit("connection-state", { peerId: peer.id, state: pc.connectionState });`, + ` this.emit("connection-state", { peerId: peer.id, state: pc.connectionState });\n${connectedBlock}`, + ); + } + } + const marker = 'pc.connectionState === "connected"'; + if (!source.includes(marker)) { + throw new Error( + `unsupported @lumencast/runtime connected stream contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// The signaling server may announce a stale `peer-left` while the current +// RTCPeerConnection is still connected and decoding frames (for example while +// the publisher is replaced under the same authored label). Do not tear down a +// live aggregate on that transient control message; the connection-state +// terminal event remains the authoritative cleanup path. A replacement peer +// can take the label through the registry generation guard below. +async function patchStalePeerLeave(path) { + let source = await readFile(path, "utf8"); + const before = source; + if (path.endsWith("meet-viewer.js")) { + source = source.replace( + ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // The pc owns the tracks — closing it ends them. The registry/consumer\n // are notified via the peer-left event (label-keyed).\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n break;\n }`, + ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // Keep a still-connected aggregate alive. The terminal connection\n // event performs authoritative cleanup after a real disconnect.\n if (remote.pc.connectionState !== "connected" || remote.stream.getTracks().length === 0) {\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n }\n break;\n }`, + ); + } else { + const sourceNeedle = ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // The pc owns the tracks — closing it ends them. The registry/consumer\n // are notified via the peer-left event (label-keyed).\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n break;\n }`; + const sourceReplacement = ` case "peer-left": {\n this.knownPeers.delete(msg.peerId);\n const remote = this.remotes.get(msg.peerId);\n if (remote) {\n // Keep a still-connected aggregate alive. The terminal connection\n // event performs authoritative cleanup after a real disconnect.\n if (remote.pc.connectionState !== "connected" || remote.stream.getTracks().length === 0) {\n remote.pc.close();\n this.remotes.delete(msg.peerId);\n this.emit("peer-left", { peerId: msg.peerId, peerName: remote.info.name });\n }\n }\n break;\n }`; + source = source.replace(sourceNeedle, sourceReplacement); + } + if ( + !source.includes("connectionState") || + !source.includes("stream.getTracks().length") + ) { + throw new Error( + `unsupported @lumencast/runtime stale peer-leave contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// Recover a viewer leg after a relay/ICE failure. Late trickle candidates are +// ignored until a fresh offer creates the next generation; otherwise an orphan +// ICE packet leaves a `new` RTCPeerConnection that can never carry a camera. +async function patchViewerRecovery(path) { + let source = await readFile(path, "utf8"); + const before = source; + const recovered = + source.includes("this.clearRemoteRetry(peer.id);") && + source.includes("this.scheduleRemoteRetry(current?.info ?? peer);"); + if (recovered && source.includes("retryTimers = ")) return; + { + const field = " knownPeers = new Map();"; + source = source.replace( + field, + `${field}\n retryTimers = new Map();\n retryAttempts = new Map();`, + ); + const tearDown = + " tearDown() {\n for (const r of this.remotes.values())\n r.pc.close();"; + source = source.replace( + tearDown, + " tearDown() {\n for (const timer of this.retryTimers.values()) clearTimeout(timer);\n this.retryTimers.clear();\n this.retryAttempts.clear();\n for (const r of this.remotes.values())\n r.pc.close();", + ); + const signalNeedle = + ' if (!remote) {\n remote = this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: "publisher" });'; + source = source.replace( + signalNeedle, + ' if (!remote) {\n if (payload.kind === "ice") return;\n remote = this.ensureRemote(this.knownPeers.get(from) ?? { id: from, name: from.slice(0, 8), role: "publisher" });', + ); + const helper = + ' scheduleRemoteRetry(peer) {\n if (!this.knownPeers.has(peer.id) || this.retryTimers.has(peer.id)) return;\n const attempt = (this.retryAttempts.get(peer.id) ?? 0) + 1;\n if (attempt > 3) return;\n const delay = Math.min(1000, 250 * 2 ** (attempt - 1));\n const timer = setTimeout(() => {\n this.retryTimers.delete(peer.id);\n if (!this.knownPeers.has(peer.id) || this.remotes.has(peer.id) || this.ws?.readyState !== this.deps.WebSocket.OPEN) return;\n this.retryAttempts.set(peer.id, attempt);\n const remote = this.ensureRemote(this.knownPeers.get(peer.id) ?? peer);\n void this.dialRemote(peer.id, remote);\n }, delay);\n this.retryTimers.set(peer.id, timer);\n }\n clearRemoteRetry(peerId) {\n const timer = this.retryTimers.get(peerId);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.retryTimers.delete(peerId);\n }\n this.retryAttempts.delete(peerId);\n }\n async dialRemote(peerId, expected) {\n const remote = this.remotes.get(peerId);\n if (!remote || expected !== undefined && remote !== expected || remote.makingOffer || remote.pc.signalingState !== "stable") return;\n try {\n remote.makingOffer = true;\n await remote.pc.setLocalDescription();\n if (remote.pc.localDescription) this.sendSignal(peerId, { kind: "sdp", description: { type: remote.pc.localDescription.type, sdp: remote.pc.localDescription.sdp } });\n } catch {\n } finally {\n remote.makingOffer = false;\n }\n }\n'; + const helperNeedle = + " /* ---- Helpers ------------------------------------------------------ */"; + source = source.replace(helperNeedle, `${helper}${helperNeedle}`); + } + { + if (!source.includes("this.clearRemoteRetry(peer.id);")) { + const connected = + /(current\.stream\.getTracks\(\)\.length > 0\s*\) \{\s*)(this\.emit\("remote-track")/; + if (!connected.test(source)) { + throw new Error( + `unsupported @lumencast/runtime connected recovery contract: ${path}`, + ); + } + source = source.replace( + connected, + "$1this.clearRemoteRetry(peer.id);\n $2", + ); + } + if (!source.includes("this.scheduleRemoteRetry(current?.info ?? peer);")) { + const terminal = + /(^[ \t]*)this\.remotes\.delete\(peer\.id\);\r?\n[ \t]*this\.emit\("peer-left", \{ peerId: peer\.id, peerName: peer\.name \}\);/m; + if (!terminal.test(source)) { + throw new Error( + `unsupported @lumencast/runtime terminal recovery contract: ${path}`, + ); + } + source = source.replace(terminal, (_match, indent) => + [ + `${indent}const current = this.remotes.get(peer.id);`, + `${indent}this.remotes.delete(peer.id);`, + `${indent}pc.close();`, + `${indent}this.emit("peer-left", { peerId: peer.id, peerName: current?.info.name ?? peer.name });`, + `${indent}this.scheduleRemoteRetry(current?.info ?? peer);`, + ].join("\n"), + ); + } + } + if ( + !source.includes("scheduleRemoteRetry") || + !source.includes("this.clearRemoteRetry(peer.id);") || + !source.includes("this.scheduleRemoteRetry(current?.info ?? peer);") + ) { + throw new Error( + `unsupported @lumencast/runtime viewer recovery contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// A recovered Solar receiver cannot rely on the runtime's automatic +// `negotiationneeded` offer: the Prism publisher owns the first offer and the +// preview injection deliberately suppresses that automatic viewer offer to +// avoid glare. Once a receiver has actually failed, however, the retry path +// is the only party that can restart the leg if the publisher did not observe +// its terminal state. Create an explicit offer for that retry. The initial +// negotiation handler still calls the no-argument form and remains suppressed +// by the preview injection, so this does not change the steady-state wire. +async function patchViewerRetryOffer(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + source = source.replace( + /await remote\.pc\.setLocalDescription\(\);/g, + "const offer = await remote.pc.createOffer();\n await remote.pc.setLocalDescription(offer);", + ); + } + if ( + !source.includes("pc.createOffer()") || + !source.includes("setLocalDescription(offer)") + ) { + throw new Error( + `unsupported @lumencast/runtime viewer retry-offer contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// A retry offer can cross a publisher-owned retry offer in flight. MeetViewer +// is receive-only in production, so the publisher's offer is authoritative: +// make the viewer polite and explicitly roll back its own retry offer before +// accepting that fresh publisher offer. This keeps a failed slot from being +// stranded in `have-local-offer` while preserving the initial one-way dial. +async function patchViewerRetryCollision(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + source = source.replace( + ` if (remote.ignoreOffer) return;\n\n await pc.setRemoteDescription(desc);`, + ` if (remote.ignoreOffer) return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n\n await pc.setRemoteDescription(desc);`, + ); + source = source.replace( + ` private isPolite(otherId: string): boolean {\n if (!this.selfId) return false;\n return this.selfId > otherId;\n }`, + ` private isPolite(_otherId: string): boolean {\n // MeetViewer is receive-only; a publisher offer always wins a retry glare.\n return true;\n }`, + ); + source = source.replace( + ` if (remote.ignoreOffer) return;\n\n await pc.setRemoteDescription(desc);`, + ` if (remote.ignoreOffer) return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n\n await pc.setRemoteDescription(desc);`, + ); + source = source.replace( + ` if (remote.ignoreOffer)\n return;\n await pc.setRemoteDescription(desc);`, + ` if (remote.ignoreOffer)\n return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n await pc.setRemoteDescription(desc);`, + ); + source = source.replace( + ` if (remote.ignoreOffer)\n return;\n await pc.setRemoteDescription(desc);`, + ` if (remote.ignoreOffer)\n return;\n if (offerCollision && desc.type === "offer") {\n try {\n await pc.setLocalDescription({ type: "rollback" });\n } catch {\n /* a stable receiver has nothing to roll back */\n }\n }\n await pc.setRemoteDescription(desc);`, + ); + source = source.replace( + ` isPolite(otherId) {\n if (!this.selfId)\n return false;\n return this.selfId > otherId;\n }`, + ` isPolite(_otherId) {\n return true;\n }`, + ); + source = source.replace( + ` isPolite(otherId) {\n if (!this.selfId)\n return false;\n return this.selfId > otherId;\n }`, + ` isPolite(_otherId) {\n return true;\n }`, + ); + } + if ( + !source.includes('setLocalDescription({ type: "rollback" })') && + !source.includes('setLocalDescription({type:"rollback"})') + ) { + throw new Error( + `unsupported @lumencast/runtime viewer retry-collision contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// WebSocket frames arrive in order, but MeetViewer's SDP/ICE handlers are +// asynchronous. Without a small protocol queue, the offer for one camera can +// enter `handleSignal` while the previous peer's `peer-joined` is still +// allocating its transceivers; the resulting generation stays `new` and the +// corresponding authored slot never receives a track. Serialize only the +// signaling state machine (not media delivery) so three camera slots can join +// deterministically without changing the wire protocol. +async function patchViewerMessageSerialization(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + const hasMessageQueueField = + /^\s+messageQueue = Promise\.resolve\(\);/m.test(source); + if (!hasMessageQueueField) { + const field = " retryAttempts = new Map();"; + const replacement = `${field}\n messageQueue = Promise.resolve();`; + source = source.replace(field, replacement); + } + source = source.replace( + /ws\.addEventListener\("message", \(ev\) => void this\.onMessage\(\(ev as MessageEvent\)\.data\)\);/g, + `ws.addEventListener("message", (ev) => {\n this.messageQueue = this.messageQueue\n .then(() => this.onMessage((ev as MessageEvent).data))\n .catch(() => undefined);\n });`, + ); + source = source.replace( + /ws\.addEventListener\("message", \(ev\) => void this\.onMessage\(ev\.data\)\);/g, + `ws.addEventListener("message", (ev) => {\n this.messageQueue = this.messageQueue\n .then(() => this.onMessage(ev.data))\n .catch(() => undefined);\n });`, + ); + } + if (!source.includes("messageQueue")) { + throw new Error( + `unsupported @lumencast/runtime viewer message contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +// A publisher retry can deliver a fresh SDP offer while Solar still holds the +// previous failed/closed receiver generation. Passing that offer to the +// terminal RTCPeerConnection is rejected by Chromium, leaving the authored +// camera slot empty. Drop only that stale generation before accepting a new +// offer; the receive-only viewer remains the answerer and the wire contract is +// unchanged. +async function patchViewerStaleOfferGeneration(path) { + let source = await readFile(path, "utf8"); + const before = source; + { + const signalPattern = + /(\n[ \t]*)let remote = this\.remotes\.get\(from\);\r?\n([ \t]*)if \(!remote\) \{/; + if (!source.includes("remote = undefined;") && signalPattern.test(source)) { + source = source.replace( + signalPattern, + (_match, lineIndent, blockIndent) => + `${lineIndent}let remote = this.remotes.get(from);\n` + + `${blockIndent}if (remote && (remote.pc.connectionState === "failed" || remote.pc.connectionState === "closed")) {\n` + + `${blockIndent} remote.pc.close();\n` + + `${blockIndent} this.remotes.delete(from);\n` + + `${blockIndent} remote = undefined;\n` + + `${blockIndent}}\n` + + `${blockIndent}if (!remote) {`, + ); + } + } + if ( + !source.includes('connectionState === "failed"') || + !source.includes("remote = undefined;") || + false + ) { + throw new Error( + `unsupported @lumencast/runtime stale viewer offer contract: ${path}`, + ); + } + if (source !== before) await writeFile(path, source); +} +async function patchViewerRecoveryTerminal(path) { + let source = await readFile(path, "utf8"); + const before = source; + if (source.includes("this.scheduleRemoteRetry(current?.info ?? peer)")) + return; + const connectedNeedle = + ' this.emit("remote-track", {\\n peerId: peer.id,\\n peerName: current.info.name,\\n stream: current.stream,\\n });'; + const connectedReplacement = + ' this.clearRemoteRetry(peer.id);\\n this.emit("remote-track", {\\n peerId: peer.id,\\n peerName: current.info.name,\\n stream: current.stream,\\n });'; + source = source.replace(connectedNeedle, connectedReplacement); + const terminalNeedle = + ' this.remotes.delete(peer.id);\\n this.emit("peer-left", { peerId: peer.id, peerName: peer.name });'; + const terminalReplacement = + ' const current = this.remotes.get(peer.id);\\n this.remotes.delete(peer.id);\\n pc.close();\\n this.emit("peer-left", { peerId: peer.id, peerName: current?.info.name ?? peer.name });\\n this.scheduleRemoteRetry(current?.info ?? peer);'; + source = source.replace(terminalNeedle, terminalReplacement); + if (source !== before) await writeFile(path, source); +} +export async function patchViewer(runtimeRoot) { + await upgradeIdentityMutation( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchRuntimeFile( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + identityDistNeedle, + identityDistReplacement, + ); + await patchKnownPeerRoster( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchRemoteGenerationGuard( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchConnectedStreamRepublish( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchStalePeerLeave( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchViewerRecovery( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchViewerRetryOffer( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchViewerRetryCollision( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchViewerMessageSerialization( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchViewerStaleOfferGeneration( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); + await patchViewerRecoveryTerminal( + join(runtimeRoot, "dist", "webrtc", "meet-viewer.js"), + ); +} diff --git a/scripts/proof/README.md b/scripts/proof/README.md new file mode 100644 index 0000000..cb6f2c7 --- /dev/null +++ b/scripts/proof/README.md @@ -0,0 +1,20 @@ +# CEF proof helpers + +Owner: Solar qualification. The parent drivers in `scripts/` own process lifetimes and output +prefixes; these files contain reusable capture/producer/analysis boundaries. + +| File | Use | +|---|---| +| `pulsar_cef.py` | Imported process/IPC wrapper; reaps only its own Pulsar child | +| `cef-capabilities.py` | `python scripts/proof/cef-capabilities.py --pulsar --output `; adapter diagnostic, not scene proof | +| `cold-start-host.mjs` | Cold-start driver's HTTP/native fixture or admitted-capsule host | +| `meet-peer.mjs` | Peer-camera driver's receive-only viewer and publisher signaling helper | +| `native-burst-producer.mjs` | Native producer with distinct transactions, expected hashes and paced/burst modes | +| `analyse-render-cadence.py` | `--prefix `; fixed receipt/LSML/completed-frame windows | +| `analyse-native-counter.py` | `--prefix `; recorded counter and complete static surfaces | +| `verify-counter-clip.py` | `--prefix `; independent 60-frame OCR/duration/cadence check | + +See [render cadence](../../docs/development/render-cadence.md) and +[qualification](../../docs/runbooks/qualification.md). All script paths are current; +source/media/evidence inputs identify a particular run. Hashes/identities, physical pixels, +native ACKs and completed presentation are measured separately. diff --git a/scripts/proof/analyse-native-counter.py b/scripts/proof/analyse-native-counter.py new file mode 100644 index 0000000..60295aa --- /dev/null +++ b/scripts/proof/analyse-native-counter.py @@ -0,0 +1,144 @@ +"""Count actual CEF counter pixels; align PTS using the recorded live HUD clock. + +No counter or frame is generated. The 1-second export selects 60 consecutive +source frames; the complete recording and OCR rows remain available as evidence. +""" +import argparse +import hashlib +import json +import re +import subprocess +from pathlib import Path + +import cv2 +import numpy as np +import pytesseract + +parser = argparse.ArgumentParser() +parser.add_argument("--prefix", required=True) +parser.add_argument("--final-deadline-ms", type=float, default=1000, + help="Explicit final-state deadline; the default preserves the 1-second burst criterion.") +parser.add_argument("--video-window-offset-ms", type=float, default=0, + help="Predetermined producer-relative 1-second export window; all original frames and first 100ms remain measured.") +parser.add_argument("--minimum-distinct-values", type=int, default=0, + help="Required actual encoded counter states in the 60-frame export; zero keeps the earlier scene-completeness criterion.") +args = parser.parse_args() +prefix = Path(args.prefix) +report_path = Path(str(prefix) + "-report.json") +report = json.loads(report_path.read_text(encoding="utf-8-sig")) +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" +video = report["video"]["path"] +cap = cv2.VideoCapture(video) +fps = cap.get(cv2.CAP_PROP_FPS) +assert abs(fps - 60) < .001, fps +rows, samples = [], [] +last_mask, counter = None, None +calls = 0 +reference = cv2.imread(str(prefix) + "-counter-zero.png") +assert reference is not None +panel_reference = reference[25:660, 475:1595] +panel_color = reference[620, 500].astype(np.int16) +assert np.min(panel_color) > 20 and np.max(panel_color) < 64, "Reference gray panel is missing" +panel_mask = np.max(np.abs(panel_reference.astype(np.int16) - panel_color), axis=2) <= 2 +# Only flat surface interiors: screenshot/JPEG and video/YUV resample asset edges. +# Erosion removes those boundaries while still checking more than half a million +# unchanged background pixels; a missing panel remains a large contiguous failure. +panel_mask = cv2.erode(panel_mask.astype(np.uint8), np.ones((9, 9), np.uint8)).astype(bool) +assert np.count_nonzero(panel_mask) > 100000, "Reference panel coverage is insufficient" +static_rows = [] +while True: + ok, frame = cap.read() + if not ok: + break + index = len(rows) + crop = frame[690:775, 470:1300] + mask = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY) > 190 + if last_mask is None or np.count_nonzero(mask != last_mask) > 20: + text = pytesseract.image_to_string(crop, config="--psm 7 -c tessedit_char_whitelist=0123456789/").strip() + found = re.fullmatch(r"(\d{1,4})/0512", text) + counter = int(found[1]) if found and int(found[1]) <= 512 else None + last_mask = mask + calls += 1 + rows.append({"frame": index, "ptsSeconds": index / fps, "counter": counter}) + panel = frame[25:660, 475:1595].astype(np.int16) + missing = np.max(np.abs(panel - panel_color), axis=2) > 12 + static_rows.append({"frame": index, "stableGrayFraction": float(1 - np.count_nonzero(missing & panel_mask) / np.count_nonzero(panel_mask))}) + # The live CEF HUD and scene counter share the same encoded composition. + # Fit after the burst, when RAF is regular; zero is clamped before the start. + if index % 15 == 0 and counter == 512: + hud = frame[50:300, 1625:1898] + text = pytesseract.image_to_string(hud, config="--psm 6") + found = re.search(r"T\s*\+\s*(\d+)\s*ms", text) + if found: + elapsed = int(found[1]) + if elapsed > 1200: + relative = elapsed + report["HUDStartEpochMs"] - report["producer"]["startedEpochMs"] + samples.append({"frame": index, "elapsedMs": elapsed, "burstPTSSeconds": index / fps - relative / 1000}) + if index % 120 == 0: + print("DECODED", index, "counter OCR", calls, flush=True) +cap.release() +assert len(samples) >= 5, "Recorded HUD clock could not establish an alignment" +offsets = np.array([value["burstPTSSeconds"] for value in samples]) +start = float(np.median(offsets)) +spread = float(np.max(np.abs(offsets - start)) * 1000) +uncertainty = 1000 / fps + spread + .5 +for row in rows: + row["relativeToBurstMs"] = (row["ptsSeconds"] - start) * 1000 +window = [row for row in rows if 0 <= row["relativeToBurstMs"] < 100] +visible = sorted({row["counter"] for row in rows if row["counter"]}) +timeline = [] +for row in rows: + if not timeline or timeline[-1]["counter"] != row["counter"]: + timeline.append(row) +assert all(a <= b for a, b in zip(visible, visible[1:])) +readable = [row for row in rows if row["counter"] is not None] +assert all(a["counter"] <= b["counter"] for a, b in zip(readable, readable[1:])), "Counter went backwards in encoded video" +first_frame = max(0, int(np.floor((start + args.video_window_offset_ms / 1000) * fps))) +selected = rows[first_frame:first_frame + 60] +assert len(selected) == 60 +target = Path(video).with_name(Path(video).stem + "-1s-60fps.mp4") +subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-y", "-i", video, + "-vf", f"select=between(n\\,{first_frame}\\,{first_frame+59}),setpts=N/(60*TB)", + "-frames:v", "60", "-an", "-c:v", "libx264", "-preset", "fast", "-crf", "18", + "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(target)], check=True) +probe = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-count_frames", "-select_streams", "v:0", "-show_entries", "stream=nb_read_frames,r_frame_rate,duration", "-of", "json", str(target)])) +assert probe["streams"][0]["nb_read_frames"] == "60" +assert abs(float(probe["streams"][0]["duration"]) - 1) < .001 +result = {"video": video, "fps": fps, "decodedFrames": len(rows), "counterOCRCalls": calls, + "staticScene": {"method": "flat reference gray interiors, 4-pixel edge margin and 12-level codec tolerance", "referenceBGR": panel_color.tolist(), "referencePixels": int(np.count_nonzero(panel_mask)), + "minimumStableGrayFraction": min(row["stableGrayFraction"] for row in static_rows), + "incompleteFrames": [row for row in static_rows if row["stableGrayFraction"] < .995]}, + "clockAlignment": {"method": "live HUD timestamp in the same recorded CEF frame", "samples": samples, + "burstPTSSeconds": start, "maxFitSpreadMs": spread, "uncertaintyMs": uncertainty}, + "window100ms": {"encodedFrames": len(window), "values": [row["counter"] for row in window], + "distinctMutationStates": len({row["counter"] for row in window if row["counter"]}), + "unreadableFrames": sum(row["counter"] is None for row in window)}, + "firstNonzeroVisibleMs": next((row["relativeToBurstMs"] for row in rows if row["counter"]), None), + "final512VisibleMs": next((row["relativeToBurstMs"] for row in rows if row["counter"] == 512), None), + "distinctCountersWholeRecording": len(visible), "timeline": timeline, + "oneSecond": {"path": str(target), "sha256": hashlib.sha256(target.read_bytes()).hexdigest(), + "producerRelativeWindowOffsetMs": args.video_window_offset_ms, + "probe": probe, "sourceFirstFrame": first_frame, "sourceLastFrame": first_frame + 59, + "counterValues": [row["counter"] for row in selected], + "distinctMutationStates": len({row["counter"] for row in selected if row["counter"]}), + "unreadableFrames": sum(row["counter"] is None for row in selected)}, "frames": rows} +Path(str(prefix) + "-video-counter.json").write_text(json.dumps(result, indent=2), encoding="utf-8") +report["videoCounterAnalysis"] = {key: value for key, value in result.items() if key != "frames"} +report["visualCriterion"] = {"finalDeadlineMs": args.final_deadline_ms, + "minimumDistinctValues": args.minimum_distinct_values, + "oneSecond": "60 consecutive source frames, no interpolation", + "staticSceneMinimum": .995} +final_in_time = result["final512VisibleMs"] is not None and result["final512VisibleMs"] <= args.final_deadline_ms +enough_distinct_values = result["oneSecond"]["distinctMutationStates"] >= args.minimum_distinct_values +report["visualResult"] = "PASS" if not result["staticScene"]["incompleteFrames"] and result["oneSecond"]["unreadableFrames"] == 0 and final_in_time and enough_distinct_values else "FAIL" +report["result"] = "PASS" if report["runtimeResult"] == "PASS" and report["visualResult"] == "PASS" else "FAIL" +report_path.write_text(json.dumps(report, indent=2), encoding="utf-8") +cap = cv2.VideoCapture(video) +for label, index in [("first-counter", next(row["frame"] for row in rows if row["counter"])), + ("100ms", max(row["frame"] for row in window)), + ("512", next(row["frame"] for row in rows if row["counter"] == 512))]: + cap.set(cv2.CAP_PROP_POS_FRAMES, index) + ok, frame = cap.read() + if ok: cv2.imwrite(str(prefix) + "-encoded-" + label + ".png", frame) +cap.release() +print("PIXEL_PROOF", json.dumps({"visualResult": report["visualResult"], "fps": fps, "oneSecond": result["oneSecond"], "firstVisibleMs": result["firstNonzeroVisibleMs"], "final512VisibleMs": result["final512VisibleMs"], "staticMinimum": result["staticScene"]["minimumStableGrayFraction"], "incompleteFrames": len(result["staticScene"]["incompleteFrames"])}), flush=True) diff --git a/scripts/proof/analyse-render-cadence.py b/scripts/proof/analyse-render-cadence.py new file mode 100644 index 0000000..0d42657 --- /dev/null +++ b/scripts/proof/analyse-render-cadence.py @@ -0,0 +1,68 @@ +"""Separate accepted transactions, LSML reception, completed frames and video pixels. + +Windows start at the producer's recorded epoch, never at a selected best frame. +Camera-only renders do not count. A covering receipt follows the actual Vision +submission and front-canvas copy; it does not measure physical screen scan-out. +""" +import argparse +import json +from pathlib import Path +from statistics import median + +parser = argparse.ArgumentParser() +parser.add_argument("--prefix", required=True) +parser.add_argument("--minimum-per-second", type=int, default=120) +args = parser.parse_args() +prefix = Path(args.prefix) +report = json.loads(Path(str(prefix) + "-report.json").read_text(encoding="utf-8-sig")) +browser = json.loads(Path(report["telemetryPath"]).read_text(encoding="utf-8-sig"))["browser"] +producer = report["producer"] +start = producer["startedEpochMs"] +receipts = producer["receipts"] +by_id = {row["id"]: row for row in receipts} +by_hash = {row["stateHash"]: row for row in receipts} +received = [row for row in browser["events"] if row["type"] == "received" and row.get("transactionId") in by_id] +covered = [row for row in browser["events"] if row["type"] == "applied" and row.get("transactionId") in by_id and row.get("presentation")] +completed = {} +for row in covered: + presentation = row["presentation"] + state = by_hash[presentation["stateHash"]] + key = presentation["throughSequence"] + completed.setdefault(key, {"counter": state["counter"], "stateHash": state["stateHash"], + "throughSequence": key, "epochMs": row["epochMs"], + "sentToCompleteMs": row["epochMs"] - state["sentEpochMs"], + "ackToCompleteMs": row["epochMs"] - state["epochMs"]}) +frames = sorted(completed.values(), key=lambda row: row["epochMs"]) +patches = [row for row in browser["vision"] if row["type"] == "frame-submitted" and row.get("patch") and row["epochMs"] >= start] +duration = producer.get("pacedDurationMs") or producer["nativeDrainMs"] + +def window(offset, width): + lower, upper = start + offset, start + offset + width + inside = lambda row: lower <= row["epochMs"] < upper + return {"offsetMs": offset, "durationMs": width, + "nativeAppliedACKs": sum(inside(row) for row in receipts), + "sequentialLSMLReceived": sum(inside(row) for row in received), + "VisionPatchSubmissions": sum(inside(row) for row in patches), + "completedDistinctMutationFrames": sum(inside(row) for row in frames), + "completedCounters": [row["counter"] for row in frames if inside(row)]} + +seconds = [window(index * 1000, 1000) for index in range(int(duration // 1000))] +hundreds = [window(index * 100, 100) for index in range(int(duration // 100))] +timings = [row["timing"] for row in patches] +capacity = bool(seconds) and all(row["completedDistinctMutationFrames"] >= args.minimum_per_second for row in seconds) +result = {"schema": "solar.render-cadence-proof.v1", "runtimeResult": report["runtimeResult"], + "criterion": f"at least {args.minimum_per_second} completed distinct mutation frames in every fixed complete 1000 ms window", + "capacityResult": "PASS" if report["runtimeResult"] == "PASS" and capacity else "FAIL", + "boundary": "actual Vision GPU submission followed by complete front-canvas copy and covering receipt; physical scan-out not measured", + "nativeCore": report["nativeCore"], + "producer": {key: producer[key] for key in ("mode", "transactionCount", "startedEpochMs", "preparationMs", "enqueueMs", "nativeDrainMs", "pacedDurationMs", "pacing")}, + "totals": {"nativeAppliedACKs": len(receipts), "sequentialLSMLReceived": len(received), + "coveredTransactions": len(covered), "VisionPatchSubmissions": len(patches), + "completedDistinctMutationFrames": len(frames)}, + "timingsMedianMs": {"engineApply": median(row["engineApplyCallMs"] for row in timings), + "engineRender": median(row["gpuSubmitCallMs"] for row in timings), + "lastStateSentToCompletedFrame": median(row["sentToCompleteMs"] for row in frames)}, + "fixedSeconds": seconds, "fixed100ms": hundreds, "completedFrames": frames, + "videoBoundary": "encoded pixel states are measured separately; a 60 fps recording cannot prove 120 physical presentations/s"} +Path(str(prefix) + "-cadence.json").write_text(json.dumps(result, indent=2), encoding="utf-8") +print(json.dumps({key: result[key] for key in ("capacityResult", "totals", "fixedSeconds", "timingsMedianMs")}, indent=2)) diff --git a/scripts/proof/cef-capabilities.py b/scripts/proof/cef-capabilities.py new file mode 100644 index 0000000..8ac01b0 --- /dev/null +++ b/scripts/proof/cef-capabilities.py @@ -0,0 +1,76 @@ +"""Read actual owned CEF capabilities; this diagnostic is not a scene proof.""" +import argparse +import asyncio +import json +from pathlib import Path +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pulsar_cef import Pulsar + +parser = argparse.ArgumentParser() +parser.add_argument("--pulsar", required=True) +parser.add_argument("--output", required=True) +args = parser.parse_args() +prefix = Path(args.output).resolve() +prefix.parent.mkdir(parents=True, exist_ok=True) +received = threading.Event() +result = {} + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + page = b"""""" + self.send_response(200) + self.send_header("Content-Type", "text/html") + self.end_headers() + self.wfile.write(page) + + def do_POST(self): + result.update(json.loads(self.rfile.read(int(self.headers["Content-Length"])))) + received.set() + self.send_response(204) + self.end_headers() + + def log_message(self, *_): + pass + + +server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) +threading.Thread(target=server.serve_forever, daemon=True).start() +pulsar = Pulsar(args.pulsar, prefix) + + +async def run(): + pulsar.start() + try: + await pulsar.connect() + await pulsar.show(f"http://127.0.0.1:{server.server_port}/") + for _ in range(100): + if received.is_set(): + break + await asyncio.sleep(.1) + if not received.is_set(): + raise RuntimeError("CEF did not report its capabilities") + Path(str(prefix) + "-cef-capabilities.json").write_text( + json.dumps(result, indent=2), encoding="utf-8") + print(json.dumps(result)) + finally: + pulsar.stop() + server.shutdown() + + +asyncio.run(run()) diff --git a/scripts/proof/cold-start-host.mjs b/scripts/proof/cold-start-host.mjs new file mode 100644 index 0000000..9612a09 --- /dev/null +++ b/scripts/proof/cold-start-host.mjs @@ -0,0 +1,231 @@ +// Complete-scene cache contract harness. Test signatures/manifest, actual product binaries. +// Its explicit boundary must never be reported as authenticated Canvas delivery. +import { createHash, generateKeyPairSync, randomBytes, sign } from "node:crypto"; +import { createServer } from "node:http"; +import { spawn } from "node:child_process"; +import { createWriteStream } from "node:fs"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { extname, resolve, sep } from "node:path"; +import { parseArgs } from "node:util"; +import { canonicalize } from "@lumencast/canonical"; +import { unzipSync } from "fflate"; +import { createCachedSceneSourceProvider, FileSceneSourceStore } from "../../dist/server/index.mjs"; +import { startNative } from "../native-lsdp-tools.mjs"; +import { startMeetProof, rtcMetrics } from "./meet-peer.mjs"; + +const { values } = parseArgs({ options: { + scene: { type: "string" }, orion: { type: "string" }, storage: { type: "string" }, + "orion-storage": { type: "string" }, output: { type: "string" }, + port: { type: "string", default: "8122" }, camera: { type: "string" }, offline: { type: "boolean" }, + meet: { type: "string" }, + capsule: { type: "string" }, +} }); +for (const key of ["scene", "orion", "storage", "orion-storage", "output"]) if (!values[key]) throw new Error("Missing " + key); +const root = resolve(import.meta.dirname, "../.."); +const dist = resolve(root, "dist/host"); +const storage = resolve(values.storage), orionStorage = resolve(values["orion-storage"]); +const orionRoot = resolve(values.orion); +for (const [directory, owner] of [[storage, root], [orionStorage, orionRoot]]) { + if (!directory.startsWith(resolve(owner, "build") + sep)) throw new Error("Proof storage must stay in its owner's build directory"); + await mkdir(directory, { recursive: true }); +} +const port = Number(values.port), origin = `http://127.0.0.1:${port}`; +const archive = new Uint8Array(await readFile(values.scene)); +const files = unzipSync(archive), source = files["scene.lsml"]; +const original = JSON.parse(new TextDecoder().decode(source)); +const hash = (data) => "sha256:" + createHash("sha256").update(data).digest("hex"); +const canvasCapsule = values.capsule ? JSON.parse(await readFile(values.capsule, "utf8")) : null; +const program = canvasCapsule ? Buffer.from(canvasCapsule.envelope.blue_program, "base64") : await readFile(resolve(orionRoot, "internal/api/testdata/scene-lec-lck.program.json")); +const portable = JSON.parse(program), programDigest = portable.program_digest; +const identityPath = resolve(storage, "test-identity.json"); +let identity; +if (!values.offline && canvasCapsule) { + const envelope = canvasCapsule.envelope; + identity = { manifest: envelope.blue_manifest, artifactSet: envelope.claims.artifact_set_digest, + jws: envelope.jws, claims: envelope.claims, sourceDigest: envelope.lsml_bundle_digest, + archiveDigest: hash(archive), programDigest, + boundary: "actual local Canvas admission/signature; real gateway identity and Blue compile; no seeded verdict or host-generated signing key", + trust: canvasCapsule.trust.keys }; + if (identity.sourceDigest !== hash(source)) throw new Error("Canvas source bytes differ from received capsule"); + await writeFile(identityPath, JSON.stringify(identity)); + await writeFile(resolve(orionStorage, "test-trust.json"), JSON.stringify(identity.trust)); +} else if (!values.offline) { + const unsigned = { schema_version: "zabcanvas.scene-blue-manifest.v1", scene_id: original.scene_id, + scene_version: original.scene_version, scene_revision: 1, revision_id: "cold-proof-revision", + validation: { verdict: "cache-contract-fixture", program_digest: programDigest }, + declarations: { operator_inputs: original.operator_inputs ?? [], scene_blueprints: original.scene_blueprints ?? [] }, + binding_closure: [{ program_digest: programDigest, origin: "unchanged existing published program; local cache contract metadata" }], + readiness: { program_ready: true, binding_closure_complete: true, offline_ready: true, missing_binding_ids: [], missing_scene_blueprint_keys: [] }, + }; + const manifest = { ...unsigned, manifest_digest: hash(canonicalize(unsigned)) }; + // Deliberately distinct identities reproduce the real Canvas wire contract. + const artifactSet = hash(canonicalize({ source: hash(source), program: programDigest, revision: unsigned.revision_id })); + const { privateKey, publicKey } = generateKeyPairSync("ed25519"); + const key = publicKey.export({ type: "spki", format: "der" }).subarray(-32).toString("base64"); + const now = Math.floor(Date.now() / 1000), kid = "local-cold-proof"; + const claims = { schema_version: "zabcanvas.resolved-scene-ref.v1", ref_id: "cold-ref", attestation_id: "cold-att", + issuer: "https://zabcanvas.internal", audience: "orion", subject: "operator-1", owner_id: "owner-1", tenant_id: "tenant-1", + stream_id: "stream-1", allowed_actions: ["prepare-preview", "take-on-air"], scene_id: original.scene_id, + revision_id: unsigned.revision_id, scene_digest: programDigest, artifact_set_digest: artifactSet, blue_program_digest: programDigest, + readiness_attestation_id: "cold-ready", readiness_digest: hash("local fixture readiness"), + readiness_expires_at: now + 900, issued_at: now, not_before: now - 1, expires_at: now + 900, kid, + canvas_locator: `/api/v1/scenes/${original.scene_id}/resolved-scene-ref/cold-ref` }; + const head = Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "zabcanvas-resolved-scene-ref+jws", kid })).toString("base64url"); + const payload = Buffer.from(JSON.stringify(claims)).toString("base64url"); + const jws = head + "." + payload + "." + sign(null, Buffer.from(head + "." + payload), privateKey).toString("base64url"); + identity = { manifest, artifactSet, jws, sourceDigest: hash(source), archiveDigest: hash(archive), programDigest, + boundary: "cache transport fixture; no claim of Canvas validation/admission", trust: { [kid]: key } }; + await writeFile(identityPath, JSON.stringify(identity)); + await writeFile(resolve(orionStorage, "test-trust.json"), JSON.stringify(identity.trust)); +} else identity = JSON.parse(await readFile(identityPath, "utf8")); +if (identity.archiveDigest !== hash(archive) || identity.programDigest !== programDigest) throw new Error("Fixture identity changed"); +const stats = { offline: !!values.offline, upstreamSourceReads: 0, localSourceReads: 0, sourceHTTP: [], blockedEgress: [], events: [] }; +const provider = createCachedSceneSourceProvider({ get: async () => { + stats.upstreamSourceReads++; + if (stats.offline) throw new Error("OFFLINE_SOURCE_UPSTREAM"); + return { sceneId: original.scene_id, sceneVersion: original.scene_version, revision: 1, sourceDigest: hash(source), + format: "lsmlz", data: archive, assets: new Map(), blueManifest: identity.manifest }; +} }, new FileSceneSourceStore(resolve(storage, "sources"))); +if (!stats.offline) await provider.get(original.scene_id); +const secret = randomBytes(32).toString("hex"); +let native, child, meet, closing = false, publisherActive = true; +const json = (response, status, value) => { response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" }); response.end(JSON.stringify(value)); }; +async function body(request) { + let text = ""; for await (const chunk of request) { text += chunk; if (text.length > 1024 * 1024) throw new Error("Body limit"); } + return text ? JSON.parse(text) : {}; +} +async function operator(path, method = "GET", data) { + const response = await fetch(`http://127.0.0.1:${port + 1}` + path, { + method, headers: { "X-Orion-Local-Auth": secret, "Content-Type": "application/json" }, + body: data === undefined ? undefined : JSON.stringify(data), signal: AbortSignal.timeout(45000), + }); + return { status: response.status, body: await response.json() }; +} +const mime = { ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".wasm": "application/wasm", ".css": "text/css", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".ttf": "font/ttf" }; +const server = createServer(async (request, response) => { + try { + const url = new URL(request.url, origin); + if (url.pathname === "/health") return json(response, 200, { ...stats, native: native?.url, nativePid: native?.pid, + orionPid: child?.pid, address: native?.address, sceneId: original.scene_id, sceneVersion: original.scene_version, + artifactSet: identity.artifactSet, manifestDigest: identity.manifest.manifest_digest, boundary: identity.boundary }); + if (url.pathname === "/peer/health") return json(response, 200, { ...(await meet.health()), meetPid: meet.pid }); + if (url.pathname === "/peer/publisher-state") return json(response, 200, { active: publisherActive }); + if (url.pathname === "/publisher.html") { response.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" }); response.end(meet.page); return; } + if (url.pathname === "/peer/action" && request.method === "POST") { + const action = await body(request); + if (action.type === "pause" || action.type === "join") publisherActive = action.type === "join"; + else if (["assign", "clear"].includes(action.type)) { + const target = "solar/" + action.lane, document = (await native.reception.read(target)).state; + document.defaults["__cam.viewer"] = JSON.stringify(meet.viewer); + for (const slot of ["@0", "@1", "@2"]) delete document.defaults["__cam.slots." + slot]; + if (action.type === "assign") document.defaults["__cam.slots." + action.slot] = "proof_peer"; + await native.reception.replace(target, document); + } else throw new Error("Unknown peer action"); + return json(response, 200, { applied: true }); + } + if (url.pathname === "/state") return json(response, 200, await native.reception.read(url.searchParams.get("resource") ?? "orion/state")); + if (url.pathname === "/stop" && request.method === "POST") { json(response, 200, { stopping: true }); setTimeout(() => void stop(), 20); return; } + if (url.pathname === "/select" && request.method === "POST") { + const document = (await native.reception.read("orion/state")).state; + document.scene_control.defaults.desired = Object.fromEntries(["program", "preview"].map(lane => [lane, { scene_id: original.scene_id, scene_version: identity.artifactSet, stream_id: "stream-1" }])); + await native.reception.replace("orion/state", document); return json(response, 200, { selected: true }); + } + if (url.pathname === "/runtime-mutation" && request.method === "POST") { + const target = "solar/" + url.searchParams.get("lane"); + const document = (await native.reception.read(target)).state; + document.defaults.cold_proof_runtime_only = "must disappear after full restart"; + await native.reception.replace(target, document); return json(response, 200, { memoryOnly: true }); + } + if (url.pathname === "/contracts") { + const selector = url.searchParams.get("lane") === "preview" ? "&target=preview" : ""; + return json(response, 200, await operator("/api/v1/cockpit/contracts?stream_id=stream-1" + selector)); + } + if (url.pathname === "/call" && request.method === "POST") { + const call = await body(request), lane = url.searchParams.get("lane"); + const selector = lane === "preview" ? "?target=preview" : ""; + return json(response, 200, await operator(`/api/v1/operator/call/${encodeURIComponent(call.blueprint)}/${encodeURIComponent(call.entrypoint)}${selector}`, "POST", { payload: call.payload })); + } + if (url.pathname === "/__render_diag" && request.method === "POST") { + stats.events.push(await body(request)); if (stats.events.length > 150) stats.events.shift(); + return json(response, 200, { received: true }); + } + const sourceRoot = `/canvas/api/v1/scenes/${original.scene_id}/`; + if (url.pathname === "/canvas/api/v1/scenes") { + return json(response, stats.offline ? 503 : 200, stats.offline ? { code: "OFFLINE" } : { items: [{ id: original.scene_id }], next_offset: null }); + } + if (url.pathname.startsWith(sourceRoot)) { + stats.sourceHTTP.push({ path: url.pathname, offline: stats.offline }); + const delivery = await provider.get(original.scene_id, { sceneVersion: original.scene_version }); + stats.localSourceReads++; + if (url.searchParams.has("version") && url.searchParams.get("version") !== original.scene_version) return json(response, 409, { code: "VERSION_MISMATCH" }); + if (url.pathname === sourceRoot + "source") return json(response, 200, { revision: 1, scene_version: original.scene_version, + source_digest: delivery.sourceDigest, lsml: `revisions/1/source.lsml?version=${original.scene_version}`, + lsmlz: `revisions/1/source.lsmlz?version=${original.scene_version}`, assets: {}, blue_manifest: delivery.blueManifest }); + if (url.pathname === sourceRoot + "revisions/1/source.lsmlz") { + response.writeHead(200, { "Content-Type": "application/zip", "X-Scene-Version": original.scene_version, + "X-Scene-Revision": "1", ETag: `"${hash(delivery.data)}"` }); return response.end(delivery.data); + } + return json(response, 404, { code: "UNKNOWN_SOURCE_ROUTE" }); + } + if (/^\/(auth|truth|ranking|quasar|blue)\//.test(url.pathname)) { + stats.blockedEgress.push(url.pathname); return json(response, 503, { code: "OFFLINE_EGRESS" }); + } + const served = url.pathname === "/host.html" ? "/index.html" : url.pathname; + const file = resolve(dist, "." + decodeURIComponent(served)); + if (!file.startsWith(dist + sep)) return json(response, 403, {}); + let data = await readFile(file); + if (url.pathname === "/host.html") { + const config = { canvasApiUrl: origin + "/canvas/api/v1", canvasToken: "local-cache-contract" }; + const camera = values.camera ? { "obs-virtual-camera": { label: values.camera } } : {}; + const bootstrap = ``; + data = Buffer.from(data.toString("utf8").replace("", bootstrap + "")); + } + response.writeHead(200, { "Content-Type": mime[extname(file)] ?? "application/octet-stream", "Cache-Control": "no-store" }); response.end(data); + } catch (error) { json(response, error.code === "ENOENT" ? 404 : 500, { code: error.code ?? error.message }); } +}); +async function stop() { + if (closing) return; closing = true; + if (child?.exitCode === null) { const exited = new Promise(yes => child.once("exit", yes)); child.kill(); await exited; } + await meet?.stop(); + await native?.stop(); server.closeAllConnections(); await new Promise(yes => server.close(yes)); +} +try { + await new Promise((yes, no) => { server.once("error", no); server.listen(port, "127.0.0.1", yes); }); + native = await startNative(undefined, original, origin, () => void stop()); + if (values.meet) meet = await startMeetProof({ root: resolve(values.meet), port: port + 3, origin, output: values.output, camera: values.camera }); + const env = { ...process.env, ORION_PROFILE: "embedded-local", ORION_LISTEN_ADDR: `127.0.0.1:${port + 1}`, + ORION_INTERNAL_ADDR: `127.0.0.1:${port + 2}`, ORION_LSDP_NATIVE_ADDRESS: native.address, + ORION_LOCAL_OPERATOR_SECRET: secret, ORION_LOCAL_AUTH_USER: identity.claims?.subject ?? "operator-1", ORION_OWNER_ID: identity.claims?.owner_id ?? "owner-1", ORION_TENANT_ID: identity.claims?.tenant_id ?? "tenant-1", + ORION_CANVAS_TRUST_PATH: resolve(orionStorage, "test-trust.json"), ORION_SQLITE_PATH: resolve(orionStorage, "retired-unused.sqlite"), + ORION_ASSET_ROOT: orionStorage, ORION_SOLAR_ROOT: dist, ORION_SCENE_CACHE_PATH: resolve(orionStorage, "catalog"), + ORION_SCENE_CONTROL_PATH: resolve(orionStorage, "selection.lsml"), ORION_STREAM_INTENT_PATH: resolve(orionStorage, "stream-control.lsml"), + ORION_SCENE_BUNDLE_PATH: resolve(orionRoot, "tests/e2e/testdata/canvas-chat-sponso.scene-bundle.json"), + ORION_CANVAS_BASE_URL: origin + "/canvas", ORION_BLUE_BASE_URL: origin + "/blue", ORION_ZABGATE_URL: origin, + ORION_ZABAUTH_VALIDATE_URL: origin + "/auth/api/v1/auth/validate", ORION_QUASAR_BASE_URL: origin + "/quasar", + ORION_DATABASE_URL: "", ORION_SERVICE_TOKEN: "", ORION_SERVICE_REFRESH_TOKEN: "", ORION_SERVICE_TOKEN_STATE_PATH: "", + ORION_LOCAL_ARTIFACT_ROOT: "", ORION_OVERLAY_APPS_PATH: "", ORION_DATASOURCES: "truth=truth,ranking=ranking" }; + const log = createWriteStream(values.output + "-orion.log"); + child = spawn(resolve(orionRoot, "build/orion-consolidated.exe"), [], { cwd: orionRoot, env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }); + child.stdout.pipe(log); child.stderr.pipe(log); + let ready = false; + for (let attempt = 0; attempt < 100; attempt++) { + if (child.exitCode !== null) throw new Error("Orion exited before readiness: " + child.exitCode); + try { const response = await fetch(`http://127.0.0.1:${port + 1}/ready`, { signal: AbortSignal.timeout(1000) }); ready = response.ok; await response.body?.cancel(); } catch { /* bounded boot wait */ } + if (ready) break; await new Promise(yes => setTimeout(yes, 100)); + } + if (!ready) throw new Error("Orion readiness timeout"); + if (!stats.offline) for (const action of ["prepare-preview", "take-on-air"]) { + const response = await operator("/api/v1/runtime/scene-catalog", "POST", { intent_id: "cold-prepare", stream_id: "stream-1", action, + resolved_scene_ref: identity.jws, blue_program: program.toString("base64"), blue_program_digest: programDigest, + lsml_bundle: Buffer.from(source).toString("base64"), lsml_bundle_digest: hash(source), blue_manifest: identity.manifest }); + if (response.status !== 200) throw new Error("Catalog admission failed: " + JSON.stringify(response)); + } + console.log(JSON.stringify({ ready: true, origin, nativePid: native.pid, orionPid: child.pid, offline: stats.offline, boundary: identity.boundary })); +} catch (error) { await stop(); throw error; } +process.once("SIGINT", () => void stop()); process.once("SIGTERM", () => void stop()); diff --git a/scripts/proof/meet-peer.mjs b/scripts/proof/meet-peer.mjs new file mode 100644 index 0000000..f4bc03a --- /dev/null +++ b/scripts/proof/meet-peer.mjs @@ -0,0 +1,77 @@ +// Real Meet signaling + shared publisher; no writes in the Meet repository. +import { randomBytes } from "node:crypto"; +import { spawn } from "node:child_process"; +import { createWriteStream } from "node:fs"; +import { resolve } from "node:path"; +import { createServer } from "node:net"; +import { build } from "esbuild"; + +export const rtcMetrics = ` +const NativeRTC=globalThis.RTCPeerConnection;let serial=0; +globalThis.RTCPeerConnection=class extends NativeRTC { + constructor(...args){super(...args);const id=++serial;const timer=setInterval(async()=>{ + if(this.connectionState==='closed'){clearInterval(timer);return} + try {const stats=await this.getStats();const media=[]; + stats.forEach(s=>{if((s.type==='inbound-rtp'||s.type==='outbound-rtp')&&(s.kind==='video'||s.mediaType==='video')) + media.push({type:s.type,framesDecoded:s.framesDecoded??0,framesSent:s.framesSent??0,bytesReceived:s.bytesReceived??0,bytesSent:s.bytesSent??0})}); + report({type:'rtc',id,state:this.connectionState,media});}catch{} + },1000);} +};`; + +export async function startMeetProof({ root, port, origin, output, camera }) { + const reservation = createServer(); + await new Promise(yes => reservation.listen(0, "127.0.0.1", yes)); + port = reservation.address().port; + await new Promise(yes => reservation.close(yes)); + const admin = randomBytes(32).toString("hex"), url = `http://127.0.0.1:${port}`; + const log = createWriteStream(output + "-meet.log"); + const child = spawn(process.execPath, ["--import", "tsx", "src/server.ts"], { + cwd: root, windowsHide: true, stdio: ["ignore", "pipe", "pipe"], + env: { ...process.env, PORT: String(port), ALLOWED_ORIGINS: origin, ADMIN_TOKEN: admin, + TURN_SECRET: randomBytes(32).toString("hex"), TURN_URLS: "stun:127.0.0.1:9" }, + }); + child.stdout.pipe(log); child.stderr.pipe(log); + const stop = async () => { + if (child.exitCode === null) { const exited = new Promise(yes => child.once("exit", yes)); child.kill(); await exited; } + }; + try { + let ready = false; + for (let i = 0; i < 80; i++) { + if (child.exitCode !== null) throw new Error("Meet exited before readiness"); + try { const response = await fetch(url + "/healthz"); ready = response.ok; await response.body?.cancel(); } catch { /* boot */ } + if (ready) break; await new Promise(yes => setTimeout(yes, 100)); + } + if (!ready) throw new Error("Meet readiness timeout"); + const response = await fetch(url + "/rooms", { method: "POST", headers: { + Authorization: `Bearer ${admin}`, "Content-Type": "application/json" }, + body: JSON.stringify({ name: "Solar local qualification", max_peers: 4, persistent: true }) }); + if (response.status !== 201) throw new Error("Meet room creation failed: " + response.status); + const room = await response.json(); + const client = await build({ entryPoints: [resolve(root, "src/client/index.ts")], bundle: true, + write: false, format: "esm", platform: "browser" }); + const options = { signalingUrl: url.replace("http:", "ws:") + "/ws", roomId: room.id, token: room.token, name: "proof_peer" }; + const page = ` + `; + return { pid: child.pid, page, stop, + viewer: { rooms: [{ signalingUrl: options.signalingUrl, roomId: room.id, token: room.viewer_token }] }, + health: async () => (await fetch(url + "/healthz")).json(), + }; + } catch (error) { await stop(); throw error; } +} diff --git a/scripts/proof/native-burst-producer.mjs b/scripts/proof/native-burst-producer.mjs new file mode 100644 index 0000000..94f27e5 --- /dev/null +++ b/scripts/proof/native-burst-producer.mjs @@ -0,0 +1,114 @@ +import { randomBytes } from "node:crypto"; +import { readNative, withNative, NativeState } from "../native-lsdp-tools.mjs"; + +const [url, mode = "burst", duration = "1000"] = process.argv.slice(2); +if (!["burst", "paced"].includes(mode)) + throw new Error("Unknown measurement mode"); +const total = 512; +const pacedDurationMs = Number(duration); +if (!Number.isFinite(pacedDurationMs) || pacedDurationMs <= 0) + throw new Error("Invalid paced duration"); +const path = "/defaults/__lit.text.text_msxz43eh_1"; +const clock = () => performance.timeOrigin + performance.now(); +await withNative(url, async (peer) => { + const baseline = await readNative(peer, "scene"); + let state = NativeState.from(baseline.state); + if (state.stateHash !== baseline.stateHash) + throw new Error("Native baseline mismatch"); + const messages = [], + hashes = [], + prepared = performance.now(); + for (let index = 1; index <= total; index++) { + const operations = [ + { op: "replace", path, value: String(index).padStart(4, "0") + "/0512" }, + ]; + messages.push({ + format: "lsdp.apply/1", + id: randomBytes(16).toString("hex"), + target: "scene", + beforeHash: state.stateHash, + operations, + require: "applied", + }); + state = state.patch(operations); + hashes.push(state.stateHash); + } + const preparationMs = performance.now() - prepared; + console.log(JSON.stringify({ ready: true, preparationMs })); + let pending = ""; + const go = await new Promise((resolve, reject) => { + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (chunk) => { + pending += chunk; + if (pending.includes("\n")) resolve(JSON.parse(pending.split("\n")[0])); + }); + process.stdin.on("error", reject); + }); + const waitUntil = async (deadline) => { + while (clock() < deadline) { + const remaining = deadline - clock(); + // This dedicated load generator must not bunch 120 Hz input into Windows' + // coarse timer ticks. Yield to I/O near the deadline; never alter the values. + await new Promise((yes) => + remaining > 25 ? setTimeout(yes, remaining - 20) : setImmediate(yes), + ); + } + }; + await waitUntil(go.startEpochMs); + const started = performance.now(), + startedEpochMs = clock(); + let chain = Promise.resolve(); + const receipts = [], + updates = []; + for (let index = 0; index < messages.length; index++) { + const message = messages[index]; + chain = chain.then(async () => { + if (mode === "paced") + await waitUntil(startedEpochMs + (index * pacedDurationMs) / total); + const sentEpochMs = clock(); + const receipt = await peer.transaction(message); + if (receipt.level !== "applied" || receipt.transactionId !== message.id) + throw new Error("Native receipt mismatch"); + if (receipt.stateHash && receipt.stateHash !== hashes[index]) + throw new Error("Native state hash mismatch"); + receipts.push({ + id: message.id, + counter: index + 1, + sentEpochMs, + epochMs: clock(), + level: receipt.level, + stateHash: hashes[index], + }); + if (receipts.length === total) + updates.push( + fetch(go.origin + "/bench/progress", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ native: receipts.length }), + }).catch(() => {}), + ); + }); + } + const enqueueMs = performance.now() - started; + await chain; + const completed = clock(); + await Promise.all(updates); + console.log( + JSON.stringify({ + mode, + operationCount: total, + transactionCount: messages.length, + preparationMs, + enqueueMs, + startedEpochMs, + lastNativeAckEpochMs: receipts.at(-1).epochMs, + nativeDrainMs: completed - startedEpochMs, + receipts, + finalValue: "0512/0512", + finalStateHash: state.stateHash, + pacedDurationMs: mode === "paced" ? pacedDurationMs : null, + pacing: "deadline with setImmediate near deadline; dedicated producer CPU", + }), + ); +}); +process.stdin.destroy(); diff --git a/scripts/proof/pulsar_cef.py b/scripts/proof/pulsar_cef.py new file mode 100644 index 0000000..48968b4 --- /dev/null +++ b/scripts/proof/pulsar_cef.py @@ -0,0 +1,113 @@ +"""Actual Pulsar process and obs-websocket v5 capture. No broadcast commands.""" +import asyncio +import base64 +import hashlib +import json +import os +from pathlib import Path +import re +import secrets +import socket +import subprocess +import threading +import websockets + +class Pulsar: + def __init__(self, executable, output): + self.executable = Path(executable) + self.prefix = Path(output) + self.output = self.prefix.parent + self.password = secrets.token_urlsafe(24) + self.ready = threading.Event() + self.endpoint = None + self.lines = [] + self.serial = 0 + self.process = None + self.fps = int(os.environ.get("SOLAR_PROOF_CEF_FPS", "60")) + self.capture_fps = int(os.environ.get("SOLAR_PROOF_CAPTURE_FPS", str(self.fps))) + if self.fps not in (60, 120): + raise ValueError("Proof capture FPS must be 60 or 120") + if self.capture_fps not in (60, 120): + raise ValueError("Proof recording FPS must be 60 or 120") + + def start(self): + with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] + env = {**os.environ, "PULSAR_PORT": str(port), "PULSAR_PASSWORD": self.password, + "PULSAR_FPS": str(self.capture_fps), "PULSAR_RESOLUTION": "1920x1080", + "PULSAR_RECORD_DIR": str(self.output)} + env.pop("PULSAR_CAPTURE_WINDOW", None) + env.pop("PULSAR_MIC_DEVICE_ID", None) + arguments = [str(self.executable), "--no-sandbox", "--enable-webgl", + "--ignore-gpu-blocklist", "--use-fake-ui-for-media-stream"] + if os.environ.get("SOLAR_PROOF_WEBGPU") == "1": + arguments.append("--enable-unsafe-webgpu") + self.process = subprocess.Popen( + arguments, + cwd=self.executable.parent, env=env, stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + encoding="utf-8", errors="replace", + creationflags=0x08000000 if os.name == "nt" else 0) + def read(): + for line in self.process.stdout: + match = re.match(r"PULSAR_READY ws=(\S+) password=(\S+)", line) + if match: + self.endpoint = match.group(1) + self.ready.set() + self.lines.append(line.replace(self.password, "")) + self.lines = self.lines[-120:] + threading.Thread(target=read, daemon=True).start() + if not self.ready.wait(45): + raise RuntimeError("Pulsar did not become ready: " + "".join(self.lines[-15:])) + + async def connect(self): + self.ws = await websockets.connect(self.endpoint, subprotocols=["obswebsocket.json"], + max_size=2**24, ping_interval=None) + hello = json.loads(await asyncio.wait_for(self.ws.recv(), 10)) + identify = {"rpcVersion": hello["d"]["rpcVersion"], "eventSubscriptions": 0} + if "authentication" in hello["d"]: + challenge = hello["d"]["authentication"] + secret = base64.b64encode(hashlib.sha256((self.password + challenge["salt"]).encode()).digest()).decode() + identify["authentication"] = base64.b64encode(hashlib.sha256((secret + challenge["challenge"]).encode()).digest()).decode() + await self.ws.send(json.dumps({"op": 1, "d": identify})) + if json.loads(await asyncio.wait_for(self.ws.recv(), 10)).get("op") != 2: + raise RuntimeError("Pulsar authentication failed") + + async def request(self, kind, data=None): + self.serial += 1 + request_id = str(self.serial) + await self.ws.send(json.dumps({"op": 6, "d": {"requestType": kind, + "requestId": request_id, "requestData": data or {}}})) + async def receive(): + while True: + message = json.loads(await self.ws.recv()) + if message.get("op") == 7 and message["d"]["requestId"] == request_id: + result = message["d"] + if not result["requestStatus"]["result"]: + raise RuntimeError("Pulsar request rejected: " + kind) + return result.get("responseData", {}) + return await asyncio.wait_for(receive(), 20) + + async def show(self, url): + response = await self.request("CallVendorRequest", {"vendorName": "pulsar-scene", + "requestType": "SetCaptureSource", "requestData": {"kind": "browser_source", + "url": url, "width": 1920, "height": 1080, "fps": self.fps, "reroute_audio": True}}) + if response.get("responseData", {}).get("kind") != "browser_source": + raise RuntimeError("Actual CEF source unavailable") + + async def screenshot(self): + response = await self.request("GetSourceScreenshot", {"sourceName": "PulsarSceneSource", + "imageFormat": "png", "imageWidth": 1920, "imageHeight": 1080}) + return base64.b64decode(response["imageData"].split(",", 1)[-1]) + + def stop(self): + if self.process and self.process.poll() is None: + # Reap only the process tree created by this proof. + if os.name == "nt": + subprocess.run(["taskkill", "/PID", str(self.process.pid), "/T", "/F"], + capture_output=True, timeout=10) + else: + self.process.terminate() + self.process.wait(timeout=10) + Path(str(self.prefix) + "-pulsar.log").write_text("".join(self.lines), encoding="utf-8") diff --git a/scripts/proof/verify-counter-clip.py b/scripts/proof/verify-counter-clip.py new file mode 100644 index 0000000..1007f78 --- /dev/null +++ b/scripts/proof/verify-counter-clip.py @@ -0,0 +1,63 @@ +"""Read every pixel counter in the exported 1-second CEF video independently. + +The full recording analyzer checks the scene and extracts consecutive frames. +This second check decodes the actual exported media, OCRs all 60 frames without +reusing previous readings, and compares them with the original recording. +""" +import argparse +import hashlib +import json +import re +import subprocess +from pathlib import Path + +import cv2 +import pytesseract + +parser = argparse.ArgumentParser() +parser.add_argument("--prefix", required=True) +args = parser.parse_args() +prefix = Path(args.prefix) +source = json.loads(Path(str(prefix) + "-video-counter.json").read_text(encoding="utf-8")) +clip = Path(source["oneSecond"]["path"]) +pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" +probe = json.loads(subprocess.check_output([ + "ffprobe", "-v", "error", "-count_frames", "-select_streams", "v:0", + "-show_entries", "stream=nb_read_frames,r_frame_rate,duration", "-of", "json", str(clip), +])) +cap = cv2.VideoCapture(str(clip)) +values = [] +while True: + ok, frame = cap.read() + if not ok: + break + text = pytesseract.image_to_string( + frame[690:775, 470:1300], + config="--psm 7 -c tessedit_char_whitelist=0123456789/", + ).strip() + match = re.fullmatch(r"(\d{1,4})/0512", text) + value = int(match[1]) if match and int(match[1]) <= 512 else None + values.append(value) +cap.release() +checks = { + "oneSecond60FPS": probe["streams"][0]["r_frame_rate"] == "60/1" + and abs(float(probe["streams"][0]["duration"]) - 1) < .001, + "exactly60Frames": len(values) == 60 and probe["streams"][0]["nb_read_frames"] == "60", + "allReadable": all(value is not None for value in values), + "sixtyDistinctStates": len({value for value in values if value is not None}) == 60, + "strictlyIncreasing": all(a is not None and b is not None and a < b + for a, b in zip(values, values[1:])), + "matchesConsecutiveSourceFrames": values == source["oneSecond"]["counterValues"], +} +report = { + "schema": "solar.counter-clip-verification.v1", + "result": "PASS" if all(checks.values()) else "FAIL", + "method": "independent OCR of every exported frame; no reading reuse", + "path": str(clip), "sha256": hashlib.sha256(clip.read_bytes()).hexdigest(), + "probe": probe, "checks": checks, "counterValues": values, + "distinctMutationStates": len({value for value in values if value is not None}), + "unreadableFrames": sum(value is None for value in values), +} +Path(str(prefix) + "-clip-verification.json").write_text(json.dumps(report, indent=2), encoding="utf-8") +print(json.dumps(report)) +raise SystemExit(0 if report["result"] == "PASS" else 1) diff --git a/scripts/prove-authenticated-canvas.mjs b/scripts/prove-authenticated-canvas.mjs new file mode 100644 index 0000000..3043d2a --- /dev/null +++ b/scripts/prove-authenticated-canvas.mjs @@ -0,0 +1,113 @@ +// Actual gateway authentication and Canvas delivery. No identity injection or fallback. +import assert from "node:assert/strict"; +import { createHash, createPublicKey, randomUUID, verify } from "node:crypto"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { createCanvasSceneSourceProvider } from "../dist/solar.js"; +import { createCachedSceneSourceProvider, FileSceneSourceStore } from "../dist/server/index.mjs"; + +const [sessionPath, credentialPath, sceneId, outputPrefix] = process.argv.slice(2); +if (!outputPrefix) throw new Error("Expected session, credential file, scene id, evidence prefix"); +const prefix = resolve(outputPrefix); +await mkdir(dirname(prefix), { recursive: true }); +const session = JSON.parse(await readFile(sessionPath, "utf8")); +const credentials = {}; +for (const line of (await readFile(credentialPath, "utf8")).split(/\r?\n/)) { + const match = /^\s*(PRISM_E2E_OPERATOR_EMAIL|PRISM_E2E_OPERATOR_PASSWORD)\s*=\s*(.*)$/.exec(line); + if (match) credentials[match[1]] = match[2].replace(/^["']|["']$/g, ""); +} +const digest = (data) => "sha256:" + createHash("sha256").update(data).digest("hex"); +const report = { result: "RUNNING", sceneId, authentication: "actual ZabAuth through ZabGate; credential RAM only", http: [], boundaries: [], cache: null }; +let token; +async function request(route, options = {}) { + const response = await fetch(session.gatewayUrl + route, { + ...options, + headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...options.headers }, + signal: AbortSignal.timeout(15000), redirect: "error", cache: "no-store", + }); + const bytes = new Uint8Array(await response.arrayBuffer()); + let json; + try { json = JSON.parse(new TextDecoder().decode(bytes)); } catch { /* report only metadata */ } + report.http.push({ method: options.method ?? "GET", route, status: response.status, + bytes: bytes.length, keys: json && typeof json === "object" ? Object.keys(json) : [], + // Never record login/session bodies or bearer-like values. + ...(route.includes("/auth/") ? {} : { bodyDigest: digest(bytes) }), + ...(response.ok ? {} : { code: json?.code ?? json?.detail?.code, requestId: json?.requestId ?? json?.request_id }), + }); + return { response, bytes, json }; +} +try { + const login = await request("/auth/api/v1/auth/login", { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: credentials.PRISM_E2E_OPERATOR_EMAIL, + password: credentials.PRISM_E2E_OPERATOR_PASSWORD, keep_signed_in: false }), + }); + delete credentials.PRISM_E2E_OPERATOR_PASSWORD; + assert.equal(login.response.status, 200, "Actual login rejected"); + token = login.json.access_token; + const me = await request("/auth/api/v1/auth/me"); + assert.equal(me.response.status, 200, "Actual identity rejected"); + report.principal = me.json.id; + const trust = await request("/canvas/api/v1/scenes/resolved-scene-ref/trust"); + assert.equal(trust.response.status, 200, "Public Canvas trust unavailable"); + const unsigned = await fetch(session.gatewayUrl + `/canvas/api/v1/scenes/${sceneId}/source`, { + redirect: "error", signal: AbortSignal.timeout(15000), + }); + report.unauthenticatedSourceStatus = unsigned.status; + await unsigned.body?.cancel(); + const source = await request(`/canvas/api/v1/scenes/${sceneId}/source`); + if (!source.response.ok) report.boundaries.push(`CANVAS_SOURCE_HTTP_${source.response.status}`); + const issuance = await request(`/canvas/api/v1/scenes/${sceneId}/resolved-scene-ref`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ canvas_request_key: `solar-offline-${randomUUID()}`, + stream_id: "solar-offline-proof", allowed_actions: ["prepare-preview", "take-on-air"] }), + }); + if (!issuance.response.ok) report.boundaries.push(`CANVAS_REFERENCE_HTTP_${issuance.response.status}`); + else { + const { jws, claims } = issuance.json; + const parts = jws.split("."); + const encodedKey = trust.json.keys[claims.kid]; + assert.equal(typeof encodedKey, "string", "Unknown public signing key shape"); + const publicKey = createPublicKey({ format: "der", type: "spki", + key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(encodedKey, "base64")]) }); + assert.ok(verify(null, Buffer.from(parts[0] + "." + parts[1]), publicKey, Buffer.from(parts[2], "base64url"))); + assert.equal(claims.subject, report.principal); + const payload = JSON.parse(Buffer.from(parts[1], "base64url")); + assert.deepEqual(payload, claims); + report.reference = { signatureVerified: true, jwsDigest: digest(jws), claims }; + assert.ok(claims.canvas_locator.startsWith(`/api/v1/scenes/${sceneId}/resolved-scene-ref/`)); + const envelope = await request("/canvas" + claims.canvas_locator); + // Dereference is broker-only. An operator bearer deliberately cannot access + // it: its 404 is an authorization boundary, not missing producer artifacts. + assert.equal(envelope.response.status, 404, "Operator unexpectedly accessed broker-only artifacts"); + report.brokerBoundary = { operatorDereferenceDenied: true, brokerRetrieval: "NOT_TESTED_REQUIRES_MTLS_DELEGATION" }; + } + if (!report.boundaries.length) { + const calls = []; + const provider = createCanvasSceneSourceProvider({ apiUrl: session.gatewayUrl + "/canvas/api/v1", token, + fetch: async (url, options) => { calls.push(new URL(url).pathname); return fetch(url, options); } }); + const cachePath = resolve("build", "authenticated-source-cache", report.principal); + const cached = createCachedSceneSourceProvider(provider, new FileSceneSourceStore(cachePath)); + const archive = await cached.get(sceneId); + const plain = await cached.get(sceneId, { format: "lsml", sceneVersion: archive.sceneVersion }); + assert.equal(archive.sourceDigest, plain.sourceDigest); + let offlineRequests = 0; + const offline = createCachedSceneSourceProvider({ get: async () => { offlineRequests++; throw new Error("OFFLINE"); } }, + new FileSceneSourceStore(cachePath)); + const restored = await offline.get(sceneId, { sceneVersion: archive.sceneVersion }); + assert.deepEqual(restored.data, archive.data); + assert.deepEqual(restored.blueManifest, archive.blueManifest); + assert.equal(offlineRequests, 0); + report.cache = { sceneVersion: archive.sceneVersion, sourceDigest: archive.sourceDigest, + archiveDigest: digest(archive.data), assets: plain.assets.size, blueManifestDigest: archive.blueManifest.manifest_digest, + bindingClosure: archive.blueManifest.binding_closure.length, offlineRequests, calls }; + } + report.result = report.boundaries.length ? "BLOCKED_PRODUCER" : "PASS_ACQUISITION_AND_CACHE"; +} catch (error) { + report.result = "FAIL"; + report.error = error.code ?? error.message; +} finally { + await writeFile(prefix + "-report.json", JSON.stringify(report, null, 2)); + console.log(JSON.stringify({ result: report.result, boundaries: report.boundaries, evidence: prefix + "-report.json" })); + if (report.result !== "PASS_ACQUISITION_AND_CACHE") process.exitCode = 1; +} diff --git a/scripts/prove-cold-start-cef.py b/scripts/prove-cold-start-cef.py new file mode 100644 index 0000000..8d4bcd9 --- /dev/null +++ b/scripts/prove-cold-start-cef.py @@ -0,0 +1,272 @@ +"""Two real process lifecycles with immutable cache, signed test capsules and CEF. + +This proves the local cache/runtime boundary, not authenticated Canvas acquisition. +The separate authenticated probe must pass before claiming the complete chain. +""" +import argparse +import asyncio +import base64 +import ctypes +import hashlib +import io +import json +import os +from pathlib import Path +import socket +import subprocess +import sys +import threading +import time +import urllib.parse +import urllib.request + +sys.dont_write_bytecode = True +from PIL import Image +from proof.pulsar_cef import Pulsar + +parser = argparse.ArgumentParser(description=__doc__) +for name in ["pulsar", "orion", "source", "output"]: + parser.add_argument("--" + name, required=True) +parser.add_argument("--camera", default="PC-LM1E Camera (0458:6006)") +parser.add_argument("--capsule") +args = parser.parse_args() +root = Path(__file__).resolve().parent.parent +prefix = Path(args.output).resolve() +prefix.parent.mkdir(parents=True, exist_ok=True) +storage = root / "build" / (prefix.name + "-cache") +orion_storage = Path(args.orion) / "build" / (prefix.name + "-cache") +with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] +origin = "http://127.0.0.1:" + str(port) +source = Path(args.source) +original_hashes = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() + for path in [source, source.with_suffix(".lsmlz")]} +report = {"result": "RUNNING", "boundary": "complete existing scene; local cache-contract manifest and test signatures; no fresh Canvas admission", + "broadcast": False, "mutatedLSMLSaved": False, "phases": [], "commands": [], "frames": []} +host = None +pulsar = None +if args.capsule: + report["boundary"] = "complete scene admitted and signed by actual local Canvas; real gateway identity and Blue compilation; producer capsule; local signing key" + report["canvasCapsuleSHA256"] = hashlib.sha256(Path(args.capsule).read_bytes()).hexdigest() + + +def runtime_artifacts(): + paths = {"orion/executable": Path(args.orion) / "build/orion-consolidated.exe", + "pulsar/executable": Path(args.pulsar), + "proof/host": root / "scripts/proof/cold-start-host.mjs", + "proof/driver": Path(__file__).resolve()} + paths.update({"solar/dist/" + path.relative_to(root / "dist").as_posix(): path + for path in (root / "dist").rglob("*") if path.is_file()}) + return {name: hashlib.sha256(path.read_bytes()).hexdigest() for name, path in sorted(paths.items())} + + +report["runtimeArtifacts"] = runtime_artifacts() + + +def http(path, data=None): + request = urllib.request.Request(origin + path, data=None if data is None else json.dumps(data).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(request, timeout=50) as response: + return json.load(response) + + +def alive(pid): + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + kernel.OpenProcess.argtypes = [ctypes.c_ulong, ctypes.c_int, ctypes.c_ulong] + kernel.OpenProcess.restype = ctypes.c_void_p + kernel.GetExitCodeProcess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)] + kernel.CloseHandle.argtypes = [ctypes.c_void_p] + handle = kernel.OpenProcess(0x1000, False, pid) + if not handle: + return False + code = ctypes.c_ulong() + try: + return bool(kernel.GetExitCodeProcess(handle, ctypes.byref(code))) and code.value == 259 + finally: + kernel.CloseHandle(handle) + + +def start_host(phase, offline): + command = ["node", str(root / "scripts/proof/cold-start-host.mjs"), "--scene", str(source.with_suffix(".lsmlz")), + "--orion", args.orion, "--storage", str(storage), "--orion-storage", str(orion_storage), + "--output", str(Path(args.orion) / prefix.parent.relative_to(root) / (prefix.name + "-" + phase)), + "--port", str(port), "--camera", args.camera] + if offline: + command.append("--offline") + if args.capsule: + command.extend(["--capsule", args.capsule]) + process = subprocess.Popen(command, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, encoding="utf-8", errors="replace", creationflags=0x08000000) + ready = threading.Event() + lines = [] + def reader(): + with Path(str(prefix) + "-" + phase + "-host.log").open("w", encoding="utf-8") as output: + for line in process.stdout: + lines.append(line) + output.write(line) + output.flush() + if '"ready":true' in line: + ready.set() + threading.Thread(target=reader, daemon=True).start() + if not ready.wait(25): + subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"], capture_output=True) + process.wait(10) + raise RuntimeError("Host not ready: " + "".join(lines[-8:])) + return process + + +async def wait_active(): + until = time.monotonic() + 45 + last = None + while time.monotonic() < until: + last = http("/state")["state"].get("scene_control", {}).get("defaults", {}).get("observed", {}) + if all(last.get(lane, {}).get("status") == "active" for lane in ["program", "preview"]): + return last + if any(value.get("status") == "failed" for value in last.values()): + raise RuntimeError("Selection failed: " + json.dumps(last)) + await asyncio.sleep(.2) + raise RuntimeError("Selection timed out: " + json.dumps(last)) + + +async def capture(phase, lane): + input_name = "PulsarSceneSource" if lane == "program" else "SolarPreviewProof" + until = time.monotonic() + 12 + while time.monotonic() < until: + response = await pulsar.request("GetSourceScreenshot", {"sourceName": input_name, "imageFormat": "png", "imageWidth": 1920, "imageHeight": 1080}) + data = base64.b64decode(response["imageData"].split(",", 1)[-1]) + image = Image.open(io.BytesIO(data)).convert("RGBA") + pixel = lambda x, y: image.getpixel((x, y))[:3] + colors = len({pixel(x, y) for y in range(30, 258, 4) for x in range(30, 444, 4)}) + if image.getchannel("A").getextrema() == (255, 255) and pixel(500, 100) == (43, 42, 42) and pixel(1700, 700) == (0, 0, 0) and pixel(200, 600) == (0, 0, 0) and colors > 80: + path = Path(str(prefix) + "-" + phase + "-" + lane + ".png") + path.write_bytes(data) + report["frames"].append({"phase": phase, "lane": lane, "path": str(path), "sha256": hashlib.sha256(data).hexdigest(), "cameraColors": colors}) + print("COLD_CEF", phase, lane, flush=True) + return + await asyncio.sleep(.15) + raise RuntimeError("Complete scene/camera absent: " + phase + "/" + lane) + + +def persistent_snapshot(): + capsules = sorted((storage / "sources").glob("*")) + if len(capsules) != 1: + raise RuntimeError("Expected one exact immutable source cache capsule") + result = {"sourceCapsuleSHA256": hashlib.sha256(capsules[0].read_bytes()).hexdigest(), + "selectionSHA256": hashlib.sha256((orion_storage / "selection.lsml").read_bytes()).hexdigest(), "catalog": []} + for path in sorted((orion_storage / "catalog").glob("*.json")): + entry = json.loads(path.read_text(encoding="utf-8")) + intent = entry["intent"] + original = json.loads(base64.b64decode(intent["lsml_bundle"])) + if "cold_proof_runtime_only" in original.get("defaults", {}): + raise RuntimeError("Mutated LSML was persisted in Orion's capsule") + result["catalog"].append({"action": entry["action"], "programDigest": intent["blue_program_digest"], + "sourceDigest": intent["lsml_bundle_digest"], "manifest": intent["blue_manifest"]}) + if len(result["catalog"]) != 2: + raise RuntimeError("Missing admitted Program/Preview capsule") + return result + + +async def run_phase(phase, offline): + global host, pulsar + host = start_host(phase, offline) + initial = http("/health") + pulsar = Pulsar(args.pulsar, Path(str(prefix) + "-" + phase)) + pulsar.start() + await pulsar.connect() + def url(lane): + return origin + "/host.html?" + urllib.parse.urlencode({"lsdp": initial["native"], "resource": "solar/" + lane}) + await pulsar.show(url("program")) + current = await pulsar.request("GetCurrentProgramScene") + await pulsar.request("CreateInput", {"sceneName": current["currentProgramSceneName"], "inputName": "SolarPreviewProof", "inputKind": "browser_source", + "inputSettings": {"url": url("preview"), "width": 1920, "height": 1080, "fps": 60, "shutdown": False}, "sceneItemEnabled": True}) + if not offline: + http("/select", {}) + observed = await wait_active() + for lane in ["program", "preview"]: + state = http("/state?resource=solar/" + lane)["state"] + if state["scene_version"] != initial["sceneVersion"] or state["x-orion-artifact-set"] != initial["artifactSet"]: + raise RuntimeError("Source/artifact identity conflated") + if offline and "cold_proof_runtime_only" in state.get("defaults", {}): + raise RuntimeError("Live state survived cold reconstruction") + await capture(phase, lane) + if offline: + contracts = http("/contracts?lane=" + lane) + if contracts["status"] != 200 or not contracts["body"]["triggers"]: + raise RuntimeError("Operator declarations not restored: " + json.dumps(contracts)) + commands = contracts["body"]["triggers"] + declared = (json.loads(base64.b64decode(json.loads(Path(args.capsule).read_text(encoding="utf-8"))["envelope"]["blue_program"])) + if args.capsule else json.loads((Path(args.orion) / "internal/api/testdata/scene-lec-lck.program.json").read_text(encoding="utf-8")))["entrypoints"] + if sorted(command["entrypoint_id"] for command in commands) != sorted(entry["id"] for entry in declared if entry["kind"] == "call"): + raise RuntimeError("Incomplete command discovery") + for command in commands: + widget = (command.get("ui") or {}).get("widget", "") + if widget not in ["", "match-selector"]: + raise RuntimeError("No test input for declared widget " + widget) + payload = {"match_id": "opaque-offline-operator-selection"} if widget else None + response = http("/call?lane=" + lane, {"blueprint": command["blueprint_id"], "entrypoint": command["entrypoint_id"], "payload": payload}) + if response["status"] != 202 or response["body"].get("status") != "fired" or "execution" not in response["body"]: + raise RuntimeError("Command dispatch failed: " + json.dumps(response)) + report["commands"].append({"lane": lane, "entrypoint": command["entrypoint_id"], "response": response["body"], "externalEffect": "no successful remote effect claimed while offline"}) + if not offline: + for lane in ["program", "preview"]: + http("/runtime-mutation?lane=" + lane, {}) + if http("/state?resource=solar/" + lane)["state"]["defaults"].get("cold_proof_runtime_only") is None: + raise RuntimeError("Runtime-only mutation not applied") + await asyncio.sleep(2) + final = http("/health") + if offline and final["upstreamSourceReads"] != 0: + raise RuntimeError("Offline source called upstream") + if any(event.get("type") == "error" for event in final["events"]): + raise RuntimeError("Renderer failed: " + json.dumps([event for event in final["events"] if event.get("type") == "error"])) + disk = persistent_snapshot() + pids = {"host": host.pid, "orion": final["orionPid"], "native": final["nativePid"], "pulsar": pulsar.process.pid} + await pulsar.ws.close() + pulsar.stop() + http("/stop", {}) + host.wait(15) + if any(alive(pid) for pid in pids.values()): + raise RuntimeError("Owned process survived full stop") + execution = {} + if offline: + log = (Path(args.orion) / prefix.relative_to(root).parent / (prefix.name + "-" + phase + "-orion.log")).read_text(encoding="utf-8") + execution = {"dispatches": log.count('"msg":"engine b operator dispatch returned"'), + "remoteQueryErrors": log.count('"msg":"engine b db query failed"'), + "remoteErrorCode": "DB_QUERY_FAILED", "externalSuccessClaimed": False} + if execution["dispatches"] != len(report["commands"]) or execution["remoteQueryErrors"] < len(report["commands"]): + raise RuntimeError("Missing dispatch/error diagnostics while offline") + report["phases"].append({"phase": phase, "pids": pids, "allProcessesReaped": True, "observed": observed, "disk": disk, "stats": final, "execution": execution}) + host = None + pulsar = None + + +try: + asyncio.run(run_phase("warm", False)) + asyncio.run(run_phase("cold-offline", True)) + before, after = report["phases"] + if before["disk"] != after["disk"]: + raise RuntimeError("Immutable sources/Blue declarations/desired selection changed") + for name in before["pids"]: + if before["pids"][name] == after["pids"][name]: + raise RuntimeError("Process not replaced: " + name) + if runtime_artifacts() != report["runtimeArtifacts"]: + raise RuntimeError("Runtime artifact changed during proof") + if not all(hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest for path, digest in original_hashes.items()): + raise RuntimeError("Original source was changed") + report["result"] = "PASS_LOCAL_COLD_START" +except Exception as error: + report["result"] = "FAIL" + report["error"] = str(error) + raise +finally: + if pulsar: + pulsar.stop() + if host and host.poll() is None: + try: + http("/stop", {}) + host.wait(15) + except Exception: + subprocess.run(["taskkill", "/PID", str(host.pid), "/T", "/F"], capture_output=True) + host.wait(10) + report["sourceUnchanged"] = all(hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest for path, digest in original_hashes.items()) + Path(str(prefix) + "-report.json").write_text(json.dumps(report, indent=2), encoding="utf-8") diff --git a/scripts/prove-native-cef.py b/scripts/prove-native-cef.py new file mode 100644 index 0000000..ea37082 --- /dev/null +++ b/scripts/prove-native-cef.py @@ -0,0 +1,228 @@ +"""Complete-scene qualification: Blue/native/Vision/Pulsar CEF, no broadcasting.""" +import argparse +import asyncio +import hashlib +import io +import json +import os +from pathlib import Path +import subprocess +import sys +import time +import urllib.parse +import urllib.request +sys.dont_write_bytecode = True +from PIL import Image +from proof.pulsar_cef import Pulsar + +parser = argparse.ArgumentParser() +parser.add_argument("--pulsar", required=True) +parser.add_argument("--orion", required=True) +parser.add_argument("--source", required=True) +parser.add_argument("--output", required=True, help="Canonical UTC-stamped proof file prefix") +parser.add_argument("--local", default="http://127.0.0.1:8100") +parser.add_argument("--query-gateway", required=True) +parser.add_argument("--duration", type=int, default=180) +args = parser.parse_args() +prefix = Path(args.output) +prefix.parent.mkdir(parents=True, exist_ok=True) +source = Path(args.source) +original = json.loads(source.read_text(encoding="utf-8")) +source_hash = hashlib.sha256(source.read_bytes()).hexdigest() +archive = source.with_suffix(".lsmlz") +archive_hash = hashlib.sha256(archive.read_bytes()).hexdigest() +orion_prefix = Path(args.orion) / "evidence/local-20261005-consolidation/forge" / prefix.name +frames = [] +report = {"result": "RUNNING", "sceneId": original["scene_id"], + "sceneVersion": original["scene_version"], "broadcast": False, + "sourceValidation": "Complete published scene bytes; animation variants RAM-only; published Blue program pinned separately"} +solar_root = Path(__file__).resolve().parent.parent +host_artifacts = [solar_root / "dist/host/index.html", *sorted((solar_root / "dist/host/assets").glob("*.js"))] +report["runtimeArtifacts"] = { + "vision": json.loads((solar_root / "public/vision/vision-assets.json").read_text(encoding="utf-8")), + "host": {str(path.relative_to(solar_root)): hashlib.sha256(path.read_bytes()).hexdigest() for path in host_artifacts}, + "native": json.loads((solar_root / "dist/native/manifest.json").read_text(encoding="utf-8")), +} + +def local(path, value=None): + request = urllib.request.Request(args.local + path, data=None if value is None else json.dumps(value).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(request, timeout=20) as response: + return json.load(response) + +def phase(category, name): + env = {**os.environ, "ORION_TEST_LSDP_NATIVE_ADDRESS": local("/health")["nativeAddress"], + "ORION_TEST_LSML_PATH": str(source), "ORION_TEST_QUERY_GATEWAY": args.query_gateway} + if category == "animation": + package, test, key = "./internal/lsdpreception", "TestRealNativeBlueAnimation", "ORION_TEST_ANIMATION_PHASE" + elif category == "operator": + package, test, key = "./internal/api", "TestRealNativeOperatorVisual", "ORION_TEST_OPERATOR_PHASE" + else: + package, test, key = "./internal/lsdpreception", "TestRealNativeVisual", "ORION_TEST_NATIVE_PHASE" + env[key] = name + result = subprocess.run(["go", "test", "-v", "-count=1", "-run", "^" + test + "$", package], + cwd=args.orion, env=env, capture_output=True, text=True, encoding="utf-8", timeout=70) + Path(str(orion_prefix) + "-" + category + "-" + name + ".log").write_text(result.stdout + result.stderr, encoding="utf-8") + if result.returncode or "--- PASS: " + test not in result.stdout: + raise RuntimeError("Actual Orion phase failed: " + category + "/" + name + " " + result.stdout[-1600:]) + print("NATIVE_PHASE", category, name, flush=True) + marker = "REAL_OPERATOR_NATIVE_PROOF " + line = next((line.split(marker, 1)[1] for line in result.stdout.splitlines() if marker in line), None) + return json.loads(line) if line else None + +def pixel(image, x, y): + return image.getpixel((x, y))[:3] + +def camera_colors(image): + return len({pixel(image, x, y) for y in range(30, 258, 4) for x in range(30, 444, 4)}) + +def difference(first, second, excluded): + a, b = first.tobytes(), second.tobytes() + compared = changed = 0 + for y in range(1080): + for x in range(1920): + if any(x0 <= x < x1 and y0 <= y < y1 for x0, y0, x1, y1 in excluded): + continue + offset = (y * 1920 + x) * 4 + compared += 1 + changed += a[offset:offset+4] != b[offset:offset+4] + return {"comparedPixels": compared, "differentPixels": changed} + +pulsar = Pulsar(args.pulsar, prefix) +async def capture(name, predicate, timeout=25): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + data = await pulsar.screenshot() + image = Image.open(io.BytesIO(data)).convert("RGBA") + if image.size == (1920, 1080) and image.getchannel("A").getextrema() == (255, 255) and predicate(image): + path = Path(str(prefix) + "-" + name + ".png") + path.write_bytes(data) + frames.append({"name": name, "path": str(path), "sha256": hashlib.sha256(data).hexdigest(), + "cameraColors": camera_colors(image), "opaque": True}) + print("CEF_FRAME", name, "camera_colors=" + str(camera_colors(image)), flush=True) + return image + await asyncio.sleep(0.15) + raise RuntimeError("Expected complete CEF frame absent: " + name) + +def url(resource, selector=None): + health = local("/health") + query = {"lsdp": health["native"], "resource": resource} + if selector: query["selector"] = selector + return args.local + "/host.html?" + urllib.parse.urlencode(query) + +async def run(): + await pulsar.connect() + kinds = await pulsar.request("GetInputKindList") + if "browser_source" not in kinds["inputKinds"]: raise RuntimeError("CEF browser_source missing") + await asyncio.to_thread(phase, "animation", "seed") + await pulsar.show(url("solar/program")) + before = await capture("before", lambda i: pixel(i,500,100) == (43,42,42) and camera_colors(i) > 80) + baseline_anchors = [pixel(before,*p) for p in [(10,10),(500,900),(1700,700),(1700,1000),(460,10)]] + def preserved(image): + return [pixel(image,*p) for p in [(10,10),(500,900),(1700,700),(1700,1000),(460,10)]] == baseline_anchors and camera_colors(image) > 80 + await asyncio.to_thread(phase, "animation", "hide") + hidden = await capture("hide", lambda i: pixel(i,500,100) == (103,103,103) and preserved(i)) + document = local("/state?resource=solar/program")["state"] + document["layout"]["children"][0]["position"]["x"] = 121 + report["structuralReceipt"] = local("/lsml?resource=solar/program", document) + hold = await capture("hold-after-structure", lambda i: pixel(i,500,100) == (103,103,103) and preserved(i)) + await asyncio.to_thread(phase, "animation", "reveal") + reveal = await capture("reveal", lambda i: pixel(i,500,100) == (43,42,42) and preserved(i)) + await asyncio.to_thread(phase, "animation", "move") + moved = await capture("move", lambda i: pixel(i,500,100) == (103,103,103) and pixel(i,550,100) == (43,42,42) and preserved(i)) + camera, panel, moved_union = (24,24,450,264), (473,24,1596,666), (473,24,1636,666) + static_checks = { + "restored": difference(before,reveal,[camera]), + "hideOthers": difference(before,hidden,[camera,panel]), + "holdFrame": difference(hidden,hold,[camera]), + "moveOthers": difference(before,moved,[camera,moved_union]), + } + if any(value["differentPixels"] for value in static_checks.values()): + raise RuntimeError("Static scene incomplete: " + json.dumps(static_checks)) + rich_colors = len(before.crop(panel).getcolors(1123*642)) + if rich_colors < 1000: raise RuntimeError("Full portraits/text/assets absent") + report["staticChecks"], report["panelDistinctColors"] = static_checks, rich_colors + await asyncio.to_thread(phase,"surface","seed") + selector = hashlib.sha256((original["scene_id"] + "\0" + original["scene_version"]).encode()).hexdigest() + await pulsar.show(url("solar/generations", selector)) + base = await capture("operator-before", lambda i: camera_colors(i) > 80 and pixel(i,500,640) == pixel(before,500,640)) + camera_before = sum(e.get("type") == "camera" for e in local("/health")["events"]) + commands = [] + for name in ("LEC","LCK"): + ids = {e.get("transactionId") for e in local("/health")["events"]} + command = await asyncio.to_thread(phase,"operator",name) + observation = None + for _ in range(100): + observation = next((e for e in reversed(local("/health")["events"]) if e.get("type") == "applied" and + e.get("transactionId") not in ids and e.get("projection",{}).get("runtime_instance_id") == "cef-operator-" + name), None) + if observation: break + await asyncio.sleep(0.2) + if not observation: raise RuntimeError("Missing Solar submitted operator frame: " + name) + await asyncio.sleep(2) + image = await capture(name, lambda i: camera_colors(i)>80) + changed = sum(pixel(image,x,y) != pixel(base,x,y) for y in range(120,590,3) for x in range(500,1570,3)) + if changed < 300: raise RuntimeError("Player area unchanged: " + name) + for point in [(500,640),(1500,620),(1000,900)]: + if pixel(image,*point) != pixel(base,*point): raise RuntimeError("Operator damaged static surface") + commands.append({"entrypoint":name,"operator":command,"solarSubmission":observation,"changedPlayerPixels":changed}) + if commands[0]["operator"]["names"] == commands[1]["operator"]["names"]: raise RuntimeError("LEC/LCK rosters identical") + report["operatorCommands"] = commands + camera_after = sum(e.get("type") == "camera" for e in local("/health")["events"]) + if camera_before != camera_after: raise RuntimeError("Camera reopened during operator commands") + report["cameraNotReopenedOnOperator"] = True + await asyncio.to_thread(phase,"surface","restore") + await pulsar.show(url("solar/program")) + local("/lsml?resource=solar/program",original) + await capture("restored", lambda i: pixel(i,500,100) == (43,42,42) and camera_colors(i)>80) + initial_stats = await pulsar.request("GetStats") + camera_before = sum(e.get("type") == "camera" for e in local("/health")["events"]) + latencies, samples = [], [] + start = time.monotonic() + count = 0 + while time.monotonic() - start < args.duration: + count += 1 + started = time.monotonic() + # Full LSML sends are diffed in Rust; only an unrelated default changes. + desired = json.loads(json.dumps(original)) + desired.setdefault("defaults",{})["__proof.sequence"] = count + receipt = await asyncio.to_thread(local,"/lsml?resource=solar/program",desired) + latencies.append((time.monotonic()-started)*1000) + if receipt["receipt"].get("status") != "completed": raise RuntimeError("Native endurance delivery failed") + image = await capture("endurance-last", lambda i: pixel(i,500,100) == (43,42,42) and camera_colors(i)>80) + if count % 6 == 0: + stats = await pulsar.request("GetStats") + samples.append(stats) + print("ENDURANCE", round(time.monotonic()-start), "s", "deliveries=" + str(count), "fps=" + str(stats.get("activeFps")), flush=True) + await asyncio.sleep(5) + final_stats = await pulsar.request("GetStats") + sorted_ms = sorted(latencies) + def percentile(fraction): return sorted_ms[min(len(sorted_ms)-1,int((len(sorted_ms)-1)*fraction))] + rendered = final_stats.get("renderTotalFrames",0) - initial_stats.get("renderTotalFrames",0) + skipped = final_stats.get("renderSkippedFrames",0) - initial_stats.get("renderSkippedFrames",0) + report["endurance"] = {"seconds":time.monotonic()-start,"deliveries":count, + "receiptMs":{"p50":percentile(.5),"p95":percentile(.95),"p99":percentile(.99)}, + "boundary":"Full-LSML local POST to native destination receipt; not physical display latency", + "renderFrames":rendered,"renderSkippedFrames":skipped,"samples":samples,"finalStats":final_stats, + "dropRatio":skipped/max(1,rendered)} + if rendered < args.duration * 40 or skipped/max(1,rendered) > .05: raise RuntimeError("Endurance render/frame-drop threshold failed") + if any(e.get("type")=="error" for e in local("/health")["events"]): raise RuntimeError("Solar runtime errors") + report["endurance"]["cameraNotReopened"] = camera_before == sum(e.get("type")=="camera" for e in local("/health")["events"]) + if not report["endurance"]["cameraNotReopened"]: raise RuntimeError("Endurance camera reopened") + report["frames"] = frames + +try: + pulsar.start() + asyncio.run(run()) + report["result"] = "PASS" +except Exception as error: + report["result"] = "FAIL" + report["error"] = str(error) + raise +finally: + try: local("/lsml?resource=solar/program",original) + finally: + pulsar.stop() + report["sourceUnchanged"] = source_hash == hashlib.sha256(source.read_bytes()).hexdigest() + report["archiveUnchanged"] = archive_hash == hashlib.sha256(archive.read_bytes()).hexdigest() + report["pulsarReaped"] = pulsar.process is None or pulsar.process.poll() is not None + Path(str(prefix) + "-report.json").write_text(json.dumps(report,indent=2),encoding="utf-8") diff --git a/scripts/prove-peer-camera-cef.py b/scripts/prove-peer-camera-cef.py new file mode 100644 index 0000000..20ce008 --- /dev/null +++ b/scripts/prove-peer-camera-cef.py @@ -0,0 +1,187 @@ +"""Complete scene, physical Meet publisher in a separate Pulsar process and two Solar CEF viewers.""" +import argparse +import asyncio +import base64 +import ctypes +import hashlib +import io +import json +import socket +import subprocess +import sys +import threading +import time +import urllib.parse +import urllib.request +from pathlib import Path + +sys.dont_write_bytecode = True +from PIL import Image +from proof.pulsar_cef import Pulsar + +parser = argparse.ArgumentParser(description=__doc__) +for name in ["pulsar", "orion", "meet", "source", "output"]: + parser.add_argument("--" + name, required=True) +parser.add_argument("--camera", default="PC-LM1E Camera (0458:6006)") +args = parser.parse_args() +root = Path(__file__).resolve().parent.parent +prefix = Path(args.output).resolve() +prefix.parent.mkdir(parents=True, exist_ok=True) +with socket.socket() as sock: + sock.bind(("127.0.0.1", 0)) + port = sock.getsockname()[1] +origin = "http://127.0.0.1:" + str(port) +source = Path(args.source) +immutable = {str(path): hashlib.sha256(path.read_bytes()).hexdigest() + for path in [source, source.with_suffix(".lsmlz")]} +report = {"result": "RUNNING", "boundary": "real loopback Meet/WebRTC, physical camera, separate publisher process; not a WAN/TURN proof", + "frames": [], "broadcast": False, "mutatedLSMLSaved": False} +host = publisher = viewers = None + + +def http(path, data=None): + request = urllib.request.Request(origin + path, data=None if data is None else json.dumps(data).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(request, timeout=50) as response: + return json.load(response) + + +def alive(pid): + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + kernel.OpenProcess.argtypes = [ctypes.c_ulong, ctypes.c_int, ctypes.c_ulong] + kernel.OpenProcess.restype = ctypes.c_void_p + kernel.GetExitCodeProcess.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_ulong)] + kernel.CloseHandle.argtypes = [ctypes.c_void_p] + handle = kernel.OpenProcess(0x1000, False, pid) + if not handle: + return False + code = ctypes.c_ulong() + try: + return bool(kernel.GetExitCodeProcess(handle, ctypes.byref(code))) and code.value == 259 + finally: + kernel.CloseHandle(handle) + + +async def wait_for(predicate, description): + deadline = time.monotonic() + 45 + while time.monotonic() < deadline: + if predicate(): + return + await asyncio.sleep(.3) + raise RuntimeError("Timed out: " + description) + + +async def frame(phase, lane, filled, empty): + name = "PulsarSceneSource" if lane == "program" else "SolarPreviewProof" + deadline = time.monotonic() + 20 + while time.monotonic() < deadline: + result = await viewers.request("GetSourceScreenshot", {"sourceName": name, "imageFormat": "png", "imageWidth": 1920, "imageHeight": 1080}) + data = base64.b64decode(result["imageData"].split(",", 1)[-1]) + image = Image.open(io.BytesIO(data)).convert("RGBA") + colors = {slot: len({image.getpixel((x, y))[:3] for y in range(start + 12, start + 225, 4) + for x in range(36, 432, 4)}) for slot, start in [("@0", 287), ("@1", 550), ("@2", 813)]} + if (image.getchannel("A").getextrema() == (255, 255) + and image.getpixel((500, 100))[:3] == (43, 42, 42) + and image.getpixel((1700, 700))[:3] == (0, 0, 0) + and all(colors[slot] > 80 for slot in filled) + and all(colors[slot] <= 2 for slot in empty)): + path = Path(str(prefix) + "-" + phase + "-" + lane + ".png") + path.write_bytes(data) + report["frames"].append({"phase": phase, "lane": lane, "path": str(path), "sha256": hashlib.sha256(data).hexdigest(), "peerSlotColors": colors}) + print("PEER_CEF", phase, lane, colors, flush=True) + return + await asyncio.sleep(.3) + raise RuntimeError("Peer pixels incorrect: " + phase + "/" + lane + " " + str(colors)) + + +async def main(): + global host, publisher, viewers + owner_prefix = Path(args.orion) / prefix.parent.relative_to(root) / prefix.name + owner_prefix.parent.mkdir(parents=True, exist_ok=True) + command = ["node", "scripts/proof/cold-start-host.mjs", "--scene", str(source.with_suffix(".lsmlz")), + "--orion", args.orion, "--meet", args.meet, "--camera", args.camera, "--port", str(port), + "--storage", str(root / "build" / (prefix.name + "-cache")), + "--orion-storage", str(Path(args.orion) / "build" / (prefix.name + "-cache")), "--output", str(owner_prefix)] + host = subprocess.Popen(command, cwd=root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, encoding="utf-8", errors="replace", creationflags=0x08000000) + ready = threading.Event() + def read(): + with Path(str(prefix) + "-host.log").open("w", encoding="utf-8") as output: + for line in host.stdout: + output.write(line); output.flush() + if '"ready":true' in line: + ready.set() + threading.Thread(target=read, daemon=True).start() + if not ready.wait(35): + raise RuntimeError("Host readiness timeout") + initial = http("/health") + peer = http("/peer/health") + report["processes"] = {"parent": host.pid, "orion": initial["orionPid"], "native": initial["nativePid"], "meet": peer["meetPid"]} + publisher = Pulsar(args.pulsar, Path(str(prefix) + "-publisher")) + publisher.start(); await publisher.connect(); await publisher.show(origin + "/publisher.html") + await wait_for(lambda: any(e.get("type") == "publisher-joined" for e in http("/health")["events"]), "physical publisher joined") + viewers = Pulsar(args.pulsar, Path(str(prefix) + "-viewers")) + viewers.start(); await viewers.connect() + report["processes"].update(publisher=publisher.process.pid, viewers=viewers.process.pid) + def url(lane): + return origin + "/host.html?" + urllib.parse.urlencode({"lsdp": initial["native"], "resource": "solar/" + lane}) + await viewers.show(url("program")) + current = await viewers.request("GetCurrentProgramScene") + await viewers.request("CreateInput", {"sceneName": current["currentProgramSceneName"], "inputName": "SolarPreviewProof", "inputKind": "browser_source", + "inputSettings": {"url": url("preview"), "width": 1920, "height": 1080, "fps": 60, "shutdown": False}, "sceneItemEnabled": True}) + http("/select", {}) + await wait_for(lambda: all(http("/state")["state"].get("scene_control", {}).get("defaults", {}).get("observed", {}).get(lane, {}).get("status") == "active" + for lane in ["program", "preview"]), "both scenes presented and activated") + for lane in ["program", "preview"]: + http("/peer/action", {"type": "assign", "lane": lane, "slot": "@0"}) + for lane in ["program", "preview"]: + await frame("joined", lane, ["@0"], ["@1", "@2"]) + await wait_for(lambda: all(any(e.get("lane") == "solar/" + lane and e.get("type") == "rtc" and e["state"] == "connected" + and any(m["type"] == "inbound-rtp" and m["framesDecoded"] > 5 for m in e["media"]) + for e in http("/health")["events"]) for lane in ["program", "preview"]), "both viewers decoded RTP frames") + report["connectedEvents"] = [e for e in http("/health")["events"] if e.get("type") == "rtc" and e["state"] == "connected"] + http("/peer/action", {"type": "pause"}) + for lane in ["program", "preview"]: + await frame("left", lane, [], ["@0", "@1", "@2"]) + await wait_for(lambda: http("/peer/health")["peers"] == 2, "publisher removed from authoritative roster") + http("/peer/action", {"type": "join"}) + for lane in ["program", "preview"]: + await frame("rejoined", lane, ["@0"], ["@1", "@2"]) + http("/peer/action", {"type": "assign", "lane": "program", "slot": "@1"}) + await frame("reassigned", "program", ["@1"], ["@0", "@2"]) + await frame("reassigned", "preview", ["@0"], ["@1", "@2"]) + http("/peer/action", {"type": "clear", "lane": "preview"}) + await frame("released", "program", ["@1"], ["@0", "@2"]) + await frame("released", "preview", [], ["@0", "@1", "@2"]) + for e in report["connectedEvents"]: + if e["lane"].startswith("solar/") and any(m["type"] == "outbound-rtp" and m["bytesSent"] > 0 for m in e["media"]): + raise RuntimeError("Receive-only viewer published video") + report["result"] = "PASS_REAL_PEER_CEF" + + +try: + asyncio.run(main()) +except Exception as error: + report.update(result="FAIL", error=str(error)) +finally: + if host and host.poll() is None: + try: + report["diagnostics"] = http("/health")["events"] + report["peerRoster"] = http("/peer/health") + except Exception: + pass + if viewers: viewers.stop() + if publisher: publisher.stop() + if host and host.poll() is None: + try: + http("/stop", {}); host.wait(15) + except Exception: + subprocess.run(["taskkill", "/PID", str(host.pid), "/T", "/F"], capture_output=True); host.wait(10) + report["sourceUnchanged"] = all(hashlib.sha256(Path(path).read_bytes()).hexdigest() == digest for path, digest in immutable.items()) + report["allProcessesReaped"] = not any(alive(pid) for pid in report.get("processes", {}).values()) + if not report["allProcessesReaped"]: + report.update(result="FAIL", error="Owned process survived stop") + Path(str(prefix) + "-report.json").write_text(json.dumps(report, indent=2), encoding="utf-8") + print(json.dumps({"result": report["result"], "error": report.get("error"), "evidence": str(prefix) + "-report.json"})) + if report["result"] != "PASS_REAL_PEER_CEF" or not report["sourceUnchanged"]: + sys.exit(1) diff --git a/scripts/prove-read-query-gateway.mjs b/scripts/prove-read-query-gateway.mjs new file mode 100644 index 0000000..05795f9 --- /dev/null +++ b/scripts/prove-read-query-gateway.mjs @@ -0,0 +1,99 @@ +// Explicit test gateway: actual authenticated read queries; token RAM only. +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { resolve, join } from "node:path"; +import { createServer } from "node:http"; +const [sessionPath, credentialPath, outputPath] = process.argv.slice(2); +if (!sessionPath || !credentialPath || !outputPath) + throw new Error( + "Expected session, credentials and owning-repo output paths.", + ); +const output = resolve(outputPath); +const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z"); +await mkdir(output, { recursive: true }); +const session = JSON.parse(await readFile(sessionPath, "utf8")); +const credentials = {}; +for (const line of (await readFile(credentialPath, "utf8")).split(/\r?\n/)) { + const match = + /^\s*(PRISM_E2E_OPERATOR_EMAIL|PRISM_E2E_OPERATOR_PASSWORD)\s*=\s*(.*)\s*$/.exec( + line, + ); + if (match) credentials[match[1]] = match[2].replace(/^["']|["']$/g, ""); +} +if (credentials.PRISM_E2E_OPERATOR_EMAIL?.toLowerCase() !== "zaboutine") + throw new Error("Proof account mismatch."); +const login = await fetch(session.gatewayUrl + "/auth/api/v1/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: credentials.PRISM_E2E_OPERATOR_EMAIL, + password: credentials.PRISM_E2E_OPERATOR_PASSWORD, + keep_signed_in: false, + }), + signal: AbortSignal.timeout(10000), +}); +delete credentials.PRISM_E2E_OPERATOR_PASSWORD; +if (!login.ok) throw new Error("Proof login rejected: " + login.status); +const token = (await login.json()).access_token; +const me = await fetch(session.gatewayUrl + "/auth/api/v1/auth/me", { + headers: { Authorization: "Bearer " + token }, + signal: AbortSignal.timeout(10000), +}); +if (!me.ok) throw new Error("Proof identity rejected: " + me.status); +const records = []; +const server = createServer(async (request, response) => { + if ( + request.method !== "POST" || + !/^\/(truth|ranking)\/api\/v1\/_query$/.test(request.url) + ) { + response.writeHead(404); + response.end(); + return; + } + try { + let body = "", + length = 0; + for await (const chunk of request) { + length += chunk.length; + if (length > 1048576) throw new Error("Query too large"); + body += chunk; + } + const remote = await fetch(session.gatewayUrl + request.url, { + method: "POST", + body, + headers: { + Authorization: "Bearer " + token, + "Content-Type": "application/json", + }, + signal: AbortSignal.timeout(10000), + }); + const text = await remote.text(); + records.push({ + path: request.url, + descriptor: JSON.parse(body), + status: remote.status, + response: JSON.parse(text), + }); + await writeFile( + join(output, stamp + "-read-queries.json"), + JSON.stringify(records, null, 2), + ); + response.writeHead(remote.status, { "Content-Type": "application/json" }); + response.end(text); + } catch (error) { + response.writeHead(502); + response.end(JSON.stringify({ error: error.name })); + } +}); +await new Promise((yes) => server.listen(0, "127.0.0.1", yes)); +console.log( + JSON.stringify({ + queryProxy: "http://127.0.0.1:" + server.address().port, + authenticated: true, + persistence: "proof responses only; no session/credential writes", + }), +); +for (const signal of ["SIGTERM", "SIGINT"]) + process.once(signal, () => { + server.closeAllConnections(); + server.close(); + }); diff --git a/scripts/prove-scene-control-cef.py b/scripts/prove-scene-control-cef.py new file mode 100644 index 0000000..8b947ce --- /dev/null +++ b/scripts/prove-scene-control-cef.py @@ -0,0 +1,81 @@ +"""Actual CEF evidence of signed scene selection, two lanes and process recovery.""" +import argparse, asyncio, base64, hashlib, io, json, os, subprocess, sys, time +from pathlib import Path +import urllib.parse, urllib.request +sys.dont_write_bytecode = True +from PIL import Image +from proof.pulsar_cef import Pulsar + +p=argparse.ArgumentParser() +for name in ['pulsar','orion','source','output','query-gateway']:p.add_argument('--'+name,required=True) +p.add_argument('--local',default='http://127.0.0.1:8100') +a=p.parse_args();prefix=Path(a.output);prefix.parent.mkdir(parents=True,exist_ok=True) +source=Path(a.source);archive=source.with_suffix('.lsmlz') +before={str(path):hashlib.sha256(path.read_bytes()).hexdigest() for path in [source,archive]} +def local(path): + with urllib.request.urlopen(a.local+path,timeout=5) as response:return json.load(response) +def pixel(image,x,y):return image.getpixel((x,y))[:3] +def colors(image):return len({pixel(image,x,y) for y in range(30,258,4) for x in range(30,444,4)}) +runtime_root=Path(__file__).resolve().parent.parent +def artifacts(): + return {'host':{str(path.relative_to(runtime_root)):hashlib.sha256(path.read_bytes()).hexdigest() for path in (runtime_root/'dist/host').rglob('*') if path.is_file()},'vision':json.loads((runtime_root/'public/vision/vision-assets.json').read_text()),'native':json.loads((runtime_root/'dist/native/manifest.json').read_text()),'orionProgramSHA256':hashlib.sha256((Path(a.orion)/'internal/api/testdata/scene-lec-lck.program.json').read_bytes()).hexdigest()} +pulsar=Pulsar(a.pulsar,prefix);go=None;report={'result':'RUNNING','broadcast':False,'longSoak':'Pulsar stability accepted by user; new paths only','frames':[],'runtimeArtifacts':artifacts()} +async def capture(phase,input_name='PulsarSceneSource'): + until=time.monotonic()+8 + while time.monotonic()80: + path=Path(str(prefix)+'-'+phase+'.png');path.write_bytes(data) + report['frames'].append({'phase':phase,'source':input_name,'path':str(path),'sha256':hashlib.sha256(data).hexdigest(),'cameraColors':colors(image),'staticGrey':pixel(image,500,100),'blackSurface':pixel(image,1700,700)}) + print('CEF_SCENE_CONTROL',phase,flush=True);return + await asyncio.sleep(.1) + raise RuntimeError('Complete scene missing: '+phase) +async def run(): + global go + await pulsar.connect();health=local('/health') + def url(resource):return a.local+'/host.html?'+urllib.parse.urlencode({'lsdp':health['native'],'resource':resource}) + await pulsar.show(url('solar/program')) + current=await pulsar.request('GetCurrentProgramScene') + await pulsar.request('CreateInput',{'sceneName':current['currentProgramSceneName'],'inputName':'SolarPreviewProof','inputKind':'browser_source','inputSettings':{'url':url('solar/preview'),'width':1920,'height':1080,'fps':60,'shutdown':False},'sceneItemEnabled':True}) + env={**os.environ,'GOTOOLCHAIN':'go1.26.6','ORION_TEST_SCENE_CONTROL_VISUAL':'1','ORION_TEST_LSDP_NATIVE_ADDRESS':health['nativeAddress'],'ORION_TEST_LSML_PATH':str(source),'ORION_TEST_QUERY_GATEWAY':a.query_gateway,'ORION_TEST_LOCAL_HTTP':a.local} + log=Path(a.orion)/'evidence/local-20261005-coordinated-runtime/forge'/(prefix.name+'-orion-cef.log');log.parent.mkdir(parents=True,exist_ok=True) + with log.open('w',encoding='utf-8') as output: + go=subprocess.Popen(['go','test','-v','-count=1','-run','^TestRealNativeSceneControlVisual$','./internal/api'],cwd=a.orion,env=env,stdout=output,stderr=subprocess.STDOUT,creationflags=0x08000000) + seen=set();until=time.monotonic()+115 + while time.monotonic() or LUMENCAST_LSDP_BIN.", + ); +let seed = { + lsml: "1.1", + scene_id: "native-interop", + scene_version: `sha256:${"0".repeat(64)}`, + layout: { type: "frame", children: [] }, + defaults: { title: "before", "escaped/key~": [1, 2] }, +}; +if (values.scene) + seed = JSON.parse( + strFromU8(unzipSync(await readFile(values.scene))["scene.lsml"]), + ); +else seed.scene_version = await bundleAddress(seed); +const sourceBefore = values.scene ? await readFile(values.scene) : null; +const native = await startNative(binary, seed, "http://127.0.0.1:8099"); +let reader, writer; +try { + writer = new BrowserLSDP(native.url); + reader = new BrowserLSDP(native.url); + await Promise.all([writer.ready, reader.ready]); + let baseline = await readNative(reader); + assert.deepEqual(baseline.state, seed); + let received; + reader.options.onTransaction = async (event) => { + assert.equal(event.kind, "applied_change"); + assert.equal(event.mutation.beforeHash, baseline.stateHash); + const next = jsonPatch.applyPatch( + structuredClone(baseline.state), + event.mutation.operations, + true, + ).newDocument; + baseline = { state: next, stateHash: nativeTreeHash(next) }; + received?.(event); + return { level: "applied", transactionId: event.mutation.id }; + }; + await reader.transaction({ + kind: "subscribe", + target: "scene", + stateHash: baseline.stateHash, + }); + const originalVersion = seed.scene_version; + const mutations = [ + [{ op: "add", path: "/defaults/native-proof", value: "native → Vision" }], + [ + { + op: "add", + path: "/layout/native-proof", + value: { items: [1, 2], "a/b~": true }, + }, + ], + [ + { op: "test", path: "/layout/native-proof/a~1b~0", value: true }, + { op: "add", path: "/layout/native-proof/items/1", value: 3 }, + { op: "remove", path: "/layout/native-proof/items/0" }, + ], + [ + { + op: "replace", + path: "/defaults/native-proof", + value: "fragmented " + "é".repeat(70_000), + }, + ], + [ + { op: "remove", path: "/defaults/native-proof" }, + { op: "remove", path: "/layout/native-proof" }, + ], + ]; + for (const operations of mutations) { + const id = randomBytes(16).toString("hex"); + let timeout; + const observed = new Promise((resolve, reject) => { + received = resolve; + timeout = setTimeout( + () => reject(new Error("Native subscription notification missing.")), + 10_000, + ); + }); + const mutation = { + format: "lsdp.apply/1", + id, + target: "scene", + beforeHash: baseline.stateHash, + operations, + require: "applied", + }; + try { + const [receipt, event] = await Promise.all([ + writer.transaction(mutation), + observed, + ]); + assert.deepEqual(receipt, { + transactionId: id, + target: "scene", + level: "applied", + }); + assert.equal(event.mutation.id, id); + assert.deepEqual(await writer.transaction(mutation), receipt); // idempotency + } finally { + clearTimeout(timeout); + } + } + assert.deepEqual(baseline.state, seed); + // Server and browser agree on the normative state hash, even on the real scene. + const serverState = await readNative(writer); + assert.equal(serverState.stateHash, baseline.stateHash); + assert.equal(serverState.state.scene_version, originalVersion); + const invalid = { + format: "lsdp.apply/1", + id: randomBytes(16).toString("hex"), + target: "scene", + beforeHash: baseline.stateHash, + operations: [ + { op: "add", path: "/defaults/native-proof", value: 1 }, + { op: "test", path: "/defaults/native-proof", value: 2 }, + ], + require: "applied", + }; + await assert.rejects(writer.transaction(invalid)); + assert.deepEqual((await readNative(writer)).state, seed); + const stale = { + ...invalid, + id: randomBytes(16).toString("hex"), + beforeHash: `tree-sha256:${"0".repeat(64)}`, + operations: [{ op: "add", path: "/defaults/native-proof", value: 1 }], + }; + await assert.rejects(writer.transaction(stale)); + const stats = await writer.transaction({ kind: "stats" }); + assert.equal(stats.commits, mutations.length); + if (sourceBefore) + assert.deepEqual(await readFile(values.scene), sourceBefore); + await reader.transaction({ kind: "unsubscribe", target: "scene" }); + reader.close(); + writer.close(); + await native.stop(); + const reset = await startNative(binary, seed, "http://127.0.0.1:8099"); + try { + await withNative(reset.url, async (peer) => { + assert.deepEqual((await readNative(peer)).state, seed); + assert.equal((await peer.transaction({ kind: "stats" })).commits, 0); + }); + } finally { + await reset.stop(); + } + console.log( + JSON.stringify({ + result: "PASS", + nativeBinary: binary, + realScene: !!values.scene, + mutations: mutations.length, + fragmentedPayload: true, + rejectedBatchAtomicity: true, + staleBaseRejected: true, + duplicateIdempotency: true, + unchangedSource: true, + restartResetsState: true, + persistence: "memory-only", + }), + ); +} finally { + reader?.close(); + writer?.close(); + await native.stop(); +} diff --git a/scripts/test-packaged-cache.mjs b/scripts/test-packaged-cache.mjs new file mode 100644 index 0000000..e17555d --- /dev/null +++ b/scripts/test-packaged-cache.mjs @@ -0,0 +1,53 @@ +// Contract fixture: real packaged API and disk I/O, no Canvas validation claim. +import assert from "node:assert/strict"; +import { createHash } from "node:crypto"; +import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises"; +import { resolve, join, sep } from "node:path"; +import { bundleAddress, canonicalize } from "@lumencast/canonical"; +import { strToU8, zipSync } from "fflate"; +import { FileSceneSourceStore, createCachedSceneSourceProvider } from "../dist/server/index.mjs"; + +const base = resolve("build"); +await mkdir(base, { recursive: true }); +const root = await mkdtemp(join(base, "packaged-cache-")); +assert.ok(root.startsWith(base + sep)); +const digest = (data) => "sha256:" + createHash("sha256").update(data).digest("hex"); +try { + const document = { lsml: "1.2", scene_id: "cache-contract", scene_version: "", layout: { type: "frame", children: [] }, defaults: { title: "source" } }; + document.scene_version = await bundleAddress(document); + const bytes = strToU8(canonicalize(document)); + const asset = strToU8("contract asset bytes"); + const assetName = `assets/${digest(asset).slice(7)}.png`; + const unsigned = { + schema_version: "zabcanvas.scene-blue-manifest.v1", + scene_id: document.scene_id, scene_revision: 1, revision_id: "contract-fixture", + scene_version: document.scene_version, validation: { status: "contract-fixture" }, + declarations: { blueprints: ["fixture-blue"] }, + binding_closure: [{ binding_id: "fixture-blue", source_digest: "fixture" }], + readiness: { program_ready: true, binding_closure_complete: true, offline_ready: true, missing_binding_ids: [], missing_scene_blueprint_keys: [] }, + }; + const manifest = { ...unsigned, manifest_digest: digest(strToU8(canonicalize(unsigned))) }; + for (const format of ["lsml", "lsmlz"]) { + const delivery = { sceneId: document.scene_id, revision: 1, sceneVersion: document.scene_version, + sourceDigest: digest(bytes), format, data: format === "lsml" ? bytes : zipSync({ "scene.lsml": bytes, [assetName]: asset }), + assets: new Map(format === "lsml" ? [[assetName, asset]] : []), blueManifest: manifest }; + let onlineReads = 0; + const directory = join(root, format); + const provider = createCachedSceneSourceProvider({ get: async () => { onlineReads++; return delivery; } }, new FileSceneSourceStore(directory)); + const online = await provider.get(document.scene_id, { format }); + online.data.fill(0); + online.blueManifest.binding_closure = []; + let offlineRequests = 0; + const offline = createCachedSceneSourceProvider({ get: async () => { offlineRequests++; throw new Error("offline"); } }, new FileSceneSourceStore(directory)); + const restored = await offline.get(document.scene_id, { format, sceneVersion: document.scene_version }); + assert.deepEqual(restored.data, delivery.data); + assert.equal(restored.blueManifest.binding_closure.length, 1); + assert.equal(onlineReads, 1); + assert.equal(offlineRequests, 0); + assert.equal((await readdir(directory)).length, 1); + await assert.rejects(offline.get(document.scene_id, { format }), /offline/); + } + console.log(JSON.stringify({ result: "PASS", boundary: "compiled server export and durable disk cache", fixture: "synthetic contract; not a Canvas validation or visual proof", formats: ["lsml", "lsmlz"], assetsAndBlueClosureRestored: true, newProviderOfflineExactReads: true, noLatestDiscoveryOffline: true, callerMutationIsolated: true, retainedLiveMutation: false })); +} finally { + await rm(root, { recursive: true, force: true }); +} diff --git a/scripts/test-peer-install.mjs b/scripts/test-peer-install.mjs new file mode 100644 index 0000000..86e7643 --- /dev/null +++ b/scripts/test-peer-install.mjs @@ -0,0 +1,99 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { cp, mkdir, mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import { join, resolve, sep } from "node:path"; +import { promisify } from "node:util"; +const exec = promisify(execFile); +const root = resolve(import.meta.dirname, ".."); +const [runtimeArchive, protocolArchive] = process.argv.slice(2); +if (!runtimeArchive || !protocolArchive) + throw new Error( + "Usage: test-peer-install ", + ); +const base = resolve(root, "build"); +await mkdir(base, { recursive: true }); +const isolated = await mkdtemp(join(base, "peer-install-")); +if (!isolated.startsWith(base + sep)) + throw new Error("Invalid owned test directory"); +const digest = (bytes, algorithm = "sha256") => + createHash(algorithm) + .update(bytes) + .digest(algorithm === "sha512" ? "base64" : "hex"); +async function inventory(directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const output = {}; + for (const entry of entries) { + const file = join(directory, entry.name); + if (entry.isDirectory()) { + for (const [name, hash] of Object.entries(await inventory(file))) + output[`${entry.name}/${name}`] = hash; + } else output[entry.name] = digest(await readFile(file)); + } + return output; +} +try { + const lock = JSON.parse( + await readFile(join(root, "package-lock.json"), "utf8"), + ); + for (const [name, archive] of [ + ["runtime", runtimeArchive], + ["protocol", protocolArchive], + ]) { + const bytes = await readFile(resolve(archive)); + assert.equal( + `sha512-${digest(bytes, "sha512")}`, + lock.packages[`node_modules/@lumencast/${name}`].integrity, + ); + const destination = join(isolated, "node_modules/@lumencast", name); + await mkdir(destination, { recursive: true }); + await exec("tar", [ + "-xzf", + resolve(archive), + "--strip-components=1", + "-C", + destination, + ]); + } + const runtime = join(isolated, "node_modules/@lumencast/runtime"); + const protocol = join(isolated, "node_modules/@lumencast/protocol"); + const before = await inventory(runtime), + protocolBefore = await inventory(protocol); + const solar = join(isolated, "node_modules/@zablab/solar"); + await mkdir(join(solar, "scripts"), { recursive: true }); + await cp( + join(root, "scripts/patch-peer-viewer.mjs"), + join(solar, "scripts/patch-peer-viewer.mjs"), + ); + await cp( + join(root, "scripts/peer-viewer"), + join(solar, "scripts/peer-viewer"), + { recursive: true }, + ); + const entry = join(solar, "scripts/patch-peer-viewer.mjs"); + await exec(process.execPath, [entry]); + const first = await inventory(runtime); + const changed = Object.keys(first) + .filter((file) => first[file] !== before[file]) + .sort(); + assert.deepEqual(changed, [ + "dist/webrtc/index.js", + "dist/webrtc/meet-viewer.js", + "dist/webrtc/peer-stream-registry.js", + ]); + assert.deepEqual(await inventory(protocol), protocolBefore); + await exec(process.execPath, [entry]); + assert.deepEqual(await inventory(runtime), first); + console.log( + JSON.stringify({ + result: "PASS", + freshHoistedInstall: true, + lockfileIntegrity: true, + onlyConsumedViewerPatched: changed, + protocolUnchanged: true, + idempotent: true, + }), + ); +} finally { + await rm(isolated, { recursive: true, force: true }); +} diff --git a/scripts/test-reception-burst.mjs b/scripts/test-reception-burst.mjs new file mode 100644 index 0000000..ffb8816 --- /dev/null +++ b/scripts/test-reception-burst.mjs @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { resolve } from "node:path"; +import { SolarReceptionServer } from "../dist/server/index.mjs"; + +const recovery = [], + failures = [], + states = []; +const server = new SolarReceptionServer({ + origins: ["http://127.0.0.1:8099"], + packageRoot: resolve("dist"), + probeIntervalMs: 0, + resources: { + "orion/state": { type: "orion.state/1", initial: { counter: 0 } }, + }, + onRecovery: (event) => recovery.push(event), + onFailure: (error) => failures.push(error.message), +}); +let close, writer; +try { + const first = await server.start(); + close = await server.watch( + "orion/state", + (state) => { + states.push(state.counter); + // External writer remains alive while this consumer deliberately falls behind. + if (state.counter === 1) + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500); + }, + (error) => failures.push(error.message), + ); + const tools = new URL("./native-lsdp-tools.mjs", import.meta.url).href; + const code = `import {withNative,NativeState} from ${JSON.stringify(tools)}; +await withNative(${JSON.stringify(first.websocketUrl)}, async peer => { +let state=NativeState.from({counter:0}); +for(let counter=1;counter<=512;counter++){ +const operations=[{op:'replace',path:'/counter',value:counter}]; +const ack=await peer.transaction({format:'lsdp.apply/1',id:counter.toString(16).padStart(32,'0'),target:'orion/state',beforeHash:state.stateHash,operations,require:'applied'}); +if(ack.level!=='applied')throw new Error('ACK missing');state=state.patch(operations); +}console.log('ACK_COUNT 512');});`; + writer = spawn(process.execPath, ["--input-type=module", "-e", code], { + windowsHide: true, + stdio: ["ignore", "pipe", "pipe"], + }); + let output = ""; + writer.stdout.on("data", (data) => { + output += data; + }); + writer.stderr.on("data", (data) => { + output += data; + }); + const exit = await new Promise((yes, no) => { + writer.once("exit", yes); + writer.once("error", no); + }); + assert.equal(exit, 0, output); + assert.match(output, /ACK_COUNT 512/); + const until = Date.now() + 10000; + while (!states.includes(512) && Date.now() < until) + await new Promise((yes) => setTimeout(yes, 25)); + assert.ok(states.includes(512), "Lagging watcher did not catch up"); + assert.equal( + server.status().pid, + first.pid, + "A lagging watcher restarted the healthy receiver", + ); + assert.deepEqual((await server.read("orion/state")).state, { counter: 512 }); + assert.deepEqual(recovery, []); + assert.deepEqual(failures, []); + console.log( + JSON.stringify({ + result: "PASS", + nativeACKCount: 512, + receiverPIDUnchanged: true, + authoritativeFinalCounter: 512, + watchFinalCounter: states.at(-1), + receivedWatchEvents: states.length, + recovery, + failures, + }), + ); +} finally { + writer?.kill(); + close?.(); + await server.stop(); +} diff --git a/scripts/test-reception-install.mjs b/scripts/test-reception-install.mjs new file mode 100644 index 0000000..d4cc60d --- /dev/null +++ b/scripts/test-reception-install.mjs @@ -0,0 +1,113 @@ +import assert from "node:assert/strict"; +import { cp, mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; +import { resolve, join, sep } from "node:path"; +import { build } from "esbuild"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { unzipSync, strFromU8 } from "fflate"; +const [scenePath, prismRoot, orionRoot] = process.argv.slice(2); +if (!scenePath || !prismRoot || !orionRoot) + throw new Error( + "Usage: test-reception-install ", + ); +const base = resolve("build"); +await mkdir(base, { recursive: true }); +const isolated = await mkdtemp(join(base, "native-installed-")); +if (!resolve(isolated).startsWith(base + sep)) + throw new Error("Invalid owned test path"); +let adapter; +try { + // Exactly the release directory layout Prism activates. No node_modules, + // scripts, source tree, Cargo or esbuild is available in this runtime root. + const releaseRoot = join(isolated, "solar"); + await cp(resolve("dist"), join(releaseRoot, "v3.0.0"), { recursive: true }); + const result = await build({ + entryPoints: [ + resolve(prismRoot, "src/main/runtime/solar/native-reception.ts"), + ], + bundle: true, + platform: "node", + format: "esm", + write: false, + plugins: [ + { + name: "installed-package-root", + setup(build) { + build.onResolve({ filter: /solar-runtime-manager$/ }, () => ({ + path: "installed-package-root", + namespace: "fixture", + })); + build.onLoad({ filter: /.*/, namespace: "fixture" }, () => ({ + contents: `export const getSolarRuntimeBundleDir=()=>${JSON.stringify(releaseRoot)};export const getActiveSolarReleaseTag=()=>"v3.0.0";`, + loader: "js", + })); + }, + }, + ], + }); + adapter = await import( + `data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString("base64")}` + ); + const native = await adapter.startNativeReception( + "http://127.0.0.1:8099", + (error) => { + throw error; + }, + ); + assert.equal((await adapter.readNativeScene("program")).state, null); + const original = JSON.parse( + strFromU8(unzipSync(await readFile(scenePath))["scene.lsml"]), + ); + const received = await adapter.receiveNativeScene("program", original); + assert.equal(received.status, "completed"); + assert.deepEqual((await adapter.readNativeScene("program")).state, original); + const patched = await adapter.receiveNativeMutations("program", [ + { + op: "replace", + path: "/defaults/__lit.text.text_msxz43eh_1", + value: "Installed reception", + }, + ]); + assert.equal(patched.level, "applied"); + assert.equal( + (await adapter.readNativeScene("program")).state.defaults[ + "__lit.text.text_msxz43eh_1" + ], + "Installed reception", + ); + assert.equal((await adapter.readNativeScene("preview")).state, null); + const probe = await promisify(execFile)( + "go", + [ + "test", + "-count=1", + "-run", + "TestRealSharedNative", + "./internal/lsdpreception", + ], + { + cwd: resolve(orionRoot), + env: { ...process.env, ORION_TEST_LSDP_NATIVE_ADDRESS: native.address }, + }, + ); + assert.match(probe.stdout, /ok/); + await adapter.stopNativeReception(); + assert.throws(() => process.kill(native.pid, 0)); + console.log( + JSON.stringify({ + result: "PASS", + nativePid: native.pid, + sharedResources: native.resources, + installedRuntime: "dist only; no source or node_modules", + prismAdapter: "actual native-reception.ts with installed-root provider", + orion: "actual Go TCP client/readiness/state route", + fullLSMLReceived: true, + explicitMutationApplied: true, + previewIsolated: true, + ownedProcessReaped: true, + }), + ); +} finally { + await adapter?.stopNativeReception(); + await rm(isolated, { recursive: true, force: true }); +} diff --git a/scripts/test-reception-recovery.mjs b/scripts/test-reception-recovery.mjs new file mode 100644 index 0000000..69c240f --- /dev/null +++ b/scripts/test-reception-recovery.mjs @@ -0,0 +1,122 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { unzipSync, strFromU8 } from "fflate"; +import { SolarReceptionServer } from "../dist/server/index.mjs"; +import { BrowserLSDP } from "../vendor/lsdp-native-browser/src/browser.js"; +const path = process.argv[2], + bytes = await readFile(path); +const original = JSON.parse(strFromU8(unzipSync(bytes)["scene.lsml"])); +const events = [], + states = [], + failures = []; +const server = new SolarReceptionServer({ + origins: ["http://127.0.0.1:8099"], + packageRoot: resolve("dist"), + probeIntervalMs: 0, + resources: { + "solar/program": { type: "solar.lsml/1", initial: original }, + "orion/state": { + type: "orion.state/1", + initial: { selection: "program", revision: 0 }, + }, + }, + onRecovery: (event) => events.push(event), + onFailure: (error) => failures.push(error.message), +}); +let peer, close; +try { + const first = await server.start(); + assert.equal(server.peers.size, 0, "no automatic resource mirror"); + close = await server.watch( + "orion/state", + (state) => states.push(state), + (error) => failures.push(error.message), + ); + // External native writer, like Orion: not a host.replace shortcut. + peer = new BrowserLSDP(first.websocketUrl); + await peer.ready; + const desired = structuredClone(original); + desired.defaults.recovery_probe = "producer-owned selection"; + await peer.transaction({ + kind: "route", + id: "f".repeat(32), + port: "solar/program", + type: "solar.lsml/1", + payload: desired, + }); + await peer.transaction({ + kind: "route", + id: "e".repeat(32), + port: "orion/state", + type: "orion.state/1", + payload: { selection: "program", revision: 2 }, + }); + const wait = async (predicate) => { + const until = Date.now() + 10000; + while (!predicate()) { + if (Date.now() > until) throw new Error("Recovery proof timed out"); + await new Promise((yes) => setTimeout(yes, 20)); + } + }; + await wait(() => states.some((s) => s.revision === 2)); + assert.equal(server.peers.size, 1, "only the explicitly requested watch"); + process.kill(first.pid); + await wait(() => events.some((e) => e.state === "ready")); + const recovered = await server.start(); + assert.notEqual(recovered.pid, first.pid); + assert.equal(recovered.address, first.address); + assert.equal(recovered.websocketUrl, first.websocketUrl); + assert.equal((await server.read("solar/program")).state, null); + assert.deepEqual((await server.read("orion/state")).state, { + selection: "program", + revision: 0, + }); + peer.close(); + peer = new BrowserLSDP(recovered.websocketUrl); + await peer.ready; + await peer.transaction({ + kind: "route", + id: "d".repeat(32), + port: "solar/program", + type: "solar.lsml/1", + payload: desired, + }); + assert.deepEqual((await server.read("solar/program")).state, desired); + await server.apply("orion/state", [ + { op: "replace", path: "/revision", value: 3 }, + ]); + await wait(() => states.some((s) => s.revision === 3)); + assert.deepEqual(failures, []); + await server.stop(); + assert.throws(() => process.kill(recovered.pid, 0)); + const clean = await server.start(); + assert.deepEqual((await server.read("solar/program")).state, original); + await server.stop(); + assert.throws(() => process.kill(clean.pid, 0)); + assert.deepEqual(await readFile(path), bytes); + console.log( + JSON.stringify({ + result: "PASS", + checks: [ + "no automatic resource mirror", + "lost scene restarts empty", + "producer republishes active selection", + "control state resets to declared seed", + "forced process death", + "same TCP/WS endpoints", + "automatic watch reconnect", + "continued mutations", + "explicit restart original", + "owned children reaped", + "archive unchanged", + ], + events, + failures, + }), + ); +} finally { + close?.(); + peer?.close(); + await server.stop(); +} diff --git a/scripts/test-reception-server.mjs b/scripts/test-reception-server.mjs new file mode 100644 index 0000000..92f7476 --- /dev/null +++ b/scripts/test-reception-server.mjs @@ -0,0 +1,230 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { randomBytes } from "node:crypto"; +import { resolve } from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { unzipSync, strFromU8 } from "fflate"; +import { + SolarReceptionServer, + packagedNativeBinary, +} from "../dist/server/index.mjs"; +import { BrowserLSDP } from "../vendor/lsdp-native-browser/src/browser.js"; +import { readNative } from "./native-lsdp-tools.mjs"; + +const archivePath = process.argv[2]; +if (!archivePath) + throw new Error("Usage: test-reception-server "); +const originalArchive = await readFile(archivePath); +const original = JSON.parse( + strFromU8(unzipSync(originalArchive)["scene.lsml"]), +); +const options = { + origins: ["http://127.0.0.1:8099"], + packageRoot: resolve("dist"), + probeIntervalMs: 0, + resources: { + "solar/program": { type: "solar.lsml/1", initial: original }, + "solar/preview": { type: "solar.lsml/1", initial: null }, + "solar/generations": { type: "orion.state/1", initial: {} }, + "solar/sessions": { type: "orion.state/1", initial: {} }, + "orion/state": { type: "orion.state/1", initial: { revision: 0 } }, + }, +}; +await packagedNativeBinary(options.packageRoot); +let failed; +const server = new SolarReceptionServer({ + ...options, + recoveryAttempts: 0, + onFailure: (error) => { + failed?.(error); + }, +}); +let subscriber; +try { + const [first, concurrent] = await Promise.all([ + server.start(), + server.start(), + ]); + assert.equal(first.pid, concurrent.pid); + await server.check(); + if (process.argv[3]) { + const result = await promisify(execFile)( + "go", + [ + "test", + "-v", + "-count=1", + "-run", + "TestRealSharedNative|TestRealNativeProducers|TestRealNativeBlueStructuralMutation|TestRealNativeTransportRecovery", + "./internal/lsdpreception", + ], + { + cwd: resolve(process.argv[3]), + env: { + ...process.env, + ORION_TEST_LSDP_NATIVE_ADDRESS: first.address, + ORION_TEST_LSML_PATH: resolve(archivePath).replace( + /\.lsmlz$/, + ".lsml", + ), + }, + }, + ); + console.log(result.stdout.trim()); + for (const [target, resource] of Object.entries(options.resources)) + await server.replace(target, resource.initial); + } + assert.deepEqual((await server.read("solar/program")).state, original); + subscriber = new BrowserLSDP(first.websocketUrl); + await subscriber.ready; + const baseline = await readNative(subscriber, "solar/program"); + let observed; + subscriber.options.onTransaction = async (event) => { + observed?.(event); + return { level: "applied" }; + }; + await subscriber.transaction({ + kind: "subscribe", + target: "solar/program", + stateHash: baseline.stateHash, + }); + let timer; + const eventPromise = new Promise((yes, no) => { + observed = yes; + timer = setTimeout(() => no(new Error("Missing native diff event")), 5000); + }); + const desired = structuredClone(original); + desired.defaults ??= {}; + desired.defaults["__lit.text.text_msxz43eh_1"] = "Full LSML, native diff"; + const id = randomBytes(16).toString("hex"); + const receipt = await server.replace("solar/program", desired, id); + const event = await eventPromise.finally(() => clearTimeout(timer)); + assert.equal(receipt.status, "completed"); + assert.deepEqual(event.mutation.operations, [ + { + op: Object.hasOwn(original.defaults ?? {}, "__lit.text.text_msxz43eh_1") + ? "replace" + : "add", + path: "/defaults/__lit.text.text_msxz43eh_1", + value: "Full LSML, native diff", + }, + ]); + assert.deepEqual(await server.replace("solar/program", desired, id), receipt); + await assert.rejects( + server.replace("solar/program", original, id), + /ID_REUSED/, + ); + assert.deepEqual((await server.read("solar/program")).state, desired); + assert.deepEqual((await server.read("orion/state")).state, { revision: 0 }); + assert.equal((await server.read("solar/preview")).state, null); + await server.replace("orion/state", { revision: 1, runtime: "Orion" }); + assert.deepEqual((await server.read("solar/program")).state, desired); + await server.apply("orion/state", [ + { op: "replace", path: "/revision", value: 2 }, + ]); + assert.deepEqual((await server.read("orion/state")).state, { + revision: 2, + runtime: "Orion", + }); + await assert.rejects( + server.apply( + "orion/state", + [{ op: "replace", path: "/revision", value: 3 }], + { beforeHash: baseline.stateHash }, + ), + /BASE_MISMATCH/, + ); + await assert.rejects( + server.apply("orion/state", [ + { op: "replace", path: "/revision", value: 3 }, + { op: "test", path: "/runtime", value: "bad" }, + ]), + /TEST_FAILED/, + ); + assert.deepEqual((await server.read("orion/state")).state, { + revision: 2, + runtime: "Orion", + }); + await server.replace("solar/preview", original); + assert.deepEqual((await server.read("solar/preview")).state, original); + // Actual native watch used by Prism's overlay reconciler: fragmented seed, + // routed full-state diff, applied leaf mutation and explicit unsubscribe. + const watched = []; + let watchFailure; + const stopWatch = await server.watch( + "orion/state", + (state) => watched.push(structuredClone(state)), + (error) => { + watchFailure = error; + }, + ); + await server.replace("orion/state", { overlay_running_test: true }); + await server.apply("orion/state", [ + { op: "add", path: "/overlay_on_air_test", value: false }, + ]); + const deadline = Date.now() + 5000; + while (watched.at(-1)?.overlay_on_air_test !== false && Date.now() < deadline) + await new Promise((resolve) => setTimeout(resolve, 20)); + assert.equal(watchFailure, undefined); + assert.deepEqual(watched.at(-1), { + overlay_running_test: true, + overlay_on_air_test: false, + }); + stopWatch(); + subscriber.close(); + subscriber = null; + await Promise.all([server.stop(), server.stop()]); + assert.throws(() => process.kill(first.pid, 0)); + const restarted = await server.start(); + assert.notEqual(restarted.pid, first.pid); + assert.deepEqual((await server.read("solar/program")).state, original); + assert.deepEqual((await server.read("orion/state")).state, { revision: 0 }); + assert.equal((await server.read("solar/preview")).state, null); + const death = new Promise((yes) => { + failed = yes; + }); + process.kill(restarted.pid); + assert.match((await death).message, /LSDP_PROCESS_EXITED|CONNECTION_LOST/); + await assert.rejects(server.read("solar/program"), /LSDP_NOT_READY/); + await server.stop(); + assert.deepEqual(await readFile(archivePath), originalArchive); + const missing = new SolarReceptionServer({ + ...options, + binaryPath: resolve("dist/native/absent.exe"), + }); + await assert.rejects(missing.start(), /ENOENT/); + await missing.stop(); + const cancelled = new SolarReceptionServer(options); + const pending = cancelled.start(); + await cancelled.stop(); + await assert.rejects(pending); + await cancelled.stop(); + console.log( + JSON.stringify({ + result: "PASS", + wire: first.wire, + resources: first.resources, + nativeDiffOperations: event.mutation.operations, + lifecycle: [ + "coalesced-start", + "ready-handshake", + "fragmented-read", + "resource-isolation", + "stable-retry", + "stale-rejection", + "atomic-rejection", + "native-control-subscription", + "stop-reaped", + "restart-original", + "unexpected-exit", + "missing-binary", + "cancelled-start", + ], + originalUnchanged: true, + }), + ); +} finally { + subscriber?.close(); + await server.stop(); +} diff --git a/src/atlas/atlas-node.ts b/src/atlas/atlas-node.ts deleted file mode 100644 index c28fbe0..0000000 --- a/src/atlas/atlas-node.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Atlas band-split transform — the Solar render side of the ADR 013 Prism -// texture-atlas broadcast mode (§3.1, issue #38). -// -// AUTHORING MODEL. Since ADR 007 (Lumencast convergence) Solar holds no -// render tree of its own: it is a thin adapter over `@lumencast/runtime`, and -// authored content is expressed as `RenderBundle` fragments built by pure -// functions (`buildWipeCoverNode`, `buildAnimationNode`). `buildAtlasRoot` is -// the same shape — a pure `RenderNode → RenderNode` transform, added as a -// composition of the EXISTING `frame` primitive, introducing NO new runtime -// primitive (that would be an upstream-runtime change, out of scope). It is -// consumed by the render-bundle serving path (Orion / the scene-server) when -// it serves an `?atlas=`-scoped URL: the served bundle's `root` is wrapped by -// this transform so the runtime renders the stacked bands unchanged. -// -// WHAT IT DOES. Given the composed scene `root` and a parsed {@link AtlasSpec} -// of N band labels, it splits `root`'s direct children into N contiguous -// z-bands at the NATIVE CAPTURE separators (`x-zab.capture` nodes — the -// on-air PLACEHOLDER that marks where a native OBS source will be composited), -// then stacks each band in its own vertical slice of a 1920×(1080·N) frame: -// -// band i → a transparent `frame` at y = i·1080, size 1920×1080, -// painting ONLY the children on band i's side of the seuil(s). -// -// The z-order the author declared is DOM order among `root`'s children (the -// runtime paints siblings in order — later = on top; there is no numeric -// z-index in the model). Everything BELOW the first capture → band 0, between -// two captures → band 1, …, above the last capture → band N-1. The capture -// separators themselves are NOT painted by any band: the native source -// replaces them, so a Solar band paints only real scenic components (ADR 013 -// §3.1 "tout ce qui est sous la première capture native → bande 0"). -// -// TRANSPARENCY. No band nor the outer stage carries a `background`, so every -// slice is fully transparent (the frame primitive paints nothing without a -// declared fill) — the unchanged broadcast-overlay behaviour, and the ADR -// requirement (each band composites over the native sources beneath it). - -import type { RenderNode } from "@lumencast/runtime"; -import type { AtlasSpec } from "./atlas-param"; - -/** Logical height of one z-band slice, in CSS px (ADR 013 §3.1 — 1920×1080 - * per band). */ -export const ATLAS_BAND_HEIGHT = 1080; - -/** Logical width of the atlas stage, in CSS px (ADR 013 §3.1). */ -export const ATLAS_STAGE_WIDTH = 1920; - -/** The native-capture primitive kind whose position in the scene tree marks - * a z-band boundary (the seuil K): the native OBS source is composited where - * this placeholder sits, between two DOM bands. */ -const CAPTURE_KIND = "x-zab.capture"; - -export interface BuildAtlasRootOptions { - /** Height of one band slice in CSS px. Defaults to {@link ATLAS_BAND_HEIGHT} - * (1080). Override for a non-1080 authoring resolution. */ - bandHeight?: number; - /** Width of the atlas stage in CSS px. Defaults to - * {@link ATLAS_STAGE_WIDTH} (1920). */ - width?: number; -} - -/** - * Wrap a composed scene `root` into the ADR 013 atlas layout: N vertically - * stacked, transparent z-bands in a 1920×(1080·N) frame, each band painting - * only the children on its side of the native-capture seuil(s). - * - * The number of bands is derived from the scene AND cross-checked against the - * requested spec: a scene with `c` `x-zab.capture` separators splits into - * `c + 1` bands, which MUST equal `spec.bands.length`. A mismatch means the - * `?atlas=` URL disagrees with the scene the server is about to serve — a - * caller (scene-server) bug, surfaced as a thrown `Error` rather than a - * silently miscut frame. - * - * @param root the composed scene root (its direct children are the z-ordered - * scenic components; DOM order is z-order). - * @param spec the parsed `?atlas=` band labels (bottom → top), see - * {@link parseAtlasParam}. - */ -export function buildAtlasRoot( - root: RenderNode, - spec: AtlasSpec, - options: BuildAtlasRootOptions = {}, -): RenderNode { - const bandHeight = options.bandHeight ?? ATLAS_BAND_HEIGHT; - const width = options.width ?? ATLAS_STAGE_WIDTH; - - const children = root.children ?? []; - - // Seuils K: the indices of the native-capture separators among root's direct - // children. `c` separators → `c + 1` z-bands. - const separators: number[] = []; - children.forEach((child, index) => { - if (child.kind === CAPTURE_KIND) separators.push(index); - }); - - const expectedBands = separators.length + 1; - if (expectedBands !== spec.bands.length) { - throw new Error( - `solar.atlas: scene has ${String(separators.length)} native capture ` + - `separator(s) → ${String(expectedBands)} z-band(s), but ?atlas= ` + - `declared ${String(spec.bands.length)} band(s)`, - ); - } - - // Partition the children into contiguous groups AROUND the separators, - // dropping the separator nodes themselves (the native source replaces them). - const bandFrames: RenderNode[] = []; - let cursor = 0; - for (let i = 0; i < spec.bands.length; i++) { - // `separators[i]` is the i-th seuil, or `undefined` past the last one → - // the final band runs to the end. `?? ` is nullish (a seuil at index 0 is - // kept), so an empty leading band is handled correctly. - const end = separators[i] ?? children.length; - const bandChildren = children.slice(cursor, end); - // Advance past this band's children AND its trailing separator (if any). - cursor = end + 1; - - bandFrames.push({ - kind: "frame", - id: `atlas-band-${spec.bands[i]}`, - props: { - // `x`/`y` are applied by the frame primitive as a translate transform; - // y offsets this band into its vertical slice. No `background` → - // transparent. `clipsContent` (default true) keeps each band's paint - // inside its own 1080 slice. - x: 0, - y: i * bandHeight, - width, - height: bandHeight, - clipsContent: true, - }, - children: bandChildren, - }); - } - - return { - kind: "frame", - id: "atlas-root", - props: { - x: 0, - y: 0, - width, - height: bandHeight * spec.bands.length, - // The stage itself must not clip the bands to a single slice, and paints - // nothing (transparent) — it is a pure container. - clipsContent: false, - }, - children: bandFrames, - }; -} diff --git a/src/atlas/atlas-param.ts b/src/atlas/atlas-param.ts deleted file mode 100644 index 724ca7c..0000000 --- a/src/atlas/atlas-param.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Atlas render-mode URL parameter — the canonical `?atlas=` reader -// (ADR 013 Prism §3.1, issue #38). -// -// The texture-atlas broadcast mode (ADR 013) renders a scene NOT as a single -// 1920×1080 image, but as N stacked vertical z-bands in a taller -// 1920×(1080·N) image, so the host (Prism / OBS-ws) can intercalate NATIVE -// capture sources between the DOM bands with arbitrary z-order — a single CEF -// browser source, one Chromium tick, zero inter-process skew. -// -// A consumer opts in through the URL query it loads Solar with: -// -// ?atlas=below,above → 2 bands (image 1920×2160) -// ?atlas=below,mid,above → 3 bands (image 1920×3240) -// -// The band tokens are FREE-FORM labels ordered BOTTOM → TOP (first token = -// bottom slice, last token = top slice). Their only structural role is to -// name / key the emitted band frames; the number of tokens is N, the band -// count. The names carry meaning for the operator/authoring side (which -// native source sits under which band), never for the geometry. -// -// This module owns ONLY the param → spec parsing. The band-splitting -// transform lives in `./atlas-node` (`buildAtlasRoot`). Parsing is total and -// side-effect-free: an absent / malformed / degenerate value resolves to -// `null` so the caller falls back to the UNCHANGED default render path — the -// non-regression contract of issue #38 (the default, atlas-less bundle is -// byte-identical to today). - -/** Parsed `?atlas=` spec: the ordered band labels, bottom → top. */ -export interface AtlasSpec { - /** Band labels, bottom slice first. `length` is N, the band count. */ - bands: readonly string[]; -} - -/** - * Parse the raw `?atlas=` query value into an {@link AtlasSpec}, or `null` - * when atlas mode is NOT requested / the value is not recognised. - * - * `null` is returned for every degenerate input so the caller keeps the - * default (non-atlas) render path exactly as before: - * - `null` / `undefined` / empty / whitespace-only → not requested; - * - no non-empty token after splitting on `,` → nothing to render; - * - a duplicate band label → ambiguous (labels key the band frames and - * must be unique), treated as unrecognised rather than silently coerced. - * - * Tokens are trimmed and empties dropped, so `?atlas=below, ,above` yields - * `["below", "above"]` and `?atlas=,` yields `null`. - */ -export function parseAtlasParam( - raw: string | null | undefined, -): AtlasSpec | null { - if (raw === null || raw === undefined) return null; - const bands = raw - .split(",") - .map((token) => token.trim()) - .filter((token) => token.length > 0); - if (bands.length === 0) return null; - if (new Set(bands).size !== bands.length) return null; - return { bands }; -} diff --git a/src/dev-entry.ts b/src/dev-entry.ts new file mode 100644 index 0000000..e23b51d --- /dev/null +++ b/src/dev-entry.ts @@ -0,0 +1,43 @@ +// Local/e2e bootstrap. Every scene is acquired from ZabCanvas and rendered by Vision. + +import { createCanvasSceneSourceProvider } from "./scenes/canvas"; +import { mount } from "./mount"; +import type { SolarMode } from "./types"; + +const params = new URLSearchParams(window.location.search); +const nativeUrl = params.get("lsdp"); +if (!nativeUrl) + throw new Error( + "SOLAR_NATIVE_LSDP_REQUIRED: pass lsdp and resource query parameters", + ); +const token = params.get("token") ?? ""; +const modeParam = params.get("mode") ?? "broadcast"; +const mode: SolarMode = (["broadcast", "control", "test"] as const).includes( + modeParam as SolarMode, +) + ? (modeParam as SolarMode) + : "broadcast"; +const target = document.getElementById("scene"); +if (!(target instanceof HTMLElement)) throw new Error("SOLAR_TARGET_REQUIRED"); + +mount({ + target, + nativeLSDP: { + url: nativeUrl, + resource: params.get("resource") ?? "scene", + selector: params.get("selector") ?? undefined, + }, + token, + mode, + sceneSourceProvider: createCanvasSceneSourceProvider({ + apiUrl: + params.get("canvas_api") ?? "https://zabgate.cyell.dev/canvas/api/v1", + token: params.get("canvas_token") ?? token, + }), + liveAudio: mode === "broadcast" || mode === "test", + onError: (error) => + console.error(`[solar dev] ${error.code}: ${error.message}`), + onStatus: (status) => { + document.documentElement.dataset.solarStatus = status; + }, +}); diff --git a/src/dev-entry.tsx b/src/dev-entry.tsx deleted file mode 100644 index 5d2319a..0000000 --- a/src/dev-entry.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Dev / E2E harness entry — NOT part of the published bundle. -// -// `index.html` loads this module so `npm run dev` and the Playwright -// harness can mount() Solar against a local mock LSDP server. Reads URL query -// params (orion / token / mode / scene / session), falls back to the embedded -// local Orion loopback endpoint, and mounts. The production counterpart is generated by -// scripts/build-host-html.mjs (inlined into dist/index.html). -// -// Excluded from tsconfig.lib.json + the dts build + the Vite library -// entry — it never ships in solar.js. - -import { mount } from "./mount"; -import { resolveShowToken } from "./internal/resolve-show-token"; -import { atlasMountOptions } from "./internal/atlas-mount"; -import type { SolarMode } from "./types"; -// Self-hosted Geist / Geist Mono @font-face — Solar's page owns its fonts -// (Prism's editor CSS never reaches it). Must be on BOTH entries so preview -// AND REC/antenne render the picked family instead of a system fallback. -import "./styles/fonts.css"; - -const params = new URLSearchParams(window.location.search); -const orionUrl = - params.get("orion") ?? "ws://127.0.0.1:4007/orion/api/v1/show/stream"; -// Mirror the host entry: the show-token may be embedded in `orionUrl`'s -// query rather than a top-level `?token=` (the Pulsar browser-source shape). -const token = resolveShowToken(orionUrl, params.get("token")); -const modeParam = params.get("mode") ?? "broadcast"; -const mode: SolarMode = ( - ["broadcast", "control", "test"] as const -).includes(modeParam as SolarMode) - ? (modeParam as SolarMode) - : "broadcast"; -const scene = params.get("scene") ?? undefined; -const testSession = params.get("session") ?? undefined; - -const target = document.getElementById("scene"); -if (!(target instanceof HTMLElement)) { - throw new Error("dev-entry : #scene target missing"); -} - -mount({ - target, - orionUrl, - token, - mode, - ...(mode === "test" && scene ? { scene } : {}), - ...(mode === "test" && testSession ? { testSession } : {}), - // ADR 013 Prism §3.1 (issue #41) — `?atlas=` opts into the texture-atlas - // z-band render. Absent/malformed → no `transformRoot` key → verbatim render. - ...atlasMountOptions(window.location.search), - onError: (err) => { - // Dev only — surface to the console. Broadcast hosts must not render - // chrome ; the operator overlay (control/test modes) is the runtime's - // own concern now. Log the SolarError fields explicitly (it is a plain - // object, not an Error) so `code`/`message` never collapse to - // `[object Object]` in a string-coercing console bridge. - console.error( - `[solar dev] ${err.code}: ${err.message}` + - (err.recoverable ? " (recoverable)" : " (fatal)"), - ); - }, -}); diff --git a/src/engine/README.md b/src/engine/README.md new file mode 100644 index 0000000..d4d9a0e --- /dev/null +++ b/src/engine/README.md @@ -0,0 +1,165 @@ +# LSDP and Vision runtime + +`native-lsdp-runtime.ts` connects to the real Lumencast Rust server through the +portable draft2 binary WebSocket client. A fragmented `state.read` loads the +full LSML resource, then a hash-pinned subscription delivers atomic tree or +application mutations. `scenes/native-document.ts` prepares temporary render +packages in memory and leaves source/Blue pins unchanged. Flat defaults use +Vision patches; structure edits use the same hidden-canvas frame swap. Its +snapshot, structural and command ACKs wait for Vision submission. Scalar state +notifications return `received` only after atomic LSML/integrity checks. Disconnect, stale state and failed +application recover through a fresh resource snapshot. The camera pool spans +document rebuilds. See `docs/development/native-lsdp.md` for local operation. +If preparation advances the resource between read and subscribe, BASE_MISMATCH +causes a fresh read/render/hash check, bounded to four attempts on that connection. +Other failures and repeated conflicts remain visible and reconnect normally. +`native-worker-client.ts` owns the dedicated `native-worker.ts` transport and +LSML verification thread. It certifies each state through its private Worker +channel; the render consumer reuses that exact structured-clone state instead of +repeating Merkle hashing on the drawing thread. Certification is scoped to the +callback context and removed after delivery, so network fields or caller-created +contexts cannot claim it. Direct reception without a Worker retains full hashing. +Simple state reception continues while CEF draws; +snapshot, structure and command receipts still await the main consumer. Both +sides abort on disconnect, including pending connection/request work. A snapshot +can bypass the main notification queue to complete same-connection resync. +`internal/native-stream.ts` owns notification identity/order/integrity and scalar +classification. The worker uses a local SHA-256 implementation for native frame +checksums; the byte-pinned upstream client still compares every checksum. This +does not change the window's crypto implementation or any native protocol bytes. +`internal/native-state.ts` applies atomic operations by copying only their paths. +Unchanged immutable subtrees retain their draft2 Merkle digests; changes to existing +object keys update only the affected persistent map branches. Incoming values, +whole-resource node/depth budgets and notification before/after hashes remain +checked. `frame-patches.ts` merges scalar state through a microtask with +one GPU submission in flight. It retains every transaction identity and emits +`solar:lsdp-applied` only after the covering Vision frame is submitted, including +`presentation.throughSequence`, final hash and mutation count. Earlier +`solar:lsdp-received` proves LSML application, never displayed pixels. Structure, +camera/animation commands and coordinated transitions drain pending frames first. +Ordinary mutation reception, application and presentation are producer-independent: +Solar never queries `orion/state` or waits for Orion for these mutations. Events +carry native transaction/state identity, without an `x-orion` projection. Only +explicit scene-selection transition phases use `control-feedback.ts`; this is +not a mutation application or presentation dependency. +Reserved `__cam.*` leaves stay in native authority, outside Vision defaults; +positional camera names therefore cannot invalidate Vision's binding grammar. +Native document replacement pauses video uploads and drains GPU work while +the new package is loaded into the persistent Vision engine. The same +media controller and tracks then resume into the new layer. CEF validation +must assert static images, text and panels as well as the camera and mutation +receipt; camera-only output is a failed scene. + +Collections retain their full resource hash while `selector` projects exactly +one generation/session into Vision. Updates to another entry are acknowledged +without changing the visible scene. Missing or deleted entries clear the +renderer. The old Orion LSDP/1 client has been removed from Solar. + +`vision-presenter.ts` owns one Vision engine, GPU canvas and presentation surface +per native host target. An empty engine starts once; fresh scene packages use +Vision's serialized `load` request on that same device/surface. The outgoing +scene's images, graph, geometry and composition caches are released. +The engine-owned font registry survives scene replacement: installation fonts +are preloaded once at engine startup, and additional scene fonts are admitted +by content digest only when new. Scene-local layout and glyph caches remain fresh. +There is no inactive source or scene retention and A-B-A loads A freshly. +Runtime packages exclude the separately admitted flat font files. WebCrypto +identifies supplied font bytes; only acknowledged digests are reused in this +engine session. New fonts still pass Rust decoding and registration limits. +Package preparation and host font discovery run concurrently. A live-media clear +acknowledges without drawing when Rust reports that its active texture was already +transparent; clearing actual live pixels still submits a frame. +`native-scene-frames.ts` owns verified source acquisition and temporary packages. +Scene handles close their observers and pending work on retirement without +terminating the host engine. Stale handles cannot submit into a newer scene. +Clearing the active scene keeps the engine ready; host disconnect terminates it. +Explicit WebGL context-loss recovery remains distinct from a scene switch. +Absolute native assignments compare accepted +documents before rendering, so finalization does not mount the prepared scene again. +Preparation carries the current and candidate sources; compact commit/finalize +assignments carry the candidate at the resource root. A fresh renderer reconstructs +that stage from the root, while older payloads with an embedded source remain valid. +Bounded `solar:native-stage` measures separate worker verification/delivery, +classification, source/package/fonts, arena reuse, image resolution and feedback. +Their numeric durations and reuse categories contain no source values or tokens. +Scene assets remain in LSMLZ and Vision +loads them directly. There is no render-bundle URL, compile step, React render +tree or renderer selection flag. +`canvas-presentation.ts` retains a synchronized front canvas for CEF. Only actual +Vision `frame-submitted` messages copy the complete GPU surface; the engine's +desynchronized canvas remains hidden. Copy failure rejects the pending presentation +receipt. Camera frames use the same boundary, keeping partial GPU draws out of +the broadcast compositor. + +`vision-frames.ts` owns the submission barrier and uses a MessageChannel task +instead of the host's 60 Hz RAF. Pending scalar values and the latest owned +bitmap per camera can share one actual frame; every caller waits for completion. +Camera-only work waits at most one 120 Hz interval for state to join it; a patch +or explicit lifecycle barrier bypasses that grace. Superseded and consumed +bitmaps are released, and failure rejects all covered callers. The supported +`loadWasm` dependency captures the real engine in `vision-media-bridge.ts`, +allowing camera upload before the same patch submission. This scheduling +interval is a policy, not evidence that the engine achieves 120 completed +mutation frames/s; fixed-window CEF measurements establish that separately. +The retained Vision candidate, independent encoded-counter checks and fixed +measurement boundaries are documented in +[`render-cadence.md`](../../docs/development/render-cadence.md). + +The temporary render package gives text-bound scene variables private string +aliases. Blue may return a numeric score while LSML uses it as text; the original +numeric value remains available to geometry and other bindings. Patch updates +refresh both uses without changing the authoritative native document, source +archive or Blue pins. Repeater row bindings retain their own context. + +Image-bound URLs are resolved by Solar against the LSML `assets.allowedHosts` +policy without forwarding credentials. The bytes become hashed assets in the +temporary RAM archive, while the native document retains the original URL. +Independent image fetches and decodes run together. New images are admitted as +immutable content-addressed assets into the retained engine before one state/GPU +submission. Rust verifies hashes, encoded dimensions and resource budgets; rejected +assets/patches preserve the accepted scene. The archive and Blue source pins do not +change. The media controller and physical camera tracks remain acquired. + +The bundled Vision engine must render canonical LSML frame `background` and +`backgrounds`, including nested panels and bound color changes. Rendering text, +images and cameras without those surfaces is an incomplete scene and does not +pass the CEF visual check. + +`live-media.ts` maps Vision's LSML live-source descriptors to Solar-owned +capture streams or the receive-only peer viewer. Every camera frame is fitted +to its declared texture and sent to Vision as an `ImageBitmap`; disconnects +clear the texture. Local references are resolved through `sources/capture.ts`; +peer slots use `sources/peers.ts`. Neither source kind is gated on broadcast +mode. + +The active scene record keeps the ZabCanvas Blue manifest pinned to the source +revision. This is the input needed by the synchronized local scene store; Blue +program invocation remains server-owned and its resulting scene changes travel +as LSDP mutations. The host can now compose the verified source cache provider; +application startup synchronization remains outside this renderer. + +`mount.ts` is the sole public lifecycle boundary. `disconnect()` tears down the +socket, in-flight LSMLZ load, peer sessions, capture streams and Vision presenter. +`setToken()` rotates the native subscription credential and reopens from a fresh +hash-verified snapshot. Canvas credentials remain owned by SceneSourceProvider; +peer credentials are refreshed from reserved native leaves or the host channel. + +Run `npm run typecheck`, `npm run lint`, `npm test`, `npm run build` and +`npm run check:bundle` after changing these paths. Hardware cameras and the live +CEF compositor still need a host-level smoke test. + +`animations.ts` consumes native LSDP `__animation.` command leaves and the +LSML animation catalogue `{id:{target,keyframes:{duration_ms,easing,steps}}}`. +Opacity, rotation and blur use private Vision bindings; translation/scale use +the existing scene swap from original geometry. The source stays untouched. +Named CSS easing and cubic-bezier are sampled from the authored curve. Unsupported +channels/easing fail explicitly before rendering. Only one animation submission +is in flight, repeated commands are deduplicated, and a new command replays. +Final frames survive an unrelated structural mutation, but clear on scene change +or removal. Geometry swaps are more expensive than scalar GPU patches. + +Persistent font admission accepts owned `VerifiedFont` snapshots as well as raw +font arrays. Only the owned snapshot may reuse its verified digest; mutable caller +arrays are copied and hashed. An already acknowledged digest causes no transfer +copy or WASM admission. Host manifest acquisition still occurs on each scene load +so changes to the registry remain observable. diff --git a/src/engine/animations.ts b/src/engine/animations.ts new file mode 100644 index 0000000..546badc --- /dev/null +++ b/src/engine/animations.ts @@ -0,0 +1,211 @@ +import type { LSMLDocument } from "../scenes/native-document"; + +type RecordValue = Record; +export interface AnimationAsset { + target: string; + keyframes: { duration_ms: number; easing?: string; steps: RecordValue[] }; +} +export type AnimationFrame = { target: string; values: Record }; +export const animationLeaf = (target: string, property: string): string => + `__solar.animation.${encodeURIComponent(target)}.${property}`; +const object = (value: unknown): RecordValue | null => + value && typeof value === "object" && !Array.isArray(value) + ? value as RecordValue : null; +const channels = new Set(["opacity", "rotation", "rotate", "blur", "translateX", "translateY", "x", "y", "scale", "scaleX", "scaleY"]); + +function values(step: RecordValue): Record { + const result: Record = {}; + for (const [property, raw] of Object.entries(step)) { + if (property === "at") continue; + let name = property, value = raw; + if (property === "filter") { + const match = typeof raw === "string" && /^blur\(([\d.]+)px\)$/.exec(raw); + if (!match) throw new Error("Vision animation filter must be blur(px)."); + name = "blur"; value = Number(match[1]); + } + if (!channels.has(name) || typeof value !== "number" || !Number.isFinite(value)) + throw new Error(`Unsupported Vision animation channel: ${property}`); + if (name.startsWith("scale") && value < 0) throw new Error("Negative animation scale is unsupported."); + result[name === "rotate" ? "rotation" : name] = value; + } + return result; +} +export function animationAssets(document: LSMLDocument): Record { + const catalogue = object(document.animations) ?? object(object(document.layout)?.animations) ?? {}; + const result: Record = {}; + for (const [id, raw] of Object.entries(catalogue)) { + const asset = object(raw), keyframes = object(asset?.keyframes); + if (typeof asset?.target !== "string" || !keyframes || !Array.isArray(keyframes.steps) || + typeof keyframes.duration_ms !== "number" || !Number.isFinite(keyframes.duration_ms) || keyframes.duration_ms < 0 || keyframes.steps.length < 1) + throw new Error(`Invalid LSML animation: ${id}`); + eased(0, String(keyframes.easing ?? "linear")); + let previous = -1; + for (const rawStep of keyframes.steps) { + const step = object(rawStep); + if (!step || typeof step.at !== "number" || !Number.isFinite(step.at) || step.at < previous || step.at < 0 || step.at > 1) + throw new Error(`Invalid animation keyframe: ${id}`); + values(step); previous = step.at; + } + result[id] = asset as unknown as AnimationAsset; + } + return result; +} +function eased(t: number, easing: string): number { + if (easing === "linear") return t; + const named: Record = { + ease: [0.25, 0.1, 0.25, 1], "ease-in": [0.42, 0, 1, 1], + "ease-out": [0, 0, 0.58, 1], "ease-in-out": [0.42, 0, 0.58, 1], + }; + const match = /^cubic-bezier\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+)\s*\)$/.exec(easing); + const curve = named[easing] ?? (match ? match.slice(1).map(Number) : null); + if (!curve || curve.some(value => !Number.isFinite(value)) || curve[0]! < 0 || curve[0]! > 1 || curve[2]! < 0 || curve[2]! > 1) + throw new Error(`Unsupported animation easing: ${easing}`); + if (t === 0 || t === 1) return t; + const bezier = (u: number, a: number, b: number): number => 3 * (1-u)**2 * u * a + 3 * (1-u) * u**2 * b + u**3; + let low = 0, high = 1; + for (let iteration = 0; iteration < 24; iteration++) { + const middle = (low + high) / 2; + if (bezier(middle, curve[0]!, curve[2]!) < t) low = middle; + else high = middle; + } + return bezier((low + high) / 2, curve[1]!, curve[3]!); +} +export function sampleAnimation(asset: AnimationAsset, progress: number): Record { + const result: Record = {}; + const steps = asset.keyframes.steps.map(step => ({ at: step.at as number, values: values(step) })); + const properties = new Set(steps.flatMap(step => Object.keys(step.values))); + for (const property of properties) { + const points = steps.filter(step => Object.hasOwn(step.values, property)); + const right = points.find(point => point.at >= progress) ?? points.at(-1)!; + const left = [...points].reverse().find(point => point.at <= progress) ?? points[0]!; + const interval = right.at - left.at; + const phase = interval > 0 ? eased(Math.max(0, Math.min(1, (progress - left.at) / interval)), asset.keyframes.easing ?? "linear") : 0; + result[property] = left.values[property]! + (right.values[property]! - left.values[property]!) * phase; + } + return result; +} +function visit(value: unknown, callback: (node: RecordValue) => void): void { + if (Array.isArray(value)) { value.forEach(item => visit(item, callback)); return; } + const node = object(value); if (!node) return; + callback(node); + if (node.children) visit(node.children, callback); + if (node.template) visit(node.template, callback); +} +/** Private bindings only: the original published LSML remains untouched. */ +export function prepareAnimationBindings(document: LSMLDocument): Record { + const aliases: Record = {}; + const targets = new Set(Object.values(animationAssets(document)).map(asset => asset.target)); + const defaults = document.defaults ??= {}; + const found = new Set(); + visit(document.layout, node => { + if (typeof node.id !== "string" || !targets.has(node.id)) return; + found.add(node.id); + const bind = object(node.bind) ?? {}; + for (const property of ["opacity", "rotation", "blur"]) { + const existing = typeof bind[property] === "string" ? defaults[bind[property] as string] : node[property]; + const path = animationLeaf(node.id, property); + if (typeof bind[property] === "string" && bind[property] !== path) aliases[path] = bind[property] as string; + defaults[path] = typeof existing === "number" ? existing : property === "opacity" ? 1 : 0; + bind[property] = path; + } + node.bind = bind; + }); + for (const target of targets) if (!found.has(target)) throw new Error(`Animation target not found: ${target}`); + return aliases; +} +export function hasGeometry(frame: AnimationFrame): boolean { + return Object.keys(frame.values).some(key => !["opacity", "rotation", "blur"].includes(key)); +} +/** Geometry channels use the existing source-preserving Vision scene swap. */ +export function animationDocument(document: LSMLDocument, frames: readonly AnimationFrame[]): LSMLDocument { + const variant = structuredClone(document); + // The package preparer owns private bindings. Adding them before an empty + // animation snapshot changes the retention key of an otherwise identical source. + if (frames.length === 0) return variant; + prepareAnimationBindings(variant); + for (const frame of frames) { + for (const property of ["opacity", "rotation", "blur"]) if (frame.values[property] !== undefined) + variant.defaults![animationLeaf(frame.target, property)] = frame.values[property]; + visit(variant.layout, node => { + if (node.id !== frame.target) return; + const base = object(node.position) ?? {}; + node.position = { + x: Number(base.x ?? 0) + (frame.values.translateX ?? frame.values.x ?? 0), + y: Number(base.y ?? 0) + (frame.values.translateY ?? frame.values.y ?? 0), + }; + const sx = frame.values.scaleX ?? frame.values.scale ?? 1; + const sy = frame.values.scaleY ?? frame.values.scale ?? 1; + if (sx < 0 || sy < 0) throw new Error("Negative animation scale is unsupported."); + if (sx !== 1 || sy !== 1) visit(node, child => { + const size = object(child.size); if (size) child.size = { ...size, w: Number(size.w) * sx, h: Number(size.h) * sy }; + const position = object(child.position); if (position && child !== node) child.position = { ...position, x: Number(position.x ?? 0) * sx, y: Number(position.y ?? 0) * sy }; + const style = object(child.style); if (style && typeof style.fontSize === "number") child.style = { ...style, fontSize: style.fontSize * sy }; + }); + }); + } + return variant; +} + +/** One queued GPU submission at a time; replay, replacement and teardown are bounded. */ +export class VisionAnimations { + private document: LSMLDocument | null = null; + private readonly seen = new Map(); + private readonly playing = new Map(); + private readonly frames = new Map(); + private scheduled: number | null = null; + private inFlight = false; + private generation = 0; + private stopped = false; + constructor(private readonly apply: (frames: readonly AnimationFrame[]) => Promise, private readonly error: (error: unknown) => void) {} + update(document: LSMLDocument): void { + if (this.document?.scene_id !== document.scene_id || this.document?.scene_version !== document.scene_version) this.reset(); + this.document = document; + const assets = animationAssets(document); + for (const [key, raw] of Object.entries(document.defaults ?? {})) { + if (!key.startsWith("__animation.")) continue; + const command = object(raw); if (!command) throw new Error("Invalid Blue animation command."); + const identity = JSON.stringify(command); + if (this.seen.get(key) === identity) continue; + const asset = assets[String(command.animation_id)]; + if (!asset) throw new Error(`Blue animation not declared: ${String(command.animation_id)}`); + this.seen.set(key, identity); + this.playing.set(asset.target, { asset, started: performance.now() }); + } + const targets = new Set(Object.values(assets).map(asset => asset.target)); + for (const target of this.frames.keys()) if (!targets.has(target)) this.frames.delete(target); + for (const target of this.playing.keys()) if (!targets.has(target)) this.playing.delete(target); + if (!this.stopped && this.playing.size && this.scheduled === null && !this.inFlight) this.schedule(); + } + private schedule(): void { + const generation = this.generation; + this.scheduled = requestAnimationFrame(now => { + this.scheduled = null; + if (this.stopped || generation !== this.generation) return; + for (const [target, play] of this.playing) { + const progress = play.asset.keyframes.duration_ms === 0 ? 1 : Math.min(1, (now - play.started) / play.asset.keyframes.duration_ms); + this.frames.set(target, { target, values: sampleAnimation(play.asset, progress) }); + if (progress >= 1) this.playing.delete(target); + } + this.inFlight = true; + void this.apply([...this.frames.values()]).catch(error => { + if (generation === this.generation) { this.playing.clear(); this.error(error); } + }).finally(() => { + this.inFlight = false; + if (!this.stopped && this.playing.size && this.scheduled === null) this.schedule(); + }); + }); + } + current(document?: LSMLDocument): readonly AnimationFrame[] { + if (!document) return [...this.frames.values()]; + if (this.document?.scene_id !== document.scene_id || this.document?.scene_version !== document.scene_version) return []; + const targets = new Set(Object.values(animationAssets(document)).map(asset => asset.target)); + return [...this.frames.values()].filter(frame => targets.has(frame.target)); + } + private reset(): void { + this.generation++; + if (this.scheduled !== null) cancelAnimationFrame(this.scheduled); + this.scheduled = null; this.playing.clear(); this.frames.clear(); this.seen.clear(); + } + dispose(): void { this.stopped = true; this.reset(); this.document = null; } + clear(): void { this.reset(); this.document = null; } +} diff --git a/src/engine/canvas-presentation.ts b/src/engine/canvas-presentation.ts new file mode 100644 index 0000000..5319286 --- /dev/null +++ b/src/engine/canvas-presentation.ts @@ -0,0 +1,32 @@ +/** CEF samples the retained front canvas, never Vision's in-progress GPU surface. */ +export class CanvasPresentation { + readonly canvas = document.createElement("canvas"); + private readonly context: CanvasRenderingContext2D; + + constructor(private readonly source: HTMLCanvasElement) { + this.canvas.setAttribute("aria-hidden", "true"); + Object.assign(this.canvas.style, { + display: "block", + width: "100%", + height: "100%", + }); + const context = this.canvas.getContext("2d", { + alpha: true, + desynchronized: false, + }); + if (!context) throw new Error("Solar compositor canvas is unavailable."); + this.context = context; + } + + submit(): void { + const { width, height } = this.source; + if (width < 1 || height < 1) + throw new Error("Vision submitted an empty surface."); + if (this.canvas.width !== width || this.canvas.height !== height) { + this.canvas.width = width; + this.canvas.height = height; + } + this.context.globalCompositeOperation = "copy"; + this.context.drawImage(this.source, 0, 0, width, height); + } +} diff --git a/src/engine/control-feedback.ts b/src/engine/control-feedback.ts new file mode 100644 index 0000000..4c4b255 --- /dev/null +++ b/src/engine/control-feedback.ts @@ -0,0 +1,70 @@ +import { BrowserLSDP } from "../../vendor/lsdp-native-browser/src/browser.js"; +import { nativeTreeHash } from "../internal/native-tree"; + +/** Explicit renderer acknowledgement, separate from the native node's receipt. */ +export async function presentationFeedback( + url: string, + lane: "program" | "preview", + value: unknown, + parent: AbortSignal, +): Promise { + const signal = AbortSignal.any([parent, AbortSignal.timeout(5000)]); + const peer = new BrowserLSDP(url); + const close = (): void => peer.close(); + signal.addEventListener("abort", close, { once: true }); + try { + await peer.ready; + for (let attempt = 0; attempt < 4; attempt++) { + let stateHash = ""; + let resolveSnapshot!: () => void; + let rejectSnapshot!: (error: unknown) => void; + const snapshot = new Promise((resolve, reject) => { + resolveSnapshot = resolve; + rejectSnapshot = reject; + }); + void snapshot.catch(() => {}); + peer.options.onClose = rejectSnapshot; + peer.options.onIncomingFailed = (_context, error) => + rejectSnapshot(error); + peer.options.onTransaction = async (state, context) => { + if ( + context.metadata.profile !== "lsdp.state.read/1" || + context.metadata.target !== "orion/state" + ) + throw new Error("SOLAR_CONTROL_SNAPSHOT_INVALID"); + stateHash = nativeTreeHash(state); + resolveSnapshot(); + return { level: "received" }; + }; + await peer.transaction( + { kind: "state.read", target: "orion/state" }, + { signal }, + ); + // The read receipt precedes the separately streamed snapshot. + await snapshot; + if (!stateHash) throw new Error("SOLAR_CONTROL_SNAPSHOT_MISSING"); + try { + await peer.transaction( + { + format: "lsdp.apply/1", + id: crypto.randomUUID().replaceAll("-", ""), + target: "orion/state", + beforeHash: stateHash, + operations: [ + { op: "add", path: `/solar_presentation_${lane}`, value }, + ], + require: "applied", + }, + { signal }, + ); + return; + } catch (error) { + if (attempt === 3 || !String(error).includes("BASE_MISMATCH")) + throw error; + } + } + } finally { + signal.removeEventListener("abort", close); + peer.close(); + } +} diff --git a/src/engine/frame-patches.ts b/src/engine/frame-patches.ts new file mode 100644 index 0000000..77a698e --- /dev/null +++ b/src/engine/frame-patches.ts @@ -0,0 +1,73 @@ +/** Merge scalar scene state for one frame, retaining every presentation receipt. */ +export class FramePatches { + private pending: Array<{ patch: Record; receipt: T }> = []; + private scheduled = false; + private running: Promise = Promise.resolve(); + private closed = false; + private busy = false; + constructor( + private readonly submit: ( + patch: Record, + receipts: T[], + ) => Promise, + private readonly failed: (error: unknown) => void, + ) {} + + add(patch: Record, receipt: T): void { + if (this.closed) throw new Error("Presentation queue closed."); + this.pending.push({ patch, receipt }); + this.schedule(); + } + + private schedule(): void { + if ( + !this.closed && + !this.busy && + this.pending.length > 0 && + !this.scheduled + ) { + this.scheduled = true; + // VisionFrames supplies the task yield and owns the one GPU submission. + // An additional channel here delays reception without adding a frame. + queueMicrotask(() => { + this.scheduled = false; + if (!this.closed) void this.flush().catch(this.failed); + }); + } + } + + flush(): Promise { + this.scheduled = false; + if (this.busy) return this.running.then(() => this.flush()); + const batch = this.pending.splice(0); + if (batch.length === 0) return this.running; + const patch = Object.assign({}, ...batch.map((item) => item.patch)); + this.busy = true; + let succeeded = false; + this.running = Promise.resolve() + .then(() => { + if (this.closed) return; + return this.submit( + patch, + batch.map((item) => item.receipt), + ); + }) + .then(() => { + succeeded = true; + }) + .finally(() => { + this.busy = false; + // A completed render already supplies the pacing boundary. Drain work + // received during that render without adding another scheduling interval. + if (succeeded && !this.closed && this.pending.length > 0) + queueMicrotask(() => void this.flush().catch(this.failed)); + }); + return this.running; + } + + close(): void { + this.closed = true; + this.scheduled = false; + this.pending = []; + } +} diff --git a/src/engine/live-media.ts b/src/engine/live-media.ts new file mode 100644 index 0000000..bea84a5 --- /dev/null +++ b/src/engine/live-media.ts @@ -0,0 +1,423 @@ +import { CAPTURE_VISUAL_KINDS } from "@lumencast/protocol"; +import type { PeerSources } from "../sources/peers"; +import type { ResolveCaptureDevice, SolarError } from "../types"; +import type { VisionLiveMediaSource, VisionSceneHandle } from "./vision-presenter"; + +const CAPTURE_KINDS = new Set(CAPTURE_VISUAL_KINDS); + +interface LocalStreamEntry { + refs: number; + stream: Promise; +} + +export class CaptureStreamPool { + private readonly entries = new Map(); + + async acquire(key: string, create: () => Promise): Promise<{ + stream: MediaStream; + release: () => void; + }> { + let entry = this.entries.get(key); + if (!entry) { + entry = { refs: 0, stream: create() }; + this.entries.set(key, entry); + void entry.stream.catch(() => { + if (this.entries.get(key) === entry) this.entries.delete(key); + }); + } + entry.refs += 1; + let released = false; + try { + const stream = await entry.stream; + return { + stream, + release: () => { + if (released) return; + released = true; + entry!.refs -= 1; + if (entry!.refs === 0 && this.entries.get(key) === entry) { + this.entries.delete(key); + for (const track of stream.getTracks()) track.stop(); + } + }, + }; + } catch (error) { + if (!released) { + released = true; + entry.refs -= 1; + } + throw error; + } + } +} + +interface MediaSession { + source: VisionLiveMediaSource; + signature: string; + video: HTMLVideoElement; + frameCanvas: HTMLCanvasElement; + context: CanvasRenderingContext2D; + releaseCapture?: () => void; + unsubscribePeer?: () => void; + videoFrameRequest?: number; + fallbackFrameRequest?: number; + ready: boolean; + dirty: boolean; + disposed: boolean; +} + +export interface LiveMediaOptions { + liveAudio: boolean; + resolveCaptureDevice: ResolveCaptureDevice; + peers: PeerSources; + capturePool?: CaptureStreamPool; + onError: (error: SolarError) => void; + getScene: () => VisionSceneHandle | null; +} + +/** Resolves host-owned camera/peer streams and uploads only changed video frames to Vision. */ +export class LiveMediaController { + private readonly sessions = new Map(); + private readonly capturePool: CaptureStreamPool; + private disposed = false; + private framePump: number | null = null; + private uploadPending = false; + private paused = false; + private uploadCompletion: Promise | null = null; + + constructor(private readonly options: LiveMediaOptions) { + this.capturePool = options.capturePool ?? new CaptureStreamPool(); + } + + /** A new Vision layer needs the current frames even when its sources are unchanged. */ + refresh(): void { + if (this.disposed) return; + for (const session of this.sessions.values()) { + if (session.ready) session.dirty = true; + } + this.schedulePump(); + } + + /** Keep tracks alive while another Vision canvas acquires the GPU context. */ + async pause(): Promise { + this.paused = true; + if (this.framePump !== null) cancelAnimationFrame(this.framePump); + this.framePump = null; + await this.uploadCompletion; + } + + resume(): void { + if (this.disposed) return; + this.paused = false; + this.refresh(); + } + + update(sources: readonly VisionLiveMediaSource[]): void { + if (this.disposed) return; + const next = new Map(sources.map((source) => [source.path, source])); + for (const [path, session] of this.sessions) { + const source = next.get(path); + if (!source || signature(source) !== session.signature) { + this.disposeSession(session); + this.sessions.delete(path); + this.clearTextures([path]); + } + } + for (const source of sources) { + const current = this.sessions.get(source.path); + if (current) { + current.source = source; + continue; + } + const session = this.createSession(source); + this.sessions.set(source.path, session); + if (source.type === "peer") this.connectPeer(session); + else void this.connectCapture(session); + } + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + if (this.framePump !== null) cancelAnimationFrame(this.framePump); + this.framePump = null; + for (const session of this.sessions.values()) this.disposeSession(session); + this.sessions.clear(); + } + + private createSession(source: VisionLiveMediaSource): MediaSession { + const video = document.createElement("video"); + video.autoplay = true; + video.playsInline = true; + video.muted = source.type !== "peer" || !this.options.liveAudio; + video.setAttribute("aria-hidden", "true"); + Object.assign(video.style, { + position: "fixed", + left: "-4px", + top: "-4px", + width: "1px", + height: "1px", + opacity: "0", + pointerEvents: "none", + }); + const frameCanvas = document.createElement("canvas"); + frameCanvas.width = boundedDimension(source.width); + frameCanvas.height = boundedDimension(source.height); + const context = frameCanvas.getContext("2d", { alpha: true }); + if (!context) throw new Error("Solar could not allocate a camera frame surface."); + document.body.append(video); + const session: MediaSession = { + source, + signature: signature(source), + video, + frameCanvas, + context, + ready: false, + dirty: false, + disposed: false, + }; + video.addEventListener("loadeddata", () => this.markReady(session), { once: true }); + video.addEventListener("error", () => this.clear(session), { once: true }); + return session; + } + + private connectPeer(session: MediaSession): void { + const slot = session.source.slot_ref; + if (!slot) { + this.clear(session); + return; + } + session.unsubscribePeer = this.options.peers.subscribePeerStream(slot, (stream) => { + if (session.disposed) return; + this.attachStream(session, stream, false); + }); + const current = this.options.peers.resolvePeerStream(slot); + if (current) this.attachStream(session, current, false); + } + + private async connectCapture(session: MediaSession): Promise { + const sourceKind = session.source.source_kind ?? ""; + if (!CAPTURE_KINDS.has(sourceKind)) { + this.clear(session); + return; + } + try { + const resolved = await this.options.resolveCaptureDevice( + session.source.device_ref ?? "", + sourceKind, + ); + if (session.disposed) return; + if (!resolved || (!resolved.deviceId && !resolved.captureSourceId)) { + this.clear(session); + return; + } + const key = `${sourceKind}\u0000${resolved.deviceId ?? resolved.captureSourceId}`; + const acquired = await this.capturePool.acquire(key, async () => { + const mediaDevices = navigator.mediaDevices; + if (!mediaDevices?.getUserMedia) throw new Error("getUserMedia is unavailable in this host."); + if (resolved.captureSourceId) { + const constraints = { + audio: false, + video: { + mandatory: { + chromeMediaSource: "desktop", + chromeMediaSourceId: resolved.captureSourceId, + }, + }, + } as unknown as MediaStreamConstraints; + return mediaDevices.getUserMedia(constraints); + } + return mediaDevices.getUserMedia({ + audio: false, + video: { deviceId: { exact: resolved.deviceId } }, + }); + }); + if (session.disposed) { + acquired.release(); + return; + } + session.releaseCapture = acquired.release; + this.attachStream(session, acquired.stream, true); + } catch (error) { + if (session.disposed) return; + this.options.onError({ + code: "CAMERA_CAPTURE_FAILED", + message: error instanceof Error ? error.message : "Camera capture failed.", + recoverable: true, + }); + this.clear(session); + } + } + + private attachStream(session: MediaSession, stream: MediaStream | null, local: boolean): void { + if (!stream) { + session.ready = false; + session.video.srcObject = null; + this.clear(session); + return; + } + session.video.muted = local || !this.options.liveAudio; + session.video.srcObject = stream; + void session.video.play().then( + () => this.markReady(session), + (error: unknown) => { + if (!session.disposed) { + this.options.onError({ + code: "CAMERA_CAPTURE_FAILED", + message: error instanceof Error ? error.message : "Live video could not start.", + recoverable: true, + }); + } + }, + ); + } + + private markReady(session: MediaSession): void { + if (session.disposed || session.video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA) return; + session.ready = true; + session.dirty = true; + this.watchVideo(session); + this.schedulePump(); + } + + private watchVideo(session: MediaSession): void { + if (session.disposed || !session.ready) return; + const video = session.video; + const candidate = video as HTMLVideoElement & { + requestVideoFrameCallback?: (callback: () => void) => number; + }; + if (candidate.requestVideoFrameCallback) { + const requestNext = (): void => { + session.videoFrameRequest = candidate.requestVideoFrameCallback!(() => { + if (session.disposed) return; + session.dirty = true; + this.schedulePump(); + requestNext(); + }); + }; + requestNext(); + } else { + const requestNext = (): void => { + session.fallbackFrameRequest = requestAnimationFrame(() => { + session.fallbackFrameRequest = undefined; + if (session.disposed) return; + session.dirty = true; + this.schedulePump(); + requestNext(); + }); + }; + requestNext(); + } + } + + private schedulePump(): void { + if (this.disposed || this.paused || this.framePump !== null || this.uploadPending) return; + this.framePump = requestAnimationFrame(() => { + this.framePump = null; + const upload = this.uploadFrames(); + this.uploadCompletion = upload; + const finished = (): void => { if (this.uploadCompletion === upload) this.uploadCompletion = null; }; + void upload.then(finished, finished); + }); + } + + private async uploadFrames(): Promise { + const scene = this.options.getScene(); + if (!scene || this.disposed || this.uploadPending) return; + const dirty = [...this.sessions.values()].filter( + (session) => session.ready && session.dirty && !session.disposed, + ); + if (dirty.length === 0) return; + this.uploadPending = true; + const frames: Array<{ path: string; bitmap: ImageBitmap }> = []; + try { + for (const session of dirty) { + session.dirty = false; + drawVideo(session); + frames.push({ path: session.source.path, bitmap: await createImageBitmap(session.frameCanvas) }); + } + await scene.updateMedia(frames); + } catch (error) { + for (const frame of frames) frame.bitmap.close(); + if (!this.disposed) { + this.options.onError({ + code: "CAMERA_CAPTURE_FAILED", + message: error instanceof Error ? error.message : "Live camera frame upload failed.", + recoverable: true, + }); + } + } finally { + this.uploadPending = false; + if ([...this.sessions.values()].some((session) => session.dirty)) this.schedulePump(); + } + } + + private clear(session: MediaSession): void { + this.clearTextures([session.source.path]); + } + + private clearTextures(paths: string[]): void { + const scene = this.options.getScene(); + if (!scene) return; + void scene.clearMedia(paths).catch((error: unknown) => { + if (this.disposed) return; + this.options.onError({ + code: "CAMERA_CAPTURE_FAILED", + message: error instanceof Error ? error.message : "Vision could not clear a live camera texture.", + recoverable: true, + }); + }); + } + + private disposeSession(session: MediaSession): void { + if (session.disposed) return; + session.disposed = true; + if (session.videoFrameRequest !== undefined) { + const candidate = session.video as HTMLVideoElement & { + cancelVideoFrameCallback?: (handle: number) => void; + }; + candidate.cancelVideoFrameCallback?.(session.videoFrameRequest); + } + if (session.fallbackFrameRequest !== undefined) { + cancelAnimationFrame(session.fallbackFrameRequest); + } + session.unsubscribePeer?.(); + session.releaseCapture?.(); + session.video.pause(); + session.video.srcObject = null; + session.video.remove(); + } +} + +function signature(source: VisionLiveMediaSource): string { + return [source.type, source.source_kind, source.device_ref, source.slot_ref, source.width, source.height, source.fit].join("\u0000"); +} + +function boundedDimension(value: number): number { + if (!Number.isSafeInteger(value) || value < 1 || value > 4096) { + throw new Error("Vision returned an invalid live-media texture size."); + } + return value; +} + +function drawVideo(session: MediaSession): void { + const { video, context, source } = session; + const width = session.frameCanvas.width; + const height = session.frameCanvas.height; + const sw = video.videoWidth; + const sh = video.videoHeight; + context.clearRect(0, 0, width, height); + if (!sw || !sh) return; + if (source.fit === "fill") { + context.drawImage(video, 0, 0, width, height); + return; + } + const scale = source.fit === "contain" + ? Math.min(width / sw, height / sh) + : Math.max(width / sw, height / sh); + const drawWidth = sw * scale; + const drawHeight = sh * scale; + const dx = (width - drawWidth) / 2; + const dy = (height - drawHeight) / 2; + context.drawImage(video, dx, dy, drawWidth, drawHeight); +} diff --git a/src/engine/native-lsdp-runtime.ts b/src/engine/native-lsdp-runtime.ts new file mode 100644 index 0000000..0004084 --- /dev/null +++ b/src/engine/native-lsdp-runtime.ts @@ -0,0 +1,949 @@ +import { disconnectVisionSession } from "./vision-presenter"; +import { createPeerViewerFromInjection } from "@lumencast/runtime"; +import type { Operation } from "fast-json-patch"; +import jsonPatch from "fast-json-patch"; +import { + BrowserLSDP, + type IncomingContext, +} from "../../vendor/lsdp-native-browser/src/browser.js"; +import { nativeTreeHash } from "../internal/native-tree"; +import { NativeState } from "../internal/native-state"; +import { + NativeWorkerClient, + verifiedNativeWorkerState, +} from "./native-worker-client"; +import { scalarOperations } from "../internal/native-stream"; +import { + defaultsPatch, + NativeSceneAssets, + requireLSML, + type LSMLDocument, +} from "../scenes/native-document"; +import { presentationFeedback } from "./control-feedback"; +import { FramePatches } from "./frame-patches"; +import { NativeSceneFrames } from "./native-scene-frames"; +import { createCaptureResolver } from "../sources/capture"; +import { createPeerSources } from "../sources/peers"; +import type { MountOptions, SolarToken } from "../types"; +import { CaptureStreamPool, LiveMediaController } from "./live-media"; +import { + VisionAnimations, + animationDocument, + animationLeaf, + hasGeometry, +} from "./animations"; +import { + activateVisionScene, + type VisionSceneHandle, +} from "./vision-presenter"; + +interface Change { + kind: "change" | "applied_change"; + sequence: number; + mutation: { + format: string; + id: string; + target: string; + beforeHash: string; + afterHash?: string; + operations: Operation[]; + }; + receipt: { + transactionId: string; + target: string; + level: string; + stateHash?: string; + }; +} + +/** Native binary transport → authoritative in-memory LSML → the same Vision presenter. */ +export class NativeLsdpRuntime { + private patches: FramePatches<{ + detail: Record; + check: () => void; + document: LSMLDocument; + }> | null = null; + private visualQueue: Promise = Promise.resolve(); + private readonly animations = new VisionAnimations( + async (frames) => { + const document = this.document; + await this.visual(async () => { + if ( + !this.active || + !document || + this.document !== document || + !this.current + ) + return; + if (frames.some(hasGeometry)) { + await this.render( + animationDocument(document, frames), + () => { + if (!this.active || this.document !== document) + throw new Error("Animation scene superseded."); + }, + this.abort!.signal, + ); + } else { + const patch: Record = {}; + for (const frame of frames) + for (const [property, value] of Object.entries(frame.values)) + patch[animationLeaf(frame.target, property)] = value; + await this.current.scene.applyPatch(patch); + } + }); + }, + (error) => this.report(error), + ); + private visual(action: () => Promise): Promise { + const work = this.visualQueue.then(action); + this.visualQueue = work.then( + () => {}, + () => {}, + ); + return work; + } + private active = false; + private peer: BrowserLSDP | null = null; + private timer: ReturnType | null = null; + private delay = 250; + private abort: AbortController | null = null; + private token: SolarToken; + private document: LSMLDocument | null = null; + private presentedDocument: LSMLDocument | null = null; + private nativeState: Pick | null = null; + private hasSnapshot = false; + private hash: string | null = null; + private sequence: number | null = null; + private assets: NativeSceneAssets | null = null; + private readonly frames: NativeSceneFrames; + private finalizedPresentation: string | null = null; + private presentation: { + id: string; + document: LSMLDocument; + scene: VisionSceneHandle; + assets: NativeSceneAssets; + committed: boolean; + finalized?: boolean; + previous: { scene: VisionSceneHandle; media: LiveMediaController } | null; + previousDocument: LSMLDocument | null; + previousAssets: NativeSceneAssets | null; + } | null = null; + private current: { + scene: VisionSceneHandle; + media: LiveMediaController; + } | null = null; + private snapshot: { + resolve: () => void; + reject: (error: unknown) => void; + } | null = null; + private readonly capturePool = new CaptureStreamPool(); + private readonly peers: ReturnType; + private readonly resolveCaptureDevice: ReturnType< + typeof createCaptureResolver + >; + + constructor(private readonly options: MountOptions) { + this.token = options.token; + this.frames = new NativeSceneFrames( + options, + (scene) => { + if (this.current?.scene === scene) + this.current.media.update(scene.mediaSources); + }, + (error) => this.report(error), + () => this.abort?.signal, + (stage, start, extra) => this.measure(stage, start, extra), + ); + this.peers = createPeerSources(options, createPeerViewerFromInjection); + this.resolveCaptureDevice = createCaptureResolver(options); + } + + private measure(stage: string, start: number, extra = {}): void { + if (performance.getEntriesByName("solar:native-stage").length >= 64) + performance.clearMeasures("solar:native-stage"); + performance.measure("solar:native-stage", { + start, + detail: { + stage, + sceneId: this.document?.scene_id, + resource: this.options.nativeLSDP.resource, + ...extra, + }, + }); + } + + start(): void { + if (this.active) return; + this.active = true; + void this.connect(); + } + + disconnect(): void { + this.active = false; + this.animations.dispose(); + if (this.timer) clearTimeout(this.timer); + this.timer = null; + this.closePeer(); + if (this.presentation) { + if (this.presentation.scene !== this.current?.scene) + this.presentation.scene.dispose(); + if ( + this.presentation.previous && + this.presentation.previous !== this.current + ) { + this.presentation.previous.scene.dispose(); + this.presentation.previous.media.dispose(); + } + this.presentation = null; + } + this.current?.media.dispose(); + this.current?.scene.dispose(); + disconnectVisionSession(this.options.target); + this.current = null; + this.document = null; + this.presentedDocument = null; + this.assets = null; + this.hash = null; + this.nativeState = null; + this.peers.dispose(); + this.options.onStatus?.("disconnected"); + } + + setToken(token: SolarToken): void { + this.token = token; + if (!this.active) return; + this.closePeer(); + this.reconnect(); + } + + private closePeer(): void { + this.patches?.close(); + this.patches = null; + this.abort?.abort(); + this.abort = null; + this.snapshot?.reject(new Error("Native LSDP connection closed.")); + this.snapshot = null; + const peer = this.peer; + this.peer = null; + peer?.close(); + } + + private reconnect(): void { + if (!this.active || this.timer) return; + this.options.onStatus?.("connecting"); + this.timer = setTimeout(() => { + this.timer = null; + void this.connect(); + }, this.delay); + this.delay = Math.min(this.delay * 2, 10_000); + } + + private async connect(): Promise { + if (!this.active || this.abort) return; + this.options.onStatus?.("connecting"); + const abort = new AbortController(); + this.abort = abort; + try { + const authToken = + typeof this.token === "string" ? this.token : await this.token.fetch(); + abort.signal.throwIfAborted(); + const callbacks = { + authToken, + onTransaction: (value: unknown, context: IncomingContext) => + this.receive(value, context, peer, abort.signal), + onIncomingFailed: (_context: IncomingContext, error: Error) => + this.fail(peer, error), + onClose: (error: Error) => this.fail(peer, error), + }; + const peer: BrowserLSDP = + typeof Worker === "function" + ? new NativeWorkerClient( + this.options.nativeLSDP.url, + callbacks, + this.options.nativeLSDP.resource, + this.options.nativeLSDP.selector, + ) + : new BrowserLSDP(this.options.nativeLSDP.url, callbacks); + this.peer = peer; + this.patches = new FramePatches( + async (patch, receipts) => { + await this.visual(async () => { + receipts.forEach((receipt) => receipt.check()); + await this.current!.scene.applyPatch(patch); + receipts.forEach((receipt) => receipt.check()); + const last = receipts.at(-1)!.detail; + this.presentedDocument = receipts.at(-1)!.document; + for (const receipt of receipts) + this.options.target.dispatchEvent( + new CustomEvent("solar:lsdp-applied", { + detail: { + ...receipt.detail, + presentation: { + throughSequence: last.sequence, + stateHash: last.stateHash, + mutationCount: receipts.length, + }, + }, + }), + ); + }); + }, + (error) => this.fail(peer, error), + ); + await peer.ready; + abort.signal.throwIfAborted(); + await this.readSubscribe(peer, abort.signal); + abort.signal.throwIfAborted(); + this.delay = 250; + this.options.onStatus?.("live"); + } catch (error) { + if (!abort.signal.aborted) { + this.report(error); + this.closePeer(); + this.reconnect(); + } + } + } + + private async readSubscribe( + peer: BrowserLSDP, + signal: AbortSignal, + ): Promise { + // Same-connection catch-up also serves explicit native resync notifications. + for (let attempt = 0; attempt < 4; attempt++) { + const ready = new Promise((resolve, reject) => { + this.snapshot = { resolve, reject }; + }); + void ready.catch(() => {}); + await peer.transaction( + { kind: "state.read", target: this.options.nativeLSDP!.resource }, + { signal }, + ); + await ready; + try { + await peer.transaction( + { + kind: "subscribe", + target: this.options.nativeLSDP!.resource, + stateHash: this.hash, + }, + { signal }, + ); + break; + } catch (error) { + if ( + attempt === 3 || + !(error instanceof Error) || + !("code" in error) || + error.code !== "BASE_MISMATCH" + ) + throw error; + signal.throwIfAborted(); + } + } + } + + private fail(peer: BrowserLSDP, error: unknown): void { + if (this.peer !== peer || !this.active) return; + this.report(error); + this.closePeer(); + this.reconnect(); + } + + private async receive( + value: unknown, + context: IncomingContext, + peer: BrowserLSDP, + signal: AbortSignal, + ): Promise { + signal = AbortSignal.any([signal, context.signal]); + const check = (): void => { + signal.throwIfAborted(); + context.signal.throwIfAborted(); + if (!this.active || this.peer !== peer) + throw new Error("Stale native LSDP delivery."); + }; + check(); + const resource = this.options.nativeLSDP!.resource; + if (context.metadata.target !== resource) + throw new Error("Unexpected native resource."); + if (context.metadata.profile === "lsdp.state.read/1") { + if (!this.snapshot) throw new Error("Unexpected native snapshot."); + try { + await this.patches?.flush(); + const verified = verifiedNativeWorkerState(context); + if (peer instanceof NativeWorkerClient && !verified) + throw new Error("Verified worker snapshot required."); + const state = verified ?? NativeState.from(value); + const handled = await this.visual(() => + this.transition(state.value, check, signal), + ); + const document = handled + ? this.document + : this.selectedDocument(state.value); + const hash = state.stateHash; + if (handled) { + /* coordinated presentation owns the visible frame */ + } else if (document) { + const operations = + this.presentedDocument && + this.presentedDocument.scene_id === document.scene_id && + this.presentedDocument.scene_version === document.scene_version + ? jsonPatch.compare(this.presentedDocument, document) + : null; + const patch = operations ? defaultsPatch(operations, document) : null; + if ( + patch && + this.current && + this.current.scene.canApplyPatch?.(patch) !== false + ) { + await this.visual(async () => { + check(); + await this.current!.scene.applyPatch(patch); + }); + } else + await this.visual(() => + this.render( + animationDocument(document, this.animations.current(document)), + check, + signal, + ), + ); + } else { + this.animations.clear(); + await this.current?.media.pause(); + await this.current?.scene.flush?.(); + this.current?.media.dispose(); + this.current?.scene.dispose(); + await this.current?.scene.flush?.(); + this.current = null; + this.assets = null; + } + check(); + this.document = document; + this.presentedDocument = document; + if (document) this.animations.update(document); + this.nativeState = state; + this.hasSnapshot = true; + this.hash = hash; + this.sequence = null; + if (document) this.reserved(document); + this.snapshot.resolve(); + this.snapshot = null; + return { level: "applied", stateHash: hash }; + } catch (error) { + this.snapshot?.reject(error); + throw error; + } + } + if ( + context.metadata.profile !== "lsdp.state.subscription/1" || + !this.hasSnapshot + ) + throw new Error("Unexpected native notification."); + const resync = value as { kind?: string; target?: string; reason?: string }; + if (resync.kind === "resync") { + if (resync.target !== resource || typeof resync.reason !== "string") + throw new Error("Invalid native resync notification."); + this.options.target.dispatchEvent( + new CustomEvent("solar:lsdp-resync", { + detail: { target: resource, reason: resync.reason }, + }), + ); + await this.readSubscribe(peer, signal); + check(); + return { level: "received" }; + } + const event = value as Change; + const application = event.kind === "applied_change"; + const mutation = event.mutation; + if ( + (!application && event.kind !== "change") || + mutation?.format !== (application ? "lsdp.apply/1" : "lsdp.tree/1") || + mutation.target !== resource || + event.receipt?.target !== resource || + event.receipt?.transactionId !== mutation.id || + !Number.isSafeInteger(event.sequence) || + (this.sequence !== null && event.sequence <= this.sequence) || + mutation.beforeHash !== this.hash + ) + throw new Error( + "Invalid native event or resource requires resynchronization.", + ); + if ( + !Array.isArray(mutation.operations) || + mutation.operations.some( + (op) => !["add", "remove", "replace", "test"].includes(op.op), + ) + ) + throw new Error("Unsupported native operation."); + const classifiedAt = performance.now(); + const verified = verifiedNativeWorkerState(context); + if (peer instanceof NativeWorkerClient && !verified) + throw new Error("Verified worker change required."); + const state = + verified ?? + (this.nativeState instanceof NativeState + ? this.nativeState.patch(mutation.operations) + : (() => { + throw new Error("Native snapshot required."); + })()); + const resourceState = state.value; + let next = this.selectedDocument(resourceState); + const hash = state.stateHash; + if ( + !application && + (hash !== mutation.afterHash || hash !== event.receipt.stateHash) + ) + throw new Error("Native event integrity check failed."); + const selector = this.options.nativeLSDP!.selector; + const prefix = selector ? `/${selector}` : ""; + const selectedOperations = selector + ? (mutation.operations + .filter( + (op) => + op.path === "" || + op.path === prefix || + op.path.startsWith(`${prefix}/`), + ) + .map((op) => ({ + ...op, + path: op.path === "" ? "" : op.path.slice(prefix.length), + })) as Operation[]) + : mutation.operations; + const changed = selectedOperations.length > 0; + // Absolute native assignments also carry coordinator metadata. Compare the + // accepted document before treating the assignment as a structural change; + // finalization must not mount the just-prepared Vision scene a second time. + const patch = next + ? (defaultsPatch(selectedOperations, next) ?? + (this.presentedDocument?.scene_id === next.scene_id && + this.presentedDocument.scene_version === next.scene_version + ? defaultsPatch(jsonPatch.compare(this.presentedDocument, next), next) + : null)) + : null; + this.measure("classify", classifiedAt); + // Only replaceable scalar state can share a frame. Native transport receives + // an honest "received" receipt; presentation receipts still follow Vision. + // Commands, structure and coordinated lane transitions remain synchronous. + const scalar = scalarOperations(selectedOperations); + if ( + scalar && + patch && + this.current && + this.patches && + this.current.scene.canApplyPatch?.(patch) !== false && + this.document?.scene_id === next?.scene_id && + this.document?.scene_version === next?.scene_version && + !next?.["x-solar-transition"] && + !this.presentation + ) { + this.document = next; + this.nativeState = state; + this.hash = hash; + this.sequence = event.sequence; + const detail = { + transactionId: mutation.id, + target: resource, + sequence: event.sequence, + stateHash: hash, + render: "patch", + }; + this.patches.add(patch, { detail, check, document: next! }); + this.options.target.dispatchEvent( + new CustomEvent("solar:lsdp-received", { detail }), + ); + return { level: "received", transactionId: mutation.id, stateHash: hash }; + } + await this.patches?.flush(); + let rendering = "unchanged"; + const coordinated = + changed && + (await this.visual(() => this.transition(resourceState, check, signal))); + if (coordinated) { + next = this.document; + rendering = "coordinated"; + } else if (!changed) { + // Changes to the other physical lane/session are acknowledged without rendering. + } else if (!next) { + rendering = "document"; + this.animations.clear(); + await this.current?.media.pause(); + await this.current?.scene.flush?.(); + this.current?.media.dispose(); + this.current?.scene.dispose(); + await this.current?.scene.flush?.(); + this.current = null; + this.assets = null; + } else if ( + next && + this.document && + (next === this.document || hash === this.hash) + ) { + rendering = "unchanged"; + } else if ( + patch && + this.current && + this.current.scene.canApplyPatch?.(patch) !== false && + this.document?.scene_id === next.scene_id && + this.document?.scene_version === next.scene_version + ) { + rendering = "patch"; + await this.visual(async () => { + check(); + await this.current!.scene.applyPatch(patch); + }); + } else { + rendering = "document"; + await this.visual(() => + this.render( + animationDocument(next!, this.animations.current(next!)), + check, + signal, + ), + ); + } + check(); + this.document = next; + this.presentedDocument = next; + if (next && changed) this.animations.update(next); + this.nativeState = state; + this.hash = hash; + this.sequence = event.sequence; + if (next && changed) this.reserved(next); + // This subscriber ACK follows Vision's frame submission. The writer's + // earlier authoritative-server ACK alone does not prove rendering. + const detail = { + transactionId: mutation.id, + target: resource, + sequence: event.sequence, + stateHash: hash, + render: rendering, + }; + this.options.target.dispatchEvent( + new CustomEvent("solar:lsdp-applied", { detail }), + ); + return application + ? { level: "applied", transactionId: mutation.id } + : { level: "applied", stateHash: hash }; + } + + private selectedDocument(state: unknown): LSMLDocument | null { + const selector = this.options.nativeLSDP!.selector; + if (!selector) { + if (state === null) return null; + const document = requireLSML(state); + if (!document["x-solar-transition"]) return document; + const clean = structuredClone(document); + delete clean["x-solar-transition"]; + return clean; + } + if (!state || typeof state !== "object" || Array.isArray(state)) + throw new Error("Native scene collection required."); + const value = Object.hasOwn(state, selector) + ? (state as Record)[selector] + : null; + return value === null ? null : requireLSML(value); + } + + private async transition( + state: unknown, + check: () => void, + signal: AbortSignal, + ): Promise { + const resource = this.options.nativeLSDP.resource; + const lane = + resource === "solar/program" + ? "program" + : resource === "solar/preview" + ? "preview" + : null; + if (!lane || !state || typeof state !== "object") return false; + const transition = (state as Record)[ + "x-solar-transition" + ] as + | { + request_id: string; + phase: string; + source?: unknown; + } + | undefined; + if (!transition) { + // Orion seeds its admitted mirror while Blue is still behind the gate. + // Finalization owns the retained compensation frame, not this source seed. + if (this.presentation?.finalized) { + const pending = this.presentation; + if (pending.previous && pending.previous.scene !== this.current?.scene) + this.frames.retire(pending.previous.scene); + if (pending.previous?.media !== this.current?.media) + pending.previous?.media.dispose(); + this.finalizedPresentation = pending.id; + this.presentation = null; + } else if (this.presentation?.committed) return true; + else if (this.presentation) { + this.presentation.scene.dispose(); + this.presentation = null; + } + return false; + } + if ( + !/^[a-zA-Z0-9_:-]{1,128}$/.test(transition.request_id) || + !["prepare", "commit", "finalize", "abort"].includes(transition.phase) + ) + throw new Error("SOLAR_TRANSITION_INVALID"); + const ack = async (phase: string, error?: string): Promise => { + const started = performance.now(); + check(); + const document = this.presentation?.document ?? this.document; + await presentationFeedback( + this.options.nativeLSDP.url, + lane, + { + request_id: transition.request_id, + transition_phase: transition.phase, + phase, + scene_id: document?.scene_id ?? null, + scene_version: document?.scene_version ?? null, + ...(error ? { error } : {}), + }, + signal, + ); + this.measure("feedback", started, { phase }); + }; + try { + if ( + transition.phase === "finalize" && + this.finalizedPresentation === transition.request_id + ) { + await ack("active"); + return true; + } + // A fresh renderer can join after commit. Its authoritative snapshot + // carries the admitted source, so it reconstructs that confirmed stage. + if ( + !this.presentation && + !this.hasSnapshot && + ["commit", "finalize"].includes(transition.phase) + ) { + const document = requireLSML( + transition.source ?? this.selectedDocument(state), + ); + const frame = await this.frames.prepare( + document, + check, + signal, + this.assets, + ); + this.presentation = { + id: transition.request_id, + document, + ...frame, + committed: false, + previous: this.current, + previousDocument: this.document, + previousAssets: this.assets, + }; + } + if (transition.phase === "prepare") { + const document = requireLSML(transition.source); + if (this.presentation?.id === transition.request_id) { + if ( + nativeTreeHash(this.presentation.document) !== + nativeTreeHash(document) + ) + throw new Error("SOLAR_TRANSITION_ID_REUSED"); + } else { + if (this.presentation) throw new Error("SOLAR_TRANSITION_BUSY"); + await this.current?.media.pause(); + await this.current?.scene.flush?.(); + try { + const frame = await this.frames.prepare( + document, + check, + signal, + this.assets, + ); + this.presentation = { + id: transition.request_id, + document, + ...frame, + committed: false, + previous: this.current, + previousDocument: this.document, + previousAssets: this.assets, + }; + } finally { + this.current?.media.resume(); + } + } + await ack("prepared"); + } else if (transition.phase === "abort") { + if (this.finalizedPresentation === transition.request_id) + throw new Error("SOLAR_TRANSITION_FINALIZED"); + const pending = this.presentation; + if (pending && pending.id !== transition.request_id) + throw new Error("SOLAR_TRANSITION_ID_MISMATCH"); + // A subscriber joining after compensation receives the restored source + // with abort metadata. Present that source before confirming recovery. + if (!pending && !this.hasSnapshot) { + const restored = this.selectedDocument(state); + if (restored) { + await this.render(restored, check, signal); + this.document = restored; + } + } + if (pending) { + await this.current?.media.pause(); + await this.current?.scene.flush?.(); + if (pending.committed) { + pending.previous?.scene.activate(); + if (this.current?.media !== pending.previous?.media) + this.current?.media.dispose(); + this.current = pending.previous; + this.document = pending.previousDocument; + this.assets = pending.previousAssets; + if (this.current) + this.current.media.update(this.current.scene.mediaSources); + } + pending.scene.dispose(); + this.presentation = null; + this.current?.media.resume(); + } + await ack("aborted"); + } else { + const pending = this.presentation; + if (!pending || pending.id !== transition.request_id) + throw new Error("SOLAR_TRANSITION_NOT_PREPARED"); + if (!pending.committed) { + if (transition.phase !== "commit" && this.hasSnapshot) + throw new Error("SOLAR_TRANSITION_NOT_COMMITTED"); + await this.current?.media.pause(); + await this.current?.scene.flush?.(); + check(); + const media = + this.current?.media ?? + new LiveMediaController({ + liveAudio: this.options.liveAudio === true, + resolveCaptureDevice: this.resolveCaptureDevice, + peers: this.peers, + capturePool: this.capturePool, + onError: (error) => this.options.onError?.(error), + getScene: () => this.current?.scene ?? pending.scene, + }); + pending.scene.activate(); // old frame remains available until Orion finalizes. + pending.previous?.scene.deactivate?.(); + this.current = { scene: pending.scene, media }; + this.assets = pending.assets; + this.document = pending.document; + media.update(pending.scene.mediaSources); + media.resume(); + pending.committed = true; + this.reserved(pending.document); + } + if (transition.phase === "finalize") { + // Confirm active before discarding the compensation frame. Lost feedback + // can be replayed without remounting or rerunning Blue. + await ack("active"); + // Retain compensation until the coordinator removes the metadata. + // A lost active receipt can still be reconciled or aborted safely. + pending.finalized = true; + } else await ack("committed"); + } + } catch (error) { + if (signal.aborted) throw error; + await ack( + "failed", + error instanceof Error ? error.message : String(error), + ); + this.report(error); + } + return true; + } + + private async render( + document: LSMLDocument, + check: () => void, + signal: AbortSignal, + ): Promise { + let next: VisionSceneHandle | null = null; + let media: LiveMediaController | null = this.current?.media ?? null; + const reusedMedia = media !== null; + const previous = this.current; + let committed = false; + try { + // Vision's main-thread WebGL canvases must not overlap old camera renders + // with staging/teardown of the next engine. Tracks stay acquired. + await previous?.media.pause(); + await previous?.scene.flush?.(); + check(); + const frame = await this.frames.prepare( + document, + check, + signal, + this.assets, + ); + next = frame.scene; + const assets = frame.assets; + check(); + media ??= new LiveMediaController({ + liveAudio: this.options.liveAudio === true, + resolveCaptureDevice: this.resolveCaptureDevice, + peers: this.peers, + capturePool: this.capturePool, + onError: (error) => this.options.onError?.(error), + getScene: () => this.current?.scene ?? next, + }); + activateVisionScene(next, previous?.scene, true); + committed = true; + this.current = { scene: next, media }; + this.assets = assets; + media.update(next.mediaSources); + await previous?.scene.flush?.(); + if (previous && previous.scene !== next) + this.frames.retire(previous.scene); + check(); + media.resume(); + if (previous && previous.media !== media) previous.media.dispose(); + } catch (error) { + if (!committed) { + if (!reusedMedia) media?.dispose(); + next?.dispose(); + } + throw error; + } finally { + // Also resume the last accepted scene if staging failed or was cancelled. + this.current?.media.resume(); + } + } + + private reserved(document: LSMLDocument): void { + const slots: Record = {}; + const defaults = document.defaults ?? {}; + for (const [key, value] of Object.entries(defaults)) { + if (key.startsWith("__cam.slots.") && typeof value === "string" && value) + slots[key.slice(12)] = value; + } + const raw = defaults["__cam.viewer"]; + let viewer: unknown = raw; + if (typeof raw === "string") { + try { + viewer = JSON.parse(raw); + } catch { + viewer = undefined; + } + } + this.peers.onReservedLeaves?.({ + ...(viewer !== undefined ? { viewer } : {}), + slots, + }); + } + + private report(error: unknown): void { + this.options.onError?.({ + code: "INTERNAL", + message: `Native LSDP: ${error instanceof Error ? error.message : String(error)}`, + recoverable: true, + }); + } +} diff --git a/src/engine/native-scene-frames.ts b/src/engine/native-scene-frames.ts new file mode 100644 index 0000000..ff1dbd4 --- /dev/null +++ b/src/engine/native-scene-frames.ts @@ -0,0 +1,126 @@ +import { readNativeAuthoringSource } from "../scenes/local-authoring"; +import type { MountOptions } from "../types"; +import { + NativeSceneAssets, + type LSMLDocument, +} from "../scenes/native-document"; +import { mountVisionScene, type VisionSceneHandle } from "./vision-presenter"; + +/** Prepare the active scene; inactive sources and GPU frames are never retained. */ +export class NativeSceneFrames { + constructor( + private readonly options: MountOptions, + private readonly onMedia: (scene: VisionSceneHandle) => void, + private readonly onError: (error: unknown) => void, + private readonly signal: () => AbortSignal | undefined, + private readonly measure: ( + stage: string, + start: number, + extra?: Record, + ) => void, + ) {} + + retire(scene: VisionSceneHandle): void { + scene.dispose(); + } + + async prepare( + document: LSMLDocument, + check: () => void, + signal: AbortSignal, + activeAssets: NativeSceneAssets | null, + ): Promise<{ scene: VisionSceneHandle; assets: NativeSceneAssets }> { + let started = performance.now(); + let assets = activeAssets; + if ( + !assets || + assets.origin.sceneId !== document.scene_id || + assets.origin.sceneVersion !== document.scene_version + ) { + const request = { + format: "lsmlz" as const, + sceneVersion: document.scene_version, + signal, + }; + const local = await readNativeAuthoringSource(document); + const origin = + local ?? + (await this.options.sceneSourceProvider.get( + document.scene_id, + request, + )); + check(); + if ( + origin.sceneId !== document.scene_id || + origin.sceneVersion !== document.scene_version + ) + throw new Error("Native resource source identity mismatch."); + assets = new NativeSceneAssets( + origin, + globalThis.fetch.bind(globalThis), + this.options.sceneImageAssetsProvider, + (start) => + this.measure("images", start, { sceneId: document.scene_id }), + ); + } + this.measure("source", started, { sceneId: document.scene_id }); + started = performance.now(); + const [renderPackage, hostFonts] = await Promise.all([ + assets + .renderPackage( + document, + signal, + this.options.nativeComposition, + true, + true, + ) + .then((result) => { + this.measure("package", started, { sceneId: document.scene_id }); + return result; + }), + Promise.resolve(this.options.fontAssetsProvider?.(signal)).then( + (result) => { + this.measure("fonts", started, { sceneId: document.scene_id }); + return result; + }, + ), + ]); + check(); + started = performance.now(); + let scene: VisionSceneHandle | null = null; + scene = await mountVisionScene( + this.options.target, + assets.origin, + {}, + () => { + if (scene) this.onMedia(scene); + }, + this.onError, + { + ...renderPackage, + hostFonts: [...(renderPackage.hostFonts ?? []), ...(hostFonts ?? [])], + installationFonts: this.options.installationFonts, + requiredFontDigests: + "provenance" in assets.origin ? assets.origin.fontDigests : [], + resolveImages: (patch) => + assets!.prepareImagePatch( + patch, + renderPackage.imageBindings, + document, + this.signal(), + ), + }, + true, + ); + try { + check(); + await scene.flush?.(); + this.measure("vision", started, { sceneId: document.scene_id }); + check(); + return { scene, assets }; + } catch (error) { + scene.dispose(); + throw error; + } + } +} diff --git a/src/engine/native-worker-client.ts b/src/engine/native-worker-client.ts new file mode 100644 index 0000000..e5e38c6 --- /dev/null +++ b/src/engine/native-worker-client.ts @@ -0,0 +1,205 @@ +import { + BrowserLSDP, + type IncomingContext, +} from "../../vendor/lsdp-native-browser/src/browser.js"; + +type Options = NonNullable[1]>; +interface VerifiedWorkerState { + readonly value: unknown; + readonly stateHash: string; +} +// Only this client's private Worker channel can certify a delivery. A field +// supplied by the network or a caller's lookalike context cannot bypass hashing. +const verifiedStates = new WeakMap(); +export const verifiedNativeWorkerState = ( + context: IncomingContext, +): VerifiedWorkerState | undefined => verifiedStates.get(context); + +/** Same consumer boundary; network/application work does not wait on CEF drawing. */ +export class NativeWorkerClient { + readonly ready: Promise; + private readonly worker = new Worker( + new URL("./native-worker.ts", import.meta.url), + { type: "module" }, + ); + private readonly abort = new AbortController(); + private serial = 0; + private readonly requests = new Map< + number, + { resolve: (value: unknown) => void; reject: (error: Error) => void } + >(); + private queue: Promise = Promise.resolve(); + private rejectReady!: (error: Error) => void; + private readonly deliveries = new Map(); + constructor( + url: string, + readonly options: Options, + resource: string, + selector?: string, + ) { + let resolve!: () => void, reject!: (error: Error) => void; + this.ready = new Promise((yes, no) => { + resolve = yes; + reject = no; + this.rejectReady = no; + }); + void this.ready.catch(() => {}); + const failed = (value: { message: string; code?: string }) => { + if (this.abort.signal.aborted) return; + const error = Object.assign(new Error(value.message), value); + reject(error); + this.close(); + options?.onClose?.(error); + }; + this.worker.onerror = (event) => failed({ message: event.message }); + this.worker.onmessage = ({ data }) => { + if (this.abort.signal.aborted) return; + if (data.type === "ready") { + resolve(); + return; + } + if (data.type === "closed") { + failed(data.error); + return; + } + if (data.type === "incoming-cancelled") { + this.deliveries.get(data.id)?.abort(); + } else if (data.type === "transaction-result") { + const call = this.requests.get(data.id); + this.requests.delete(data.id); + if (data.error) + call?.reject( + Object.assign(new Error(data.error.message), data.error), + ); + else call?.resolve(data.value); + } else if (data.type === "incoming") { + const delivery = new AbortController(); + this.deliveries.set(data.id, delivery); + const deliver = async () => { + if (this.abort.signal.aborted) return; + const received = performance.now(); + if ( + Number.isFinite(data.verificationMs) && + Number.isFinite(data.sentAt) + ) { + if (performance.getEntriesByName("solar:native-stage").length >= 64) + performance.clearMeasures("solar:native-stage"); + performance.measure("solar:native-stage", { + start: received, + duration: Math.max(0, data.verificationMs ?? 0), + detail: { + stage: "worker-verify", + resource, + phase: + data.metadata.profile === "lsdp.state.read/1" + ? "snapshot" + : "change", + }, + }); + performance.measure("solar:native-stage", { + start: Math.min( + received, + Math.max(0, data.sentAt - performance.timeOrigin), + ), + end: received, + detail: { stage: "worker-delivery", resource }, + }); + } + const context: IncomingContext = { + metadata: data.metadata, + signal: AbortSignal.any([this.abort.signal, delivery.signal]), + }; + try { + context.signal.throwIfAborted(); + if (data.verifiedState) { + if ( + data.metadata.target !== resource || + !/^tree-sha256:[a-f0-9]{64}$/.test(data.verifiedState.stateHash) + ) + throw new Error("Invalid verified worker delivery."); + verifiedStates.set(context, data.verifiedState); + } + const value = await options?.onTransaction?.(data.value, context); + context.signal.throwIfAborted(); + this.worker.postMessage({ type: "result", id: data.id, value }); + } catch (error) { + if (this.abort.signal.aborted) return; + this.worker.postMessage({ + type: "result", + id: data.id, + error: { + message: error instanceof Error ? error.message : String(error), + }, + }); + options?.onIncomingFailed?.( + context, + error instanceof Error ? error : new Error(String(error)), + ); + } finally { + verifiedStates.delete(context); + this.deliveries.delete(data.id); + } + }; + if (data.metadata.profile === "lsdp.state.read/1") void deliver(); + else this.queue = this.queue.then(deliver); + } + }; + this.worker.postMessage({ + type: "connect", + url, + authToken: options?.authToken, + resource, + selector, + }); + } + async transaction( + value: unknown, + options?: { signal?: AbortSignal }, + ): Promise { + options?.signal?.throwIfAborted(); + let stopWaiting!: () => void; + const cancelledReady = new Promise((_resolve, reject) => { + stopWaiting = () => reject(new Error("Native transaction cancelled.")); + options?.signal?.addEventListener("abort", stopWaiting, { once: true }); + }); + try { + await Promise.race([this.ready, cancelledReady]); + } finally { + options?.signal?.removeEventListener("abort", stopWaiting); + } + options?.signal?.throwIfAborted(); + this.abort.signal.throwIfAborted(); + const id = ++this.serial; + return await new Promise((resolve, reject) => { + const cancelled = () => { + const call = this.requests.get(id); + this.requests.delete(id); + if (!this.abort.signal.aborted) + this.worker.postMessage({ type: "cancel-transaction", id }); + call?.reject(new Error("Native transaction cancelled.")); + }; + options?.signal?.addEventListener("abort", cancelled, { once: true }); + this.requests.set(id, { + resolve: (value) => { + options?.signal?.removeEventListener("abort", cancelled); + resolve(value); + }, + reject: (error) => { + options?.signal?.removeEventListener("abort", cancelled); + reject(error); + }, + }); + this.worker.postMessage({ type: "transaction", id, value }); + }); + } + close(): void { + if (this.abort.signal.aborted) return; + this.abort.abort(); + this.rejectReady(new Error("Native worker closed.")); + this.worker.terminate(); + this.deliveries.clear(); + for (const call of this.requests.values()) + call.reject(new Error("Native worker closed.")); + this.requests.clear(); + } +} diff --git a/src/engine/native-worker.ts b/src/engine/native-worker.ts new file mode 100644 index 0000000..8b4087e --- /dev/null +++ b/src/engine/native-worker.ts @@ -0,0 +1,153 @@ +import { BrowserLSDP } from "../../vendor/lsdp-native-browser/src/browser.js"; +import { NativeState } from "../internal/native-state"; +import { NativeTreeHasher } from "../internal/native-tree"; +import { workerDigest } from "../internal/native-digest"; +import { + nativeChange, + scalarOperations, + type NativeChange, +} from "../internal/native-stream"; + +const scope = globalThis as unknown as { + onmessage: (event: MessageEvent) => void; + postMessage: (value: unknown) => void; +}; +workerDigest(crypto.subtle); +// Snapshot catch-up retains verified immutable subtrees on this one resource. +// Every new snapshot still validates JSON and checks whole-resource budgets. +const hasher = new NativeTreeHasher(); +let peer: BrowserLSDP, + state: NativeState | null = null, + sequence: number | null = null, + serial = 0; +const waiting = new Map< + number, + { resolve: (value: unknown) => void; reject: (error: Error) => void } +>(); +const transactions = new Map(); +const serializedError = (error: unknown) => ({ + message: error instanceof Error ? error.message : String(error), + ...(error && typeof error === "object" && "code" in error + ? { code: String(error.code) } + : {}), +}); +scope.onmessage = ({ data }) => { + if (data.type === "cancel-transaction") { + transactions.get(data.id)?.abort(); + return; + } + if (data.type === "result") { + const call = waiting.get(data.id); + waiting.delete(data.id); + if (data.error) + call?.reject(Object.assign(new Error(data.error.message), data.error)); + else call?.resolve(data.value); + return; + } + if (data.type === "connect") { + const transport = { + authToken: data.authToken, + onClose: (error: Error) => + scope.postMessage({ type: "closed", error: serializedError(error) }), + onIncomingFailed: ( + _context: import("../../vendor/lsdp-native-browser/src/browser.js").IncomingContext, + error: Error, + ) => scope.postMessage({ type: "closed", error: serializedError(error) }), + onTransaction: async ( + value: unknown, + context: import("../../vendor/lsdp-native-browser/src/browser.js").IncomingContext, + ) => { + const verificationStarted = performance.now(); + const id = ++serial; + const event = value as NativeChange; + let scalar = false; + if (context.metadata.target !== data.resource) + throw new Error("Unexpected native resource."); + if (context.metadata.profile === "lsdp.state.read/1") { + state = NativeState.from(value, hasher); + sequence = null; + } else if ( + context.metadata.profile === "lsdp.state.subscription/1" && + event.kind !== "resync" + ) { + if (!state) throw new Error("Native snapshot required."); + const next = nativeChange(state, event, data.resource, sequence); + scalar = + !data.selector && + scalarOperations(event.mutation.operations) && + next.value !== null && + typeof next.value === "object" && + !(next.value as Record)["x-solar-transition"]; + state = next; + sequence = event.sequence; + } + const delivered = new Promise((resolve, reject) => + waiting.set(id, { resolve, reject }), + ); + const cancelled = () => + scope.postMessage({ type: "incoming-cancelled", id }); + context.signal.addEventListener("abort", cancelled, { once: true }); + void delivered + .finally(() => context.signal.removeEventListener("abort", cancelled)) + .catch(() => {}); + scope.postMessage({ + type: "incoming", + id, + value, + metadata: context.metadata, + // Verification (portable JSON, limits, sequence and Merkle hashes) + // happened above. Structured clone owns a separate rendering copy. + ...(event?.kind !== "resync" && state + ? { + verifiedState: { + value: state.value, + stateHash: state.stateHash, + }, + } + : {}), + verificationMs: performance.now() - verificationStarted, + sentAt: performance.timeOrigin + performance.now(), + }); + if (scalar) { + // Application is verified in this isolated LSML thread; no pixel claim. + void delivered.catch((error) => + scope.postMessage({ + type: "closed", + error: serializedError(error), + }), + ); + return { + level: "received", + transactionId: event.mutation.id, + stateHash: state!.stateHash, + }; + } + return await delivered; + }, + }; + peer = new BrowserLSDP(data.url, transport); + void peer.ready + .then(() => scope.postMessage({ type: "ready" })) + .catch((error) => + scope.postMessage({ type: "closed", error: serializedError(error) }), + ); + return; + } + if (data.type === "transaction") { + const abort = new AbortController(); + transactions.set(data.id, abort); + void peer + .transaction(data.value, { signal: abort.signal }) + .then((value) => + scope.postMessage({ type: "transaction-result", id: data.id, value }), + ) + .catch((error) => + scope.postMessage({ + type: "transaction-result", + id: data.id, + error: serializedError(error), + }), + ) + .finally(() => transactions.delete(data.id)); + } +}; diff --git a/src/engine/vision-frames.ts b/src/engine/vision-frames.ts new file mode 100644 index 0000000..a69ee5d --- /dev/null +++ b/src/engine/vision-frames.ts @@ -0,0 +1,132 @@ +export interface VisionFrame { + patch: Record; + frames: Array<{ path: string; bitmap: ImageBitmap }>; + images?: Array<{ path: string; bytes: Uint8Array }>; +} + +/** One submission combines replaceable state and the latest owned camera images. */ +export class VisionFrames { + private patch: Record = {}; + private frames = new Map(); + private images = new Map(); + private waiting: Array<{ + resolve: () => void; + reject: (error: Error) => void; + }> = []; + private running: Promise = Promise.resolve(); + private busy = false; + private closed = false; + private scheduled = false; + private mediaTimer: ReturnType | null = null; + private readonly channel = new MessageChannel(); + + constructor(private readonly submit: (frame: VisionFrame) => Promise) { + this.channel.port1.onmessage = () => { + this.scheduled = false; + void this.flush().catch(() => {}); // Each caller receives its rejection. + }; + } + + addPatch( + patch: Record, + images: NonNullable = [], + ): Promise { + if (this.closed) + return Promise.reject(new Error("Vision frame queue closed.")); + Object.assign(this.patch, patch); + for (const { path, bytes } of images) this.images.set(path, bytes); + return this.pending(); + } + + addMedia(frames: VisionFrame["frames"]): Promise { + if (this.closed) { + frames.forEach(({ bitmap }) => bitmap.close()); + return Promise.reject(new Error("Vision frame queue closed.")); + } + for (const { path, bitmap } of frames) { + this.frames.get(path)?.close(); + this.frames.set(path, bitmap); + } + return this.pending(); + } + + private pending(): Promise { + const result = new Promise((resolve, reject) => + this.waiting.push({ resolve, reject }), + ); + this.schedule(); + return result; + } + + private schedule(): void { + if (this.closed || this.busy || this.scheduled || !this.waiting.length) + return; + if (Object.keys(this.patch).length === 0) { + // A camera-only frame may wait one 120 Hz interval for an arriving patch. + // Patch arrival or an explicit flush bypasses this bounded media grace. + this.mediaTimer ??= setTimeout(() => { + this.mediaTimer = null; + this.scheduled = true; + this.channel.port2.postMessage(null); + }, 1000 / 120); + return; + } + if (this.mediaTimer !== null) clearTimeout(this.mediaTimer); + this.mediaTimer = null; + this.scheduled = true; + // Rendering must not be capped by the host's 60 Hz RAF or nested timer clamp. + this.channel.port2.postMessage(null); + } + + flush(): Promise { + if (this.mediaTimer !== null) clearTimeout(this.mediaTimer); + this.mediaTimer = null; + if (this.busy) return this.running.then(() => this.flush()); + if (this.closed || this.waiting.length === 0) return Promise.resolve(); + const waiting = this.waiting.splice(0); + const frame: VisionFrame = { + patch: this.patch, + frames: [...this.frames].map(([path, bitmap]) => ({ path, bitmap })), + ...(this.images.size + ? { images: [...this.images].map(([path, bytes]) => ({ path, bytes })) } + : {}), + }; + this.patch = {}; + this.frames.clear(); + this.images.clear(); + this.busy = true; + this.running = Promise.resolve().then(async () => { + try { + if (this.closed) throw new Error("Vision frame queue closed."); + await this.submit(frame); + if (this.closed) throw new Error("Vision frame queue closed."); + waiting.forEach(({ resolve }) => resolve()); + } catch (failure) { + const error = + failure instanceof Error ? failure : new Error(String(failure)); + waiting.forEach(({ reject }) => reject(error)); + throw error; + } finally { + frame.frames.forEach(({ bitmap }) => bitmap.close()); + this.busy = false; + this.schedule(); + } + }); + return this.running; + } + + close(): void { + if (this.closed) return; + this.closed = true; + if (this.mediaTimer !== null) clearTimeout(this.mediaTimer); + this.mediaTimer = null; + this.channel.port1.close(); + this.channel.port2.close(); + this.frames.forEach((bitmap) => bitmap.close()); + this.frames.clear(); + this.patch = {}; + this.images.clear(); + const error = new Error("Vision frame queue closed."); + this.waiting.splice(0).forEach(({ reject }) => reject(error)); + } +} diff --git a/src/engine/vision-media-bridge.ts b/src/engine/vision-media-bridge.ts new file mode 100644 index 0000000..5a5b4d2 --- /dev/null +++ b/src/engine/vision-media-bridge.ts @@ -0,0 +1,40 @@ +interface MediaEngine { + update_live_image(path: string, bitmap: ImageBitmap): void; +} +type EngineFactory = (...args: unknown[]) => Promise; + +/** Capture the real engine through Vision's supported dependency injection point. */ +export class VisionMediaBridge { + private engine: MediaEngine | null = null; + + module(wasm: Record): Record { + const Engine = wasm.VisionGpuEngine as Record; + return { + ...wasm, + VisionGpuEngine: new Proxy(Engine, { + get: (target, property) => { + const method = Reflect.get(target, property); + if ( + property !== "create_html_canvas" && + property !== "create_html_canvas_with_fonts" && + property !== "create_empty_html_canvas" + ) + return method; + return async (...args: unknown[]) => { + const engine = await (method as EngineFactory).apply(target, args); + if (typeof engine.update_live_image !== "function") + throw new Error("Vision live image API is unavailable."); + this.engine = engine; + return engine; + }; + }, + }), + }; + } + + upload(frames: Array<{ path: string; bitmap: ImageBitmap }>): void { + if (!this.engine) throw new Error("Vision engine is disconnected."); + for (const { path, bitmap } of frames) + this.engine.update_live_image(path, bitmap); + } +} diff --git a/src/engine/vision-presenter.ts b/src/engine/vision-presenter.ts new file mode 100644 index 0000000..f5e756c --- /dev/null +++ b/src/engine/vision-presenter.ts @@ -0,0 +1,720 @@ +import { VerifiedFont } from "../scenes/verified-font"; +import type { SceneRenderDelivery } from "../scenes/types"; +import { visionImageValue, visionTextPatch } from "../scenes/native-document"; +import { CanvasPresentation } from "./canvas-presentation"; +import { VisionFrames } from "./vision-frames"; +import { VisionMediaBridge } from "./vision-media-bridge"; + +export interface VisionLiveMediaSource { + path: string; + type: "capture" | "peer"; + source_kind?: string; + device_ref?: string; + slot_ref?: string; + fit?: "cover" | "contain" | "fill" | string; + width: number; + height: number; +} + +export interface VisionPresenterMessage { + type: string; + seq?: number; + message?: string; + phase?: string; + mediaSources?: VisionLiveMediaSource[]; +} + +const COMPLETED_REQUESTS = new Set([ + "frame-submitted", + "engine-ready", + "fonts-ready", + "scene-cleared", + "media-applied", + "media-cleared", + "resize-unchanged", +]); + +// Published LSML may name the shared editor families without embedding them. +// Admit the licensed host faces once, alongside any scene-owned font assets. +const hostFonts = [ + "geist-latin.woff2", + "geist-mono-latin.woff2", + "figtree-variable.ttf", + "alex-brush-400.woff2", + "ibm-plex-mono-400.woff2", + "ibm-plex-mono-500.woff2", + "ibm-plex-mono-600.woff2", + "space-grotesk-400.woff2", + "space-grotesk-500.woff2", + "space-grotesk-600.woff2", + "space-grotesk-700.woff2", +]; +let hostFontBytes: Promise | null = null; +function loadHostFonts(): Promise { + hostFontBytes ??= Promise.all( + hostFonts.map(async (path) => { + const response = await fetch(new URL(`fonts/${path}`, document.baseURI)); + if (!response.ok) throw new Error(`Solar host font unavailable: ${path}`); + return new Uint8Array(await response.arrayBuffer()); + }), + ).catch((error) => { + hostFontBytes = null; + throw error; + }); + return hostFontBytes; +} + +interface PresenterEvent { + data: VisionPresenterMessage; +} + +interface VisionPresenter { + addEventListener( + type: "message", + listener: (event: PresenterEvent) => void, + ): void; + postMessage(request: Record): void; + terminate(): void; + flush(): Promise; +} + +export interface VisionSceneHandle { + readonly sceneId: string; + readonly sceneVersion: string; + readonly mediaSources: readonly VisionLiveMediaSource[]; + applyPatch(patch: Record): Promise; + canApplyPatch?(patch: Record): boolean; + resize(width: number, height: number): Promise; + activate(): void; + deactivate?(): void; + updateMedia( + frames: Array<{ path: string; bitmap: ImageBitmap }>, + ): Promise; + clearMedia(paths: string[]): Promise; + dispose(): void; + /** Drain queued GPU work, including teardown after disposal. */ + flush?(): Promise; +} + +type PresenterModule = { + createMainThreadPresenter: ( + canvas: HTMLCanvasElement, + dependencies: Record, + ) => VisionPresenter; +}; + +export interface VisionRenderPackage { + data: Uint8Array; + assets?: Array<{ path: string; bytes: Uint8Array }>; + sceneVersion: string; + /** Native defaults include LSML-owned __lit bindings. Camera control stays separate. */ + lsmlDefaults?: boolean; + textBindings?: Record; + imageBindings?: Record; + imageAssets?: Record; + imageValues?: Record; + animationBindings?: Record; + geometryBindings?: Record; + hostFonts?: Array; + requiredFontDigests?: readonly string[]; + surface?: { width: number; height: number }; + installationFonts?: () => AsyncIterable; + resolveImages?: (patch: Record) => Promise<{ + values: Record; + assets: Array<{ path: string; bytes: Uint8Array }>; + }>; +} + +interface PersistentVisionSession { + layer: HTMLDivElement; + canvas: HTMLCanvasElement; + presentation: CanvasPresentation; + presenter: VisionPresenter | null; + bridge: VisionMediaBridge; + sequence: number; + owner: object; + closed: boolean; + fontsReady: boolean; + admittedFonts: Set; + onMessage: (event: PresenterEvent) => void; +} +const visionSessions = new WeakMap(); +const sceneLoads = new WeakMap>(); + +function createSessionPresenter( + module: PresenterModule, + canvas: HTMLCanvasElement, + bridge: VisionMediaBridge, +): VisionPresenter { + return module.createMainThreadPresenter(canvas, { + loadWasm: async () => + bridge.module( + await import( + /* @vite-ignore */ new URL( + "vision/pkg/lumencast_vision_web.js", + document.baseURI, + ).href + ), + ), + }); +} +function listenToSession(session: PersistentVisionSession): void { + session.presenter!.addEventListener("message", (event) => + session.onMessage(event), + ); +} + +/** End the host session; a scene switch must never call this. */ +export function disconnectVisionSession(target: HTMLElement): void { + const session = visionSessions.get(target); + if (!session) return; + visionSessions.delete(target); + session.closed = true; + session.presenter?.terminate(); + session.layer.remove(); +} + +/** Mount one revision-pinned LSMLZ scene into Vision and wait for its first frame. */ +export async function mountVisionScene( + target: HTMLElement, + delivery: SceneRenderDelivery, + initialState: Record, + onMediaSources: (sources: readonly VisionLiveMediaSource[]) => void, + onError: (error: unknown) => void, + renderPackage?: VisionRenderPackage, + persistent = false, +): Promise { + if (!persistent) + return mountVisionSceneNow( + target, + delivery, + initialState, + onMediaSources, + onError, + renderPackage, + ); + const preceding = sceneLoads.get(target) ?? Promise.resolve(); + const load = preceding.then(() => + mountVisionSceneNow( + target, + delivery, + initialState, + onMediaSources, + onError, + renderPackage, + true, + ), + ); + sceneLoads.set( + target, + load.then( + () => {}, + () => {}, + ), + ); + return load; +} + +/** Mount one revision-pinned LSMLZ scene into Vision and wait for its first frame. */ +async function mountVisionSceneNow( + target: HTMLElement, + delivery: SceneRenderDelivery, + initialState: Record, + onMediaSources: (sources: readonly VisionLiveMediaSource[]) => void, + onError: (error: unknown) => void, + renderPackage?: VisionRenderPackage, + persistent = false, +): Promise { + if (delivery.format !== "lsmlz" && !renderPackage) { + throw new Error("Vision scene delivery must be an LSMLZ archive."); + } + + const existing = persistent ? visionSessions.get(target) : undefined; + const owner = {}; + const layer = existing?.layer ?? document.createElement("div"); + layer.dataset.solarVisionScene = delivery.sceneVersion; + Object.assign(layer.style, { + position: "absolute", + inset: "0", + opacity: "0", + pointerEvents: "none", + }); + if (getComputedStyle(target).position === "static") { + target.style.position = "relative"; + } + + const canvas = existing?.canvas ?? document.createElement("canvas"); + canvas.setAttribute("aria-hidden", "true"); + Object.assign(canvas.style, { + display: "block", + width: "100%", + height: "100%", + opacity: "0", + position: "absolute", + inset: "0", + }); + const presentation = existing?.presentation ?? new CanvasPresentation(canvas); + layer.append(canvas, presentation.canvas); + target.append(layer); + + let presenter: VisionPresenter | null = existing?.presenter ?? null; + let sequence = 0; + let disposed = false; + let width = + renderPackage?.surface?.width ?? Math.max(1, target.clientWidth || 1920); + let height = + renderPackage?.surface?.height ?? Math.max(1, target.clientHeight || 1080); + if (renderPackage?.surface) + Object.assign(layer.style, { width: `${width}px`, height: `${height}px` }); + let mediaSources: readonly VisionLiveMediaSource[] = []; + let resizeObserver: ResizeObserver | null = null; + let removeWindowResize: (() => void) | null = null; + let termination: Promise | null = null; + const mediaBridge = existing?.bridge ?? new VisionMediaBridge(); + const session: PersistentVisionSession | null = persistent + ? (existing ?? { + layer, + canvas, + presentation, + presenter: null, + bridge: mediaBridge, + sequence: 0, + owner, + closed: false, + fontsReady: false, + admittedFonts: new Set(), + onMessage: () => {}, + }) + : null; + if (session) { + session.owner = owner; + visionSessions.set(target, session); + } + let frameQueue: VisionFrames | null = null; + const waiters = new Map< + number, + { + resolve: () => void; + reject: (error: Error) => void; + } + >(); + + const presentError = (message: VisionPresenterMessage): Error => { + const error = new Error( + `Vision ${message.phase ?? "presenter"}: ${message.message ?? "render failed"}`, + ); + Object.assign(error, { phase: message.phase }); + return error; + }; + const onMessage = (event: PresenterEvent): void => { + if (disposed) return; + const message = event.data; + if (message.mediaSources) { + mediaSources = message.mediaSources; + onMediaSources(mediaSources); + } + if (message.type === "error") { + const error = presentError(message); + let rejectedRequest = false; + if (message.seq !== undefined) { + const waiter = waiters.get(message.seq); + if (waiter) { + waiters.delete(message.seq); + waiter.reject(error); + rejectedRequest = true; + } + } + if (!rejectedRequest) onError(error); + return; + } + if (message.type === "frame-submitted") { + try { + presentation.submit(); + } catch (error) { + const failure = + error instanceof Error ? error : new Error(String(error)); + const waiter = + message.seq === undefined ? null : waiters.get(message.seq); + if (message.seq !== undefined) waiters.delete(message.seq); + if (waiter) waiter.reject(failure); + else onError(failure); + return; + } + } + if (!COMPLETED_REQUESTS.has(message.type) || message.seq === undefined) + return; + const waiter = waiters.get(message.seq); + if (waiter) { + waiters.delete(message.seq); + waiter.resolve(); + } + }; + + const requestFrame = (request: Record): Promise => { + if ( + !presenter || + disposed || + (session && (session.closed || session.owner !== owner)) + ) + return Promise.reject(new Error("Vision scene is disconnected.")); + const seq = session ? ++session.sequence : ++sequence; + return new Promise((resolve, reject) => { + waiters.set(seq, { resolve, reject }); + presenter!.postMessage({ ...request, seq }); + }); + }; + + try { + const presenterUrl = new URL( + "vision/ui/mainpresenter.mjs", + document.baseURI, + ); + const presenterModule = (await import( + /* @vite-ignore */ presenterUrl.href + )) as PresenterModule; + if (!presenter) + presenter = persistent + ? createSessionPresenter(presenterModule, canvas, mediaBridge) + : presenterModule.createMainThreadPresenter(canvas, { + fetch: async ( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise => { + if (String(input) === "/scene") { + return new Response( + new Uint8Array(renderPackage?.data ?? delivery.data), + { + status: 200, + headers: { "content-type": "application/zip" }, + }, + ); + } + if (String(input) === "/fonts") + return Response.json( + [...hostFonts, ...(renderPackage?.hostFonts ?? [])].map( + (_, i) => `/fonts/${i}`, + ), + ); + if (String(input).startsWith("/fonts/")) { + const index = Number(String(input).slice(7)); + const bytes = Number.isInteger(index) + ? [ + ...(await loadHostFonts()), + ...(renderPackage?.hostFonts ?? []).map((font) => + VerifiedFont.isVerified(font) ? font.copy() : font, + ), + ][index] + : undefined; + return bytes + ? new Response(new Uint8Array(bytes)) + : new Response(null, { status: 404 }); + } + return fetch(input, init); + }, + loadWasm: async () => + mediaBridge.module( + await import( + /* @vite-ignore */ new URL( + "vision/pkg/lumencast_vision_web.js", + document.baseURI, + ).href + ), + ), + }); + if (session?.closed) { + presenter.terminate(); + throw new Error("Vision host session disconnected."); + } + if (session) { + session.onMessage = onMessage; + if (!session.presenter) { + session.presenter = presenter; + listenToSession(session); + } + } else presenter.addEventListener("message", onMessage); + + if (!existing) + await requestFrame({ + type: persistent ? "init-empty" : "init", + canvas, + width, + height, + sentAt: performance.timeOrigin + performance.now(), + }); + const admitFonts = async ( + fonts: Array, + ): Promise => { + if (!session) return; + // Native WebCrypto avoids WASM SHA compression on every switch. Only + // digests acknowledged by Rust belong to this engine's font bank. + const entries = await Promise.all( + fonts.map((font) => + VerifiedFont.isVerified(font) ? font : VerifiedFont.admit(font), + ), + ); + const pending = new Map( + entries + .filter((entry) => !session.admittedFonts.has(entry.digest)) + .map((entry) => [entry.digest, entry]), + ); + if (!pending.size) return; + let batch = new Map(); + let batchBytes = 0; + const submit = async (): Promise => { + if (!batch.size) return; + await requestFrame({ + type: "preload-fonts", + fonts: [...batch.values()], + }); + for (const digest of batch.keys()) session.admittedFonts.add(digest); + batch = new Map(); + batchBytes = 0; + }; + for (const [digest, font] of pending) { + const bytes = font.copy(); + if (bytes.length > 64 * 1024 * 1024) + throw new Error("Font preload exceeds byte limit."); + if (batch.size === 64 || batchBytes + bytes.length > 64 * 1024 * 1024) + await submit(); + batch.set(digest, bytes); + batchBytes += bytes.length; + } + await submit(); + }; + if (session && !session.fontsReady) { + if (renderPackage?.installationFonts) { + for await (const fonts of renderPackage.installationFonts()) + await admitFonts(fonts); + } else await admitFonts(await loadHostFonts()); + session.fontsReady = true; + } + if (persistent) { + await admitFonts(renderPackage?.hostFonts ?? []); + for (const digest of renderPackage?.requiredFontDigests ?? []) + if (!session?.admittedFonts.has(digest)) + throw new Error(`SOLAR_AUTHORING_FONT_NOT_ADMITTED: ${digest}`); + await requestFrame({ + type: "load", + ...(renderPackage?.assets ? { assets: renderPackage.assets } : {}), + packageBytes: new Uint8Array(renderPackage?.data ?? delivery.data), + fonts: [], + sentAt: performance.timeOrigin + performance.now(), + }); + } + const filterState = renderPackage?.lsmlDefaults + ? (state: Record) => + Object.fromEntries( + Object.entries(state).filter( + ([path]) => !path.startsWith("__cam."), + ), + ) + : visibleState; + const projectState = ( + state: Record, + ): Record => { + const result = visionTextPatch( + filterState(state), + renderPackage?.textBindings ?? {}, + ); + for (const [path, aliases] of Object.entries( + renderPackage?.geometryBindings ?? {}, + )) { + if (Object.hasOwn(state, path)) { + const value = state[path]; + if ( + !Array.isArray(value) || + value.length !== 2 || + value.some((v) => typeof v !== "number" || !Number.isFinite(v)) + ) + throw new Error(`Invalid editable position: ${path}`); + result[aliases[0]] = value[0]; + result[aliases[1]] = value[1]; + } + } + for (const [alias, path] of Object.entries( + renderPackage?.animationBindings ?? {}, + )) { + if (Object.hasOwn(state, path)) result[alias] = state[path]; + } + for (const [path, alias] of Object.entries( + renderPackage?.imageBindings ?? {}, + )) { + if (Object.hasOwn(state, path)) { + const value = state[path]; + result[alias] = visionImageValue( + value, + renderPackage?.imageAssets ?? {}, + ); + } + } + return result; + }; + const snapshot = projectState(initialState); + if (Object.keys(snapshot).length > 0) { + await requestFrame({ + type: "patch", + patchJson: JSON.stringify(snapshot), + }); + } + if (disposed) + throw new Error("Vision scene was disconnected during initialization."); + frameQueue = new VisionFrames(async (frame) => { + await presenter!.flush(); + if (disposed) throw new Error("Vision scene is disconnected."); + if (Object.keys(frame.patch).length > 0) { + // Upload camera pixels before the state patch's one real GPU render. + mediaBridge.upload(frame.frames); + await requestFrame({ + type: "patch", + patchJson: JSON.stringify(frame.patch), + ...(frame.images ? { images: frame.images } : {}), + }); + } else if (frame.frames.length > 0) { + await requestFrame({ type: "media-frames", frames: frame.frames }); + } + }); + const updateSize = (nextWidth: number, nextHeight: number): void => { + const boundedWidth = Math.round(nextWidth); + const boundedHeight = Math.round(nextHeight); + if ( + boundedWidth < 1 || + boundedHeight < 1 || + (boundedWidth === width && boundedHeight === height) + ) + return; + width = boundedWidth; + height = boundedHeight; + void requestFrame({ type: "resize", width, height }).catch(onError); + }; + if (renderPackage?.surface) { + // Pulsar samples fixed pixel bands; a stale browser viewport must not stretch them. + } else if (typeof ResizeObserver === "function") { + resizeObserver = new ResizeObserver((entries) => { + const entry = entries.at(-1); + if (entry) + updateSize(entry.contentRect.width, entry.contentRect.height); + }); + resizeObserver.observe(target); + } else { + const onWindowResize = (): void => + updateSize(target.clientWidth, target.clientHeight); + window.addEventListener("resize", onWindowResize); + removeWindowResize = () => + window.removeEventListener("resize", onWindowResize); + } + if (!renderPackage?.surface) + updateSize(target.clientWidth, target.clientHeight); + return { + sceneId: delivery.sceneId, + sceneVersion: renderPackage?.sceneVersion ?? delivery.sceneVersion, + get mediaSources() { + return mediaSources; + }, + canApplyPatch: (patch) => + Boolean(renderPackage?.resolveImages) || + Object.keys(renderPackage?.imageBindings ?? {}).every((path) => { + const value = patch[path]; + return ( + !Object.hasOwn(patch, path) || + visionImageValue(value, renderPackage?.imageAssets ?? {}) === + renderPackage?.imageValues?.[path] + ); + }), + applyPatch: async (patch) => { + const filtered = projectState(patch); + const images = await renderPackage?.resolveImages?.(patch); + for (const [path, value] of Object.entries(images?.values ?? {})) + filtered[renderPackage!.imageBindings![path]!] = value; + if (Object.keys(filtered).length > 0) { + await frameQueue!.addPatch(filtered, images?.assets); + } + }, + resize: async (nextWidth, nextHeight) => { + const boundedWidth = Math.max(1, Math.round(nextWidth)); + const boundedHeight = Math.max(1, Math.round(nextHeight)); + if (boundedWidth === width && boundedHeight === height) return; + width = boundedWidth; + height = boundedHeight; + await frameQueue!.flush(); + await requestFrame({ type: "resize", width, height }); + }, + activate: () => { + if (!disposed && (!session || session.owner === owner)) + layer.style.opacity = "1"; + }, + deactivate: () => { + if (!disposed && (!session || session.owner === owner)) + layer.style.opacity = "0"; + }, + updateMedia: (frames) => { + if (frames.length === 0) return Promise.resolve(); + return frameQueue!.addMedia(frames); + }, + clearMedia: (paths) => { + // A reused media controller can retire paths from the previous scene. + // Only textures owned by this accepted scene can be cleared in its engine. + const owned = new Set(mediaSources.map((source) => source.path)); + const current = paths.filter((path) => owned.has(path)); + if (current.length === 0) return Promise.resolve(); + return frameQueue! + .flush() + .then(() => requestFrame({ type: "media-clear", paths: current })); + }, + flush: () => + termination ?? frameQueue!.flush().then(() => presenter?.flush()), + dispose: () => { + if (disposed) return; + disposed = true; + frameQueue?.close(); + resizeObserver?.disconnect(); + removeWindowResize?.(); + if (!session) { + presenter?.terminate(); + layer.remove(); + } else if (session.owner === owner) { + layer.style.opacity = "0"; + const seq = ++session.sequence; + presenter?.postMessage({ type: "clear-scene", seq }); + session.onMessage = () => {}; + } + termination = presenter?.flush() ?? Promise.resolve(); + presenter = null; + for (const waiter of waiters.values()) + waiter.reject(new Error("Vision scene disconnected.")); + waiters.clear(); + }, + }; + } catch (error) { + resizeObserver?.disconnect(); + removeWindowResize?.(); + if (!session) { + presenter?.terminate(); + layer.remove(); + } else { + session.onMessage = () => {}; + presenter?.postMessage({ type: "clear-scene", seq: ++session.sequence }); + layer.style.opacity = "0"; + } + throw error; + } +} + +export function activateVisionScene( + scene: VisionSceneHandle, + previous?: VisionSceneHandle, + retainPrevious = false, +): void { + scene.activate(); + if (previous && previous !== scene) { + if (retainPrevious) previous.deactivate?.(); + else previous.dispose(); + } +} + +export function visibleState( + state: Record, +): Record { + return Object.fromEntries( + Object.entries(state).filter(([path]) => !path.startsWith("__")), + ); +} diff --git a/src/host-entry.ts b/src/host-entry.ts new file mode 100644 index 0000000..c0f4eae --- /dev/null +++ b/src/host-entry.ts @@ -0,0 +1,183 @@ +import { VerifiedFont } from "./scenes/verified-font"; +import { installationFontBatches } from "./scenes/installation-fonts"; +// Production bootstrap for the single LSDP-to-Vision renderer. + +import { createCanvasSceneSourceProvider } from "./scenes/canvas"; +import { + createLocalImageAssetsProvider, + type LocalRenderAssetEndpoint, +} from "./scenes/local-images"; +import { mount } from "./mount"; +import type { SolarMode } from "./types"; + +interface SolarHostConfig { + nativeComposition?: { width: number; height: number }; + canvasApiUrl?: string; + canvasToken?: string; + fontAssetsUrl?: string; + installationFontCatalog?: { url: string; token: string }; + nativeLSDP?: { url: string; resource: string; selector?: string }; +} + +const DEFAULT_CANVAS_API = "https://zabgate.cyell.dev/canvas/api/v1"; +const params = new URLSearchParams(window.location.search); +const config = (globalThis as { __SOLAR_CONFIG__?: SolarHostConfig }) + .__SOLAR_CONFIG__; + +const nativeUrl = config?.nativeLSDP?.url ?? params.get("lsdp") ?? undefined; +const connectionUrl = nativeUrl; +if (!connectionUrl) { + document.body.textContent = "Solar: local native LSDP URL required (lsdp)"; + throw new Error("SOLAR_LSDP_REQUIRED"); +} +try { + const parsed = new URL(connectionUrl); + const loopback = /^(127\.0\.0\.1|localhost|\[::1\]|::1)$/i.test( + parsed.hostname, + ); + if (!loopback || !/^(ws|wss):$/i.test(parsed.protocol)) { + throw new Error("SOLAR_NATIVE_LSDP_REQUIRED"); + } +} catch { + document.body.textContent = "Solar: local native LSDP connection required"; + throw new Error("SOLAR_NATIVE_LSDP_REQUIRED"); +} + +const token = config?.nativeLSDP ? "" : (params.get("token") ?? ""); +const canvasApiUrl = config?.canvasApiUrl ?? DEFAULT_CANVAS_API; +const canvasToken = config?.canvasToken ?? params.get("canvas_token") ?? token; +const fontCache = new Map(); +async function localFonts(signal: AbortSignal): Promise { + const url = new URL(config!.fontAssetsUrl!); + if ( + !/^(127\.0\.0\.1|localhost|\[::1\])$/i.test(url.hostname) || + !/^https?:$/.test(url.protocol) + ) + throw new Error("SOLAR_FONT_HOST_REQUIRED"); + const headers = { Authorization: `Bearer ${canvasToken}` }; + const response = await fetch(url, { + headers, + signal, + cache: "no-store", + redirect: "error", + }); + if (!response.ok) throw new Error("SOLAR_FONT_MANIFEST_UNAVAILABLE"); + const hashes: unknown = await response.json(); + if ( + !Array.isArray(hashes) || + hashes.length > 64 || + hashes.some( + (hash) => typeof hash !== "string" || !/^[a-f0-9]{64}$/.test(hash), + ) + ) + throw new Error("SOLAR_FONT_MANIFEST_INVALID"); + for (const key of fontCache.keys()) + if (!hashes.includes(key)) fontCache.delete(key); + const fonts: VerifiedFont[] = []; + let size = 0; + for (const hash of hashes) { + let bytes = fontCache.get(hash); + if (!bytes) { + const asset = await fetch(`${url.href}/${hash}`, { + headers, + signal, + redirect: "error", + }); + if ( + !asset.ok || + Number(asset.headers.get("content-length")) > 8 * 1024 * 1024 + ) + throw new Error("SOLAR_FONT_ASSET_UNAVAILABLE"); + bytes = await VerifiedFont.admit( + new Uint8Array(await asset.arrayBuffer()), + hash, + ); + } + size += bytes.byteLength; + if (size > 8 * 1024 * 1024) throw new Error("SOLAR_FONT_RESOURCE_LIMIT"); + fontCache.set(hash, bytes); + fonts.push(bytes); + } + return fonts; +} +const modeParam = params.get("mode") ?? "broadcast"; +const mode: SolarMode = (["broadcast", "control", "test"] as const).includes( + modeParam as SolarMode, +) + ? (modeParam as SolarMode) + : "broadcast"; +const target = document.getElementById("scene"); +if (!(target instanceof HTMLElement)) { + document.body.textContent = "Solar: #scene target missing"; + throw new Error("SOLAR_TARGET_REQUIRED"); +} + +const canvasOptions = { apiUrl: canvasApiUrl, token: canvasToken }; +const upstream = createCanvasSceneSourceProvider(canvasOptions); +mount({ + sceneImageAssetsProvider: globalThis.__ZAB_RENDER_ASSET_ENDPOINT__ + ? createLocalImageAssetsProvider(globalThis.__ZAB_RENDER_ASSET_ENDPOINT__) + : undefined, + nativeComposition: config?.nativeComposition, + target, + nativeLSDP: { + url: nativeUrl, + resource: config?.nativeLSDP?.resource ?? params.get("resource") ?? "scene", + selector: + config?.nativeLSDP?.selector ?? params.get("selector") ?? undefined, + }, + token, + mode, + sceneSourceProvider: upstream, + fontAssetsProvider: config?.fontAssetsUrl ? localFonts : undefined, + installationFonts: config?.installationFontCatalog + ? () => installationFontBatches(config.installationFontCatalog!) + : undefined, + liveAudio: mode === "broadcast" || mode === "test", + onError: (error) => { + console.error( + `[solar] ${error.code}: ${error.message}` + + (error.recoverable ? " (recoverable)" : " (fatal)"), + ); + }, + onStatus: (status) => { + document.documentElement.dataset.solarStatus = status; + }, +}); + +declare global { + var __ZAB_RENDER_ASSET_ENDPOINT__: LocalRenderAssetEndpoint | undefined; + // Set by the trusted embedding host before this module loads. Scene identity + // always comes from LSDP snapshots; this config contains source API access only. + var __SOLAR_CONFIG__: SolarHostConfig | undefined; + var __ZAB_CAPTURE_DEVICES__: + | Record< + string, + { + label?: string; + deviceId?: string; + captureSourceId?: string; + kind?: string; + } + > + | undefined; + var __ZAB_CAPTURE_DEFAULT_SCREEN__: + | { + label?: string; + deviceId?: string; + captureSourceId?: string; + kind?: string; + } + | null + | undefined; + var __ZAB_CAPTURE_RESOLVE_DEFAULT_SCREEN__: + | (() => Promise<{ + label?: string; + deviceId?: string; + captureSourceId?: string; + kind?: string; + } | null>) + | undefined; + var __ZAB_PEER_VIEWER__: unknown | undefined; + var __ZAB_DISABLE_PEER_VIEWER__: boolean | undefined; +} diff --git a/src/host-entry.tsx b/src/host-entry.tsx deleted file mode 100644 index c6421f0..0000000 --- a/src/host-entry.tsx +++ /dev/null @@ -1,168 +0,0 @@ -// Host / standalone bootstrap entry — the production counterpart of -// src/dev-entry.tsx, compiled into the self-contained `dist/host/` bundle. -// -// Per ADR 001 (dual-build) this entry is the root of the *host* Vite -// target: an app-mode build that inlines every runtime dep (react, -// react-dom, @preact/signals-react, framer-motion) so the emitted JS -// carries ZERO bare ESM specifiers. That bundle is what Orion -// static-serves at /static/solar/v{N}/ and the Pulsar CEF loads with no -// bundler and no '); + put("host.html", ''); + put("src/index.ts", "export {};\n"); + put("src/server/index.ts", "export {};\n"); + put("src/dev-entry.ts", "export {};\n"); + put("src/host-entry.ts", "export {};\n"); + put("scripts/README.md", "# Tools\n"); + run({ root, put }); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +test("test imports cannot retain an orphan or an unused product export", () => + fixture(({ root, put }) => { + put("src/used.ts", "export const live = 1; export const testOnly = 2;\n"); + put("src/index.ts", 'export {live} from "./used";\n'); + put("src/orphan.ts", "export const orphan = 1;\n"); + put( + "tests/test.ts", + 'import {testOnly} from "../src/used"; import {orphan} from "../src/orphan"; void testOnly; void orphan;\n', + ); + const graph = sourceGraph(root); + assert.deepEqual(graph.unused, ["src/orphan.ts"]); + assert.ok(graph.unusedExports.some((e) => e.name === "testOnly")); + })); +test("worker is reached through its actual URL edge and disappears when that edge is removed", () => + fixture(({ root, put }) => { + put("src/worker.ts", "postMessage(1);\n"); + put( + "src/host-entry.ts", + 'new Worker(new URL("./worker.ts", import.meta.url));\n', + ); + assert.deepEqual(sourceGraph(root).unused, []); + put("src/host-entry.ts", "export {};\n"); + assert.deepEqual(sourceGraph(root).unused, ["src/worker.ts"]); + })); +test("public type aliases/re-exports remain contracts and broken imports/dependencies fail", () => + fixture(({ root, put }) => { + put("src/contract.ts", "export interface Contract { value: number }\n"); + put("src/index.ts", 'export type {Contract} from "./contract";\n'); + assert.deepEqual(sourceGraph(root).unusedExports, []); + put("src/host-entry.ts", 'import "./missing";\n'); + put( + "package.json", + JSON.stringify({ + dependencies: { unused: "1" }, + scripts: { check: "node check.mjs" }, + }), + ); + const report = sourceGraph(root); + assert.ok(report.problems.some((p) => p.includes("missing"))); + assert.deepEqual(report.unusedDependencies, ["unused"]); + })); +test("documentation detects broken links/headings, commands, paths and unowned tools", () => + fixture(({ root, put }) => { + put( + "README.md", + "# Fixture\n[bad](absent.md)\n[self](README.md#absent)\n`npm run absent`\n`src/absent.ts`\nnode scripts/absent.mjs --input fixture\n", + ); + put("scripts/orphan.mjs", "export {};\n"); + const report = documentationReport(root); + for (const fragment of [ + "broken link", + "missing heading", + "missing npm command", + "missing current path", + "missing current tool", + "missing from its responsibility README", + ]) + assert.ok( + report.failures.some((p) => p.includes(fragment)), + fragment, + ); + })); +test("historical evidence and external repository commands stay outside Solar documentation checks", () => + fixture(({ root, put }) => { + put("docs/adr/old.md", "[old](absent.md)\n`npm run absent`\n"); + put("evidence/old.md", "[old](absent.md)\n"); + put("README.md", "# Fixture\nZabCanvas/scripts/prove-vision-source.py\n"); + assert.deepEqual(documentationReport(root).failures, []); + })); diff --git a/tests/unit/animation-compositing.test.tsx b/tests/unit/animation-compositing.test.tsx deleted file mode 100644 index 5063ad1..0000000 --- a/tests/unit/animation-compositing.test.tsx +++ /dev/null @@ -1,273 +0,0 @@ -// Proving test for the ADR 011 I7 live bug — the keyframe WRAPPER must be a -// REAL compositing box, not a `display:contents` element. -// -// THE BUG (proven live by frame-diff, tir I7 #2). The render bundle Orion -// serves for `core.animation.play@1` was CORRECT: a keyframed `frame` -// wrapper carrying the target's static geometry (x:80,y:360, 160×160) with -// the animated `transform`/`opacity` keyframes, and the resolved target -// (`anim_box`, x/y stripped, its own size+fill) NESTED beneath it. Yet at -// the antenna the box rendered 100×100 pinned at (0,0), immobile — no -// translateX, no fade. Root cause: `@lumencast/runtime`'s `KeyframePlayer` -// wrapped the played subtree in a ``. -// A `display:contents` element generates NO box, so the browser silently -// dropped the animated `transform`/`opacity` framer-motion wrote onto it — -// the geometry the wrapper carried never composited and the nested target -// rendered dead at its default origin. -// -// THE FIX (patches/@lumencast+runtime+0.6.0.patch). The player now renders -// `` — a real compositing -// box that (a) carries the animated channels onto live pixels and (b), being -// positioned, is the containing block for the absolutely-positioned Frame -// nested beneath, so the target's authored x/y are preserved. -// -// This test mounts Solar end-to-end over a fake LSDP/1.1 transport against a -// bundle shaped exactly like the live render-bundle (wrapper frame with the -// target geometry + nested target), and asserts on the actual DOM that the -// keyframe wrapper is a COMPOSITING box carrying the animated transform — -// not a `display:contents` element. happy-dom's wall clock is -// non-deterministic, so we assert the STRUCTURE that makes compositing -// possible (a positioned box with a transform/opacity style), not a precise -// mid-tween pixel sample (that is the Playwright E2E's job). - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { encodeFrame, snapshot, delta } from "@lumencast/protocol"; -import type { LeafValue } from "@lumencast/protocol"; -import type { RenderBundle, RenderNode } from "@lumencast/runtime"; -import { mount } from "../../src/mount"; -import { buildAnimationNode } from "../../src/overlay/animation"; - -const SCENE_ID = "scene-anim"; -const SCENE_VERSION = "sha256-anim-0001"; -// The scalar generation leaf the exec op increments (ADR 011 §3.2). -const LEAF = "__anim.anim_box"; - -// The authored keyframe geometry the live render-bundle carries: -// translateX 0→120→360→400 px over 1500 ms, opacity 0→1, ease-out. -const KEYFRAMES = { - duration_ms: 1500, - easing: "ease-out" as const, - steps: [ - { at: 0, transform: { translateX: 0 }, opacity: 0 }, - { at: 0.3, transform: { translateX: 120 }, opacity: 1 }, - { at: 0.8, transform: { translateX: 360 }, opacity: 1 }, - { at: 1, transform: { translateX: 400 }, opacity: 1 }, - ], -}; - -// The resolved target overlay node — `anim_box` shape, x/y stripped by the -// lowering (the wrapper carries the position), keeping its own size + fill. -const NESTED_TARGET: RenderNode = { - kind: "shape", - id: "anim_box", - props: { width: 160, height: 160, fill: "#C81E5A" }, -}; - -// The wrapper node exactly as the live render-bundle serves it: the target's -// STATIC geometry (x:80, y:360, 160×160) on the wrapper props, the animated -// keyframes keyed on the scalar leaf, and the target NESTED beneath. -const ANIM_NODE = buildAnimationNode({ - leafPath: LEAF, - keyframes: KEYFRAMES, - id: "anim_play", - props: { x: 80, y: 360, width: 160, height: 160 }, - children: [NESTED_TARGET], -}); - -const BUNDLE: RenderBundle = { - scene_version: SCENE_VERSION, - root: { kind: "stack", children: [ANIM_NODE] }, -}; - -// --- fake LSDP/1.1 transport ------------------------------------------ - -class FakeWebSocket { - static readonly CONNECTING = 0; - static readonly OPEN = 1; - static readonly CLOSING = 2; - static readonly CLOSED = 3; - static last: FakeWebSocket | null = null; - readonly CONNECTING = 0; - readonly OPEN = 1; - readonly CLOSING = 2; - readonly CLOSED = 3; - readyState = 0; - protocol = "lsdp.v1.1"; - onopen: (() => void) | null = null; - onmessage: ((ev: { data: string }) => void) | null = null; - onclose: ((ev: { code: number; reason: string }) => void) | null = null; - onerror: ((ev: unknown) => void) | null = null; - - constructor(public url: string) { - FakeWebSocket.last = this; - queueMicrotask(() => { - this.readyState = this.OPEN; - this.onopen?.(); - }); - } - send(raw: string): void { - const frame = JSON.parse(raw) as { type?: string }; - if (frame.type === "subscribe") { - const snap = snapshot({ - seq: 1, - scene_id: SCENE_ID, - scene_version: SCENE_VERSION, - // The scalar generation leaf starts at 0 (ADR 011 §3.2). - state: { [LEAF]: 0 }, - }); - queueMicrotask(() => this.onmessage?.({ data: encodeFrame(snap) })); - } - } - push(frame: ReturnType): void { - this.onmessage?.({ data: encodeFrame(frame) }); - } - close(): void { - this.readyState = this.CLOSED; - this.onclose?.({ code: 1000, reason: "client closing" }); - } -} - -afterEach(() => { - vi.restoreAllMocks(); - FakeWebSocket.last = null; -}); - -const sleep = (ms: number): Promise => new Promise((r) => setTimeout(r, ms)); -async function waitFor(predicate: () => boolean, timeout = 2000): Promise { - const start = Date.now(); - for (;;) { - if (predicate()) return; - if (Date.now() - start > timeout) throw new Error("waitFor: timeout"); - await sleep(10); - } -} - -/** The keyframe player's wrapper — the element that carries the animated - * transform/opacity. Post-fix it is a positioned compositing box - * (`position:absolute; inset:0`) wrapping the nested target Frame. */ -function keyframeWrapper(target: HTMLElement): HTMLElement | null { - // We resolve via the live CSSOM `style` PROPERTY (not an attribute-substring - // selector): framer-motion writes the animated `transform`/`opacity` to the - // element's style object, and happy-dom does not always reflect those into - // the serialized `style` attribute. The player is the positioned `inset:0` - // box (NOT `display:contents`) that CONTAINS the nested 160px target Frame — - // that containment distinguishes it from the scene's other positioned - // wrappers. The broadcast mode also mounts an OUTER `inset:0` scene - // container that contains the player, so several boxes match : we pick the - // INNERMOST (the keyframe player nests beneath the scene container, never - // the reverse), which is the box carrying the played transform/opacity. - const candidates = Array.from(target.querySelectorAll("div")).filter( - (el) => - el.style.position === "absolute" && - (el.style.inset === "0px" || el.style.inset === "0") && - Array.from(el.querySelectorAll("div")).some((c) => c.style.width === "160px"), - ); - if (candidates.length === 0) return null; - // The innermost candidate is the one not containing any other candidate. - return ( - candidates.find((el) => !candidates.some((other) => other !== el && el.contains(other))) ?? - null - ); -} - -describe("ADR 011 I7 — keyframe wrapper composites geometry + transform", () => { - it("renders the keyframe player as a real positioned box, NOT display:contents", async () => { - const target = document.createElement("div"); - document.body.appendChild(target); - vi.stubGlobal( - "fetch", - vi.fn(async () => new Response(JSON.stringify(BUNDLE), { status: 200 })), - ); - vi.stubGlobal("WebSocket", FakeWebSocket as unknown as typeof WebSocket); - - const handle = mount({ - target, - orionUrl: "ws://127.0.0.1:4007/orion/api/v1/show/stream", - token: "operator-token", - mode: "broadcast", - }); - - // Wait for the keyframe player's compositing box to mount. - await waitFor(() => keyframeWrapper(target) !== null); - const wrapper = keyframeWrapper(target)!; - - // THE REGRESSION GUARD: the wrapper must NOT be a `display:contents` - // element (the dead-pixel bug). It must be a real box. - expect(wrapper.style.display).not.toBe("contents"); - expect(wrapper.style.position).toBe("absolute"); - - // The nested target frame renders BENEATH the wrapper (the wrapper is its - // containing block), keeping its authored size + fill. The target's - // geometry is composed INSIDE the animated wrapper — not at the document - // origin with a default size. - const box = - Array.from(wrapper.querySelectorAll("div")).find( - (c) => c.style.width === "160px", - ) ?? null; - expect(box, "nested 160px target must render inside the wrapper").not.toBeNull(); - expect(box!.style.width).toBe("160px"); - expect(box!.style.height).toBe("160px"); - - handle.disconnect(); - target.remove(); - }); - - it("composites the keyframe translateX + opacity onto the wrapper box (not dropped)", async () => { - const target = document.createElement("div"); - document.body.appendChild(target); - vi.stubGlobal( - "fetch", - vi.fn(async () => new Response(JSON.stringify(BUNDLE), { status: 200 })), - ); - vi.stubGlobal("WebSocket", FakeWebSocket as unknown as typeof WebSocket); - - const handle = mount({ - target, - orionUrl: "ws://127.0.0.1:4007/orion/api/v1/show/stream", - token: "operator-token", - mode: "broadcast", - }); - await waitFor(() => keyframeWrapper(target) !== null); - - // The play runs on mount of the keyed wrapper (the scalar leaf seeded to 0 - // by the snapshot). framer-motion's wall clock is non-deterministic under - // happy-dom mid-tween, but the SETTLED frame is deterministic: at the end - // of the 1500 ms sequence the wrapper holds the LAST keyframe waypoint - // (translateX 400 px, opacity 1). We wait for the wrapper to reach that - // settled transform — proof the keyframe `translateX` AND `opacity` BOTH - // composited onto a real box. - // - // Pre-fix this could NEVER pass: (a) `display:contents` dropped every - // animated channel, and (b) the authored `translateX` key was emitted - // verbatim to framer (which animates `x`/`y`), so the translation was - // silently ignored even on a real box. Both halves of the I7 live bug. - await waitFor(() => { - const w = keyframeWrapper(target); - return w !== null && w.style.transform.includes("translateX(400px)"); - }, 4000); - const wrapper = keyframeWrapper(target)!; - expect(wrapper.style.transform, "wrapper must carry the keyframe translateX").toContain( - "translateX(400px)", - ); - expect(wrapper.style.opacity, "wrapper must carry the keyframe opacity").toBe("1"); - // The fix preserves the nested target's authored geometry: it sits at - // (80,360) 160×160 INSIDE the translating/fading wrapper, not at the - // origin with a default size. - const box = Array.from(wrapper.querySelectorAll("div")).find( - (c) => c.style.width === "160px", - )!; - expect(box.style.transform).toContain("translateX(80px)"); - expect(box.style.transform).toContain("translateY(360px)"); - - // A leaf delta re-triggers the replay (ADR 011 §3.2 — the exec op's tick - // increments the scalar generation leaf), and the wrapper re-settles to - // the composited end-state. Proof the reactive replay path also composites. - FakeWebSocket.last?.push(delta({ seq: 2, patches: [{ path: LEAF, value: 1 as LeafValue }] })); - await waitFor(() => { - const w = keyframeWrapper(target); - return w !== null && w.style.transform.includes("translateX(400px)"); - }, 4000); - - handle.disconnect(); - target.remove(); - }); -}); diff --git a/tests/unit/animation-node.test.ts b/tests/unit/animation-node.test.ts deleted file mode 100644 index 011c1ad..0000000 --- a/tests/unit/animation-node.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildAnimationNode } from "../../src/overlay/animation"; -import { buildWipeCoverNode } from "../../src/overlay/wipe-cover"; - -// I2 (ADR 011 §3.3) — the Solar oracle twin of Orion's lower_animation.go -// buildAnimationNode. These tests pin the bundle-fragment shape the Go↔TS -// parity test (Orion-side) asserts against, and prove wipe-cover is the -// degenerate case of the general builder (§3.5 / I4). - -describe("buildAnimationNode()", () => { - it("frames an authored keyframe block into a full-screen frame keyed on the scalar leaf", () => { - const node = buildAnimationNode({ - leafPath: "__anim.ov", - id: "anim-1", - keyframes: { - duration_ms: 500, - easing: "ease-out", - steps: [ - { at: 0, opacity: 0 }, - { at: 1, opacity: 1 }, - ], - }, - }); - - expect(node).toEqual({ - kind: "frame", - id: "anim-1", - props: { width: "100%", height: "100%", background: "#C81E5A" }, - keyframes: { - key: "__anim.ov", - duration_ms: 500, - easing: "ease-out", - steps: [ - { at: 0, opacity: 0 }, - { at: 1, opacity: 1 }, - ], - }, - }); - }); - - it("overrides any authored key with the compiler-bound leaf (ADR 011 §3.3)", () => { - const node = buildAnimationNode({ - leafPath: "__anim.scoreboard", - keyframes: { - key: "authored.leaf.to.be.overridden", - duration_ms: 200, - easing: "linear", - steps: [ - { at: 0, opacity: 1 }, - { at: 1, opacity: 0 }, - ], - }, - }); - expect(node.keyframes?.key).toBe("__anim.scoreboard"); - // id defaults to "wipe-cover" for byte-parity with the degenerate node. - expect(node.id).toBe("wipe-cover"); - }); - - it("frames the general animation.play path as a transform WRAPPER dimensioned to the target, with the target NESTED (I7 geometry fix)", () => { - // The I7 live-bug fix: the general path is NOT a full-screen aplat - // (translating/fading a 1920×1080 uniform fill is invisible). The - // wrapper is sized/positioned to the target overlay, and the target is - // nested beneath so it inherits the animated transform/opacity. The - // wrapper carries NO background — it is a transparent transform host. - const node = buildAnimationNode({ - leafPath: "__anim.anim_box", - id: "anim-1", - props: { x: 80, y: 360, width: 160, height: 160 }, - children: [ - { - kind: "shape", - id: "anim_box", - props: { width: 160, height: 160, background: "#C81E5A" }, - }, - ], - keyframes: { - duration_ms: 500, - easing: "ease-out", - steps: [ - { at: 0, transform: { translateX: 0 } }, - { at: 1, transform: { translateX: 200 } }, - ], - }, - }); - - expect(node).toEqual({ - kind: "frame", - id: "anim-1", - // wrapper geometry = the target's box; NO background (transparent host) - props: { x: 80, y: 360, width: 160, height: 160 }, - keyframes: { - key: "__anim.anim_box", - duration_ms: 500, - easing: "ease-out", - steps: [ - { at: 0, transform: { translateX: 0 } }, - { at: 1, transform: { translateX: 200 } }, - ], - }, - // target nested beneath → inherits the animated transform - children: [ - { - kind: "shape", - id: "anim_box", - props: { width: 160, height: 160, background: "#C81E5A" }, - }, - ], - }); - }); - - it("subsumes wipe-cover: a reveal/hold/retract asset frames identically (§3.5)", () => { - // The wipe-cover specialisation expressed as a general Animation Asset: - // the same 4-step opacity geometry buildWipeCoverNode emits, framed by - // the general builder — proving no geometry is duplicated (I4). - const reveal = 400; - const hold = 500; - const retract = 400; - const total = reveal + hold + retract; - const wipe = buildWipeCoverNode({ - leafPath: "__inputs.blue.m10-scene-control.scene_control", - overlay: { kind: "wipe-cover", reveal_ms: reveal, hold_ms: hold, retract_ms: retract }, - }); - const general = buildAnimationNode({ - leafPath: "__inputs.blue.m10-scene-control.scene_control", - keyframes: { - duration_ms: total, - easing: "ease-in-out", - steps: [ - { at: 0, opacity: 0 }, - { at: reveal / total, opacity: 1 }, - { at: (reveal + hold) / total, opacity: 1 }, - { at: 1, opacity: 0 }, - ], - }, - }); - expect(general).toEqual(wipe); - }); -}); diff --git a/tests/unit/animations.test.ts b/tests/unit/animations.test.ts new file mode 100644 index 0000000..fa37650 --- /dev/null +++ b/tests/unit/animations.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { animationAssets, animationDocument, animationLeaf, prepareAnimationBindings, sampleAnimation, VisionAnimations } from "../../src/engine/animations"; +import type { LSMLDocument } from "../../src/scenes/native-document"; + +const document: LSMLDocument = { + lsml: "1.2", scene_id: "animation-proof", scene_version: `sha256:${"a".repeat(64)}`, + defaults: { visible: 0.8 }, + layout: { kind: "frame", id: "panel", position: { x: 30, y: 20 }, size: { w: 100, h: 50 }, bind: { opacity: "visible" }, children: [] }, + animations: { reveal: { target: "panel", keyframes: { duration_ms: 100, easing: "linear", steps: [{ at: 0, opacity: 0, translateX: 0 }, { at: 1, opacity: 1, translateX: 40 }] } } }, +}; +afterEach(() => vi.unstubAllGlobals()); +describe("Vision Blue animation adapter", () => { + it("samples sparse channels and holds the last frame", () => { + const asset = animationAssets(document).reveal!; + expect(sampleAnimation(asset, 0.5)).toEqual({ opacity: 0.5, translateX: 20 }); + expect(sampleAnimation(asset, 1)).toEqual({ opacity: 1, translateX: 40 }); + }); + it("preserves source and maps numeric bound opacity to a private patch", () => { + const variant = structuredClone(document); + const aliases = prepareAnimationBindings(variant); + expect(aliases[animationLeaf("panel", "opacity")]).toBe("visible"); + expect(variant.defaults![animationLeaf("panel", "opacity")]).toBe(0.8); + expect(document.defaults).toEqual({ visible: 0.8 }); + }); + it("uses original geometry on every sample without cumulative translation", () => { + const frames = [{ target: "panel", values: { translateX: 20, opacity: 0.5 } }]; + const variant = animationDocument(document, frames); + expect((variant.layout as Record).position).toEqual({ x: 50, y: 20 }); + expect(variant.defaults![animationLeaf("panel", "opacity")]).toBe(0.5); + expect(animationDocument(document, frames)).toEqual(variant); + expect((document.layout as Record).position).toEqual({ x: 30, y: 20 }); + }); + it("rejects missing targets and unsupported channels before rendering", () => { + expect(() => prepareAnimationBindings({ ...document, layout: { kind: "frame", id: "other" } })).toThrow("not found"); + expect(() => animationAssets({ ...document, animations: { bad: { target: "panel", keyframes: { duration_ms: 100, steps: [{ at: 0, unknown: 1 }] } } } })).toThrow("Unsupported"); + expect(() => animationAssets({ ...document, animations: { bad: { target: "panel", keyframes: { duration_ms: 100, easing: "invalid", steps: [{ at: 0 }] } } } })).toThrow("easing"); + }); + it("uses the authored CSS cubic bezier rather than an approximate named curve", () => { + const asset = animationAssets(document).reveal!; + expect(sampleAnimation({ ...asset, keyframes: { ...asset.keyframes, easing: "cubic-bezier(0,0,1,1)" } }, 0.5).opacity).toBeCloseTo(0.5); + expect(sampleAnimation({ ...asset, keyframes: { ...asset.keyframes, easing: "ease-in" } }, 0.5).opacity).toBeCloseTo(0.31536, 4); + }); + it("bounds submissions while a GPU frame is pending and retains the final state only for the same scene", async () => { + let callback: FrameRequestCallback | null = null, complete: (() => void) | null = null; + const schedule = vi.fn((next: FrameRequestCallback) => { callback = next; return 1; }); + vi.stubGlobal("requestAnimationFrame", schedule); vi.stubGlobal("cancelAnimationFrame", vi.fn()); + const player = new VisionAnimations(() => new Promise(resolve => { complete = resolve; }), vi.fn()); + const scene = structuredClone(document); + scene.defaults!["__animation.reveal"] = { animation_id: "reveal", command_id: "one" }; + player.update(scene); + (callback as unknown as FrameRequestCallback)(performance.now() + 1000); + scene.defaults!["__animation.reveal"] = { animation_id: "reveal", command_id: "two" }; + player.update(scene); + expect(schedule).toHaveBeenCalledTimes(1); + expect(player.current(scene)[0]?.values.opacity).toBe(1); + expect(player.current({ ...scene, scene_id: "other" })).toEqual([]); + (complete as unknown as () => void)(); + await new Promise(resolve => setTimeout(resolve, 0)); + expect(schedule).toHaveBeenCalledTimes(2); + player.clear(); + expect(player.current()).toEqual([]); + player.dispose(); + }); + it("replays a new command, deduplicates idle updates and cancels on disposal", async () => { + let callback: FrameRequestCallback | null = null; + const schedule = vi.fn((next: FrameRequestCallback) => { callback = next; return 1; }); + const cancel = vi.fn(); + vi.stubGlobal("requestAnimationFrame", schedule); vi.stubGlobal("cancelAnimationFrame", cancel); + const apply = vi.fn(async () => {}), error = vi.fn(); + const player = new VisionAnimations(apply, error); + const scene = structuredClone(document); + scene.defaults!["__animation.reveal"] = { animation_id: "reveal", command_id: "one" }; + player.update(scene); player.update(scene); + expect(schedule).toHaveBeenCalledTimes(1); + const first = callback as unknown as FrameRequestCallback; + first(performance.now() + 1000); + await new Promise(resolve => setTimeout(resolve, 0)); + expect(apply).toHaveBeenCalledTimes(1); + scene.defaults!["__animation.reveal"] = { animation_id: "reveal", command_id: "two" }; + player.update(scene); + expect(schedule).toHaveBeenCalledTimes(2); + player.dispose(); + expect(cancel).toHaveBeenCalledWith(1); + expect(error).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/atlas-mount-e2e.test.tsx b/tests/unit/atlas-mount-e2e.test.tsx deleted file mode 100644 index 3b4d262..0000000 --- a/tests/unit/atlas-mount-e2e.test.tsx +++ /dev/null @@ -1,145 +0,0 @@ -// End-to-end proof that `?atlas=` reaches the REAL mount point and the runtime -// (@lumencast/runtime ≥ 0.12.3) actually applies `buildAtlasRoot` to the loaded -// bundle (ADR 013 Prism §3.1, issue #41). No mock of the runtime here — this -// drives Solar's production `mount()` with a fake WS + fake fetch (same seam as -// bundle-fetch-auth.test), delivers a snapshot that triggers the bundle fetch, -// and asserts the PAINTED DOM reflects the z-band split when `?atlas=` is on and -// stays verbatim when it is off. This is the "real mount() exercised with -// ?atlas=" test Vigil required on the aborted #40. - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { encodeFrame, snapshot } from "@lumencast/protocol"; -import type { RenderNode } from "@lumencast/runtime"; -import { mount } from "../../src/mount"; -import { atlasMountOptions } from "../../src/internal/atlas-mount"; - -const ORION_URL = "ws://127.0.0.1:4007/orion/api/v1/show/stream.lsdp"; -const SCENE_ID = "scene-atlas"; -const SCENE_VERSION = "sha256:atlas-e2e"; - -// A composed scene with ONE native-capture separator → 2 z-bands, matching -// `?atlas=below,above`. The runtime fetches this as the render bundle; with the -// atlas transform wired it must be split into stacked band frames. -const SCENE_ROOT: RenderNode = { - kind: "stack", - children: [ - { kind: "frame", id: "below-box", props: { width: 400, height: 200 } }, - { kind: "x-zab.capture", id: "cam", props: { width: 1920, height: 1080 } }, - { kind: "frame", id: "above-box", props: { width: 300, height: 150 } }, - ], -}; - -class FakeWebSocket { - static instances: FakeWebSocket[] = []; - static OPEN = 1; - static CLOSED = 3; - readyState = 1; - protocol = "lsdp.v1.1"; - onopen: (() => void) | null = null; - onmessage: ((ev: { data: string }) => void) | null = null; - onerror: ((ev: unknown) => void) | null = null; - onclose: ((ev: { code: number; reason: string }) => void) | null = null; - sent: string[] = []; - - constructor( - public url: string, - public protocols?: string[], - ) { - FakeWebSocket.instances.push(this); - queueMicrotask(() => this.onopen?.()); - } - send(data: string): void { - this.sent.push(data); - } - close(): void { - this.readyState = FakeWebSocket.CLOSED; - } - pushSnapshot(): void { - this.onmessage?.({ - data: encodeFrame( - snapshot({ - seq: 1, - scene_id: SCENE_ID, - scene_version: SCENE_VERSION, - state: {}, - }), - ), - }); - } -} - -describe("mount() applies the atlas transform end-to-end", () => { - let originalWS: typeof globalThis.WebSocket; - let originalFetch: typeof globalThis.fetch; - - beforeEach(() => { - FakeWebSocket.instances = []; - originalWS = globalThis.WebSocket; - originalFetch = globalThis.fetch; - // @ts-expect-error — install the fake WS the runtime picks up. - globalThis.WebSocket = FakeWebSocket; - globalThis.fetch = vi.fn(async () => ({ - ok: true, - status: 200, - statusText: "OK", - json: async () => ({ scene_version: SCENE_VERSION, root: SCENE_ROOT }), - })) as unknown as typeof fetch; - }); - - afterEach(() => { - globalThis.WebSocket = originalWS; - globalThis.fetch = originalFetch; - }); - - async function driveMount(search: string): Promise { - const target = document.createElement("div"); - document.body.appendChild(target); - mount({ - target, - orionUrl: ORION_URL, - token: "fake", - mode: "broadcast", - ...atlasMountOptions(search), - }); - await Promise.resolve(); - expect(FakeWebSocket.instances).toHaveLength(1); - FakeWebSocket.instances[0]!.pushSnapshot(); - // A loading shell also contains divs. Wait for authored scene paint, - // not merely for the host's first DOM element. - await vi.waitFor( - () => - expect( - Array.from(target.querySelectorAll("div")).some( - (element) => element.style.width === "400px", - ), - ).toBe(true), - { timeout: 3_000 }, - ); - return target; - } - - it("splits the scene into stacked z-bands when ?atlas= is set", async () => { - const target = await driveMount("?atlas=below,above"); - await vi.waitFor(() => { - const divs = Array.from(target.querySelectorAll("div")); - // The atlas stage is the full 2×1080 height, with two 1080 band slices — - // proof the runtime ran buildAtlasRoot on the fetched bundle. - expect(divs.some((d) => d.style.height === "2160px")).toBe(true); - expect(divs.filter((d) => d.style.height === "1080px").length).toBe(2); - }); - }); - - it("renders the fetched bundle verbatim when ?atlas= is absent (non-regression)", async () => { - const target = await driveMount("?mode=broadcast"); - // Give any transform a chance to (wrongly) run before asserting absence. - await Promise.resolve(); - const divs = Array.from(target.querySelectorAll("div")); - // No 1920×2160 atlas stage — the scene is painted verbatim, NOT stacked - // into z-bands (the discriminator: only the atlas transform emits a - // 2×1080 stage; the raw scene has no node that tall). - expect(divs.some((d) => d.style.height === "2160px")).toBe(false); - // The authored scenic boxes still paint. - expect(divs.some((d) => d.style.width === "400px")).toBe(true); - expect(divs.some((d) => d.style.width === "300px")).toBe(true); - }); -}); diff --git a/tests/unit/atlas-mount.test.ts b/tests/unit/atlas-mount.test.ts deleted file mode 100644 index 50333ef..0000000 --- a/tests/unit/atlas-mount.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Wiring proof for the `?atlas=` render mode (ADR 013 Prism §3.1, issue #41). -// -// Two seams under test, both at the REAL mount boundary (not the pure #38/#39 -// functions in isolation): -// -// 1. `atlasMountOptions(search)` — the host-entry helper that reads `?atlas=` -// and produces the `transformRoot` slice of MountOptions. The load-bearing -// contract is KEY ABSENCE when atlas mode is off (issue #41 criterion 1): -// the returned object must NOT carry a `transformRoot` key, so spreading it -// into `mount()` leaves the render path byte-identical to today. -// -// 2. `mount()` → runtime forwarding — Solar's mount must hand `transformRoot` -// through to `@lumencast/runtime`'s mount VERBATIM when present, and OMIT -// the key entirely when absent. Asserted by mocking the runtime mount and -// inspecting the captured options (`'transformRoot' in opts`), exactly the -// "mount() called WITHOUT the transformRoot key" check the issue asks for. - -import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { RenderNode } from "@lumencast/runtime"; - -const { mountSpy } = vi.hoisted(() => ({ mountSpy: vi.fn() })); - -// Keep every real runtime export (createPeerViewerFromInjection, types, …) and -// override only `mount` so we can capture the options Solar forwards. -vi.mock("@lumencast/runtime", async (orig) => { - const actual = await orig(); - return { ...actual, mount: mountSpy }; -}); - -import { mount } from "../../src/mount"; -import { atlasMountOptions } from "../../src/internal/atlas-mount"; -import type { MountOptions } from "../../src/types"; - -// A composed scene root with exactly ONE native-capture separator → 2 z-bands, -// matching `?atlas=below,above`. -const rootWithOneCapture: RenderNode = { - kind: "stack", - children: [ - { kind: "frame", id: "below-box", props: { width: 400, height: 200 } }, - { kind: "x-zab.capture", id: "cam", props: { width: 1920, height: 1080 } }, - { kind: "frame", id: "above-box", props: { width: 300, height: 150 } }, - ], -}; - -// A root with NO capture separator → 1 z-band. Feeding it to a 2-band spec is -// the N-mismatch the transform must throw on. -const rootNoCapture: RenderNode = { - kind: "stack", - children: [{ kind: "frame", id: "solo", props: { width: 100, height: 100 } }], -}; - -describe("atlasMountOptions() — entry wiring", () => { - it("omits transformRoot entirely when ?atlas= is absent (non-regression)", () => { - const opts = atlasMountOptions("?mode=broadcast"); - // Key ABSENCE, not `{ transformRoot: undefined }` — the spread must leave - // MountOptions untouched. - expect("transformRoot" in opts).toBe(false); - }); - - it("omits transformRoot when ?atlas= is malformed (parse → null)", () => { - expect("transformRoot" in atlasMountOptions("?atlas=,")).toBe(false); - expect("transformRoot" in atlasMountOptions("?atlas=below,below")).toBe( - false, - ); - }); - - it("binds transformRoot to buildAtlasRoot for a valid ?atlas= spec", () => { - const opts = atlasMountOptions("?atlas=below,above"); - expect(typeof opts.transformRoot).toBe("function"); - - const out = opts.transformRoot!(rootWithOneCapture); - // Real buildAtlasRoot applied: a 1920×2160 atlas stage wrapping 2 band - // frames keyed by the requested labels. - expect(out.id).toBe("atlas-root"); - expect(out.props?.height).toBe(2160); - const bandIds = (out.children ?? []).map((c) => c.id); - expect(bandIds).toEqual(["atlas-band-below", "atlas-band-above"]); - }); - - it("lets the buildAtlasRoot N-mismatch throw through the closure (no silent fail)", () => { - const opts = atlasMountOptions("?atlas=below,above"); - // Scene has 0 separators → 1 band, spec declared 2 → visible throw. - expect(() => opts.transformRoot!(rootNoCapture)).toThrow( - /scene has 0 native capture separator/, - ); - }); -}); - -describe("mount() forwards transformRoot to @lumencast/runtime", () => { - const base: MountOptions = { - target: document.createElement("div"), - orionUrl: "ws://127.0.0.1:4007/orion/api/v1/show/stream", - token: "fake-token", - mode: "broadcast", - }; - - beforeEach(() => { - mountSpy.mockReset(); - mountSpy.mockReturnValue({ disconnect: vi.fn(), setToken: vi.fn() }); - }); - - it("calls the runtime WITHOUT a transformRoot key when the host omits it", () => { - mount({ ...base }); - expect(mountSpy).toHaveBeenCalledTimes(1); - const opts = mountSpy.mock.calls[0]![0] as Record; - // The exact non-regression assertion: the key is absent from the options - // the runtime receives, not merely `undefined`. - expect("transformRoot" in opts).toBe(false); - }); - - it("forwards the transformRoot closure verbatim when the host provides it", () => { - mount({ ...base, ...atlasMountOptions("?atlas=below,above") }); - const opts = mountSpy.mock.calls[0]![0] as { - transformRoot?: (r: RenderNode) => RenderNode; - }; - expect("transformRoot" in opts).toBe(true); - - // The forwarded closure is the real atlas transform, wired to the parsed - // spec — applying it splits the scene into the declared bands. - const out = opts.transformRoot!(rootWithOneCapture); - expect(out.id).toBe("atlas-root"); - expect((out.children ?? []).length).toBe(2); - }); - - it("forwards image warming only after explicit opt-in", () => { - mount({ ...base }); - expect("preloadRosterImages" in (mountSpy.mock.calls[0]![0] as object)).toBe(false); - mount({ ...base, preloadRosterImages: true }); - expect(mountSpy.mock.calls[1]![0]).toMatchObject({ preloadRosterImages: true }); - }); -}); diff --git a/tests/unit/atlas-node.test.ts b/tests/unit/atlas-node.test.ts deleted file mode 100644 index ef813ee..0000000 --- a/tests/unit/atlas-node.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { describe, expect, it } from "vitest"; -import type { RenderNode } from "@lumencast/runtime"; -import { - ATLAS_BAND_HEIGHT, - ATLAS_STAGE_WIDTH, - buildAtlasRoot, -} from "../../src/atlas/atlas-node"; -import { parseAtlasParam } from "../../src/atlas/atlas-param"; - -// Structural proof of the ADR 013 atlas band-split transform (issue #38). -// buildAtlasRoot is a pure RenderNode → RenderNode transform (Solar's builder -// doctrine, like buildWipeCoverNode); these tests pin the emitted band tree -// exactly, including the per-band z-threshold membership, the N×1080 stage -// dimensions, and the transparency (no `background` anywhere). - -// Distinct scenic components + a native-capture separator. DOM order is -// z-order: `below-*` sit under the capture, `above-*` on top. -const belowText: RenderNode = { kind: "text", id: "below-title", props: { text: "BELOW" } }; -const belowShape: RenderNode = { kind: "shape", id: "below-bar" }; -const capture: RenderNode = { kind: "x-zab.capture", id: "cam", props: { width: 1920, height: 1080 } }; -const aboveText: RenderNode = { kind: "text", id: "above-scoreboard", props: { text: "ABOVE" } }; - -const twoBandRoot: RenderNode = { - kind: "stack", - children: [belowText, belowShape, capture, aboveText], -}; - -/** Every `background` value found on this node and its descendants. */ -function collectBackgrounds(node: RenderNode): unknown[] { - const here = node.props?.background; - const child = (node.children ?? []).flatMap(collectBackgrounds); - return here === undefined ? child : [here, ...child]; -} - -describe("buildAtlasRoot()", () => { - it("stacks two z-bands split at the native-capture seuil", () => { - const spec = parseAtlasParam("below,above"); - if (spec === null) throw new Error("fixture: spec must parse"); - - const atlas = buildAtlasRoot(twoBandRoot, spec); - - expect(atlas).toEqual({ - kind: "frame", - id: "atlas-root", - props: { - x: 0, - y: 0, - width: ATLAS_STAGE_WIDTH, - height: ATLAS_BAND_HEIGHT * 2, - clipsContent: false, - }, - children: [ - { - kind: "frame", - id: "atlas-band-below", - props: { - x: 0, - y: 0, - width: ATLAS_STAGE_WIDTH, - height: ATLAS_BAND_HEIGHT, - clipsContent: true, - }, - // Only the components UNDER the capture, capture itself dropped. - children: [belowText, belowShape], - }, - { - kind: "frame", - id: "atlas-band-above", - props: { - x: 0, - y: ATLAS_BAND_HEIGHT, - width: ATLAS_STAGE_WIDTH, - height: ATLAS_BAND_HEIGHT, - clipsContent: true, - }, - // Only the components OVER the capture. - children: [aboveText], - }, - ], - }); - }); - - it("gives the stage the full N×1080 height and each band its own slice", () => { - const spec = parseAtlasParam("below,mid,above"); - if (spec === null) throw new Error("fixture: spec must parse"); - - const root: RenderNode = { - kind: "stack", - children: [ - belowText, - { ...capture, id: "cam-a" }, - aboveText, - { ...capture, id: "cam-b" }, - { kind: "text", id: "top", props: { text: "TOP" } }, - ], - }; - - const atlas = buildAtlasRoot(root, spec); - - expect(atlas.props?.height).toBe(ATLAS_BAND_HEIGHT * 3); - const bands = atlas.children ?? []; - expect(bands.map((b) => b.props?.y)).toEqual([ - 0, - ATLAS_BAND_HEIGHT, - ATLAS_BAND_HEIGHT * 2, - ]); - // Band membership around the two capture separators. - expect(bands[0]?.children).toEqual([belowText]); - expect(bands[1]?.children).toEqual([aboveText]); - expect(bands[2]?.children?.map((c) => c.id)).toEqual(["top"]); - }); - - it("yields an empty band when a capture leads its slice (cam-only band)", () => { - const spec = parseAtlasParam("below,above"); - if (spec === null) throw new Error("fixture: spec must parse"); - - // Capture first → the 'below' band has no scenic component (the native - // source alone fills that slice). - const root: RenderNode = { - kind: "stack", - children: [capture, aboveText], - }; - - const atlas = buildAtlasRoot(root, spec); - const bands = atlas.children ?? []; - expect(bands[0]?.children).toEqual([]); - expect(bands[1]?.children).toEqual([aboveText]); - }); - - it("keeps every band and the stage transparent (no background)", () => { - const spec = parseAtlasParam("below,above"); - if (spec === null) throw new Error("fixture: spec must parse"); - - const atlas = buildAtlasRoot(twoBandRoot, spec); - // The only backgrounds present come from authored scenic children, never - // from the atlas frames the transform introduces. - expect(collectBackgrounds(atlas)).toEqual([]); - }); - - it("honours a non-default band height / width", () => { - const spec = parseAtlasParam("below,above"); - if (spec === null) throw new Error("fixture: spec must parse"); - - const atlas = buildAtlasRoot(twoBandRoot, spec, { - bandHeight: 720, - width: 1280, - }); - expect(atlas.props).toMatchObject({ width: 1280, height: 1440 }); - expect(atlas.children?.[1]?.props).toMatchObject({ y: 720, height: 720 }); - }); - - it("throws when the scene's capture count disagrees with ?atlas=", () => { - const spec = parseAtlasParam("below,mid,above"); // 3 bands - if (spec === null) throw new Error("fixture: spec must parse"); - - // twoBandRoot has ONE capture → 2 bands, not 3. - expect(() => buildAtlasRoot(twoBandRoot, spec)).toThrow(/atlas/i); - }); - - it("builds a single transparent band when the root has no children", () => { - const spec = parseAtlasParam("only"); - if (spec === null) throw new Error("fixture: spec must parse"); - - const atlas = buildAtlasRoot({ kind: "stack" }, spec); - expect(atlas.children).toHaveLength(1); - expect(atlas.children?.[0]?.children).toEqual([]); - }); -}); diff --git a/tests/unit/atlas-param.test.ts b/tests/unit/atlas-param.test.ts deleted file mode 100644 index 773da8b..0000000 --- a/tests/unit/atlas-param.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { parseAtlasParam } from "../../src/atlas/atlas-param"; - -// The `?atlas=` reader (ADR 013 Prism §3.1, issue #38). Parsing is total and -// resolves to `null` for every degenerate value so the caller falls back to -// the UNCHANGED default render path. - -describe("parseAtlasParam()", () => { - it("returns null when atlas mode is not requested (absent param)", () => { - // The non-regression sentinel: no `?atlas=` → null → the caller keeps the - // default, atlas-less bundle byte-identical to today. - expect(parseAtlasParam(null)).toBeNull(); - expect(parseAtlasParam(undefined)).toBeNull(); - }); - - it("returns null for empty / whitespace-only / comma-only values", () => { - expect(parseAtlasParam("")).toBeNull(); - expect(parseAtlasParam(" ")).toBeNull(); - expect(parseAtlasParam(",")).toBeNull(); - expect(parseAtlasParam(" , , ")).toBeNull(); - }); - - it("parses a two-band below,above spec", () => { - expect(parseAtlasParam("below,above")).toEqual({ - bands: ["below", "above"], - }); - }); - - it("parses a single-band spec (degenerate N=1)", () => { - expect(parseAtlasParam("only")).toEqual({ bands: ["only"] }); - }); - - it("parses N bands preserving bottom→top order", () => { - expect(parseAtlasParam("below,mid,above")).toEqual({ - bands: ["below", "mid", "above"], - }); - }); - - it("trims tokens and drops empty ones", () => { - expect(parseAtlasParam(" below , ,above ")).toEqual({ - bands: ["below", "above"], - }); - }); - - it("returns null on duplicate band labels (ambiguous keying)", () => { - expect(parseAtlasParam("below,below")).toBeNull(); - expect(parseAtlasParam("a,b,a")).toBeNull(); - }); -}); diff --git a/tests/unit/atlas-render.test.tsx b/tests/unit/atlas-render.test.tsx deleted file mode 100644 index 08ff0ac..0000000 --- a/tests/unit/atlas-render.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { afterEach, describe, expect, it } from "vitest"; -import type { RenderBundle, RenderNode } from "@lumencast/runtime"; -import { renderBundleHeadless } from "@lumencast/runtime"; -import { buildAtlasRoot } from "../../src/atlas/atlas-node"; -import { parseAtlasParam } from "../../src/atlas/atlas-param"; - -// DOM proof that the atlas transform PAINTS through the runtime's real -// broadcast render path (not just a structural tree): each band frame lands -// in the DOM at its slice height, with a transparent background, in a -// 1920×(1080·N) stage. Renders the atlas root headless (no WS, no fetch) — -// enough to assert geometry + transparency of the frames the transform emits. - -let handles: { unmount(): void }[] = []; - -afterEach(() => { - for (const h of handles) h.unmount(); - handles = []; -}); - -async function renderAtlas(root: RenderNode, atlasParam: string): Promise { - const spec = parseAtlasParam(atlasParam); - if (spec === null) throw new Error("fixture: spec must parse"); - const bundle: RenderBundle = { - scene_version: "sha256-atlas-test", - root: buildAtlasRoot(root, spec), - }; - const target = document.createElement("div"); - document.body.appendChild(target); - const handle = renderBundleHeadless({ bundle, target }); - handles.push(handle); - await handle.ready; - return target; -} - -describe("atlas render (headless DOM)", () => { - it("paints N transparent band slices in a taller stage", async () => { - const root: RenderNode = { - kind: "stack", - children: [ - { kind: "frame", id: "below-box", props: { width: 400, height: 200 } }, - { kind: "x-zab.capture", id: "cam", props: { width: 1920, height: 1080 } }, - { kind: "frame", id: "above-box", props: { width: 300, height: 150 } }, - ], - }; - - const target = await renderAtlas(root, "below,above"); - const divs = Array.from(target.querySelectorAll("div")); - - // The stage is the full N×1080 height. - expect(divs.some((d) => d.style.height === "2160px")).toBe(true); - // Two band slices, each 1080 tall. - const bandSlices = divs.filter((d) => d.style.height === "1080px"); - expect(bandSlices.length).toBe(2); - // Every frame the transform introduces is transparent — no fill declared. - for (const d of divs) { - expect(d.style.background).toBe(""); - expect(d.style.backgroundColor).toBe(""); - } - // The authored scenic boxes still paint (one per band). - expect(divs.some((d) => d.style.width === "400px")).toBe(true); - expect(divs.some((d) => d.style.width === "300px")).toBe(true); - }); -}); diff --git a/tests/unit/broadcast-leaderboard-repro.test.tsx b/tests/unit/broadcast-leaderboard-repro.test.tsx deleted file mode 100644 index 91a5ef8..0000000 --- a/tests/unit/broadcast-leaderboard-repro.test.tsx +++ /dev/null @@ -1,193 +0,0 @@ -// Reproduction of the prod "Solar paints black in mode=broadcast" incident -// (repo Solar v0.2.8, live leaderboard scene `57dc631f`). -// -// THE TRAP this test exists to pin: the existing render.test.tsx mounts a -// bundle whose text node binds the RENDER-vocab key `value` — and that -// works. Prod paints black because Orion shipped a bundle whose text node -// binds the AUTHORING-vocab key `text` (the natural Canvas key for a text -// node's content). The runtime's text primitive reads `resolved.value` -// only (prop-allowlist: text → {value, …}, `text` is NOT consumed). So a -// `text:`-keyed binding lands on `resolved.text` (ignored) while -// `resolved.value` stays undefined → empty span → black. -// -// This test reproduces the EXACT prod inputs end-to-end through Solar's -// real mount() over @lumencast/runtime in mode=broadcast: -// - snapshot leaf __vars..leaderboard_display = "1. GIDEON — 9\n…" -// - bundle text node bound on the SAME leaf, but keyed `text:` -// and proves the leaderboard text is absent from the rendered DOM (the -// black). The companion fixed-input case (binding keyed `value:`) proves -// the same leaf DOES paint once the binding key is the render vocab — i.e. -// the defect is the binding KEY, lowered by Orion, not Solar's render path. - -import { afterEach, describe, expect, it, vi } from "vitest"; -import { encodeFrame, snapshot } from "@lumencast/protocol"; -import type { RenderBundle } from "@lumencast/runtime"; -import { mount } from "../../src/mount"; - -const SCENE_ID = "57dc631f"; -const SCENE_VERSION = "sha256-f6c907-leaderboard"; - -// The exact bound leaf the wire emits (double-dot path preserved verbatim). -const LEADERBOARD_LEAF = "__vars..leaderboard_display"; -const LEADERBOARD_TEXT = - "1. GIDEON — 9\n2. Teddy — 8\n3. Loki — 7\n4. Gumayusi — 7\n5. Namgung — 6"; - -const SNAPSHOT_STATE = { - [LEADERBOARD_LEAF]: LEADERBOARD_TEXT, -}; - -// PROD bundle (the black): text node bound on the leaf via the AUTHORING -// key `text` — exactly what Orion shipped for scene 57dc631f. -const BUNDLE_TEXT_KEYED: RenderBundle = { - scene_version: SCENE_VERSION, - root: { - kind: "stack", - children: [ - { - kind: "text", - id: "leaderboard", - props: { colour: "#ffd200" }, - bindings: { text: LEADERBOARD_LEAF }, - }, - ], - }, -}; - -// FIXED bundle: same leaf, keyed on the RENDER vocab `value` (what Orion's -// lowering must emit). Proves the leaf and snapshot are correct and the -// only defect is the binding key. -const BUNDLE_VALUE_KEYED: RenderBundle = { - scene_version: SCENE_VERSION, - root: { - kind: "stack", - children: [ - { - kind: "text", - id: "leaderboard", - props: { colour: "#ffd200" }, - bindings: { value: LEADERBOARD_LEAF }, - }, - ], - }, -}; - -class FakeWebSocket { - static readonly CONNECTING = 0; - static readonly OPEN = 1; - static readonly CLOSING = 2; - static readonly CLOSED = 3; - static last: FakeWebSocket | null = null; - - readonly CONNECTING = 0; - readonly OPEN = 1; - readonly CLOSING = 2; - readonly CLOSED = 3; - - readyState = 0; - protocol = "lsdp.v1.1"; - onopen: (() => void) | null = null; - onmessage: ((ev: { data: string }) => void) | null = null; - onclose: ((ev: { code: number; reason: string }) => void) | null = null; - onerror: ((ev: unknown) => void) | null = null; - - constructor( - public url: string, - _protocols?: string | string[], - ) { - FakeWebSocket.last = this; - queueMicrotask(() => { - this.readyState = this.OPEN; - this.onopen?.(); - }); - } - - send(raw: string): void { - const frame = JSON.parse(raw) as { type?: string }; - if (frame.type === "subscribe") { - const snap = snapshot({ - seq: 8, - scene_id: SCENE_ID, - scene_version: SCENE_VERSION, - state: SNAPSHOT_STATE, - }); - queueMicrotask(() => this.onmessage?.({ data: encodeFrame(snap) })); - } - } - - close(_code?: number, _reason?: string): void { - this.readyState = this.CLOSED; - this.onclose?.({ code: 1000, reason: "client closing" }); - } -} - -afterEach(() => { - vi.restoreAllMocks(); - FakeWebSocket.last = null; -}); - -function mountBroadcast(bundle: RenderBundle, target: HTMLElement) { - vi.stubGlobal( - "fetch", - vi.fn(async () => new Response(JSON.stringify(bundle), { status: 200 })), - ); - vi.stubGlobal("WebSocket", FakeWebSocket as unknown as typeof WebSocket); - return mount({ - target, - orionUrl: "ws://127.0.0.1:4007/orion/api/v1/show/stream", - token: "show-token", - mode: "broadcast", - }); -} - -describe("Solar broadcast leaderboard render (prod incident 57dc631f)", () => { - it("REPRODUCES the black: a `text:`-keyed binding leaves the span empty", async () => { - const target = document.createElement("div"); - document.body.appendChild(target); - - const handle = mountBroadcast(BUNDLE_TEXT_KEYED, target); - - // Wait until broadcast mode has mounted the scene tree (the span node - // exists), so we assert on a rendered-but-empty span, not on a race. - await waitFor(() => target.querySelector("span") !== null); - // Give any binding resolution a chance to paint, then assert the leaf - // text is absent — the black. - await new Promise((r) => setTimeout(r, 50)); - - expect(target.textContent).not.toContain("GIDEON"); - expect(target.querySelector("span")?.textContent ?? "").toBe(""); - - handle.disconnect(); - target.remove(); - }); - - it("paints once the binding is keyed on the render vocab `value`", async () => { - const target = document.createElement("div"); - document.body.appendChild(target); - - const handle = mountBroadcast(BUNDLE_VALUE_KEYED, target); - - await waitFor(() => target.textContent?.includes("GIDEON") === true); - - expect(target.textContent).toContain("1. GIDEON — 9"); - expect(target.textContent).toContain("5. Namgung — 6"); - // The jaune is on the span (render path honoured the colour prop). - expect(target.querySelector("span")?.style.color).not.toBe(""); - - handle.disconnect(); - target.remove(); - }); -}); - -async function waitFor( - predicate: () => boolean, - { timeout = 2000, interval = 10 }: { timeout?: number; interval?: number } = {}, -): Promise { - const start = Date.now(); - for (;;) { - if (predicate()) return; - if (Date.now() - start > timeout) { - throw new Error("waitFor: predicate not satisfied within timeout"); - } - await new Promise((r) => setTimeout(r, interval)); - } -} diff --git a/tests/unit/bundle-fetch-auth.test.tsx b/tests/unit/bundle-fetch-auth.test.tsx deleted file mode 100644 index 21a1f6c..0000000 --- a/tests/unit/bundle-fetch-auth.test.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// Empirical proof for the live-finale black-screen bug (BUNDLE_FETCH_FAILED: -// 401). This drives Solar's REAL mount() with the Pulsar browser-source URL -// shape — the show-token embedded in `orionUrl`'s `?token=` — and asserts the -// render-bundle GET goes out with `Authorization: Bearer `. -// -// Before the fix the host entry read a (non-existent) top-level `?token=`, so -// mount({ token: "" }) → resolveCurrentToken() empty → no Authorization header -// → Orion 401. This test fails on the old path and passes on the new one. It -// exercises the full production seam (resolveShowToken → mount → runtime -// WsClient.resolveCurrentToken → bundle fetcher), not just "it compiles". - -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { encodeFrame, snapshot } from "@lumencast/protocol"; -import { mount } from "../../src/mount"; -import { resolveShowToken } from "../../src/internal/resolve-show-token"; - -const SHOW_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzaG93In0.sig-_123"; -const ORION_URL = `ws://127.0.0.1:4007/orion/api/v1/show/stream.lsdp?token=${SHOW_TOKEN}`; -const SCENE_ID = "scene-finale"; -const SCENE_VERSION = "sha256:abc123"; - -// A minimal fake WebSocket that immediately "opens" with the 1.1 subprotocol -// negotiated, so the runtime sends `subscribe`; the test then pushes a -// snapshot frame, which triggers the bundle fetch under test. -class FakeWebSocket { - static instances: FakeWebSocket[] = []; - static OPEN = 1; - static CLOSED = 3; - readyState = 1; - protocol = "lsdp.v1.1"; - onopen: (() => void) | null = null; - onmessage: ((ev: { data: string }) => void) | null = null; - onerror: ((ev: unknown) => void) | null = null; - onclose: ((ev: { code: number; reason: string }) => void) | null = null; - sent: string[] = []; - - constructor( - public url: string, - public protocols?: string[], - ) { - FakeWebSocket.instances.push(this); - queueMicrotask(() => this.onopen?.()); - } - send(data: string): void { - this.sent.push(data); - } - close(): void { - this.readyState = FakeWebSocket.CLOSED; - } - pushSnapshot(): void { - this.onmessage?.({ - data: encodeFrame( - snapshot({ - seq: 1, - scene_id: SCENE_ID, - scene_version: SCENE_VERSION, - state: {}, - }), - ), - }); - } -} - -describe("render-bundle fetch carries Authorization from the show-token", () => { - let fetchMock: ReturnType; - let originalWS: typeof globalThis.WebSocket; - let originalFetch: typeof globalThis.fetch; - - beforeEach(() => { - FakeWebSocket.instances = []; - originalWS = globalThis.WebSocket; - originalFetch = globalThis.fetch; - // @ts-expect-error — install the fake WS for the runtime to pick up. - globalThis.WebSocket = FakeWebSocket; - - fetchMock = vi.fn(async () => ({ - ok: true, - status: 200, - statusText: "OK", - json: async () => ({ - scene_version: SCENE_VERSION, - root: { kind: "stack" }, - }), - })); - globalThis.fetch = fetchMock as unknown as typeof fetch; - }); - - afterEach(() => { - globalThis.WebSocket = originalWS; - globalThis.fetch = originalFetch; - }); - - it("attaches `Authorization: Bearer ` to the bundle GET", async () => { - const target = document.createElement("div"); - document.body.appendChild(target); - - // This is exactly what the host entry does: pull the token out of the - // packed orionUrl, then mount with it. - const token = resolveShowToken(ORION_URL, null); - expect(token).toBe(SHOW_TOKEN); // guards the extraction half of the fix - - mount({ target, orionUrl: ORION_URL, token, mode: "broadcast" }); - - // Let the WS "open" + subscribe, then deliver the snapshot that drives - // the bundle fetch. - await Promise.resolve(); - expect(FakeWebSocket.instances).toHaveLength(1); - FakeWebSocket.instances[0]!.pushSnapshot(); - - // The fetcher resolves the token (async) then fetches — flush microtasks. - await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); - - const [url, init] = fetchMock.mock.calls[0]!; - // Right artefact: gateway-prefixed render-bundle with the content hash. - expect(String(url)).toContain( - "/orion/api/v1/scenes/scene-finale/render-bundle", - ); - // The load-bearing assertion: the header is present and carries the token. - expect(init).toBeDefined(); - const headers = (init as RequestInit).headers as Record; - expect(headers.Authorization).toBe(`Bearer ${SHOW_TOKEN}`); - }); - - it("sends NO Authorization header when the orionUrl carries no token (regression guard for the old empty-token path)", async () => { - const target = document.createElement("div"); - document.body.appendChild(target); - - const noTokenUrl = - "ws://127.0.0.1:4007/orion/api/v1/show/stream.lsdp"; - const token = resolveShowToken(noTokenUrl, null); - expect(token).toBe(""); - - mount({ target, orionUrl: noTokenUrl, token, mode: "broadcast" }); - - await Promise.resolve(); - FakeWebSocket.instances[0]!.pushSnapshot(); - await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); - - const [, init] = fetchMock.mock.calls[0]!; - // No token → header-less fetch (runtime v0.5.0 behaviour) — proves the - // header is driven by the token, i.e. the token IS what was missing. - expect(init).toBeUndefined(); - }); -}); diff --git a/tests/unit/canvas-presentation.test.ts b/tests/unit/canvas-presentation.test.ts new file mode 100644 index 0000000..67fa860 --- /dev/null +++ b/tests/unit/canvas-presentation.test.ts @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { CanvasPresentation } from "../../src/engine/canvas-presentation"; +afterEach(() => vi.restoreAllMocks()); +describe("atomic CEF canvas presentation", () => { + it("copies only explicit completed frames into a synchronized retained surface", () => { + const drawImage = vi.fn(), + context = { drawImage, globalCompositeOperation: "source-over" }; + const get = vi + .spyOn(HTMLCanvasElement.prototype, "getContext") + .mockReturnValue(context as unknown as CanvasRenderingContext2D); + const source = document.createElement("canvas"); + source.width = 1920; + source.height = 1080; + const presentation = new CanvasPresentation(source); + expect(get).toHaveBeenCalledWith("2d", { + alpha: true, + desynchronized: false, + }); + expect(drawImage).not.toHaveBeenCalled(); + presentation.submit(); + expect(drawImage).toHaveBeenCalledWith(source, 0, 0, 1920, 1080); + expect(context.globalCompositeOperation).toBe("copy"); + expect(presentation.canvas.width).toBe(1920); + source.width = 1280; + presentation.submit(); + expect(presentation.canvas.width).toBe(1280); + expect(drawImage).toHaveBeenCalledTimes(2); + }); + it("does not manufacture a submitted frame if its copy fails", () => { + vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({ + drawImage: () => { + throw new Error("GPU copy failed"); + }, + } as unknown as CanvasRenderingContext2D); + const presentation = new CanvasPresentation( + document.createElement("canvas"), + ); + expect(() => presentation.submit()).toThrow("GPU copy failed"); + }); +}); diff --git a/tests/unit/capture-resolution.test.ts b/tests/unit/capture-resolution.test.ts index 0cb66aa..6974d4f 100644 --- a/tests/unit/capture-resolution.test.ts +++ b/tests/unit/capture-resolution.test.ts @@ -1,235 +1,29 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import type { MountOptions } from "../../src/types"; +import { describe, expect, it, vi } from "vitest"; +import { createCaptureResolver } from "../../src/sources/capture"; -const { mountRuntime } = vi.hoisted(() => ({ - mountRuntime: vi.fn((..._args: unknown[]) => ({ - disconnect: vi.fn(), - setToken: vi.fn(), - })), -})); +describe("Vision capture resolution", () => { + it("forwards a logical camera reference to the embedding host", async () => { + const hostResolver = vi.fn(async () => ({ deviceId: "camera-7" })); + const resolve = createCaptureResolver({ resolveCaptureDevice: hostResolver }); -vi.mock("@lumencast/runtime", () => { - return { - mount: mountRuntime, - createPeerViewerFromInjection: vi.fn(() => { - throw new Error("capture resolver test must not create a peer viewer"); - }), - }; -}); - -const CAPTURE_GLOBAL = "__ZAB_CAPTURE_DEVICES__"; -const DEFAULT_SCREEN_GLOBAL = "__ZAB_CAPTURE_DEFAULT_SCREEN__"; - -function baseOptions(overrides: Partial = {}): MountOptions { - return { - target: document.createElement("div"), - orionUrl: "ws://127.0.0.1:4007/orion/api/v1/show/stream", - token: "fake-token", - mode: "broadcast", - ...overrides, - }; -} - -async function loadMount(): Promise { - vi.resetModules(); - return (await import("../../src/mount")).mount; -} - -function resolver(): ( - deviceRef: string, - sourceKind: string, -) => Promise<{ deviceId?: string; captureSourceId?: string } | null> { - const args = mountRuntime.mock.calls.at(-1); - if (args === undefined) throw new Error("mount() was not called"); - const options = args[0] as Record; - return options.resolveCaptureDevice as ( - deviceRef: string, - sourceKind: string, - ) => Promise<{ deviceId?: string; captureSourceId?: string } | null>; -} - -afterEach(() => { - vi.unstubAllGlobals(); - delete (globalThis as Record)[CAPTURE_GLOBAL]; - delete (globalThis as Record)[DEFAULT_SCREEN_GLOBAL]; - delete (globalThis as Record) - .__ZAB_CAPTURE_RESOLVE_DEFAULT_SCREEN__; - mountRuntime.mockClear(); -}); - -describe("Solar's default capture-device resolver", () => { - it("awaits a deferred default only for an unbound screen, never a camera/window or an explicit mapping", async () => { - const deferred = vi.fn(async () => ({ captureSourceId: "screen:lazy:0" })); - ( - globalThis as Record - ).__ZAB_CAPTURE_RESOLVE_DEFAULT_SCREEN__ = deferred; - (globalThis as Record)[CAPTURE_GLOBAL] = { - pinned: { captureSourceId: "screen:pinned:0" }, - }; - const mount = await loadMount(); - mount(baseOptions()); - expect(deferred).not.toHaveBeenCalled(); - expect(await resolver()("camera", "media.camera")).toBeNull(); - expect(await resolver()("window", "media.window")).toBeNull(); - expect(await resolver()("pinned", "media.screen")).toEqual({ - captureSourceId: "screen:pinned:0", - }); - expect(deferred).not.toHaveBeenCalled(); - expect(await resolver()("default", "media.screen")).toEqual({ - captureSourceId: "screen:lazy:0", - }); - expect(deferred).toHaveBeenCalledTimes(1); - }); - it("keeps the broadcast CEF on a placeholder so native Pulsar owns the camera", async () => { - const hostResolver = vi.fn(async () => ({ deviceId: "host-camera" })); - const mount = await loadMount(); - mount(baseOptions({ resolveCaptureDevice: hostResolver })); - - expect(await resolver()("camera", "media.camera")).toBeNull(); - expect(hostResolver).not.toHaveBeenCalled(); - }); - - it("retains host camera acquisition for control/editor mounts", async () => { - const hostResolver = vi.fn(async () => ({ deviceId: "host-camera" })); - const mount = await loadMount(); - mount(baseOptions({ mode: "control", resolveCaptureDevice: hostResolver })); - - expect(await resolver()("camera", "media.camera")).toEqual({ - deviceId: "host-camera", - }); - expect(hostResolver).toHaveBeenCalledWith("camera", "media.camera"); - }); - - it("allows only Prism's marked diagnostic window to inspect broadcast captures", async () => { - const hostResolver = vi.fn(async () => ({ deviceId: "diagnostic-camera" })); - const mount = await loadMount(); - mount( - baseOptions({ - resolveCaptureDevice: hostResolver, - captureInBrowser: true, - }), - ); - - expect(await resolver()("camera", "media.camera")).toEqual({ - deviceId: "diagnostic-camera", + await expect(resolve("camera-ref", "media.webcam")).resolves.toEqual({ + deviceId: "camera-7", }); - expect(hostResolver).toHaveBeenCalledWith("camera", "media.camera"); + expect(hostResolver).toHaveBeenCalledWith("camera-ref", "media.webcam"); }); - it("warms the origin, maps labels, caches the map, and passes capture IDs through", async () => { - const stop = vi.fn(); - const getUserMedia = vi.fn(async () => ({ getTracks: () => [{ stop }] })); - const enumerateDevices = vi.fn(async () => [ - { label: "", deviceId: "hidden" }, - { label: "Local camera", deviceId: "local-camera" }, - ]); - vi.stubGlobal("navigator", { - mediaDevices: { getUserMedia, enumerateDevices }, - }); - (globalThis as Record)[CAPTURE_GLOBAL] = { - camera: { label: "Local camera" }, - missing: { label: "No such camera" }, - unlabeled: {}, - screen: { captureSourceId: "desktop-screen" }, + it("resolves the pinned default screen only when no explicit source exists", async () => { + const hostResolver = vi.fn(async () => null); + const resolve = createCaptureResolver({ resolveCaptureDevice: hostResolver }); + (globalThis as Record).__ZAB_CAPTURE_DEFAULT_SCREEN__ = { + captureSourceId: "screen:main", }; + const defaultResolver = createCaptureResolver({}); - const mount = await loadMount(); - mount(baseOptions({ mode: "control" })); - const resolve = resolver(); - - expect(await resolve("camera", "media.camera")).toEqual({ - deviceId: "local-camera", - }); - expect(await resolve("missing", "media.microphone")).toBeNull(); - expect(await resolve("unlabeled", "media.camera")).toBeNull(); - expect(await resolve("unknown", "media.camera")).toBeNull(); - expect(await resolve("screen", "media.screen")).toEqual({ - captureSourceId: "desktop-screen", - }); - expect(await resolve("screen", "media.window")).toEqual({ - captureSourceId: "desktop-screen", + await expect(resolve("window-ref", "media.window")).resolves.toBeNull(); + await expect(defaultResolver("screen-ref", "media.screen")).resolves.toEqual({ + captureSourceId: "screen:main", }); - expect(await resolve("screen", "media.app")).toEqual({ - captureSourceId: "desktop-screen", - }); - expect(await resolve("missing", "media.camera")).toBeNull(); - expect(getUserMedia).toHaveBeenCalledWith({ video: true }); - expect(stop).toHaveBeenCalledTimes(1); - expect(enumerateDevices).toHaveBeenCalledTimes(1); - }); - - it("uses Prism's machine-local default screen mapping for an unbound screen ref", async () => { - vi.stubGlobal("navigator", { - mediaDevices: { - getUserMedia: vi.fn(async () => ({ getTracks: () => [] })), - enumerateDevices: vi.fn(async () => []), - }, - }); - (globalThis as Record)[DEFAULT_SCREEN_GLOBAL] = { - captureSourceId: "screen:0:0", - }; - - const mount = await loadMount(); - mount(baseOptions()); - - expect(await resolver()("screen-ref", "media.screen")).toEqual({ - captureSourceId: "screen:0:0", - }); - }); - - it("returns a placeholder when enumerateDevices is unavailable", async () => { - vi.stubGlobal("navigator", { mediaDevices: {} }); - (globalThis as Record)[CAPTURE_GLOBAL] = { - camera: { label: "Local camera" }, - }; - const mount = await loadMount(); - mount(baseOptions({ mode: "control" })); - - expect(await resolver()("camera", "media.camera")).toBeNull(); - }); - - it("keeps resolving after a denied warm-up", async () => { - const enumerateDevices = vi.fn(async () => [ - { label: "Local microphone", deviceId: "local-microphone" }, - ]); - vi.stubGlobal("navigator", { - mediaDevices: { - getUserMedia: vi.fn(async () => { - throw new Error("permission denied"); - }), - enumerateDevices, - }, - }); - (globalThis as Record)[CAPTURE_GLOBAL] = { - microphone: { label: "Local microphone" }, - }; - const mount = await loadMount(); - mount(baseOptions({ mode: "control" })); - - expect(await resolver()("microphone", "media.microphone")).toEqual({ - deviceId: "local-microphone", - }); - }); - - it("returns a placeholder when enumeration itself fails or navigator is absent", async () => { - vi.stubGlobal("navigator", { - mediaDevices: { - getUserMedia: undefined, - enumerateDevices: vi.fn(async () => { - throw new Error("enumeration failed"); - }), - }, - }); - (globalThis as Record)[CAPTURE_GLOBAL] = { - camera: { label: "Local camera" }, - }; - let mount = await loadMount(); - mount(baseOptions()); - expect(await resolver()("camera", "media.camera")).toBeNull(); - - vi.stubGlobal("navigator", undefined); - mount = await loadMount(); - mount(baseOptions()); - expect(await resolver()("camera", "media.camera")).toBeNull(); + expect(hostResolver).toHaveBeenCalledWith("window-ref", "media.window"); }); }); diff --git a/tests/unit/control-feedback.test.ts b/tests/unit/control-feedback.test.ts new file mode 100644 index 0000000..edb1624 --- /dev/null +++ b/tests/unit/control-feedback.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { presentationFeedback } from "../../src/engine/control-feedback"; +const mocks = vi.hoisted(() => ({ + reads: 0, + writes: 0, + conflicts: 0, + peers: [] as Array<{ + close: ReturnType; + options: Record unknown>; + }>, +})); +vi.mock("../../vendor/lsdp-native-browser/src/browser.js", () => ({ + BrowserLSDP: class { + ready = Promise.resolve(); + options: Record unknown> = {}; + close = vi.fn(); + constructor() { + mocks.peers.push(this); + } + async transaction(value: Record) { + if (value.kind === "state.read") { + mocks.reads++; + // Native read receipt arrives before the separately streamed snapshot. + setTimeout(() => { + void this.options.onTransaction!( + { external: "keep" } as never, + { + metadata: { profile: "lsdp.state.read/1", target: "orion/state" }, + } as never, + ); + }, 5); + return {}; + } + mocks.writes++; + if (mocks.conflicts-- > 0) throw new Error("BASE_MISMATCH"); + return { level: "applied" }; + } + }, +})); +beforeEach(() => { + mocks.reads = 0; + mocks.writes = 0; + mocks.conflicts = 0; + mocks.peers = []; +}); +it("waits for the streamed read before acknowledging a renderer phase", async () => { + await presentationFeedback( + "ws://localhost", + "program", + { phase: "prepared" }, + new AbortController().signal, + ); + expect(mocks.reads).toBe(1); + expect(mocks.writes).toBe(1); + expect(mocks.peers[0]!.close).toHaveBeenCalledOnce(); +}); +it("rereads after a proven conflict and closes the connection after exhaustion", async () => { + mocks.conflicts = 2; + await presentationFeedback( + "ws://localhost", + "preview", + { phase: "active" }, + new AbortController().signal, + ); + expect(mocks.reads).toBe(3); + expect(mocks.writes).toBe(3); + mocks.conflicts = 10; + await expect( + presentationFeedback( + "ws://localhost", + "preview", + {}, + new AbortController().signal, + ), + ).rejects.toThrow("BASE_MISMATCH"); + expect(mocks.peers.at(-1)!.close).toHaveBeenCalledOnce(); +}); diff --git a/tests/unit/editable-bindings.test.ts b/tests/unit/editable-bindings.test.ts new file mode 100644 index 0000000..0085a88 --- /dev/null +++ b/tests/unit/editable-bindings.test.ts @@ -0,0 +1,154 @@ +import { expect, it } from "vitest"; +import { prepareEditableBindings } from "../../src/scenes/editable-bindings"; + +it("projects editable geometry and paint while keeping scalar style bindings live", () => { + const shape = { + kind: "shape", + size: { w: 1, h: 2 }, + bindAnimate: { "transform.translate": "position" }, + bindUniversal: { width: "width", height: "height", opacity: "opacity" }, + bindStyle: { fill: "fill", radius: "radius" }, + }; + const text = { + kind: "text", + bindStyle: { size: "font", colour: "colour", weight: "weight" }, + }; + const document = { + scene_id: "draft", + scene_version: "base", + defaults: { + position: [30, 40], + width: 200, + height: 100, + }, + layout: { kind: "frame", children: [shape], template: text }, + }; + const base = structuredClone(document); + const variant = structuredClone(document); + expect(prepareEditableBindings(variant)).toEqual({ + position: ["__solar.geometry.n0.x", "__solar.geometry.n0.y"], + }); + expect(variant.layout.children[0]!).toMatchObject({ + bind: { + "position.x": "__solar.geometry.n0.x", + "position.y": "__solar.geometry.n0.y", + "size.w": "width", + "size.h": "height", + opacity: "opacity", + fill: "fill", + cornerRadius: "radius", + }, + bindStyle: {}, + }); + expect(variant.layout.children[0]!).not.toHaveProperty("bindUniversal"); + expect(variant.layout.template.bindStyle).toEqual({ + fontSize: "font", + color: "colour", + fontWeight: "weight", + }); + expect(document).toEqual(base); + expect(variant.defaults).toMatchObject({ + "__solar.geometry.n0.x": 30, + "__solar.geometry.n0.y": 40, + }); + expect(prepareEditableBindings(variant)).toEqual({}); +}); + +it("keeps Pulsar native captures between distinct transparent Vision bands", async () => { + const { prepareNativeComposition } = + await import("../../src/scenes/editable-bindings"); + const document = { + scene_id: "scene", + scene_version: "base", + layout: { + kind: "frame", + id: "root", + fill: "#ff0000", + bind: { fill: "background" }, + size: { w: 1920, h: 1080 }, + children: [ + { + kind: "frame", + id: "below", + children: [ + { kind: "x-zab.capture", size: { w: 100, h: 50 } }, + { kind: "x-zab.meet-peer", "x-zab.slotRef": "@0" }, + ], + }, + { kind: "x-zab.capture", id: "native", size: { w: 100, h: 50 } }, + { kind: "text", id: "above", bind: { value: "title" } }, + ], + }, + }; + prepareNativeComposition(document, { width: 1920, height: 1080 }); + const layout = document.layout as unknown as { + size: { w: number; h: number }; + children: Array<{ + position: { x: number; y: number }; + children: Array<{ + id?: string; + fill?: string; + bind: Record; + children: Array<{ id: string; children: Array<{ kind: string }> }>; + }>; + }>; + }; + expect(layout.size).toEqual({ w: 1920, h: 2160 }); + expect(layout.children[0]!.position).toEqual({ x: 0, y: 0 }); + expect(layout.children[1]!.position).toEqual({ x: 0, y: 1080 }); + const below = layout.children[0]!.children[0]!, + above = layout.children[1]!.children[0]!; + expect(below.children[0]!.id).toBe("below"); + expect(below.children[0]!.children[0]!.kind).toBe("frame"); + expect(below.children[0]!.children[1]!.kind).toBe("x-zab.meet-peer"); + expect(above.children[0]!.id).toBe("above"); + expect(below.fill).toBe("#ff0000"); + expect(above).not.toHaveProperty("fill"); + expect(above.bind).not.toHaveProperty("fill"); + expect(below).not.toHaveProperty("id"); + expect(above).not.toHaveProperty("id"); +}); + +it("omits empty trailing capture bands while retaining inner z-order gaps", async () => { + const { prepareNativeComposition } = + await import("../../src/scenes/editable-bindings"); + const document = { + scene_id: "tail", + scene_version: "base", + layout: { + kind: "frame", + size: { w: 1920, h: 1080 }, + children: [ + { kind: "shape", id: "bottom" }, + { kind: "x-zab.capture", id: "a" }, + { kind: "x-zab.capture", id: "b" }, + ], + }, + }; + prepareNativeComposition(document, { width: 1920, height: 1080 }); + expect(document.layout.size.h).toBe(1080); + expect(document.layout.children.map((node) => node.id)).toEqual(["bottom"]); + const middle = { + scene_id: "middle", + scene_version: "base", + layout: { + kind: "frame", + size: { w: 1920, h: 1080 }, + children: [ + { kind: "shape", id: "bottom" }, + { kind: "x-zab.capture", id: "a" }, + { kind: "x-zab.capture", id: "b" }, + { kind: "text", id: "top" }, + { kind: "x-zab.capture", id: "tail" }, + ], + }, + }; + prepareNativeComposition(middle, { width: 1920, height: 1080 }); + expect(middle.layout.size.h).toBe(3240); + const bands = middle.layout.children as unknown as Array<{ + position: { y: number }; + children: Array<{ children: unknown[] }>; + }>; + expect(bands[1]!.children[0]!.children).toEqual([]); + expect(bands[2]!.position.y).toBe(2160); +}); diff --git a/tests/unit/editable-preview-fast-path.test.ts b/tests/unit/editable-preview-fast-path.test.ts deleted file mode 100644 index f2245a1..0000000 --- a/tests/unit/editable-preview-fast-path.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -// @vitest-environment happy-dom - -import { beforeEach, describe, expect, it } from "vitest"; - -import { - applyEditableFastPatch, - applyEditableTranslatePatch, - EditablePreviewDeltaGate, - editablePreviewFastPathEnabled, - parseAcceptedEditablePatches, - parseEditableFastPatch, - parseEditableTranslatePatch, - readEditablePreviewSidebandUrl, -} from "../../src/internal/editable-preview-fast-path"; - -const PANEL_TOKEN = "70616e656c"; - -describe("editable Preview compositor fast path", () => { - beforeEach(() => { - document.body.innerHTML = ""; - }); - - it("decodes only bounded atomic editable translations", () => { - expect( - parseEditableTranslatePatch({ - path: `__editable.${PANEL_TOKEN}.translate`, - value: [61, 40], - }), - ).toEqual({ componentId: "panel", property: "translate", x: 61, y: 40 }); - expect( - parseEditableTranslatePatch({ - path: `__editable.${PANEL_TOKEN}.translate`, - value: [Number.NaN, 40], - }), - ).toBeNull(); - expect( - parseEditableTranslatePatch({ path: "blue.input", value: [1, 2] }), - ).toBeNull(); - }); - - it("mirrors snapshot, ordering, duplicate, gap and scene-change gates", () => { - const gate = new EditablePreviewDeltaGate(); - const delta = (seq: number, x: number): string => - JSON.stringify({ - type: "delta", - seq, - patches: [ - { - path: `__editable.${PANEL_TOKEN}.translate`, - value: [x, 40], - }, - ], - }); - - expect(gate.accept(delta(2, 10))).toEqual([]); - expect(gate.accept(JSON.stringify({ type: "snapshot", seq: 1 }))).toEqual( - [], - ); - expect(gate.accept(delta(2, 20))).toEqual([ - { componentId: "panel", property: "translate", x: 20, y: 40 }, - ]); - expect(gate.accept(delta(2, 30))).toEqual([]); - expect(gate.accept(delta(4, 40))).toEqual([]); - expect(gate.accept(delta(5, 50))).toEqual([]); - expect(gate.accept(JSON.stringify({ type: "snapshot", seq: 8 }))).toEqual( - [], - ); - expect(gate.accept(delta(9, 90))).toHaveLength(1); - expect( - gate.accept(JSON.stringify({ type: "scene_changed", seq: 10 })), - ).toEqual([]); - expect(gate.accept(delta(11, 110))).toEqual([]); - }); - - it("mutates only the matching Solar compositor wrapper", () => { - document.body.innerHTML = ` -
-
-
- `; - expect( - applyEditableTranslatePatch({ - componentId: "panel", - property: "translate", - x: 61, - y: 40, - }), - ).toBe(true); - const panels = document.querySelectorAll( - '[data-lumencast-bind-animate="panel"]', - ); - expect([...panels].map((panel) => panel.style.transform)).toEqual([ - "translate3d(61px, 40px, 0px)", - "translate3d(61px, 40px, 0px)", - ]); - expect([...panels].map((panel) => panel.style.willChange)).toEqual([ - "transform", - "transform", - ]); - expect( - document.querySelector( - '[data-lumencast-bind-animate="other"]', - )?.style.transform, - ).toBe(""); - }); - - it("applies bounded geometry, text, style, visibility, order and source leaves", () => { - document.body.innerHTML = ` -
-
Before
-
- `; - const token = (componentId: string) => - Array.from(new TextEncoder().encode(componentId), (byte) => - byte.toString(16).padStart(2, "0"), - ).join(""); - const patch = (componentId: string, property: string, value: unknown) => - parseEditableFastPatch({ - path: `__editable.${token(componentId)}.${property}`, - value, - }); - for (const candidate of [ - patch("panel", "width", 640), - patch("panel", "height", 360), - patch("panel", "rotation", 12), - patch("panel", "opacity", 0.5), - patch("panel", "zIndex", 9), - patch("panel", "fill", "#ff0066"), - patch("title", "value", "After"), - patch("title", "fontSize", 48), - patch("title", "colour", "#fff"), - patch("photo", "src", "https://assets.example/photo.png"), - ]) { - expect(candidate).not.toBeNull(); - expect(applyEditableFastPatch(candidate!)).toBe(true); - } - const panel = document.querySelector( - '[data-lumencast-bind-animate="panel"]', - )!; - expect(panel.style.width).toBe("640px"); - expect(panel.style.height).toBe("360px"); - expect(panel.style.rotate).toBe("12deg"); - expect(panel.style.opacity).toBe("0.5"); - expect(panel.style.zIndex).toBe("9"); - expect(panel.querySelector("rect")?.getAttribute("fill")).toBe("#ff0066"); - const title = document.querySelector( - '[data-lumencast-bind-animate="title"] span', - )!; - expect(title.textContent).toBe("After"); - expect(title.style.fontSize).toBe("48px"); - expect(title.style.color).not.toBe(""); - expect( - document.querySelector( - '[data-lumencast-bind-animate="photo"] img', - )?.src, - ).toBe("https://assets.example/photo.png"); - expect(patch("panel", "opacity", 2)).toBeNull(); - expect(patch("photo", "src", "javascript:alert(1)")).toBeNull(); - }); - - it("requires the explicit Preview URL capability flag", () => { - expect(editablePreviewFastPathEnabled("?editable_fast=1")).toBe(true); - expect(editablePreviewFastPathEnabled("?editable_fast=0")).toBe(false); - expect(editablePreviewFastPathEnabled("?mode=broadcast")).toBe(false); - const sideband = - "ws://127.0.0.1:4317/api/v1/show/editable-preview.ws?local_editor_token=cap&role=solar"; - expect( - readEditablePreviewSidebandUrl( - `?editable_fast=1&editable_fast_url=${encodeURIComponent(sideband)}`, - ), - ).toBe(sideband); - expect( - readEditablePreviewSidebandUrl( - "?editable_fast=1&editable_fast_url=wss%3A%2F%2Fevil.test%2Fsteal", - ), - ).toBeNull(); - }); - - it("accepts only Orion-confirmed editable sideband frames", () => { - expect( - parseAcceptedEditablePatches( - JSON.stringify({ - type: "accepted_patch", - scene_id: "editable-1", - edit_seq: 2, - patches: [ - { - path: `__editable.${PANEL_TOKEN}.translate`, - value: [61, 40], - }, - ], - }), - ), - ).toEqual([{ componentId: "panel", property: "translate", x: 61, y: 40 }]); - expect( - parseAcceptedEditablePatches( - JSON.stringify({ - type: "ack", - patches: [ - { - path: `__editable.${PANEL_TOKEN}.translate`, - value: [61, 40], - }, - ], - }), - ), - ).toEqual([]); - }); -}); diff --git a/tests/unit/editable-preview-sideband.test.ts b/tests/unit/editable-preview-sideband.test.ts deleted file mode 100644 index 670da97..0000000 --- a/tests/unit/editable-preview-sideband.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -// @vitest-environment happy-dom - -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { installEditablePreviewSideband } from "../../src/internal/editable-preview/sideband"; - -type Patch = { componentId: string; property: string; value: unknown }; - -const teardowns: (() => void)[] = []; - -function sideband(root: ParentNode = document) { - let now = 0; - let nextFrameId = 0; - const frames = new Map(); - vi.spyOn(performance, "now").mockImplementation(() => now); - vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { - const id = ++nextFrameId; - frames.set(id, callback); - return id; - }); - vi.stubGlobal("cancelAnimationFrame", (id: number) => frames.delete(id)); - - class TestSocket extends EventTarget { - close = vi.fn(); - } - const socket = new TestSocket(); - const Socket = vi.fn(function () { - return socket; - }); - const stop = installEditablePreviewSideband( - "ws://127.0.0.1/editable-preview", - root, - Socket as unknown as typeof WebSocket, - ); - teardowns.push(stop); - - return { - frames, - stop, - close: socket.close, - accept(...patches: Patch[]) { - socket.dispatchEvent( - new MessageEvent("message", { - data: JSON.stringify({ - type: "accepted_patch", - patches: patches.map(({ componentId, property, value }) => ({ - path: `__editable.${Array.from(new TextEncoder().encode(componentId), (byte) => byte.toString(16).padStart(2, "0")).join("")}.${property}`, - value, - })), - }), - }), - ); - }, - frame(at = now + 16) { - now = at; - const callbacks = [...frames.values()]; - frames.clear(); - for (const callback of callbacks) callback(now); - }, - }; -} - -afterEach(() => { - for (const stop of teardowns.splice(0)) stop(); - vi.restoreAllMocks(); - vi.unstubAllGlobals(); - document.body.innerHTML = ""; -}); - -describe("editable Preview sideband convergence targets", () => { - it("reuses DOM selections while reasserting values overwritten by the runtime", () => { - document.body.innerHTML = ` -
- Title -
`; - const panel = document.getElementById("panel")!; - const text = panel.querySelector("span")!; - const paint = panel.querySelector("rect")!; - const bindings = vi.spyOn(document, "querySelectorAll"); - const descendants = vi.spyOn(panel, "querySelectorAll"); - const first = vi.spyOn(panel, "querySelector"); - const preview = sideband(); - preview.accept( - { componentId: "panel", property: "width", value: 640 }, - { componentId: "panel", property: "fontSize", value: 48 }, - { componentId: "panel", property: "fill", value: "#ff0066" }, - { componentId: "panel", property: "translate", value: [61, 40] }, - ); - expect(bindings).toHaveBeenCalledTimes(1); - expect(descendants).toHaveBeenCalledTimes(2); - expect(first).toHaveBeenCalledTimes(1); - - panel.style.width = "1px"; - panel.style.transform = "none"; - text.style.fontSize = "1px"; - paint.setAttribute("fill", "#000"); - preview.frame(); - preview.frame(); - expect(panel.style.width).toBe("640px"); - expect(panel.style.getPropertyPriority("width")).toBe("important"); - expect(panel.style.transform).toBe("translate3d(61px, 40px, 0px)"); - expect(text.style.fontSize).toBe("48px"); - expect(paint.getAttribute("fill")).toBe("#ff0066"); - expect(bindings).toHaveBeenCalledTimes(1); - expect(descendants).toHaveBeenCalledTimes(2); - expect(first).toHaveBeenCalledTimes(1); - }); - - it("refreshes descendant selections synchronously after a subtree replacement", () => { - document.body.innerHTML = ` -
- Before -
`; - const panel = document.getElementById("panel")!; - const oldText = panel.querySelector("span")!; - const preview = sideband(); - preview.accept( - { componentId: "panel", property: "value", value: "Accepted" }, - { componentId: "panel", property: "width", value: 640 }, - { componentId: "panel", property: "fill", value: "#ff0066" }, - { - componentId: "panel", - property: "src", - value: "https://assets.example/new.png", - }, - ); - panel.innerHTML = - '
Replacement
'; - oldText.textContent = "Detached"; - preview.frame(); - expect(panel.querySelector("span")?.textContent).toBe("Accepted"); - expect(panel.querySelector("div")?.style.width).toBe("640px"); - expect(panel.querySelector("path")?.getAttribute("fill")).toBe("#ff0066"); - expect(panel.querySelector("img")?.src).toBe( - "https://assets.example/new.png", - ); - expect(oldText.textContent).toBe("Detached"); - }); - - it("finds initially missing wrappers and follows complete wrapper replacement", async () => { - const preview = sideband(); - preview.accept({ componentId: "panel", property: "opacity", value: 0.5 }); - document.body.innerHTML = - '
'; - preview.frame(); - const first = document.getElementById("first")!; - expect(first.style.opacity).toBe("0.5"); - - document.body.innerHTML = - '
'; - first.style.opacity = "0.9"; - await new Promise((resolve) => setTimeout(resolve, 0)); - preview.frame(); - expect(document.getElementById("second")?.style.opacity).toBe("0.5"); - expect(first.style.opacity).toBe("0.9"); - }); - - it("preserves duplicate ordering and all translation targets after binding changes", () => { - document.body.innerHTML = ` -
-
`; - const a = document.getElementById("a")!; - const b = document.getElementById("b")!; - const preview = sideband(); - preview.accept( - { componentId: "panel", property: "opacity", value: 0.5 }, - { componentId: "panel", property: "translate", value: [20, 30] }, - ); - expect(a.style.opacity).toBe("0.5"); - expect(b.style.opacity).toBe(""); - document.body.prepend(b); - a.style.opacity = "0.8"; - a.style.transform = "none"; - b.style.transform = "none"; - preview.frame(); - expect(b.style.opacity).toBe("0.5"); - expect(a.style.opacity).toBe("0.8"); - expect(a.style.transform).toBe("translate3d(20px, 30px, 0px)"); - expect(b.style.transform).toBe(a.style.transform); - - b.setAttribute("data-lumencast-bind-animate", "other"); - b.style.opacity = "0.9"; - b.style.transform = "none"; - preview.frame(); - expect(a.style.opacity).toBe("0.5"); - expect(b.style.opacity).toBe("0.9"); - expect(b.style.transform).toBe("none"); - }); - - it("retains text selections across text-node mutations and accepts newer values", async () => { - document.body.innerHTML = - '
Before
'; - const bindings = vi.spyOn(document, "querySelectorAll"); - const preview = sideband(); - preview.accept({ - componentId: "panel", - property: "value", - value: "Accepted", - }); - preview.frame(); - await new Promise((resolve) => setTimeout(resolve, 0)); - preview.frame(); - preview.accept({ componentId: "panel", property: "value", value: "Newer" }); - preview.frame(); - expect(document.querySelector("span")?.textContent).toBe("Newer"); - expect(bindings).toHaveBeenCalledTimes(1); - }); - - it("releases selections at expiry and teardown, including late socket messages", () => { - document.body.innerHTML = - '
'; - const disconnect = vi.spyOn(MutationObserver.prototype, "disconnect"); - const preview = sideband(); - preview.accept({ componentId: "panel", property: "opacity", value: 0.5 }); - preview.frame(1250); - expect(preview.frames.size).toBe(0); - expect(disconnect).toHaveBeenCalledTimes(1); - - document.body.innerHTML = - '
'; - preview.accept({ componentId: "panel", property: "opacity", value: 0.6 }); - expect(document.getElementById("new")?.style.opacity).toBe("0.6"); - preview.stop(); - expect(disconnect).toHaveBeenCalledTimes(2); - expect(preview.close).toHaveBeenCalledWith(1000, "solar teardown"); - expect(preview.frames.size).toBe(0); - preview.accept({ componentId: "panel", property: "opacity", value: 0.9 }); - expect(document.getElementById("new")?.style.opacity).toBe("0.6"); - expect(preview.frames.size).toBe(0); - }); - - it("falls back to fresh selections when MutationObserver is unavailable", () => { - vi.stubGlobal("MutationObserver", undefined); - document.body.innerHTML = '
'; - const preview = sideband(); - preview.accept({ componentId: "panel", property: "opacity", value: 0.5 }); - document.body.innerHTML = - '
'; - preview.frame(); - expect(document.getElementById("new")?.style.opacity).toBe("0.5"); - }); -}); diff --git a/tests/unit/fonts.test.ts b/tests/unit/fonts.test.ts deleted file mode 100644 index d0bf93d..0000000 --- a/tests/unit/fonts.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Self-hosted Geist @font-face regression guard. -// -// The bug: a scene whose LSML text carries `fontFamily: "Geist"` (the Prism -// picker default, emitted verbatim by from-scene.ts) rendered with the system -// fallback in preview AND on-air, because Solar's OWN served page declared no -// `@font-face` for Geist and bundled no font file — Prism's editor CSS never -// reaches Solar's page. This test pins the fix at the source level so it cannot -// silently regress: both entry points must load the fonts CSS, the CSS must -// declare Geist + Geist Mono, and every referenced woff2 must exist on disk -// (i.e. the asset URL resolves — no 404 once Vite emits it). -// -// The served-page counterpart (the built dist/host page actually carries a -// stylesheet with the @font-face and the woff2 fetches 200) is asserted -// in tests/e2e/host-bundle.spec.ts. - -import { existsSync, readFileSync } from "node:fs"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; - -const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", ".."); -const FONTS_CSS = resolve(ROOT, "src", "styles", "fonts.css"); -const STYLES_DIR = dirname(FONTS_CSS); - -const css = readFileSync(FONTS_CSS, "utf8"); - -describe("Geist self-hosting", () => { - it("is imported by BOTH entry points (preview + host/CEF)", () => { - // Preview / editor / e2e harness path. - const dev = readFileSync(resolve(ROOT, "src", "dev-entry.tsx"), "utf8"); - // Served host / Pulsar CEF / atlas path. - const host = readFileSync(resolve(ROOT, "src", "host-entry.tsx"), "utf8"); - expect(dev).toMatch(/import\s+["']\.\/styles\/fonts\.css["']/); - expect(host).toMatch(/import\s+["']\.\/styles\/fonts\.css["']/); - }); - - it("declares an @font-face for Geist and Geist Mono", () => { - // A weight-range @font-face per family, mirroring Prism (Geist 400–700, - // Geist Mono 400–600) — one variable woff2 covers the whole axis. - const faces = css.match(/@font-face\s*\{[^}]*\}/g) ?? []; - const families = faces.map((f) => { - const m = f.match(/font-family:\s*["']([^"']+)["']/); - return m?.[1]; - }); - expect(families).toContain("Geist"); - expect(families).toContain("Geist Mono"); - - // Every declared face must use font-display: swap (instant first paint). - for (const face of faces) { - expect(face).toMatch(/font-display:\s*swap/); - } - }); - - it("references only woff2 assets that exist on disk (URL resolves)", () => { - const urls = [...css.matchAll(/url\(\s*["']([^"']+)["']\s*\)/g)].map( - (m) => m[1]!, - ); - // At least the two Geist families must be wired. - expect(urls.length).toBeGreaterThanOrEqual(2); - for (const url of urls) { - const assetPath = resolve(STYLES_DIR, url); - expect( - existsSync(assetPath), - `font asset referenced by fonts.css does not exist: ${url}`, - ).toBe(true); - } - }); - - it("gives the scene root a Geist-first fallback stack", () => { - // A text node with no explicit fontFamily inherits a coherent baseline - // instead of the bare browser default. - expect(css).toMatch( - /#scene\s*\{[^}]*font-family:\s*["']?Geist["']?\s*,/, - ); - }); -}); diff --git a/tests/unit/frame-patches.test.ts b/tests/unit/frame-patches.test.ts new file mode 100644 index 0000000..bec628f --- /dev/null +++ b/tests/unit/frame-patches.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it, vi } from "vitest"; +import { FramePatches } from "../../src/engine/frame-patches"; + +describe("frame-paced scalar presentation", () => { + it("merges 512 ordered states into one frame while retaining every receipt", async () => { + const submit = vi.fn(async () => {}), + failed = vi.fn(); + const queue = new FramePatches(submit, failed); + for (let counter = 1; counter <= 512; counter++) + queue.add({ counter }, counter); + await queue.flush(); + expect(submit).toHaveBeenCalledOnce(); + expect(submit.mock.calls[0]).toEqual([ + { counter: 512 }, + Array.from({ length: 512 }, (_, i) => i + 1), + ]); + expect(failed).not.toHaveBeenCalled(); + queue.close(); + }); + it("retains arrivals during a blocked GPU submission and drains before structure work", async () => { + let finish!: () => void; + const submit = vi.fn(async () => { + if (submit.mock.calls.length === 1) + await new Promise((yes) => { + finish = yes; + }); + }); + const queue = new FramePatches(submit, vi.fn()); + queue.add({ counter: 1 }, 1); + const first = queue.flush(); + await Promise.resolve(); + for (let i = 2; i <= 512; i++) queue.add({ counter: i }, i); + const barrier = queue.flush(); + expect(submit).toHaveBeenCalledTimes(1); + finish(); + await first; + await barrier; + expect(submit).toHaveBeenCalledTimes(2); + expect(submit.mock.calls[1]).toEqual([ + { counter: 512 }, + Array.from({ length: 511 }, (_, i) => i + 2), + ]); + queue.close(); + }); + it("propagates presentation failure and cancels unapplied work on disconnect", async () => { + const submit = vi.fn(async () => { + throw new Error("GPU rejected"); + }); + const queue = new FramePatches(submit, vi.fn()); + queue.add({ counter: 1 }, 1); + await expect(queue.flush()).rejects.toThrow("GPU rejected"); + queue.close(); + expect(() => queue.add({ counter: 2 }, 2)).toThrow("closed"); + const cancelled = new FramePatches(submit, vi.fn()); + cancelled.add({ counter: 3 }, 3); + cancelled.close(); + await cancelled.flush(); + expect(submit).toHaveBeenCalledOnce(); + }); + it("uses render completion to drain arrivals without an extra empty animation frame", async () => { + let finish!: () => void; + const submit = vi.fn(async () => { + if (submit.mock.calls.length === 1) + await new Promise((resolve) => { + finish = resolve; + }); + }); + const queue = new FramePatches(submit, vi.fn()); + queue.add({ counter: 1 }, 1); + const first = queue.flush(); + await Promise.resolve(); + queue.add({ counter: 2 }, 2); + finish(); + await first; + await vi.waitFor(() => expect(submit).toHaveBeenCalledTimes(2)); + expect(submit.mock.calls[1]).toEqual([{ counter: 2 }, [2]]); + queue.close(); + }); +}); diff --git a/tests/unit/installation-fonts.test.ts b/tests/unit/installation-fonts.test.ts new file mode 100644 index 0000000..9160f6f --- /dev/null +++ b/tests/unit/installation-fonts.test.ts @@ -0,0 +1,81 @@ +// @vitest-environment node +import { afterEach, expect, it } from "vitest"; +import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { InstallationFonts } from "../../src/server/installation-fonts"; +import { installationFontBatches } from "../../src/scenes/installation-fonts"; +const roots: string[] = []; +const services: InstallationFonts[] = []; +afterEach(async () => { + for (const service of services.splice(0)) await service.stop(); + for (const root of roots.splice(0)) { + if (!root.startsWith(join(tmpdir(), "solar-font-test-"))) + throw new Error("test cleanup boundary"); + await rm(root, { recursive: true, force: true }); + } +}); +it("persists immutable fonts, reuses unchanged files and admits later installation changes", async () => { + const root = await mkdtemp(join(tmpdir(), "solar-font-test-")); + roots.push(root); + const source = join(root, "source"), + cache = join(root, "cache"); + await mkdir(source); + await writeFile(join(source, "one.ttf"), Buffer.from("one")); + await writeFile(join(source, "same.otf"), Buffer.from("one")); + await writeFile(join(source, "bitmap.fon"), Buffer.from("unsupported")); + const first = new InstallationFonts(cache); + await first.prepare([source]); + expect(first.report).toMatchObject({ + filesRead: 2, + reused: 0, + files: 1, + bytes: 3, + }); + expect(first.report.unsupported).toHaveLength(1); + const warm = new InstallationFonts(cache); + await warm.prepare([source]); + expect(warm.report).toMatchObject({ filesRead: 0, reused: 2, files: 1 }); + await writeFile(join(source, "new.woff2"), Buffer.from("new")); + const changed = new InstallationFonts(cache); + services.push(changed); + await changed.prepare([source]); + expect(changed.report).toMatchObject({ filesRead: 1, reused: 2, files: 2 }); + const endpoint = await changed.start(["http://127.0.0.1:8099"]); + expect((await fetch(endpoint.url)).status).toBe(403); + expect( + ( + await fetch(endpoint.url, { + headers: { + Origin: "https://untrusted.example", + Authorization: `Bearer ${endpoint.token}`, + }, + }) + ).status, + ).toBe(403); + const fonts: Uint8Array[] = []; + for await (const batch of installationFontBatches(endpoint)) + fonts.push(...batch); + expect(fonts.map((x) => Buffer.from(x).toString()).sort()).toEqual([ + "new", + "one", + ]); + const entries = await ( + await fetch(endpoint.url, { + headers: { Authorization: `Bearer ${endpoint.token}` }, + }) + ).json(); + await writeFile(join(cache, entries[0].hash), "corrupted"); + await expect(async () => { + for await (const batch of installationFontBatches(endpoint)) void batch; + }).rejects.toThrow("INSTALLATION_FONT_ASSET_UNAVAILABLE"); +}); +it("refuses non-local installation endpoints before fetching", async () => { + await expect(async () => { + for await (const batch of installationFontBatches({ + url: "https://example.com/fonts", + token: "local", + })) + void batch; + }).rejects.toThrow("INSTALLATION_FONT_HOST_REQUIRED"); +}); diff --git a/tests/unit/live-audio-entry.test.ts b/tests/unit/live-audio-entry.test.ts deleted file mode 100644 index a8aab2e..0000000 --- a/tests/unit/live-audio-entry.test.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Live-audio opt-in proof for the served bundle (@lumencast/runtime ≥ 0.13.0, -// MountOptions.liveAudio). The guest-audio bug is that a `meet.peer` / -// `x-zab.meet-peer` peer's `
\n \n );\n}\n","// Anti-silent-drop diagnostics channel (ADR 001 §3.4 D4, issue #34).\n//\n// Every render-side diagnostic — rejected colour/filter/path/typography\n// value, unknown prop, unrendered spec'd field — flows through\n// `emitDiagnostic`. The diagnostic is an EVENT, not a console.log :\n// hosts subscribe via `MountOptions.onDiagnostic` (wired by `mount()`)\n// and receive a structured `{ nodeId, field, reason }`. When no handler\n// is registered, the runtime falls back to a DEV-only `console.warn`\n// so authors still see drops during development — and `broadcast`\n// builds stay silent on the console, per the CLAUDE.md \"no logs in\n// broadcast\" rule.\n//\n// ── Hygiene contract (Bastion R9, ADR 001 §5.1) ─────────────────────\n// A diagnostic NEVER carries the value of a leaf or a prop — only the\n// node id, the field name and a STATIC reason string. Leaf values can\n// hold sensitive on-air content ; they must not transit any diagnostic\n// channel. Callers pass field names and literal reasons exclusively.\n// The R9 sentinel test (r9-sentinel.test.tsx) enforces this end to end,\n// and statically checks that `console.warn` only exists in this module.\n// ─────────────────────────────────────────────────────────────────────\n\n/** Placeholder id for nodes that don't declare an `id`. */\nexport const ANON_NODE_ID = \"\";\n\nexport interface RenderDiagnostic {\n /** `RenderNode.id` of the node the field belongs to (RC#7), or\n * `ANON_NODE_ID` when the node has none. */\n nodeId: string;\n /** Name of the field/prop concerned (e.g. `text.colour`,\n * `shape.paths.data`, `bindAnimate.opacity`). Never its value (R9). */\n field: string;\n /** Static reason — why the field was rejected or not rendered. */\n reason: string;\n}\n\nexport type DiagnosticHandler = (diagnostic: RenderDiagnostic) => void;\n\nconst handlers = new Set();\n\n/**\n * Register a diagnostics handler (one per `mount()`, plus tests).\n * Returns the unregister function. Multiple concurrent mounts each\n * receive every diagnostic — node ids are bundle-scoped, so a host\n * running several mounts should disambiguate on its side.\n */\nexport function addDiagnosticsHandler(handler: DiagnosticHandler): () => void {\n handlers.add(handler);\n return () => {\n handlers.delete(handler);\n };\n}\n\n/**\n * Emit one anti-drop diagnostic. `field` and `reason` MUST be static\n * strings / field names — never interpolate a prop or leaf value (R9).\n */\nexport function emitDiagnostic(nodeId: string | undefined, field: string, reason: string): void {\n const diagnostic: RenderDiagnostic = { nodeId: nodeId ?? ANON_NODE_ID, field, reason };\n if (handlers.size > 0) {\n for (const handler of handlers) {\n try {\n handler(diagnostic);\n } catch {\n // A host handler that throws must never break the render path.\n }\n }\n return;\n }\n // DEV-only console fallback — broadcast builds log nothing.\n if (import.meta.env.DEV) {\n console.warn(\n `[lumencast] node \"${diagnostic.nodeId}\": field \"${field}\" ${reason} (value withheld per R9)`,\n );\n }\n}\n","// Runtime half of the R8 filter gate (ADR 001 §5.1 R8, issue #42).\n//\n// The compiler clamps `filter` values at lowering (`lowerFilter`,\n// packages/compiler/src/compile.ts) — but a filter value pushed by a\n// LIVE LSDP delta reaches the runtime through `resolveProps` /\n// `animateBindings` without ever passing through the compiler. R8\n// requires the clamp at compile AND at runtime : an unbounded filter is\n// a compositing DoS in CEF. Every filter value that can reach an inline\n// style at render time MUST pass through this module.\n//\n// NOTE on duplication : these caps intentionally mirror the compiler's\n// `MAX_FILTER_BLUR_PX` / `MAX_FILTER_BRIGHTNESS` constants. Unifying\n// them behind a single shared module is tracked by issue #41 (same\n// model as the shared colour module) — do NOT change one side without\n// the other until #41 lands.\n//\n// ── Linear-time justification (RC#12) ────────────────────────────────\n// The string form is validated by a single ANCHORED regex made of\n// literals and bounded quantifiers ({1,7} / {1,4} digit runs, one\n// optional space run) — exactly one possible parse per input, no\n// backtracking blow-up. Inputs longer than MAX_FILTER_STRING_LEN are\n// rejected before the regex runs.\n// ─────────────────────────────────────────────────────────────────────\n\nimport { emitDiagnostic } from \"./diagnostics\";\n\n/** Max CSS `blur()` radius accepted at runtime, in px (mirror of the\n * compiler cap — see issue #41). */\nexport const MAX_FILTER_BLUR_PX = 400;\n/** Max bounded static-blur pass count accepted at runtime. Mirrors the\n * compiler's fixed approximation bound; each pass is still capped at\n * MAX_FILTER_BLUR_PX (R8). */\nexport const MAX_STATIC_BLUR_PASSES = 7;\n/** Max CSS `brightness()` factor accepted at runtime (mirror of the\n * compiler cap — see issue #41 ; spec §6.1 blesses clamping to 4). */\nexport const MAX_FILTER_BRIGHTNESS = 4;\n\nconst MAX_FILTER_STRING_LEN = 64;\n\n/** Max px magnitude for a static `node.shadow[]` entry's `spread`\n * (ADR 014 R2/R8, mirrors the compiler's `MAX_SHADOW_SPREAD_PX`). */\nexport const MAX_SHADOW_SPREAD_PX = 1000;\n/** Max px magnitude for a static `node.shadow[]` entry's `x`/`y` offset\n * (mirrors the compiler's `MAX_SHADOW_OFFSET_PX`). */\nexport const MAX_SHADOW_OFFSET_PX = 4096;\n/** Max px for `node.noise.noiseSize` / `node.texture.{noiseSize,radius}`\n * (ADR 014 Tier B, mirrors the compiler's `MAX_NOISE_SIZE_PX`). */\nexport const MAX_NOISE_SIZE_PX = 2000;\n\nconst CAPS: Record = {\n blur: MAX_FILTER_BLUR_PX,\n brightness: MAX_FILTER_BRIGHTNESS,\n backdropBlur: MAX_FILTER_BLUR_PX,\n noiseSize: MAX_NOISE_SIZE_PX,\n};\n\nexport type FilterChannel = \"blur\" | \"brightness\" | \"backdropBlur\" | \"noiseSize\";\n\n/**\n * Gate one live numeric filter channel (R8 runtime half).\n *\n * Returns the clamped value, or `null` when the value is rejected\n * (non-number, non-finite, negative — including `-0`, which would\n * stringify to an accepted `0`). A `null` MUST be handled as \"keep the\n * last known-good value / identity\" — never apply the raw input.\n */\nexport function clampFilterChannel(channel: FilterChannel, value: unknown): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n if (value < 0 || Object.is(value, -0)) return null;\n const cap = CAPS[channel];\n return value > cap ? cap : value;\n}\n\n/**\n * Runtime half of the imported static layer-blur approximation gate. A live\n * bundle may bypass the compiler, so only a finite positive integer up to the\n * fixed pass bound is allowed to influence the generated filter list.\n */\nexport function clampStaticBlurPasses(value: unknown): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value < 1) return null;\n return Math.min(MAX_STATIC_BLUR_PASSES, Math.floor(value));\n}\n\n/** Symmetric channel — `node.shadow[]`'s `x`/`y`/`spread` (ADR 014 R2/R8).\n * Unlike `blur`/`backdropBlur`, these are legitimately signed (a negative\n * spread shrinks the shadow, an offset can point either way) — clamps to\n * `[-max, max]` instead of rejecting negatives. Returns `null` only for\n * non-finite input. */\nexport type SymmetricChannel = \"shadowSpread\" | \"shadowOffset\";\n\nconst SYMMETRIC_CAPS: Record = {\n shadowSpread: MAX_SHADOW_SPREAD_PX,\n shadowOffset: MAX_SHADOW_OFFSET_PX,\n};\n\nexport function clampSymmetricChannel(channel: SymmetricChannel, value: unknown): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n const cap = SYMMETRIC_CAPS[channel];\n if (value > cap) return cap;\n if (value < -cap) return -cap;\n return value;\n}\n\n/** Clamp a unit-interval value (`node.noise.density`, `node.glass.*`, a\n * noise tint's r/g/b/a channel — ADR 014 Tier B) to `[0, 1]`. Returns\n * `null` only for non-finite / non-number input ; unlike\n * `clampFilterChannel` it does NOT reject negatives, it clamps them to 0\n * (there's no \"identity\" fallback semantics needed for a 0..1 fraction). */\nexport function clampUnitInterval(value: unknown): number | null {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n if (value < 0) return 0;\n if (value > 1) return 1;\n return value;\n}\n\n/** Normalise a degree value into `[0, 360)` (`node.glass.lightAngle`).\n * Not a DoS vector (feeds a CSS gradient angle) — wraps rather than\n * rejecting ; non-finite/non-number falls back to 0. */\nexport function normalizeDegrees(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return 0;\n return ((value % 360) + 360) % 360;\n}\n\n// The ONLY string form the compiler ever emits (`lowerFilter`) :\n// `blur(px) brightness()`. Anything else — extra functions,\n// `url(`, negative signs, exponents — is rejected by construction\n// (the grammar has no `-`, no `e`, no second parenthesis pair).\nconst FILTER_STRING_RE =\n /^blur\\((\\d{1,7}(?:\\.\\d{1,4})?)px\\) brightness\\((\\d{1,7}(?:\\.\\d{1,4})?)\\)$/;\n\n/** Identity filter — matches the compiler's neutral emission and\n * `INITIAL_IDENTITY.filter` in transitions.ts. */\nexport const FILTER_IDENTITY = \"blur(0px) brightness(1)\";\n\n/**\n * Gate a CSS filter STRING reaching framer-motion at runtime\n * (`animate_initial.filter`, keyframe `steps[].filter`). Hand-crafted\n * bundles bypass the compiler clamps, so the runtime re-validates and\n * re-clamps (R8). Returns the safe, clamped canonical string or `null`\n * on rejection — never the raw input.\n */\nexport function sanitizeCssFilterString(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n if (value.length === 0 || value.length > MAX_FILTER_STRING_LEN) return null;\n const m = FILTER_STRING_RE.exec(value);\n if (!m) return null;\n const blur = clampFilterChannel(\"blur\", Number(m[1]));\n const brightness = clampFilterChannel(\"brightness\", Number(m[2]));\n if (blur === null || brightness === null) return null;\n return `blur(${blur}px) brightness(${brightness})`;\n}\n\n/**\n * Diagnostic for a rejected filter value. Bastion R9 (ADR 001 §5.1) :\n * the rejected VALUE is never logged nor forwarded — only `node.id`\n * (RC#7, issue #34), the field name and a static reason. Routed through\n * the structured diagnostics channel (events, no logs in `broadcast`).\n */\nexport function warnRejectedFilter(field: string, nodeId?: string): void {\n emitDiagnostic(\n nodeId,\n field,\n \"rejected unsafe filter value : outside the R8 caps or not a finite number >= 0\",\n );\n}\n","// Local Transition type + Framer Motion translation.\n//\n// LSML 1.0 §6 declares `animate` directives at the primitive level (transition,\n// transform, opacity, filter). LSDP/1.1 §3.2.2 added per-leaf transition\n// directives on delta patches — incoming deltas can carry a transition hint\n// that overrides the bundle-level default for the next animation cycle.\n// `parseWireTransition` ingests the wire shape ; `Store.lastTransition(path)`\n// surfaces the most-recent directive to the renderer.\n//\n// We deliberately animate only GPU-friendly properties (transform, opacity,\n// filter). Primitives enforce this at the DOM level by exposing those props as\n// motion-bindable values rather than raw CSS.\n\nimport { sanitizeCssFilterString, warnRejectedFilter } from \"../render/filter-clamp\";\n\nexport type TransitionKind = \"none\" | \"tween\" | \"spring\" | \"crossfade\";\n\nexport interface TweenTransition {\n kind: \"tween\";\n duration_ms: number;\n ease?: \"linear\" | \"cubic-in\" | \"cubic-out\" | \"cubic-in-out\";\n}\n\nexport interface SpringTransition {\n kind: \"spring\";\n stiffness?: number;\n damping?: number;\n /** LSML 1.1 §6.2 — spring mass (kg). Default 1 (framer default). */\n mass?: number;\n}\n\nexport interface CrossfadeTransition {\n kind: \"crossfade\";\n duration_ms?: number;\n}\n\nexport interface NoTransition {\n kind: \"none\";\n}\n\nexport type Transition = NoTransition | TweenTransition | SpringTransition | CrossfadeTransition;\n\nexport type FramerEasing = \"linear\" | \"easeIn\" | \"easeOut\" | \"easeInOut\";\n\nexport interface FramerTransition {\n duration?: number;\n ease?: FramerEasing;\n type?: \"tween\" | \"spring\";\n stiffness?: number;\n damping?: number;\n mass?: number;\n}\n\nconst NO_ANIMATION: FramerTransition = { duration: 0 };\n\nconst EASE_MAP: Record = {\n linear: \"linear\",\n \"cubic-in\": \"easeIn\",\n \"cubic-out\": \"easeOut\",\n \"cubic-in-out\": \"easeInOut\",\n};\n\nexport function toFramer(t: Transition | undefined): FramerTransition {\n if (!t || t.kind === \"none\") return NO_ANIMATION;\n if (t.kind === \"tween\") {\n return {\n type: \"tween\",\n duration: (t.duration_ms ?? 0) / 1000,\n ease: t.ease ? (EASE_MAP[t.ease] ?? \"easeOut\") : \"easeOut\",\n };\n }\n if (t.kind === \"spring\") {\n return {\n type: \"spring\",\n ...(t.stiffness !== undefined ? { stiffness: t.stiffness } : {}),\n ...(t.damping !== undefined ? { damping: t.damping } : {}),\n ...(t.mass !== undefined ? { mass: t.mass } : {}),\n };\n }\n // crossfade at the per-prop level degenerates into a tween on opacity.\n return {\n type: \"tween\",\n duration: (t.duration_ms ?? 400) / 1000,\n ease: \"easeInOut\",\n };\n}\n\n// --- mount-play (LSML 1.1 `animate.from`) ---------------------------\n\n/** Identity (animation-end) value for each framer key an `animate.from`\n * may declare. A primitive that doesn't natively animate a given key\n * still converges it to this neutral value on mount so the element ends\n * up visually correct (e.g. a `from.scale: 0.85` settles at `scale: 1`). */\nconst INITIAL_IDENTITY: Record = {\n opacity: 1,\n scale: 1,\n scaleX: 1,\n scaleY: 1,\n rotate: 0,\n x: 0,\n y: 0,\n // LSML §6.1 filter identity — both functions are always present so\n // framer interpolates between structurally-identical filter lists\n // (the compiler emits the same two-function form, clamped per R8).\n filter: \"blur(0px) brightness(1)\",\n};\n\nexport interface MountPlay {\n initial: Record;\n animate: Record;\n}\n\n/**\n * Default mount-play timing — applies when a node carries an\n * `animate_initial` (LSML 1.1 `animate.from`) but no per-prop\n * `transitions` entry resolves for any animated key. The compiler\n * documents that `from` without an explicit `transition` mount-plays\n * \"with the runtime's default timing\" ; before this constant existed the\n * fallback was `toFramer(undefined)` → `{ duration: 0 }`, which snapped\n * the element straight to its settled state (the mount-play never\n * visibly played). 400 ms ease-out matches the runtime's other implicit\n * timings (crossfade fallback, scene-track fade).\n */\nexport const DEFAULT_MOUNT_PLAY_TRANSITION: Transition = {\n kind: \"tween\",\n duration_ms: 400,\n ease: \"cubic-out\",\n};\n\n/**\n * Resolve the transition a primitive should hand framer-motion.\n *\n * `keys` are the primitive's native animated prop keys, scanned in\n * order (e.g. `[\"opacity\", \"src\"]` for Image). When the node also\n * carries an `animate_initial`, the lookup widens to the keys the\n * mount-play actually moves (`from.scale` may have lowered a `scale`\n * transition that an opacity-only primitive would otherwise never look\n * up), and — critically — falls back to\n * `DEFAULT_MOUNT_PLAY_TRANSITION` instead of \"no animation\" : a\n * mount-play must tween, never complete in zero frames.\n *\n * Without `animate_initial` the prior behaviour is preserved exactly :\n * first declared transition among `keys`, else `undefined` (deltas\n * snap unless a transition is declared).\n */\nexport function resolveTransition(\n transitionFor: (key: string) => Transition | undefined,\n keys: string[],\n animateInitial?: Record,\n): Transition | undefined {\n for (const key of keys) {\n const t = transitionFor(key);\n if (t !== undefined) return t;\n }\n if (animateInitial && Object.keys(animateInitial).length > 0) {\n for (const key of Object.keys(animateInitial)) {\n const t = transitionFor(key);\n if (t !== undefined) return t;\n }\n return DEFAULT_MOUNT_PLAY_TRANSITION;\n }\n return undefined;\n}\n\n/**\n * Build framer-motion `initial` / `animate` props for a primitive that\n * may carry an LSML 1.1 `animate.from` initial state.\n *\n * `base` is the primitive's own animated target (e.g. `{ opacity }` for\n * Image/Text/Shape, or `{ opacity, x, y, scale, rotate }` for Frame).\n * `initial` is the lowered `animate.from` map (or `undefined`).\n *\n * When `initial` is absent, this returns `{ initial: base, animate: base }`\n * — framer mounts at the target and never moves, exactly the prior\n * no-mount-play behaviour (backward compatible). When `initial` is\n * present, the element mounts at `initial` and animates to `base`,\n * augmented with identity convergence for any `from` key the primitive\n * doesn't already drive — so the mount-play plays out and settles\n * correctly even on opacity-only primitives.\n */\nexport function mountPlay(\n base: Record,\n initial: Record | undefined,\n nodeId?: string,\n): MountPlay {\n if (!initial || Object.keys(initial).length === 0) {\n // No `from` → mount directly at target. Pinning `initial` to the\n // target (rather than letting framer infer from current style)\n // preserves the existing \"no jump, no mount-play\" behaviour.\n return { initial: base, animate: base };\n }\n // R8 runtime half (ADR 001 §5.1, issue #42) — `animate_initial` may\n // come from a hand-crafted bundle that never went through the\n // compiler clamps. Re-gate the filter string ; rejected → drop the\n // key (the element mounts at the identity filter instead).\n let from = initial;\n if (initial[\"filter\"] !== undefined) {\n const safe = sanitizeCssFilterString(initial[\"filter\"]);\n from = { ...initial };\n if (safe === null) {\n warnRejectedFilter(\"animate_initial.filter\", nodeId);\n delete from[\"filter\"];\n } else {\n from[\"filter\"] = safe;\n }\n }\n const animate: Record = { ...base };\n for (const key of Object.keys(from)) {\n if (!(key in animate)) {\n animate[key] = INITIAL_IDENTITY[key] ?? 0;\n }\n }\n return { initial: from, animate };\n}\n\n/**\n * Parse a wire-format `TransitionSpec` (LSDP/1.1 §3.2.2) into the\n * runtime's local Transition type. Returns `undefined` for malformed\n * input so the caller falls back to whatever bundle-level default\n * applies. The wire shape uses kebab-case `easing` values\n * (`linear`, `ease-in`, `ease-out`, `ease-in-out`) which we map to\n * the runtime's `cubic-*` vocabulary.\n */\nexport function parseWireTransition(value: unknown): Transition | undefined {\n if (typeof value !== \"object\" || value === null) return undefined;\n const v = value as Record;\n const kind = v.kind;\n if (kind === \"snap\") {\n return { kind: \"none\" };\n }\n if (kind === \"tween\") {\n const duration_ms = typeof v.duration_ms === \"number\" ? v.duration_ms : 0;\n const easing = WIRE_EASING_MAP[v.easing as string] ?? \"cubic-out\";\n return { kind: \"tween\", duration_ms, ease: easing };\n }\n if (kind === \"spring\") {\n const out: SpringTransition = { kind: \"spring\" };\n if (typeof v.stiffness === \"number\") out.stiffness = v.stiffness;\n if (typeof v.damping === \"number\") out.damping = v.damping;\n // LSML §6.2 — mass rides the same wire spring shape.\n if (typeof v.mass === \"number\") out.mass = v.mass;\n return out;\n }\n return undefined;\n}\n\nconst WIRE_EASING_MAP: Record = {\n linear: \"linear\",\n \"ease-in\": \"cubic-in\",\n \"ease-out\": \"cubic-out\",\n \"ease-in-out\": \"cubic-in-out\",\n};\n","import { batch } from \"@preact/signals-react\";\nimport type { DeltaFrame } from \"@lumencast/protocol\";\nimport type { Store } from \"./store.js\";\nimport { parseWireTransition } from \"../animate/transitions\";\n\n/** Apply an LSDP/1 delta. All patches in the frame land in a single signals\n * batch — components reading multiple paths see them flip in one render pass.\n *\n * LSDP/1.1 §3.2.2 — a patch may carry a `transition` directive overriding\n * the bundle-level default for the next animation cycle on that leaf. We\n * thread it through the store so the renderer reads the correct directive\n * alongside the new value. */\nexport function applyDelta(store: Store, frame: DeltaFrame): void {\n batch(() => {\n for (const patch of frame.patches) {\n const transition = parseWireTransition(patch.transition);\n if (transition !== undefined) {\n store.setWithTransition(patch.path, patch.value, transition);\n } else {\n store.set(patch.path, patch.value);\n }\n }\n });\n}\n","import type { SnapshotFrame } from \"@lumencast/protocol\";\nimport type { Store } from \"./store.js\";\n\n/** Apply an LSDP/1 snapshot to the store. Replaces the entire state — paths\n * not present in the snapshot are reset to `undefined`. */\nexport function applySnapshot(store: Store, frame: SnapshotFrame): void {\n store.reset(frame.state);\n}\n","// Reserved `__cam.*` LSDP leaves — surfaced to the host, never rendered.\n//\n// ADR Blue 009 §3.2–3.3 (axe 1, antenne). The meet-cam antenne path needs two\n// pieces of RUNTIME, stream-level ZabCam state that travel on the LSDP wire as\n// RESERVED leaves (they bind to no node and are never painted) :\n//\n// - `__cam.slots.` = \"\" (Orion #267 deltas, §3.3)\n// which peer currently fills an authored `x-zab.meet-peer` slot.\n// - `__cam.viewer` = { rooms: [{ signalingUrl, roomId, token }] } (Orion #268)\n// the receive-only viewer credentials for the active stream.\n//\n// The runtime does NOT join rooms, hold creds, or re-key slots itself — that is\n// the host's WebRTC viewer (Solar `peer-viewer/*`). The runtime's only job is to\n// SURFACE these reserved leaves to the host through `MountOptions.onReservedLeaves`\n// so Solar can feed `__cam.viewer` into its viewer injection and drive its\n// slot-binding registry's `assign(slotRef, peer_label | null)` from `__cam.slots.*`.\n// Receive-only : the token flows host→viewer→join ; the runtime never reads it.\n\n/** Every reserved cam leaf lives under this prefix. */\nexport const CAM_RESERVED_PREFIX = \"__cam.\";\n\n/** One scalar leaf per bound slot : `__cam.slots.` = \"\"\n * (ADR Blue 009 §3.3). Mirrors Solar's `CAM_SLOTS_PREFIX` — keep them in sync. */\nexport const CAM_SLOTS_PREFIX = \"__cam.slots.\";\n\n/** The single viewer-credentials leaf (ADR Blue 009 §3.2, Orion #268). */\nexport const CAM_VIEWER_LEAF = \"__cam.viewer\";\n\n/** The reserved cam state surfaced to the host in one shot on every change. */\nexport interface ReservedCamLeaves {\n /** `__cam.viewer` — receive-only viewer creds for the active stream. Opaque to\n * the runtime (shape `{ rooms: [{ signalingUrl, roomId, token }] }` validated\n * host-side) ; `undefined` when the leaf is absent. */\n viewer?: unknown;\n /** `slotRef → peer_label` snapshot from the `__cam.slots.*` subtree. A slot\n * ABSENT from this map is UNBOUND — the host releases it (`assign(slotRef,\n * null)`) so the `x-zab.meet-peer` node falls back to its placeholder. Only\n * non-empty string values are kept. */\n slots: Record;\n}\n\n/** A reserved cam leaf the runtime forwards rather than renders. */\nexport function isReservedCamPath(path: string): boolean {\n return path === CAM_VIEWER_LEAF || path.startsWith(CAM_SLOTS_PREFIX);\n}\n\n/** Project a raw reserved-leaf state into the host-facing shape. Defensive : a\n * malformed slot value (non-string / empty) or empty slotRef is dropped. */\nfunction project(raw: Map): ReservedCamLeaves {\n const slots: Record = {};\n let viewer: unknown;\n for (const [path, value] of raw) {\n if (path === CAM_VIEWER_LEAF) {\n if (value !== undefined && value !== null) viewer = value;\n } else if (path.startsWith(CAM_SLOTS_PREFIX)) {\n const slotRef = path.slice(CAM_SLOTS_PREFIX.length);\n if (slotRef !== \"\" && typeof value === \"string\" && value !== \"\") slots[slotRef] = value;\n }\n }\n return viewer !== undefined ? { viewer, slots } : { slots };\n}\n\n/** A stable identity key for change detection — two `ReservedCamLeaves` with the\n * same content always produce the same key regardless of insertion order. */\nfunction identity(leaves: ReservedCamLeaves): string {\n const slots = Object.keys(leaves.slots)\n .sort()\n .map((k) => `${k}=${leaves.slots[k]}`)\n .join(\"&\");\n const viewer = leaves.viewer === undefined ? \"\" : JSON.stringify(leaves.viewer);\n return `${slots}|${viewer}`;\n}\n\nexport interface ReservedLeafObserver {\n /** Reseed from a full snapshot's state (reserved leaves not present are\n * dropped). Emits when the projected state changed. */\n onSnapshot(state: Record): void;\n /** Apply a delta's patches ; emits only when a reserved leaf actually moved. */\n onDelta(patches: ReadonlyArray<{ path: string; value: unknown }>): void;\n}\n\n/** Track the reserved `__cam.*` leaves across snapshots + deltas and `emit` the\n * host-facing projection whenever it changes (de-duplicated by content, so an\n * unrelated scene's deltas never call back). Created only when the host supplies\n * `onReservedLeaves` — zero cost otherwise. */\nexport function createReservedLeafObserver(\n emit: (leaves: ReservedCamLeaves) => void,\n): ReservedLeafObserver {\n const raw = new Map();\n // Seed with the empty projection's identity so a plain scene (no cam leaves)\n // never fires a spurious empty emit ; a later transition to/from cam state does.\n let last = identity({ slots: {} });\n\n const flush = (): void => {\n const leaves = project(raw);\n const key = identity(leaves);\n if (key === last) return;\n last = key;\n emit(leaves);\n };\n\n return {\n onSnapshot(state) {\n raw.clear();\n for (const [path, value] of Object.entries(state)) {\n if (isReservedCamPath(path)) raw.set(path, value);\n }\n flush();\n },\n onDelta(patches) {\n let touched = false;\n for (const patch of patches) {\n if (isReservedCamPath(patch.path)) {\n raw.set(patch.path, patch.value);\n touched = true;\n }\n }\n if (touched) flush();\n },\n };\n}\n","// State store — one signal per leaf path.\n//\n// Integration point between the WS layer (snapshot + delta) and the render\n// layer. Each path Lumencast has ever seen owns a `Signal`;\n// readers subscribe via @preact/signals-react `useSignals()` and re-render\n// only when their path's value changes.\n//\n// LSDP/1.1 §3.2.2 — incoming deltas may carry a per-leaf `transition`\n// directive. The store keeps the most-recent directive per path so the\n// renderer can pick it up on the next animation cycle. Snapshots clear\n// any pending transitions for the affected paths (snapshots are not\n// animated transitions).\n\nimport { signal, type Signal, batch } from \"@preact/signals-react\";\nimport type { Transition } from \"../animate/transitions\";\n\nexport interface Store {\n /** Get-or-create one stable signal per path. New paths start as `undefined`. */\n signal(path: string): Signal;\n /** Apply a single leaf write. */\n set(path: string, value: unknown): void;\n /** Apply a single leaf write with an LSDP/1.1 §3.2.2 transition directive.\n * The directive lives in a separate signal so the renderer can subscribe\n * to it independently. Passing `undefined` clears any pending directive. */\n setWithTransition(path: string, value: unknown, transition: Transition | undefined): void;\n /** Get-or-create one stable transition signal per path. Read the most-recent\n * transition directive (or undefined\n * when no directive has been applied since the last snapshot). The\n * returned signal is reactive — components reading via `useSignals()`\n * re-render when the directive changes. */\n transitionSignal(path: string): Signal;\n /**\n * Replace the whole state — used by `apply-snapshot`. Existing signals are\n * reused (subscribers stay attached); paths missing from the snapshot reset\n * to `undefined`. Pending per-path transitions are cleared (a snapshot is\n * a state restore, not an animated change).\n */\n reset(state: Record): void;\n /** Snapshot of every known path → current value. For debug / state inspector. */\n toRecord(): Record;\n}\n\nclass StoreImpl implements Store {\n private readonly signals = new Map>();\n private readonly transitions = new Map>();\n\n signal(path: string): Signal {\n let s = this.signals.get(path);\n if (!s) {\n s = signal(undefined);\n this.signals.set(path, s);\n }\n return s;\n }\n\n transitionSignal(path: string): Signal {\n let s = this.transitions.get(path);\n if (!s) {\n s = signal(undefined);\n this.transitions.set(path, s);\n }\n return s;\n }\n\n set(path: string, value: unknown): void {\n const s = this.signal(path);\n if (!shallowEqual(s.peek(), value)) {\n s.value = value;\n }\n }\n\n setWithTransition(path: string, value: unknown, transition: Transition | undefined): void {\n batch(() => {\n const ts = this.transitionSignal(path);\n // Update transition before value so the render that observes the\n // new value sees the correct transition.\n if (ts.peek() !== transition) ts.value = transition;\n const s = this.signal(path);\n if (!shallowEqual(s.peek(), value)) s.value = value;\n });\n }\n\n reset(state: Record): void {\n batch(() => {\n const seen = new Set();\n for (const [path, value] of Object.entries(state)) {\n seen.add(path);\n const s = this.signal(path);\n if (!shallowEqual(s.peek(), value)) {\n s.value = value;\n }\n // Snapshots are not animated transitions — clear any pending\n // per-path directive (LSDP/1.1 §3.2.2 — directives apply to\n // the NEXT delta only, snapshots reseed state authoritatively).\n const ts = this.transitions.get(path);\n if (ts && ts.peek() !== undefined) ts.value = undefined;\n }\n for (const path of this.signals.keys()) {\n if (!seen.has(path)) {\n const s = this.signals.get(path);\n if (s && s.peek() !== undefined) s.value = undefined;\n }\n }\n });\n }\n\n toRecord(): Record {\n const out: Record = {};\n for (const [path, s] of this.signals.entries()) {\n out[path] = s.peek();\n }\n return out;\n }\n}\n\nexport function createStore(): Store {\n return new StoreImpl();\n}\n\nfunction shallowEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n if (typeof a !== typeof b) return false;\n if (typeof a !== \"object\") return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false;\n }\n return true;\n }\n const ao = a as Record;\n const bo = b as Record;\n const ak = Object.keys(ao);\n const bk = Object.keys(bo);\n if (ak.length !== bk.length) return false;\n for (const k of ak) {\n if (ao[k] !== bo[k]) return false;\n }\n return true;\n}\n","// Render bundle — the runtime's flat, pre-compiled representation of a scene.\n//\n// The bundle is content-addressed by `scene_version` (sha256 of the\n// canonical JSON form). Lumencast fetches it once per `scene_version` and\n// caches forever; the server serves it with long-TTL immutable cache headers.\n//\n// Note on shape: this `RenderBundle` is the flat, runtime-internal form. The\n// canonical *authoring* format (LSML 1.0, see lumencast-protocol/spec/LSML-1.md)\n// uses inline `bind: { value: \"path\" }` per primitive instead of a `bindings`\n// map. A compiler step (forthcoming `@lumencast/compiler`) will translate\n// LSML 1.0 → RenderBundle. For now, callers who want to feed an LSML 1.0\n// bundle pre-compile or use a hand-rolled adapter.\n\nimport type { Transition } from \"../animate/transitions.js\";\nimport type { Keyframes } from \"../animate/keyframes.js\";\n\n// --- bundle shape ----------------------------------------------------\n\nexport type RenderKind =\n | \"stack\"\n | \"grid\"\n | \"frame\"\n | \"text\"\n | \"image\"\n | \"shape\"\n | \"media\"\n | \"repeat\"\n | \"instance\"\n // ADR 006 §3.3/§3.5 — the unified source kind. Every source crossing the\n // Prism export arrives as a `meet.peer` node ; the runtime resolves its\n // `peer_label → MediaStream` (WebRTC viewer) and renders it in `srcObject`.\n | \"meet.peer\"\n // Zab vendor primitive (RFC-0001, §17.1) — a transparent capture\n // placeholder. Recognised by the Zab-plugin runtime ; reserves a box and\n // renders nothing.\n | \"x-zab.capture\"\n // Zab vendor primitive (ADR Blue 009 §3.1, Amendment 2) — a transparent\n // meet-peer SLOT placeholder. Declares only a logical `x-zab.slotRef` (which\n // slot receives a meet peer) + geometry ; carries NO cam/peer identity. The\n // runtime resolves `slotRef → peer_label` from stream-level ZabCam state and\n // renders the bound peer ; an unbound slot renders a transparent box.\n | \"x-zab.meet-peer\";\n\nexport interface RenderNode {\n kind: RenderKind;\n /** Stable identifier for keyed reconciliation. */\n id?: string;\n /** Static props (frozen at build/compile time). */\n props?: Record;\n /** Prop name → state path. The render layer subscribes the path's signal\n * and applies the value to the named prop on each change. */\n bindings?: Record;\n /** Default transition per bound prop. Aligns with LSML 1.0 §6 `animate`\n * directives. The runtime applies these as CSS transitions / Framer Motion\n * configs at render time. */\n transitions?: Record;\n /** LSML 1.1 §6 `animate.from` — mount-time initial state, lowered to a\n * flat framer-motion `initial` map (keys: `opacity`, `scale`, `rotate`,\n * `x`, `y`, `filter`). When present, the rendering primitive passes this\n * as framer-motion `initial={...}` so the element mounts in this state\n * and animates to its declared target on mount (mount-play). When absent,\n * the primitive applies no `initial` and the prior no-mount-play\n * behaviour holds (backward compatible). */\n animate_initial?: Record;\n /** LSML 1.1 §6.3 — animation targets bound to leaf paths\n * (`bindAnimate`). Keys are the spec property names (`opacity`,\n * `transform.translate`, `transform.scale`, `transform.rotate`,\n * `filter.blur`, `filter.brightness`, plus the kind's colour-typed\n * property per §6.5 : `style.color` / `fill` / `background`) ; values\n * are LeafPaths. The runtime subscribes each path's leaf-grain signal\n * and retargets a Framer motion value on change — continuous\n * interpolation toward the live value, no remount. Deltas are\n * coalesced per frame (one retarget max per rAF per binding,\n * ADR 001 RC#13). */\n animateBindings?: Record;\n /** LSML 1.1 §6.6 — multi-step keyframe sequence played on mount or\n * whenever `keyframes.key` (LeafPath) changes. Coexists with\n * `transitions` ; the runtime applies whichever was last triggered\n * (no blending — see §6.6 last paragraph). */\n keyframes?: Keyframes;\n /** LSML 1.1 §6.7 — only meaningful on `repeat`. Each iteration's\n * animations start `index * stagger_ms` after iteration 0. */\n stagger_ms?: number;\n /** Children — already-inlined primitives only. */\n children?: RenderNode[];\n}\n\nexport type OperatorInputType =\n | \"boolean\"\n | \"number\"\n | \"text\"\n | \"select\"\n | \"enum\"\n | \"path-ref\"\n | \"colour\"\n | \"duration\";\n\nexport interface OperatorInput {\n path: string;\n label: string;\n type: OperatorInputType;\n default?: unknown;\n group?: string;\n writable_by?: string[];\n [extra: string]: unknown;\n}\n\nexport interface ExternalAdapter {\n key: string;\n label: string;\n kind: string;\n target_paths: string[];\n [extra: string]: unknown;\n}\n\nexport interface Asset {\n id: string;\n url: string;\n kind: string;\n [extra: string]: unknown;\n}\n\nexport interface RenderBundle {\n scene_version: string;\n root: RenderNode;\n /** Initial leaf-grain values authored by the bundle. Hosts may override\n * these at the headless seam, but compilation must not discard them. */\n defaults?: Record;\n operator_inputs?: OperatorInput[];\n external_adapters?: ExternalAdapter[];\n /** Bundle-level asset declarations. `allowedHosts` is the host\n * allowlist (LSML 1.2 §3.2, Bastion T1) every image / image-fill `src`\n * is gated against at render BEFORE reaching the DOM. Absent / empty =\n * deny every remote host (deny-by-default). Other keys (`fonts`,\n * `preload`) are forwarded opaquely by the compiler. */\n assets?: { allowedHosts?: string[]; [key: string]: unknown };\n /** LSML 1.1 §17.3 — capability profiles required for correct rendering.\n * Each entry is an `x-./` string. The runtime\n * checks every behavioural entry against its supported list ; an\n * unrecognised behavioural profile raises BUNDLE_INCOMPATIBLE per\n * §17.3.1. Authoring profiles (`x-.authoring/`, §17.5.1)\n * are advisory : ignored at render time, never a rejection cause. */\n profiles?: string[];\n}\n\n/**\n * Profiles the JS runtime advertises support for. Bundle authors who\n * declare `profiles: [...]` get a hard `BUNDLE_INCOMPATIBLE` rejection\n * when any entry is not in this set (LSML 1.1 §17.3.1).\n *\n * 1.1 ships with no standard profiles ; future minors / vendor specs\n * register here. The `x-lumencast.color-srgb-1.0` entry is the\n * default-color-space marker ; bundles that opt into a perceptual\n * space (OKLCH) would request a different profile and currently\n * reject.\n */\nexport const SUPPORTED_PROFILES: ReadonlySet = new Set([\n \"x-lumencast.color-srgb-1.0\",\n // RFC-0001 / ADR 004 — this runtime ships the Zab capture plugin, so a\n // bundle declaring `x-zab.capture/1` in `profiles[]` is compatible (it is\n // NOT rejected as BUNDLE_INCOMPATIBLE, §17.3.1).\n \"x-zab.capture/1\",\n]);\n\n// LSML 1.1 §17.5.1 + ADR 001 RC#14 — authoring-profile detection.\n//\n// An authoring profile is advisory : a runtime that does not support it\n// MUST NOT reject the bundle and renders the underlying primitives as if\n// the profile were absent. Detection matches the COMPLETE identifier form\n// `x-.authoring/` :\n//\n// - `x-` prefix, then one or more lowercase name segments separated by\n// dots, where `.authoring` is the EXACT TERMINAL segment before `/` ;\n// - `` is a bare integer (no `.minor`) ;\n// - never a substring test : a behavioural profile whose name merely\n// *contains* `.authoring` in a non-terminal position is NOT exempted\n// and keeps §17.3.1 hard-rejection semantics.\n//\n// Anti-ReDoS note : both regexes below are anchored and unambiguous —\n// the character classes exclude the `.` and `/` separators, so there is\n// exactly one possible parse per input (linear time, no backtracking).\nconst AUTHORING_NAME_RE = /^x-[a-z0-9-]+(?:\\.[a-z0-9-]+)*$/;\nconst AUTHORING_MAJOR_RE = /^(?:0|[1-9][0-9]*)$/;\nconst AUTHORING_SUFFIX = \".authoring\";\n\n/** True when `id` has the complete authoring-profile form\n * `x-.authoring/` (LSML 1.1 §17.5.1, ADR 001 RC#14).\n * Such profiles are advisory : ignored at render time, never rejected. */\nexport function isAuthoringProfile(id: string): boolean {\n const slash = id.indexOf(\"/\");\n if (slash < 0) return false;\n const name = id.slice(0, slash);\n const major = id.slice(slash + 1);\n if (!AUTHORING_MAJOR_RE.test(major)) return false;\n if (!name.endsWith(AUTHORING_SUFFIX)) return false;\n // `name` minus the terminal `.authoring` segment must still be a valid\n // `x-[....]` prefix — this is what makes `.authoring`\n // a real terminal segment rather than a substring of another one\n // (e.g. `x-evilauthoring/1` or `x-evil.authoring.fx/1` do not match).\n return AUTHORING_NAME_RE.test(name.slice(0, -AUTHORING_SUFFIX.length));\n}\n\nexport class BundleIncompatibleError extends Error {\n public readonly code = \"BUNDLE_INCOMPATIBLE\" as const;\n public readonly unsupportedProfiles: string[];\n constructor(unsupportedProfiles: string[]) {\n super(\n `BUNDLE_INCOMPATIBLE: profile(s) not supported by this runtime: ${unsupportedProfiles.join(\n \", \",\n )}`,\n );\n this.name = \"BundleIncompatibleError\";\n this.unsupportedProfiles = unsupportedProfiles;\n }\n}\n\n/** Validate a bundle's `profiles[]` against the runtime's supported\n * set. Throws `BundleIncompatibleError` listing every offending entry\n * when at least one behavioural profile is not supported.\n *\n * Authoring profiles (`x-.authoring/`, LSML 1.1 §17.5.1)\n * are advisory and skipped : their absence from the supported set is\n * never a rejection cause. Every other (behavioural) unsupported profile\n * keeps the hard §17.3.1 `BUNDLE_INCOMPATIBLE` rejection.\n *\n * Malformed-shape guard : `bundle` may come from an unchecked\n * `json as RenderBundle` cast on untrusted server JSON\n * (`FetcherImpl.get`). A non-array `profiles` or a non-string entry is\n * therefore reachable at runtime and is rejected as\n * `BundleIncompatibleError` (typed, code BUNDLE_INCOMPATIBLE) — never a\n * raw TypeError. The diagnostic never echoes the malformed value, only a\n * shape placeholder. */\nexport function validateBundleProfiles(\n bundle: { profiles?: string[] },\n supported: ReadonlySet = SUPPORTED_PROFILES,\n): void {\n const profiles: unknown = bundle.profiles;\n if (!profiles) return;\n if (!Array.isArray(profiles)) {\n throw new BundleIncompatibleError([\"\"]);\n }\n if (profiles.length === 0) return;\n const missing = profiles\n .filter((p) => typeof p !== \"string\" || (!isAuthoringProfile(p) && !supported.has(p)))\n .map((p) => (typeof p === \"string\" ? p : \"\"));\n if (missing.length > 0) {\n throw new BundleIncompatibleError(missing);\n }\n}\n\n// --- fetch + cache ---------------------------------------------------\n\nexport interface BundleFetcher {\n /** Fetch the bundle for a scene version. Cached forever by hash. */\n get(sceneId: string, sceneVersion: string): Promise;\n /** Inject a bundle directly — used by tests and for the \"scene already in\n * flight\" handoff path. */\n preload(bundle: RenderBundle): void;\n}\n\n/** Resolves the absolute URL of a scene's render bundle. Supplied by the\n * host (`MountOptions.resolveBundleUrl`) when the server is not at the\n * default host-root LSDP/1 layout — e.g. reached through a gateway prefix\n * (`https://gw/orion/api/v1/scenes/{id}/render-bundle?v={hash}`). */\nexport type BundleUrlResolver = (sceneId: string, sceneVersion: string) => string;\n\nexport interface BundleFetcherOptions {\n /** Base URL of the server. The fetcher constructs\n * `${baseUrl}/lsdp/v1/scenes/{id}/bundle?v={hash}`. Ignored when\n * `resolveUrl` is provided. */\n baseUrl: string;\n /** Path prefix for bundle resolution. Defaults to `/lsdp/v1/scenes`.\n * Ignored when `resolveUrl` is provided. */\n pathPrefix?: string;\n /** When set, takes full control of URL construction — the host owns the\n * whole URL (base, path prefix and `/bundle` vs `/render-bundle` suffix).\n * Lets a gateway-prefixed server be addressed without changing the\n * host-root default. */\n resolveUrl?: BundleUrlResolver;\n /** Resolve the bearer token used to authenticate each bundle GET. The\n * render-bundle endpoint is auth-gated identically to the LSDP/1 WS\n * subscription, so the fetch carries the same session token as\n * `Authorization: Bearer `. Resolved per request so a token swap\n * (`setToken`) takes effect on the next fetch ; a `LumencastTokenProvider`\n * is awaited. When omitted — or when it resolves to an empty/undefined\n * value — no `Authorization` header is sent (v0.5.0 behaviour). */\n getAuthToken?: () => string | undefined | Promise;\n fetchImpl?: typeof fetch;\n}\n\nclass FetcherImpl implements BundleFetcher {\n private readonly cache = new Map();\n private readonly inFlight = new Map>();\n private readonly baseUrl: string;\n private readonly pathPrefix: string;\n private readonly resolveUrl: BundleUrlResolver | undefined;\n private readonly getAuthToken: BundleFetcherOptions[\"getAuthToken\"];\n private readonly fetchImpl: typeof fetch;\n\n constructor(opts: BundleFetcherOptions) {\n this.baseUrl = opts.baseUrl.replace(/\\/$/, \"\");\n this.pathPrefix = (opts.pathPrefix ?? \"/lsdp/v1/scenes\").replace(/\\/$/, \"\");\n this.resolveUrl = opts.resolveUrl;\n this.getAuthToken = opts.getAuthToken;\n this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);\n }\n\n /** Build the request init carrying the bearer token, if any. Returns\n * `undefined` when no token is available — the fetch stays header-less,\n * preserving v0.5.0 behaviour. */\n private async buildInit(): Promise {\n if (!this.getAuthToken) return undefined;\n const token = await this.getAuthToken();\n if (!token) return undefined;\n return { headers: { Authorization: `Bearer ${token}` } };\n }\n\n private buildUrl(sceneId: string, sceneVersion: string): string {\n if (this.resolveUrl) {\n return this.resolveUrl(sceneId, sceneVersion);\n }\n return `${this.baseUrl}${this.pathPrefix}/${encodeURIComponent(sceneId)}/bundle?v=${encodeURIComponent(sceneVersion)}`;\n }\n\n preload(bundle: RenderBundle): void {\n // LSML 1.1 §17.3.1 — reject early if any declared profile is\n // unsupported by this runtime. Authors get an actionable error\n // instead of a silent rendering glitch.\n validateBundleProfiles(bundle);\n this.cache.set(bundle.scene_version, bundle);\n }\n\n async get(sceneId: string, sceneVersion: string): Promise {\n const cached = this.cache.get(sceneVersion);\n if (cached) return cached;\n // Bundles are content-addressed by scene_version, so a concurrent miss for\n // the same version can share a single fetch. The entry is cleared in\n // `finally` — a rejected fetch is never cached, so a later call retries.\n const existing = this.inFlight.get(sceneVersion);\n if (existing) return existing;\n const promise = this.fetchBundle(sceneId, sceneVersion).finally(() => {\n this.inFlight.delete(sceneVersion);\n });\n this.inFlight.set(sceneVersion, promise);\n return promise;\n }\n\n private async fetchBundle(sceneId: string, sceneVersion: string): Promise {\n const url = this.buildUrl(sceneId, sceneVersion);\n const init = await this.buildInit();\n const response = init ? await this.fetchImpl(url, init) : await this.fetchImpl(url);\n if (!response.ok) {\n throw new Error(`bundle fetch failed: ${response.status} ${response.statusText}`);\n }\n const json = (await response.json()) as RenderBundle;\n if (json.scene_version !== sceneVersion) {\n throw new Error(\n `bundle scene_version mismatch: expected ${sceneVersion}, got ${json.scene_version}`,\n );\n }\n validateBundleProfiles(json);\n this.cache.set(sceneVersion, json);\n return json;\n }\n}\n\nexport function createBundleFetcher(opts: BundleFetcherOptions): BundleFetcher {\n return new FetcherImpl(opts);\n}\n","// Exponential backoff schedule for the WS reconnect loop.\n//\n// Aggressive at first (200 ms, 400 ms, 800 ms…) then capped at 5 s so a\n// sustained outage doesn't hammer the gateway. Jittered to avoid thundering\n// herds when several Lumencast instances reconnect together (e.g. a CEF\n// host + a webview waking from suspend at the same time).\n//\n// The reference schedule in LSDP/1 §7 is \"0 ms, 500 ms, 1 s, 2 s, 4 s, 8 s,\n// 15 s, 30 s, 60 s cap\". This implementation defaults are tighter; both are\n// MAY-bounded by the spec.\n\nexport interface ReconnectScheduleOptions {\n /** First delay in milliseconds. */\n initial?: number;\n /** Maximum delay in milliseconds. */\n max?: number;\n /** Multiplicative factor between attempts (>= 1). */\n factor?: number;\n /** Jitter as a fraction of the delay (0 disables, 0.2 = ±20 %). */\n jitter?: number;\n /** Random source — only injected for tests. */\n random?: () => number;\n}\n\nconst DEFAULTS: Required> = {\n initial: 200,\n max: 5_000,\n factor: 2,\n jitter: 0.2,\n};\n\nexport interface ReconnectSchedule {\n /** Returns the delay to wait before the n-th attempt (1-indexed). */\n delayFor(attempt: number): number;\n /** Reset to attempt 1 (called on a successful connection). */\n reset(): void;\n /** Current attempt counter. */\n readonly attempt: number;\n}\n\nclass ScheduleImpl implements ReconnectSchedule {\n private _attempt = 0;\n constructor(\n private readonly opts: Required>,\n private readonly random: () => number,\n ) {}\n\n get attempt(): number {\n return this._attempt;\n }\n\n delayFor(attempt: number): number {\n if (!Number.isInteger(attempt) || attempt < 1) {\n throw new RangeError(`attempt must be a positive integer, got ${attempt}`);\n }\n this._attempt = attempt;\n const base = Math.min(\n this.opts.initial * Math.pow(this.opts.factor, attempt - 1),\n this.opts.max,\n );\n if (this.opts.jitter <= 0) return base;\n const offset = (this.random() * 2 - 1) * this.opts.jitter * base;\n return Math.max(0, base + offset);\n }\n\n reset(): void {\n this._attempt = 0;\n }\n}\n\nexport function createReconnectSchedule(opts: ReconnectScheduleOptions = {}): ReconnectSchedule {\n const merged = {\n initial: opts.initial ?? DEFAULTS.initial,\n max: opts.max ?? DEFAULTS.max,\n factor: opts.factor ?? DEFAULTS.factor,\n jitter: opts.jitter ?? DEFAULTS.jitter,\n };\n if (merged.initial <= 0) throw new RangeError(\"initial must be > 0\");\n if (merged.max < merged.initial) throw new RangeError(\"max must be >= initial\");\n if (merged.factor < 1) throw new RangeError(\"factor must be >= 1\");\n if (merged.jitter < 0 || merged.jitter > 1) throw new RangeError(\"jitter must be within [0, 1]\");\n return new ScheduleImpl(merged, opts.random ?? Math.random);\n}\n","// LSDP/1 WebSocket client.\n//\n// Lifecycle (LSDP/1 §6):\n// 1. open() — opens the WS with subprotocol `lsdp.v1`\n// 2. on open: send `subscribe` with the resolved token (and scene + session\n// for test mode)\n// 3. server replies `snapshot` (seq=1) → emit onSnapshot\n// 4. subsequent `delta` / `scene_changed` / `error` / `pong` are dispatched\n// 5. on a sequence gap → close + reconnect (fresh snapshot)\n// 6. on close → reconnect with backoff, unless close was triggered by close()\n// 7. setToken() opens a parallel WS with the new token; once its snapshot\n// lands, atomically swap and close the old socket — no rendering gap\n\nimport {\n decodeServerFrame,\n encodeFrame,\n LumencastError,\n SequenceTracker,\n WS_SUBPROTOCOL_V1_1,\n WS_SUBPROTOCOLS,\n input as inputFrame,\n subscribe as subscribeFrame,\n type DeltaFrame,\n type ErrorCode,\n type ErrorFrame,\n type Patch,\n type SceneChangedFrame,\n type SceneRosterFrame,\n type SnapshotFrame,\n} from \"@lumencast/protocol\";\nimport type { LumencastToken } from \"../types.js\";\nimport {\n createReconnectSchedule,\n type ReconnectSchedule,\n type ReconnectScheduleOptions,\n} from \"./reconnect.js\";\n\nexport type ConnectionStatus = \"disconnected\" | \"connecting\" | \"live\";\n\nexport interface WsClientOptions {\n url: string;\n token: LumencastToken;\n /** Optional scene identifier (test mode). */\n scene?: string;\n /** Optional session identifier (test mode). */\n session?: string;\n /** Override the WebSocket constructor (for tests / non-browser hosts). */\n webSocketImpl?: typeof WebSocket;\n /** Reconnect tuning. */\n reconnect?: ReconnectScheduleOptions;\n /** Inject scheduler for tests. */\n scheduler?: {\n setTimeout: typeof globalThis.setTimeout;\n clearTimeout: typeof globalThis.clearTimeout;\n };\n\n onStatus?: (status: ConnectionStatus) => void;\n onSnapshot?: (frame: SnapshotFrame) => void;\n onDelta?: (frame: DeltaFrame) => void;\n onSceneChanged?: (frame: SceneChangedFrame) => void;\n /** Out-of-band roster advertisement (LSDP/1.1 `scene_roster`, additive).\n * Carries no sequence — dispatched without touching the sequence tracker. */\n onSceneRoster?: (frame: SceneRosterFrame) => void;\n onServerError?: (frame: ErrorFrame) => void;\n /** Wire-level / codec / unrecoverable errors. */\n onTransportError?: (err: TransportError) => void;\n}\n\nexport class TransportError extends Error {\n public readonly recoverable: boolean;\n public readonly code: ErrorCode;\n public override readonly cause?: unknown;\n constructor(\n message: string,\n recoverable: boolean,\n code: ErrorCode = \"INTERNAL\",\n cause?: unknown,\n ) {\n super(message);\n this.name = \"TransportError\";\n this.recoverable = recoverable;\n this.code = code;\n this.cause = cause;\n }\n}\n\ntype Timer = ReturnType;\n\ninterface InternalScheduler {\n setTimeout: typeof globalThis.setTimeout;\n clearTimeout: typeof globalThis.clearTimeout;\n}\n\nexport class WsClient {\n private status: ConnectionStatus = \"disconnected\";\n private socket: WebSocket | null = null;\n private token: LumencastToken;\n private readonly url: string;\n private readonly WebSocketCtor: typeof WebSocket;\n private readonly schedule: ReconnectSchedule;\n private readonly seq = new SequenceTracker();\n private readonly opts: WsClientOptions;\n private readonly scheduler: InternalScheduler;\n\n private reconnectTimer: Timer | null = null;\n private active = true;\n\n constructor(opts: WsClientOptions) {\n this.opts = opts;\n this.url = opts.url;\n this.token = opts.token;\n const ctor = opts.webSocketImpl ?? globalThis.WebSocket;\n if (!ctor) {\n throw new TypeError(\n \"Lumencast WsClient: no WebSocket implementation found in this environment\",\n );\n }\n this.WebSocketCtor = ctor;\n this.schedule = createReconnectSchedule(opts.reconnect);\n this.scheduler = opts.scheduler ?? {\n setTimeout: globalThis.setTimeout.bind(globalThis),\n clearTimeout: globalThis.clearTimeout.bind(globalThis),\n };\n }\n\n /** Open and start the connection lifecycle. Idempotent. */\n start(): void {\n if (!this.active) return;\n if (this.socket || this.status === \"connecting\") return;\n void this.openSocket();\n }\n\n /** Resolve the current session token (the one used for the WS\n * subscription). Mirrors `setToken` swaps. Used by the bundle fetcher to\n * authenticate the render-bundle GET with the same credential. A\n * `LumencastTokenProvider` is awaited. */\n resolveCurrentToken(): Promise {\n return resolveToken(this.token);\n }\n\n /** Send `input` patches to the server. No-op if not connected. */\n sendInput(patches: Patch[]): void {\n if (!this.socket || this.socket.readyState !== this.WebSocketCtor.OPEN) return;\n if (patches.length === 0) return;\n this.socket.send(encodeFrame(inputFrame(patches)));\n }\n\n /** Replace the auth token. Closes and reopens with the new token. */\n setToken(token: LumencastToken): void {\n this.token = token;\n if (!this.active) return;\n if (this.socket) {\n this.closeSocket();\n this.scheduleReconnect(true);\n }\n }\n\n /** Tear down for good. No more reconnect attempts. */\n close(): void {\n if (!this.active) return;\n this.active = false;\n this.cancelReconnect();\n this.closeSocket();\n this.setStatus(\"disconnected\");\n }\n\n // --- internals --------------------------------------------------\n\n private async openSocket(): Promise {\n if (!this.active) return;\n this.setStatus(\"connecting\");\n\n let resolvedToken: string;\n try {\n resolvedToken = await resolveToken(this.token);\n } catch (err) {\n this.opts.onTransportError?.(\n new TransportError(\n `failed to resolve token: ${(err as Error).message}`,\n true,\n \"AUTH_DENIED\",\n err,\n ),\n );\n this.scheduleReconnect();\n return;\n }\n if (!this.active) return;\n\n let socket: WebSocket;\n try {\n // Advertise both 1.1 (preferred) and 1.0 (fallback) ; the server\n // picks one. Spread to a mutable array — the WebSocket constructor\n // type expects string[].\n socket = new this.WebSocketCtor(this.url, [...WS_SUBPROTOCOLS]);\n } catch (err) {\n this.opts.onTransportError?.(\n new TransportError(\n `failed to open WebSocket: ${(err as Error).message}`,\n true,\n \"INTERNAL\",\n err,\n ),\n );\n this.scheduleReconnect();\n return;\n }\n\n this.socket = socket;\n socket.onopen = () => this.handleOpen(resolvedToken);\n socket.onmessage = (event) => this.handleMessage(event);\n socket.onerror = (event) => this.handleError(event);\n socket.onclose = (event) => this.handleClose(event);\n }\n\n private handleOpen(token: string): void {\n if (!this.socket) return;\n // LSDP/1.1 §4.1, §18 — if we have a previously-observed seq AND the\n // negotiated subprotocol is 1.1, request an incremental resume.\n // The server will EITHER ship buffered deltas (cache stays valid)\n // OR a fresh snapshot which we rebase via observeSnapshot.\n const subprotocol = this.socket.protocol;\n const canResume = subprotocol === WS_SUBPROTOCOL_V1_1 && this.seq.last > 0;\n const sinceSequence = canResume ? this.seq.last : undefined;\n if (!canResume) {\n // Fresh subscription (no resume) — reset the tracker baseline.\n this.seq.reset();\n }\n const frame = subscribeFrame({\n token,\n ...(this.opts.scene !== undefined ? { scene: this.opts.scene } : {}),\n ...(this.opts.session !== undefined ? { session: this.opts.session } : {}),\n ...(sinceSequence !== undefined ? { since_sequence: sinceSequence } : {}),\n });\n this.socket.send(encodeFrame(frame));\n }\n\n private handleMessage(event: MessageEvent): void {\n const data = typeof event.data === \"string\" ? event.data : \"\";\n if (!data) return;\n let frame;\n try {\n frame = decodeServerFrame(data);\n } catch (err) {\n const message = err instanceof LumencastError ? err.message : (err as Error).message;\n const code: ErrorCode = err instanceof LumencastError ? err.code : \"INTERNAL\";\n this.opts.onTransportError?.(new TransportError(`codec: ${message}`, true, code, err));\n this.closeSocket();\n this.scheduleReconnect();\n return;\n }\n if (frame === null) return; // unknown frame type — forward-compat ignore\n\n switch (frame.type) {\n case \"snapshot\": {\n // LSDP/1.1 §18.1.1 — snapshot rebases the tracker to its seq\n // value. This is the ONLY valid way to set or change the\n // tracker's baseline (handles fresh sub, scene_changed, and\n // back-pressure recovery uniformly).\n if (frame.seq < 1) {\n this.opts.onTransportError?.(\n new TransportError(`snapshot seq must be >= 1, got ${frame.seq}`, true, \"VERSION_GAP\"),\n );\n this.closeSocket();\n this.scheduleReconnect();\n return;\n }\n this.seq.observeSnapshot(frame.seq);\n this.schedule.reset();\n this.setStatus(\"live\");\n this.opts.onSnapshot?.(frame);\n return;\n }\n case \"delta\": {\n const obs = this.seq.observe(frame.seq);\n if (obs.kind === \"gap\") {\n this.opts.onTransportError?.(\n new TransportError(\n `sequence gap: expected ${this.seq.last + 1}, got ${frame.seq}`,\n true,\n \"VERSION_GAP\",\n ),\n );\n this.closeSocket();\n this.scheduleReconnect();\n return;\n }\n if (obs.kind === \"duplicate\") return; // silent drop per LSDP/1 §5\n this.opts.onDelta?.(frame);\n return;\n }\n case \"scene_changed\": {\n // The next snapshot will rebase the tracker via observeSnapshot.\n // Reset here so the tracker doesn't fault on the SceneChanged's\n // own seq (which advances prev's counter one final step).\n this.seq.reset();\n this.opts.onSceneChanged?.(frame);\n return;\n }\n case \"error\": {\n this.opts.onServerError?.(frame);\n if (!frame.recoverable) this.close();\n return;\n }\n case \"scene_roster\": {\n // Out-of-band roster metadata: NOT part of the snapshot/delta stream.\n // It carries no seq and MUST NOT touch the sequence tracker — forward\n // it verbatim so the host can warm its render-bundle cache.\n this.opts.onSceneRoster?.(frame);\n return;\n }\n case \"pong\":\n return;\n }\n }\n\n private handleError(_event: Event): void {\n // The browser does not give us a real reason — `close` will follow.\n }\n\n private handleClose(event: CloseEvent): void {\n this.socket = null;\n if (!this.active) {\n this.setStatus(\"disconnected\");\n return;\n }\n if (event.code === 4401 || event.code === 4403 || event.code === 1008) {\n // Auth-related close codes: not recoverable without operator intervention.\n this.opts.onTransportError?.(\n new TransportError(`server closed: ${event.code} ${event.reason}`, false, \"AUTH_DENIED\"),\n );\n this.close();\n return;\n }\n this.scheduleReconnect();\n }\n\n private scheduleReconnect(immediate = false): void {\n if (!this.active) return;\n this.cancelReconnect();\n const attempt = (this.schedule.attempt || 0) + 1;\n const delay = immediate ? 0 : this.schedule.delayFor(attempt);\n this.setStatus(\"disconnected\");\n this.reconnectTimer = this.scheduler.setTimeout(() => {\n this.reconnectTimer = null;\n void this.openSocket();\n }, delay);\n }\n\n private cancelReconnect(): void {\n if (this.reconnectTimer) {\n this.scheduler.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n private closeSocket(): void {\n if (this.socket) {\n try {\n this.socket.close(1000, \"client closing\");\n } catch {\n // ignore\n }\n this.socket = null;\n }\n }\n\n private setStatus(next: ConnectionStatus): void {\n if (this.status === next) return;\n this.status = next;\n this.opts.onStatus?.(next);\n }\n}\n\nasync function resolveToken(token: LumencastToken): Promise {\n if (typeof token === \"string\") return token;\n return await token.fetch();\n}\n","import type { MountOptions } from \"../types\";\n\n/** Throws on invalid mount options. Exposed separately so unit tests\n * can exercise it without mounting a real React root. */\nexport function validateOptions(options: MountOptions): void {\n if (!(options.target instanceof HTMLElement)) {\n throw new TypeError(\"mount: `target` must be an HTMLElement\");\n }\n if (typeof options.serverUrl !== \"string\" || options.serverUrl.length === 0) {\n throw new TypeError(\"mount: `serverUrl` must be a non-empty string\");\n }\n if (options.mode === \"test\") {\n if (!options.testSession) {\n throw new TypeError(\"mount: `testSession` is required when mode === 'test'\");\n }\n if (!options.scene) {\n throw new TypeError(\"mount: `scene` is required when mode === 'test'\");\n }\n }\n}\n","// Render-side host-allowlist context (LSML 1.2, ADR 002 #E + #F ; Bastion T1/T2).\n//\n// CANONICAL host-gate module for the runtime. There is exactly ONE\n// `AllowedHostsProvider` and ONE allowlist context for the whole render\n// tree. The bundle's `assets.allowedHosts` rides this context from the\n// render root down to every consumer that places an untrusted asset URL\n// into the DOM :\n//\n// - image-fill `src` on frame / shape backgrounds (#F, via `gateImageFills`\n// / `gateSrc` in `fill.tsx`),\n// - the `` of the `image` primitive (#F — closes the latent 1.1\n// hole where `image.tsx` placed `src` with no host check at all),\n// - a `mask.source`-image `href` (#E, via `checkHostAllowed` in\n// `mask.tsx`, which reads the allowlist off this same context through\n// `tree.tsx`).\n//\n// The underlying decision is ALWAYS delegated to `@lumencast/protocol`'s\n// `checkHostAllowed` / `isHostAllowed` (the #C foundation, single source of\n// truth for host + scheme matching). This module never re-implements that\n// logic ; it only threads the allowlist and adapts it for each call-site.\n//\n// Deny-by-default : a consumer rendered outside any provider, or one whose\n// bundle declares no `allowedHosts`, sees `undefined` — which\n// `checkHostAllowed` treats as \"no allowlist → reject every remote host\".\n// There is no path by which a missing provider silently re-opens the gate.\n//\n// The context value is a read-only, mount-stable `string[] | undefined`\n// (the allowlist is part of the content-addressed bundle), so a plain React\n// context is the right tool.\n\nimport { createContext, useContext, type ReactNode } from \"react\";\nimport { checkHostAllowed } from \"@lumencast/protocol\";\nimport { emitDiagnostic } from \"./diagnostics\";\n\nconst AllowedHostsCtx = createContext(undefined);\nexport type HostAssetPolicy = (url: string) => boolean;\nconst HostAssetCtx = createContext(undefined);\n\n/**\n * Provide the bundle's host allowlist to the render subtree. Mounted ONCE at\n * the render root by each mode (broadcast / control / test), wrapping\n * ``. The value should come from {@link readAllowedHosts} so the\n * legacy `Asset[]` and the LSML 1.2 object `assets` shapes are both handled\n * and non-string entries are dropped.\n *\n * Prop name is `hosts` (the canonical render-side spelling, #F).\n */\nexport function AllowedHostsProvider({\n hosts,\n isHostAssetUrl,\n children,\n}: {\n hosts: readonly string[] | undefined;\n isHostAssetUrl?: HostAssetPolicy;\n children: ReactNode;\n}) {\n return (\n \n {children}\n \n );\n}\n\n/** Read the active host allowlist. `undefined` when no provider is mounted —\n * which `checkHostAllowed` treats as deny-by-default (never a passthrough). */\nexport function useAllowedHosts(): readonly string[] | undefined {\n return useContext(AllowedHostsCtx);\n}\n\nexport function useHostAssetPolicy(): HostAssetPolicy | undefined {\n return useContext(HostAssetCtx);\n}\n\n/** Read `assets.allowedHosts` defensively off a render bundle, for the mode\n * that provides the context. The LSML 1.2 compiler forwards `assets` in the\n * object form `{ allowedHosts?: string[] }` (see `RenderBundle.assets`) ;\n * a legacy bundle may still carry the old `Asset[]` form. Either way we\n * extract a `string[]` of hostnames, or `undefined` (deny-by-default). A\n * non-string entry is dropped — it can never match `new URL().hostname`. */\nexport function readAllowedHosts(bundle: { assets?: unknown }): readonly string[] | undefined {\n const assets = bundle.assets as { allowedHosts?: unknown } | undefined;\n const list = assets?.allowedHosts;\n if (!Array.isArray(list)) return undefined;\n const hosts = list.filter((h): h is string => typeof h === \"string\");\n return hosts.length > 0 ? hosts : undefined;\n}\n\n/**\n * Gate an asset `src` against the host + scheme allowlist BEFORE it reaches\n * the DOM (Bastion T1/T2). Returns the `src` unchanged when allowed, or\n * `undefined` when rejected — in which case the caller MUST omit the asset\n * (never passthrough). On rejection an R9-clean diagnostic is emitted : it\n * carries only `{ nodeId, field, reason }`, never the URL itself.\n *\n * The decision is delegated to `checkHostAllowed` and is deny-by-default :\n * an absent / empty `allowedHosts` rejects every remote host.\n * `undefined`/non-string/empty `src` resolves to `undefined` (absent asset)\n * WITHOUT a diagnostic — that is a primitive with no source, not a rejected\n * one.\n */\nexport function gateSrc(\n src: unknown,\n allowedHosts: readonly string[] | undefined,\n field: string,\n nodeId?: string,\n isHostAssetUrl?: HostAssetPolicy,\n): string | undefined {\n if (typeof src !== \"string\" || src.length === 0) return undefined;\n // The host must match an exact URL it minted from authenticated raster bytes.\n // Never allow an arbitrary blob: supplied by an untrusted scene or delta.\n if (src.length <= 2048 && src.startsWith(\"blob:\") && isHostAssetUrl?.(src) === true) return src;\n const decision = checkHostAllowed(src, allowedHosts);\n if (decision.allowed) return src;\n emitDiagnostic(nodeId, field, decision.reason ?? \"asset host/scheme rejected\");\n return undefined;\n}\n","// Optional, bounded image decoding for bundles already warmed by a scene\n// roster. This never runs in the headless renderer and never fetches a URL\n// that the ordinary render-side host gate would reject.\n\nimport { isHostAllowed } from \"@lumencast/protocol\";\nimport type { RenderBundle, RenderNode } from \"./bundle\";\nimport { readAllowedHosts } from \"./allowed-hosts\";\n\nconst MAX_NODES = 4096;\nconst MAX_IMAGES_PER_BUNDLE = 4;\nconst MAX_RETAINED_IMAGES = 12;\nconst MAX_RETAINED_PIXELS = 12_000_000;\n\nexport interface RosterImagePreloadResult {\n candidates: number;\n decoded: number;\n notReady: number;\n}\n\n/** Bundle-authored raster sources only. A bound `src` can have an authored\n * fallback that the first snapshot still uses; a later live value is never\n * guessed or fetched here. */\nfunction imageSources(bundle: RenderBundle): string[] {\n const allowedHosts = readAllowedHosts(bundle);\n const found = new Set();\n const stack: RenderNode[] = bundle.root && typeof bundle.root === \"object\" ? [bundle.root] : [];\n let visited = 0;\n\n const add = (value: unknown): void => {\n if (found.size >= MAX_IMAGES_PER_BUNDLE || typeof value !== \"string\") return;\n // Inline data images already have their bytes. The protocol gate below\n // retains the exact scheme + host policy used by the render path.\n if (!/^https:\\/\\//i.test(value) || !isHostAllowed(value, allowedHosts)) return;\n found.add(value);\n };\n\n while (stack.length > 0 && visited < MAX_NODES && found.size < MAX_IMAGES_PER_BUNDLE) {\n const node = stack.pop()!;\n visited += 1;\n if (!node || typeof node !== \"object\") continue;\n const props = node.props;\n if (props) {\n if (node.kind === \"image\") add(props.src);\n const fillKey =\n node.kind === \"shape\" ? \"fills\" : node.kind === \"frame\" ? \"backgrounds\" : null;\n if (fillKey && Array.isArray(props[fillKey])) {\n for (const fill of props[fillKey]) {\n if (fill && typeof fill === \"object\" && fill.kind === \"image\") add(fill.src);\n }\n }\n const mask = props.mask;\n if (mask && typeof mask === \"object\") {\n const typed = mask as {\n type?: unknown;\n op?: unknown;\n source?: { kind?: unknown; src?: unknown };\n };\n if (\n (typed.type === \"alpha\" || typed.type === \"luminance\") &&\n (typed.op === \"intersect\" || typed.op === \"subtract\" || typed.op === \"union\") &&\n typed.source?.kind === \"image\"\n ) {\n add(typed.source.src);\n }\n }\n }\n if (Array.isArray(node.children)) {\n for (let index = node.children.length - 1; index >= 0; index -= 1) {\n const child = node.children[index];\n if (child && typeof child === \"object\") stack.push(child);\n }\n }\n }\n return [...found];\n}\n\n/** A mount-local cache, so disconnect releases decoded image references.\n * Even when the roster is large, it starts at most 12 image loads and retains\n * no more than 12 MP of successful decodes. A switch never waits on it. */\nexport function createRosterImagePreloader(): {\n warm: (bundle: RenderBundle) => Promise;\n clear: () => void;\n} {\n const retained = new Map();\n const inFlight = new Map>();\n let retainedPixels = 0;\n let active = true;\n\n async function decode(url: string): Promise {\n if (retained.has(url)) return true;\n const existing = inFlight.get(url);\n if (existing) return existing;\n if (!active || retained.size + inFlight.size >= MAX_RETAINED_IMAGES) return false;\n\n const pending = (async () => {\n try {\n const image = new Image();\n image.decoding = \"async\";\n if (typeof image.decode === \"function\") {\n image.src = url;\n await image.decode();\n } else {\n await new Promise((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = () => reject(new Error(\"image preload failed\"));\n image.src = url;\n });\n }\n const pixels = image.naturalWidth * image.naturalHeight;\n if (\n !active ||\n !Number.isFinite(pixels) ||\n pixels <= 0 ||\n pixels + retainedPixels > MAX_RETAINED_PIXELS\n )\n return false;\n retained.set(url, image);\n retainedPixels += pixels;\n return true;\n } catch {\n // Best-effort only: the actual scene retains its normal image path.\n return false;\n }\n })();\n inFlight.set(url, pending);\n try {\n return await pending;\n } finally {\n inFlight.delete(url);\n }\n }\n\n return {\n async warm(bundle) {\n const urls = imageSources(bundle);\n const outcomes = await Promise.all(urls.map(decode));\n const decoded = outcomes.filter(Boolean).length;\n return { candidates: urls.length, decoded, notReady: urls.length - decoded };\n },\n clear() {\n active = false;\n retained.clear();\n inFlight.clear();\n retainedPixels = 0;\n },\n };\n}\n","// Public mount() entry — the only surface a host (browser, CEF, OBS plugin,\n// iframe) interacts with. Lifecycle and contract: see RUNTIME-API.md.\n\nimport { batch, signal } from \"@preact/signals-react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { flushSync } from \"react-dom\";\nimport { createElement } from \"react\";\nimport { LumencastApp } from \"./app.js\";\nimport { applyDelta } from \"./state/apply-delta.js\";\nimport { applySnapshot } from \"./state/apply-snapshot.js\";\nimport { createReservedLeafObserver } from \"./state/reserved-leaves.js\";\nimport { createStore } from \"./state/store.js\";\nimport { createBundleFetcher, type BundleFetcher, type RenderBundle } from \"./render/bundle.js\";\nimport { WsClient, type ConnectionStatus, type TransportError } from \"./transport/ws.js\";\nimport { validateOptions } from \"./internal/validate-options.js\";\nimport { addDiagnosticsHandler } from \"./render/diagnostics.js\";\nimport { createRosterImagePreloader } from \"./render/roster-image-preload.js\";\nimport type { LumencastError, LumencastHandle, LumencastToken, MountOptions } from \"./types.js\";\n\nexport function mount(options: MountOptions): LumencastHandle {\n validateOptions(options);\n options.onStatus?.(\"disconnected\");\n\n let store = createStore();\n const previewStoreSignal = options.realtimeDeltas ? signal(store) : undefined;\n const baseUrl = deriveBaseUrl(options.serverUrl);\n // The render-bundle endpoint is auth-gated like the LSDP/1 WS. Resolve the\n // current session token per fetch (mirrors `setToken`) so each bundle GET\n // carries `Authorization: Bearer `. `ws` is assigned below; the\n // closure runs only at fetch time, after assignment.\n const bundleFetcher = createBundleFetcher({\n baseUrl,\n ...(options.resolveBundleUrl !== undefined ? { resolveUrl: options.resolveBundleUrl } : {}),\n getAuthToken: () => ws.resolveCurrentToken(),\n });\n\n const bundleSignal = signal(null);\n const statusSignal = signal(\"disconnected\");\n const crossfadeKeySignal = signal(\"__initial__\");\n const commitSignal = signal<{ sceneId: string; sceneVersion: string } | null>(null);\n let snapshotEpoch = 0;\n let pendingStore: ReturnType | null = null;\n\n const setStatus = (status: ConnectionStatus): void => {\n statusSignal.value = status;\n options.onStatus?.(status);\n };\n\n const reportError = (err: LumencastError): void => {\n options.onError?.(err);\n };\n\n let active = true;\n\n // Render-bundle versions already warmed (or warming) by the preload path.\n // Keeps both roster sources (the `scene_roster` frame and the `preloadRoster`\n // mount option) idempotent — a version is fetched by the warmer at most once.\n const warmedVersions = new Set();\n const rosterImages = options.preloadRosterImages === true ? createRosterImagePreloader() : null;\n\n // ADR Blue 009 §3.2–3.3 — surface the reserved `__cam.*` LSDP leaves (the\n // slot→peer assignments + the receive-only viewer creds) to the host so its\n // WebRTC viewer (Solar) can drive room joins + `x-zab.meet-peer` slot re-keying.\n // The runtime never joins, holds creds, or re-keys ; it only forwards. Created\n // only when the host opts in — zero cost on the preview/headless paths.\n const reservedLeaves = options.onReservedLeaves\n ? createReservedLeafObserver(options.onReservedLeaves)\n : undefined;\n\n // ADR 001 §3.4 (issue #34) — anti-silent-drop diagnostics are events\n // surfaced to the host, never console logs in `broadcast` mode.\n const removeDiagnosticsHandler = options.onDiagnostic\n ? addDiagnosticsHandler(options.onDiagnostic)\n : undefined;\n\n const ws = new WsClient({\n url: options.serverUrl,\n token: options.token,\n ...(options.webSocketImpl !== undefined ? { webSocketImpl: options.webSocketImpl } : {}),\n ...(options.scene !== undefined ? { scene: options.scene } : {}),\n ...(options.testSession !== undefined ? { session: options.testSession } : {}),\n onStatus: setStatus,\n onSnapshot: (frame) => {\n if (!active) return;\n const epoch = ++snapshotEpoch;\n const incoming = createStore();\n applySnapshot(incoming, frame);\n pendingStore = incoming;\n void onSnapshot(\n bundleFetcher,\n bundleSignal,\n crossfadeKeySignal,\n frame.scene_id,\n frame.scene_version,\n () => {\n const state = incoming.toRecord();\n if (\n previewStoreSignal &&\n crossfadeKeySignal.value !== `${frame.scene_id}::${frame.scene_version}`\n ) {\n // A Preview crossfade keeps the outgoing tree mounted briefly.\n // Give the incoming tree its own leaf store rather than resetting\n // the store still read by that outgoing tree. This is transition\n // ownership, not a cache: no scene store is retained for reuse.\n store = incoming;\n previewStoreSignal.value = store;\n } else {\n store.reset(state);\n }\n reservedLeaves?.onSnapshot(state);\n pendingStore = null;\n },\n reportError,\n epoch,\n );\n options.onMetric?.({\n name: \"snapshot_received\",\n scene_id: frame.scene_id,\n path_count: Object.keys(frame.state).length,\n });\n },\n onDelta: (frame) => {\n if (!active) return;\n // Do not apply incoming leaves to the outgoing tree while its bundle\n // loads. Staging in a separate store preserves the latest values.\n if (pendingStore) {\n applyDelta(pendingStore, frame);\n return;\n }\n const start = performance.now();\n applyDelta(store, frame);\n reservedLeaves?.onDelta(frame.patches);\n options.onMetric?.({\n name: \"delta_applied\",\n duration_ms: performance.now() - start,\n });\n options.onMetric?.({ name: \"delta_received\", count: 1, path_count: frame.patches.length });\n },\n onSceneChanged: (frame) => {\n if (!active) return;\n ++snapshotEpoch;\n pendingStore = createStore();\n // The fresh snapshot that follows is the source of truth — it carries\n // the new scene_version, drives the bundle fetch, and flips the\n // crossfade key. Nothing eager to do here.\n options.onMetric?.({\n name: \"scene_changed\",\n from: bundleSignal.value?.scene_version ?? null,\n to: frame.scene_version,\n });\n },\n onSceneRoster: (frame) => {\n if (!active) return;\n // The server advertised the show roster (LSDP/1.1 `scene_roster`).\n // Warm every scene's render bundle in the background so the first switch\n // to each is a cache hit, not a blocking fetch.\n warmRoster(frame.entries, \"frame\");\n },\n onServerError: (frame) => {\n reportError({\n code: frame.code,\n message: frame.message,\n recoverable: frame.recoverable,\n });\n },\n onTransportError: (err) => {\n reportError(transportToLumencastError(err));\n },\n });\n\n ws.start();\n\n // Public preload surface (#87b) — warm a host-supplied roster right after\n // mount, before any switch. Same warmer + cache as the `scene_roster` frame.\n if (options.preloadRoster !== undefined && options.preloadRoster.length > 0) {\n warmRoster(options.preloadRoster, \"option\");\n }\n\n const root: Root = createRoot(options.target);\n root.render(\n createElement(LumencastApp, {\n mode: options.mode,\n store,\n ...(previewStoreSignal ? { previewStoreSignal } : {}),\n bundleSignal,\n statusSignal,\n crossfadeKeySignal,\n commitSignal,\n ...(options.sceneTransition !== undefined\n ? { sceneTransition: options.sceneTransition }\n : {}),\n ...(options.onSceneCommit !== undefined ? { onSceneCommit: options.onSceneCommit } : {}),\n sendInput: (patches) => ws.sendInput(patches),\n ...(options.isHostAssetUrl !== undefined ? { isHostAssetUrl: options.isHostAssetUrl } : {}),\n // ADR 004 §A1.3 — thread the host capture resolver to the runtime context\n // so the `x-zab.capture` primitive's ACQUIRE mode can pin a device.\n ...(options.resolveCaptureDevice !== undefined\n ? { resolveCaptureDevice: options.resolveCaptureDevice }\n : {}),\n // ADR 006 #4 — thread the host peer-stream resolver (supplied by the\n // WebRTC viewer #3) so the `media` primitive's LIVE mode can render a\n // peer's MediaStream in `srcObject`.\n ...(options.resolvePeerStream !== undefined\n ? { resolvePeerStream: options.resolvePeerStream }\n : {}),\n // ADR 006 #3 — reactive variant : the LIVE `media` node re-renders when a\n // peer connects/leaves mid-show. `createPeerViewer()` supplies it.\n ...(options.subscribePeerStream !== undefined\n ? { subscribePeerStream: options.subscribePeerStream }\n : {}),\n // Un-mute LIVE peer `