igneum/scene/feed-contract.md
igneum-labs 8e267ae147 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

78 lines
4.8 KiB
Markdown

# 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.