igneum/scene/feed-contract.md
igneum-labs 3ec72d984a Chain scene 2.0.3: /live and the home fold paint on every push whatever the document's visibility says; the renderer, its palette and its feed contract move to one shared folder scene/ with byte-equal copies checked by the gate (7 October 2026, 15:2x UK)
The blank /live (the project lead, 14:3x UK): IgneumDag 2.0.2 painted only from a requestAnimationFrame loop gated on document.hidden and
the IntersectionObserver, so a page that loaded with document.hidden true (the desktop pane, a background tab) and whose embedder
never fired visibilitychange took every push, reported live and never drew a frame. 2.0.3 paints the current picture synchronously
on push, size and theme change; the motion loop alone waits for a visible document and an intersecting canvas. Confirmed headless
on build-2 against the live feed: hidden document 0 lit pixels before, 110,007 after; the never-intersecting observer repaints
on every push. Known-failed test tools/scene/paint-test.cjs (the 2.0.2 renderer draws nothing in the same world).

The second 2.0.3 change: the phone rule (30 s window, four lanes) keys on the viewport width, not the canvas width; a 640 px hero
on a 1,440 px laptop was rendering as a phone while the app's card beside it was not.

scene/ is the one source: live-dag.js, proof-core.js, tokens.css (the fourteen palette tokens, the brand package's values, dark and
light), feed-contract.md and .json (one JSON shape for the observer's /api/live and the app's api/live), a recorded reply as the
fixture. tools/scene/sync.mjs writes the copies and the scene-tokens block into site.css and app.css; --check is the gate line
(byte-equal scripts, an equal block, the names defined nowhere else, a print block excepted), --self-test fails five known cases
first. The site's token definitions move out of the package's :root line into the block; no value changes on the site.
tools/scene/feed-contract.mjs validates a reply against the key lists; its test refuses a miner rewritten to "you", a float now,
a stray key. Three new lines in tools/ci/pre-push.sh. The app side (branch scene-parity, for 0.3.21) takes the same folder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit f774353461)
2026-10-07 13:51:47 +00:00

4.8 KiB

The live feed contract

One JSON shape. The chain scene (scene/live-dag.js, scene/proof-core.js) reads it, and two producers write it:

Producer Where Source of the rows
The observer site/api/live.mjs (GET /api/live?window=N, igneum.network) Neon tables the observer (tools/observer/observer.mjs) fills from node 1
The miner app app/igneum-app/src/live.rs (GET api/live?window=N on the engine's port) The observer's reply, passed through; when the observer is unreachable, the local node's igneum_getRecentBlocks shaped into the same rows (partial: true)

The machine-readable key lists live in scene/feed-contract.json. tools/scene/feed-contract.mjs validates a reply against them (the gate runs it over the recorded fixture and over known-bad shapes); the app's Rust test reads the same file, so neither side can add a field or drop one without the other noticing.

Top level

Key Type Meaning
ok true A reply with ok: false carries error and nothing else
now ISO 8601 string The producer's clock when the reply was built
state object The network state, below
blocks array The window's blocks, oldest first, at most 1,800
miners array Vote keys seen in the last 10 minutes
events array The last 30 observer events
finality object Checkpoints and weights
proving object The proving layer
partial true, app only The rows came from the local node: no parents, no proof state, no checkpoints

state

stale, age_s, network, node_version, height (the chain block number), block_count, header_count, blue_score, difficulty, hashes_per_second_estimate, peers, mempool, blocks_60s, blocks_per_second_60s, blocks_per_minute (10 numbers, oldest first), miners_10m, observer_started_at, observer_lag_s, queue_depth, updated_at. A value the producer does not know is null, never missing.

App extensions, additive and ignored by the scene: source (observer or node), you_blocks (this machine's blocks in the window), node (the local node's own numbers for the Cards tab).

A block

Key Type Meaning
hash string, the first 16 hex Both producers cut the hash the same way, so a hash reads the same everywhere
number number or null The chain block number once planned; null off the chain or before the plan
blue_score, daa number or null
ts number Header time in milliseconds
rx number When the producer first saw the block, milliseconds
parents array of hash strings Empty on a partial reply
chain boolean On the selected chain
miner string, 8 hex The vote key id. NEVER a word: the app marks its own blocks through the scene's mine option, not by rewriting this field
color blue, red or pending GHOSTDAG inclusion
locked boolean A locked checkpoint block
final boolean At or before the newest lock
shards array of {i, n, state, prover, lag, payout, pgas} state is planned, proving, verified or paid
proven boolean Every shard verified or paid

miners[], events[], finality, proving

A miner is {id (8 hex), blocks, share (percent of the 10 minutes), last_seen (ISO or null), engine (string or null)}. An event is {ts, kind, text}. finality is {supported, active, next_index, latest_locked_index, latest_locked_hash, latest_locked_blue_score, params, weights, checkpoints}; a checkpoint is {index, hash, blue_score, daa, state (pending or locked), signed, active, total, fraction_active, fraction_total, votes, voters, locked_at, updated_at}. proving is {supported: false, reason} or the supported object (active, activation_daa, tip_daa, verifier, pool, paid_shards_total, shard_budget_pgas, blocks_10m, blocks_fully_proven_10m, shards_proven_10m, shards_paid_10m, median_proof_lag_s, provers_10m).

What the scene does with it

IgneumDag.mount(canvas, opts).push(reply) normalises every block (unknown colours become pending, unknown shard states unknown, a hash over 256 characters or a non-numeric ts drops the row), keeps the newest 340 s, and paints at once. The window the scene shows (opts.window, 30 to 300 s) is independent of the window the page asks the producer for: both the site and the app ask for 300 s so the viewer can pan and zoom out to the full range (?window=300).

The recorded fixture

scene/fixtures/live-2026-10-07.json is one real /api/live?window=300 reply (7 October 2026, 13:17 UTC, 905 blocks, 15 miners, 40 checkpoints, 14 proven blocks, 50 excluded). The contract test validates it; the parity test renders it through both surfaces; the paint test pushes it into a hidden document.