Runtime

Browser runtime and client graphs

The browser does not get a second implementation of LZR. It gets the same render core, compiled ahead of time into a client-graph package and executed by a wasm request engine that a ServiceWorker owns. This page documents that boundary: what the packager emits, what the format guarantees, which operations the engine accepts, what the ServiceWorker adds, and — the part most runtime documentation skips — exactly what the browser cannot do. Every artifact quoted below is this site's own build, and two tables are read live from the graph.

Three artifacts, one authority

Nothing in the browser discovers anything. The graph decides, at build time, what a browser is allowed to render, and hands it over as bytes:

The browser-runtime chain

Each row is a real artifact of this project; the recipes are in src/rendered/public/_hypermake/runtime/client-graphs/ and src/rendered/public/assets/.

StageArtifactProduced by
Packagepublic://_hypermake/runtime/client-graphs/!owner.hmcpack2hypermake client-graph package
Enginepublic://assets/hypermake-request-engine.wasm + wasm_exec.jsGo js/wasm build of cmd/hypermake-request-engine-wasm
Hostpublic://assets/lzr-runtime-sw.js (ServiceWorker) and lzr-runtime.js (window)runtime/browser/, published by graph-owned copy recipes

The engine executes; it never fetches. The ServiceWorker fetches, stores, and mutates; it never renders LZR itself. Two flags stamped into every package make that non-negotiable — this site's carries fakeBackendForbidden: true and pageLocalDOMPlannerForbidden: true: neither a stub backend nor a page-local DOM planner may stand in for the real graph.

Packaging a client graph

A client graph is bang-owned: a directory src/!name/ declares a route family that the packager compiles into one package. This site owns three — an admin vertical, a local-settings vertical, and the playground:

This site's playground package recipe src/rendered/public/_hypermake/runtime/client-graphs/tape-playground-bang.json.make
public://_hypermake/runtime/client-graphs/!tape-playground.hmcpack2: src/!tape-playground/*.html src/_components/*.html config.json data/tape-mini/a.json data/tape-mini/labels.json ../../bin/hypermake | rendered/public/_hypermake/runtime/client-graphs/
	../../bin/hypermake client-graph package --root . --owner !tape-playground --bootstrap index.html --out "$@"
		

One command writes a whole artifact family, not one file. The route index (!owner.route-index.json) enumerates it, each entry carrying both identities described below. Read live from this build:

Artifacts emitted for one owner

File names under /_hypermake/runtime/client-graphs/, rendered from that owner's route-index.json at build time — this table cannot drift from the packager.

KindFileScopeSchema
canonical-owner!tape-playground.hmcpack2fullhypermake.client-graph-package.v2
debug-owner-json!tape-playground.debug.jsonderivedhypermake.client-graph-package.v1
compatibility-owner-hmcpack2!tape-playground.compat.hmcpack2derivedhypermake.client-graph-package.v1
canonical-route!tape-playground.route.tape-playground-card.hmcpack2routehypermake.client-graph-package.v2
compatibility-route-hmcpack2!tape-playground.route.tape-playground-card.compat.hmcpack2derivedhypermake.client-graph-package.v1
debug-route-json!tape-playground.route.tape-playground-card.debug.jsonderivedhypermake.client-graph-package.v1
compatibility-route-json!tape-playground.route.tape-playground-card.jsonderivedhypermake.client-graph-package.v1
canonical-route!tape-playground.route.tape-playground-compact.hmcpack2routehypermake.client-graph-package.v2
compatibility-route-hmcpack2!tape-playground.route.tape-playground-compact.compat.hmcpack2derivedhypermake.client-graph-package.v1
debug-route-json!tape-playground.route.tape-playground-compact.debug.jsonderivedhypermake.client-graph-package.v1
compatibility-route-json!tape-playground.route.tape-playground-compact.jsonderivedhypermake.client-graph-package.v1
compatibility-json!tape-playground.jsonderivedhypermake.client-graph-package.v1

Three tiers. canonical-owner is the production artifact — typed HMCPACK2, schema hypermake.client-graph-package.v2. The compatibility-* and debug-* rows are derived projections at schema …v1: the .json sibling is a readable compatibility form, the .debug.json an explicitly non-executable projection, carrying "executable": false and its own schema hypermake.client-graph-debug-projection.v1. The *.route.* rows are per-route shards, so one route installs without its siblings.

The route manifest

Owners do not collide by convention; a generated manifest maps public routes to owners and pins each package by digest. This is read live from this build:

public://_hypermake/runtime/client-graphs/manifest.json

Schema hypermake.client-graph-route-manifest.v2. graphSetId covers the whole set, so a partial update is detectable.

OwnerCanonical artifactArtifact digest
!admin!admin.hmcpack2667f9f7ae2572cf2…
!local!local.hmcpack210cd41f87b2310bd…
!tape-playground!tape-playground.hmcpack24f226412d35d6b99…

And the route ownership it publishes: /admin/dashboard.html!admin · /admin/summary.html!admin · /local/settings.html!local · /tape-playground/card.html!tape-playground · /tape-playground/compact.html!tape-playground ·

Two identities: meaning and bytes

A package carries a semantic identity inside itself — owner, scope (full or route), route id, public route, and the graphDigest of the graph it was compiled from. It cannot carry its own artifact identity, because no byte artifact can contain the SHA-256 of itself. That digest lives outside, in the manifest and route index, and the loader verifies it before installation. Verify it by hand:

Artifact digest, checked against the manifest shell
sha256sum rendered/public/_hypermake/runtime/client-graphs/!tape-playground.hmcpack2
# d4de1350a3dbdfb844b9063018afaa6b1ab9faf61fc79482d8731c96eae78b02
# — the same value this page prints in the manifest table above
		

The result is a load ladder with no soft failures: schema, package format, ABI, semantic-identity well-formedness, expectation match, then byte digest — each mismatch a stable refusal code, listed under the engine surface.

HMCPACK2, the package format

The canonical artifact is not JSON. internal/clientgraph/hmcpack2 encodes a JSON-shaped value into a typed binary container: the magic HMCPACK2T\n, then interned tables of deterministic strings and object schemas, then typed values (null, false, true, i32, u32, f64, string, array, object). Verified header of this site's package:

First bytes of a real package shell
head -c 40 rendered/public/_hypermake/runtime/client-graphs/!tape-playground.hmcpack2 | od -c
# 0000000   H   M   C   P   A   C   K   2   T  \n 353 001  \0  \0 …
# 0000020  \0  \0  \v  \0  \0  \0   a   c   t   i   o   n   P   a   c   k
		

Decoding is bounded on every axis — total bytes (64 MiB), table entries, collection sizes, nesting depth (512), reference validity, and trailing data — and every failure has a stable code: magic_invalid, truncated, limit_exceeded, reference_invalid, tag_unknown, trailing_bytes. The envelope's own top-level fields are fixed (identity, engine contract, capabilities, bootstrap, data stores, sections, compiled artifacts, the two forbidden-fallback flags); domain payloads inside stay JSON-shaped because the graph compiler owns their types.

The compatibility .json form exposes the same content as digested sections — eight in this site's playground package: header, routes, data-stores, render-streams, bindings-and-mutations, protocol-resources, client-extension-packages, debug-source-projection. Each carries its own byteLength and digest, which is what makes partial verification and route sharding possible.

The compiled render stream

A packaged route does not carry a template. It carries a lowered stream (lzr.stream.v1) of typed ops plus side tables — static runs, dynamic tokens, shape gates, scopes, expression programs, dependency programs, collection manifests, frame calls. The op vocabulary is eight kinds, and that is the whole executor contract:

Stream op kinds

internal/lzr/ir/ir.go. There is no component op, no include op, and no parse op — see the note below.

OpWhat it does
emit_static_runEmit a precompiled literal run of markup by id.
emit_dynamic_tokenEvaluate an expression program and emit its value, with the escape context (html_text / html_attr) fixed at compile time.
shape_gateBranch or iterate: the gate's shape program decides an if outcome or a collection manifest.
set_varBind a name from an expression program (the lowered <set>).
enter_scope / exit_scopePush and pop a scope frame by id.
frame_callInvoke a <frame> by its compiled call record.
checkpointA named resumption point for fragment-level patching.

Verified op census of this site's /tape-playground/card.html route — a template with a <set> pair, an <if>/<else>, a <foreach>, and two component calls:

Op counts from the packaged route shell
jq '.projection.compiledArtifacts.renderRoutes[0].renderStream.ops
    | group_by(.kind) | map({(.[0].kind): length}) | add' \
  rendered/public/_hypermake/runtime/client-graphs/!tape-playground.route.tape-playground-card.debug.json
# {"checkpoint":3,"emit_dynamic_token":7,"emit_static_run":15,"set_var":2,"shape_gate":2}
		

Each route also carries bindings: rows of {key, dataRoot, pointer, exprProgramId, dynamicTokenIds, targets} connecting a mutable data address to the exact tokens it feeds. That table is how a JSON-Patch write becomes a targeted DOM update rather than a re-render.

The wasm request engine

The engine publishes five globals and nothing else:

JS entry points

cmd/hypermake-request-engine-wasm/main.go. Startup is registration-driven: the module calls __hypermakeRequestEngineReady; no loader may poll for it.

GlobalArgumentReturns
hypermakeRequestEngineLoadPackagepackage bytes or text, plus an optional ArtifactIdentity JSON stringLoad result or a typed refusal.
hypermakeRequestEngineExecuterequest JSONResponse envelope with ok, status, engine, abi, and on success html/bindings/routeTiming.
hypermakeStatechartInitial / …Step / …Tickstatechart request JSONSee statecharts.

Exactly four operations are accepted; anything else is refused with operation_unsupported:

Accepted operations

executeRequest in main.go. renderMode is reported back so a consumer can tell a streamed render from a direct-HTML shortcut.

OperationMeaning
render-bindingsResolve the route's binding table against the current data stores and protocol resources — values only, no HTML. Unresolvable pointers are skipped, not errors.
render-route-htmlExecute a route stream supplied in the request. Reports renderMode frozen-compiled-graph.
render-route-html-residentSame, but the route is resolved from the package already installed in the engine, and resident data stores are merged under the request's own.
finalize-route-htmlPost-process finished HTML: expand <fa-icon> from packaged protocol resources, then minify. Reports postprocessed-static-html.

Every render answers with a routeTiming map the engine measured itself: engineTotalMs plus per-phase plan costs (planDigestMs, planDynamicEvalMs, planSetEvalMs, planShapeEvalMs, planCollection*Ms, planForeachBodyMs, planFrameEvalMs) and the matching evaluation counts. The playground renders that map directly; it is not an animation.

Refusal reasons

Verbatim reason codes. A refusal is always {ok:false, status:'refused', reason} — the engine never degrades silently.

StageReasons
Requestrequest_missing, request_json_invalid, operation_unsupported
Package intakepackage_missing, package_read_failed, package_json_invalid, package_schema_unsupported, package_format_unsupported, package_abi_unsupported, package_routes_missing
HMCPACK2 decodepackage_hmcpack2_magic_invalid, package_hmcpack2_truncated, package_hmcpack2_decode_failed, package_hmcpack2_project_failed, package_hmcpack2_unmarshal_failed
Identitypackage_artifact_identity_missing, package_artifact_identity_invalid, package_graph_identity_invalid, package_graph_identity_mismatch, package_artifact_digest_mismatch
Renderresident_route_missing, request_engine_render_stream_missing, request_engine_frame_result_missing, request_engine_frame_cycle:<routeKey>, request_engine_finalize_html_missing, request_engine_finalize_icon_missing_name

Note the asymmetry in the identity rows: a v2 typed package requires the caller to pass the expected ArtifactIdentity. Loading a canonical package without one is package_artifact_identity_missing — there is no "trust the bytes you were handed" path.

Routes, params, and frames in the browser

The engine resolves a requested path against the resident routes by id, public route, route path, path pattern, or source reference. A pathPattern containing {name} segments matches by segment count with URL-decoding, and ties are broken by the most literal segments; otherwise trailing segments bind positionally through the route's paramSchema.

Matched params enter the render context in the same shape the server render uses:

Route context available to expressions context
route.path              requested path
route.pathList          its segments, decoded
route.resolvedPath      the route's own declared path
route.resolvedPathList  its segments
route.resolvedID        the route id
route.params            the bound parameters
ui.route.*              the same object, mirrored
		

Each param is additionally lifted to a bare top-level name, unless it collides with an existing name or with a reserved root (agents, branches, data, detail, graph, mutations, plugins, runtime, runtimePlugin, scope, source, testing, timeline, ui, route).

<frame> — whose authoring surface is documented on the LZR language reference — resolves recursively inside the engine. Each non-container frame call resolves its src to another resident route, renders it, and splices the result into the frame host. The recursion carries a route stack and refuses re-entry with request_engine_frame_cycle:<routeKey>. Two deliberate non-errors: a frame declared as a container keeps its server-authored placeholder body untouched, and a frame pointing at a route that is not in this shard is left empty on purpose, so the browser frame runtime can fetch that route's own shard.

Expression parity with the CLI

The browser evaluates expressions with the same code the CLI does. internal/lzr/render/compat_expr.go carries no js/wasm build exclusion and has no wasm-specific twin, and the package is in the engine's real dependency closure:

Verified: the shared evaluator is linked into the browser engine shell
GOOS=js GOARCH=wasm go list -deps ./cmd/hypermake-request-engine-wasm | grep internal/lzr/
# internal/lzr/ir, collection, address, htmls, fragment, intern, tapeledger,
# datafrag, datatransform, expr, rendercompat, richtext, templateexpr, render
		

So bracket indexing, negative list indices, the pipe registry, protocol-read semantics, and truthiness behave in the browser exactly as documented on expressions and pipes. There is no browser subset of the dialect and no second evaluator to keep in step. What differs is not the language but the resolvable inputs, which is the next section.

What the browser genuinely cannot do

The limits are structural — enforced by the build closure and by package-time refusals, not by discipline:

Honest limits

Absences verified against the js/wasm dependency closure above; refusals quoted from internal/clientgraph/package.go.

Not availableHow it is enforced
Parsing or compiling a templateinternal/lzr/parse, compile, component, componentpack, client, and legacy are absent from the js/wasm closure. The browser executes streams; it cannot author them.
Full-document render and scope debuggingrender/foreach.go and render/debug.go are build-tagged !js ||!wasm.
Tape-ledger emissionrender/tapeledger.go carries the same build tag; the ledger is a server-side artifact.
rendered:// readsPackager refusal: "server-filesystem reads … must be represented as packaged protocol resources, data roots, or explicit route context before browser execution".
file:// readsPackager refusal: "host-filesystem reads … not available to the browser request engine".
Unpackaged public:// target readsRefused unless the address resolves to a packaged protocol resource — otherwise it "depend[s] on the server graph".

Everything the browser can read was decided at package time and shipped: data stores (keyed by their data:// address), protocol resources, and explicit route context. The playground package carries exactly two data stores, data://tape-mini/a.json and data://tape-mini/labels.json; nothing else is reachable from those routes.

The ServiceWorker runtime

A bang-owner directory becomes a live surface only when the ServiceWorker installs its package. The worker owns a fixed set of same-origin runtime routes — no discovery, no wildcards:

ServiceWorker-owned routes under /_hypermake/runtime/

runtime/browser/lzr-runtime-sw.js. Every response is wrapped in a hypermake.authority-envelope.v1 so the Inspector can tell browser state from graph state.

RouteMethodRole
/route-state, /data-storeGETCurrent route projection and data-store values, with generations.
/diagnostics, /probeGETTyped runtime diagnostics (worker, wasm engine, client pipeline) and liveness.
/patch, /patchesPOST / GETApply a typed JSON-Patch mutation; list stored mutations.
/writable-resourcePOSTMutate through a named resource declared in clientGraph.writableResources.
/action-pack, /host-commandPOSTInvoke a declared action-pack action; serve its host calls.
/sync, /remote-data-change, /reset-dataPOSTOutbound sync, inbound remote change reconciliation, and store reset.
/client-pipeline, /extensions, /projection-cacheGET / POSTClient pipeline runs, installed extension packages, projection-cache control.

Mutations are typed packets (hypermake.runtime.patch.v1) carrying the package's runtimeRootDigest, the target route, the data:// target, an expectedDataStoreGeneration, and a JSON-Patch array. Generations are monotonic and checked: a write against a stale generation is refused rather than merged; an invalidation at or below the current generation answers status: "stale". A successful mutation returns the affected binding keys and broadcasts hypermake.runtime.data.changed — which is how a patch reaches the DOM without a re-render. Named writable resources add a layer: the packet names a resource, and the worker resolves dataTarget, patchPointer, and patchOp from the package; a resolved client:// target routes to a window provider instead of worker storage.

Action packs are the sanctioned way for browser code to have effects. Each declared action carries an effect list drawn from a fixed vocabulary and capabilities that scope those effects to concrete addresses; browser.host-call is the effect that lets an action reach back through /_hypermake/runtime/host-command. This site's own package declares one, lzr-testing/admin/update-campaign-title, whose capabilities name data://campaign.json and the host calls runtime.data.read, runtime.data.patch, runtime.sync.enqueue — the declaration is in hypermake.json and is compiled into the package header.

The live proof of this whole layer is the bang-owned ServiceWorker runtime canary: it installs the worker, reaches diagnostics, persists a typed patch in worker-owned storage, and reads the wasm-backed admin route state — without the fake backend or a page-local DOM planner.

Statecharts in the browser

The same wasm module hosts the statechart runtime as three operations over an IR the graph compiled (hypermake statechart compile --input CHART.scxml, schema hypermake.statechart.ir.v1): initial returns the initial snapshot, step applies one event, tick runs to quiescence under a maxSteps bound. Responses carry hypermake.browser-statechart-runtime.v1 plus diagnostics; refusals are statechart_ir_invalid, statechart_request_json_invalid, statechart_operation_unsupported. Browser and server must agree exactly, checked against internal/hyperstatechart/testdata/browser-runtime-agreement.v1.json.

Weight, disclosed

The engine is not small, and this site does not round the number down. Measured on this build:

Browser-runtime asset weight

Byte counts from this project's rendered/public/assets/. Budgets from tools/site-weight-budget.json, enforced by the graph-owned site-page-weight canary.

AssetBytesBudgetState
hypermake-request-engine.wasm21,594,827 raw · 4,443,327 gz · 3,114,362 br7,500,000over
lzr-runtime-sw.js277,60380,000over
lzr-runtime.js180,09070,000over

Verify this page

Reproduce every claim above shell
hypermake client-graph package --root . --owner !tape-playground --bootstrap index.html --out /tmp/pkg.hmcpack2
hypermake client-graph manifest --out /tmp/manifest.json
hypermake explain public://_hypermake/runtime/client-graphs/!tape-playground.hmcpack2
hypermake affected-by data/tape-mini/a.json
hypermake explain rendered/public/docs/browser-runtime.html
		

Source authorities, in reading order: internal/clientgraph/package.go (packaging, browser-protocol refusals), internal/clientgraph/hmcpack2/ (format and identity contract), internal/hypercli/client_graph.go (the artifact family and its validation), internal/lzr/ir/ir.go (stream op vocabulary), cmd/hypermake-request-engine-wasm/main.go (engine surface, route matching, frame recursion), internal/hyperstatechart/browser_runtime.go (statechart operations), runtime/browser/lzr-runtime-sw.js and runtime/browser/README.md (ServiceWorker contract).