Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions .file-size-exceptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
7 changes: 6 additions & 1 deletion .github/workflows/file-sizes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 18 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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" \
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ Thumbs.db
.idea

# Build caches
/build/
*.tsbuildinfo
__pycache__/
*.py[cod]

# Test artefacts
coverage
Expand Down
19 changes: 19 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -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/
241 changes: 105 additions & 136 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 `<Crossfade>`
- **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 -- <path-or-symbol>` and `npm run code:find -- <query>`.
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.
Loading
Loading