# p8v2 visualization — standalone specification

**Status:** specification only. No program has been written against it yet.
**Written:** 2026-07-29. Revised 2026-07-29 after review (chain direction reversed,
k=0 admitted, verification moved into an OFRA block, HTML viewer added).
**Subject:** a 3-D rendering of E and P for the p8v2 P-program, run over a fixed
128-byte prefix of enwik9, with k = 8 and N = 128.

---

## 0. Self-containment contract

This file is the *complete* input. An implementer given only this file — with no
access to enwik9, to `tests/pprog/p8v2-words.md`, to the cmpr repository, or to
any prior conversation — must be able to produce the intended animation and still
images. Everything needed is restated here: the input bytes (§2), the model
semantics (§4), the geometry (§5), the visual encoding (§6), the camera (§7), the
frame lists (§8), and the outputs (§9). §12 is a checkable acceptance list, and
its numbers are real: they were computed from the fixture, not estimated.

Two consequences of that contract, both deliberate:

- Where the source design is **undecided**, this file supplies an explicit
  placeholder so the program runs. Every placeholder is fenced into §4.5–§4.8 and
  is marked in the rendering itself (§6.3). A placeholder is scaffolding, not a
  design decision, and must never be read as one.
- Where the source design is **ambiguous**, this file picks a reading and records
  the pick in §14, so a later reader can see exactly what was chosen and change it.

The file is itself an OFRA document: §2.3 is a fenced, runnable block, and the
prose around it is that block's `:pre` / `:post` runs.

---

## 1. What p8v2 is, in the terms this visualization needs

p8v2 is a lossless-compression program in the P-programming formalism. A program
is a tuple:

- **E** — the event spaces (ESs). An ES is a finite set of mutually exclusive
  atomic events. At any instant each atomic event carries a strength in `[0, 255]`;
  255 means "absolutely activated / known".
- **T** — the transducers, i.e. the I/O primitives that connect the program to the
  outside (here: one byte in, one byte out, both clocked at 200000 Hz).
- **P** — the patterns. A pattern is a relation between events (typically: an
  antecedent event in one ES predicts a consequent event in another, or the same
  ES at another time step). Patterns are bidirectional in principle.
- **f** — the update function: the machinery by which P is applied to T at each
  step, including timing and the resolution of competing predictions. In p8v2 this
  is called **settling**. *(Not yet specified — see §4.8.)*
- **ω** — the learning rule: what patterns and events get created, scored, kept and
  pruned, hence how the model architecture grows at run time. In p8v2 ω also owns
  the hyperparameters and the sparsification of the memory trace. *(Not yet
  specified — see §4.6 and §4.7.)*

The compression story in one paragraph: bytes stream into a **memory chain** —
a shift register of ESs where cell `e_i` holds the byte from `i` time steps ago,
and `e_0` is the live input. A model predicts each byte from its predecessors. The
compressed file (the **memory trace**) records only the bytes the model got wrong
(the **surprising** bytes) plus the gaps between them, unary-coded. Better model ⇒
fewer surprises ⇒ smaller file. p8 adds a second level above the byte level: joint
events over spans of `k` consecutive bytes ("tokens"), which predict the next byte
and compete with the shorter-context models. **This visualization exists to make
that competition visible**, and to make visible where the design is not yet pinned
down.

The predecessor program, p7, had only the one-byte model (`k = 1`, an order-1
Markov table storing just the single most likely successor per byte). Everything
p8v2 adds sits at `k ≥ 2`.

**The `k` ladder starts at 0.** `k = 0` is the marginal frequency distribution over
bytes — the model with no antecedent at all, and therefore the best available
prediction at a position with zero bytes of prior input. It is not decoration: it
is what makes position 0 predictable-in-principle and what any longer-context model
must beat.

---

## 2. The fixture

### 2.1 The bytes

Exactly **128 bytes**, all printable US-ASCII in `[0x20, 0x7E]`, no newline. The
program embeds this literal string and encodes it as ASCII; there is no file to
read.

```
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.3/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLoca
```

Refer to it as `B`, indexed `B[0] … B[127]`.

### 2.2 Anchors (assert these at start-up)

| index | byte | note |
|---|---|---|
| 0 | `<` | start of the MediaWiki export root element |
| 10 | ` ` | end of `<mediawiki` |
| 17 | `"` | end of `xmlns="` |
| 18 | `h` | start of the first `http://…` URL |
| 57 | `/` | end of the first URL (40 bytes, indices 18–57) |
| 58 | `"` | |
| 60 | `x` | start of `xmlns:xsi="` |
| 70 | `"` | |
| 71 | `h` | start of the second URL (41 bytes, indices 71–111) |
| 111 | `e` | last byte of `instance` |
| 112 | `"` | |
| 114 | `x` | start of `xsi:schemaLoca…` |
| 127 | `a` | final byte of the fixture |

`len(B) == 128` and every anchor must match, or the program aborts. All thirteen
anchors have been verified against the embedded string.

### 2.3 Provenance, and how to verify it

`B[0:64]` — `<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.3/" xmln` —
is the first 64 bytes of enwik9, quoted verbatim in the p8v2 design document as a
worked example. `B[64:128]` continues the standard MediaWiki 0.3 export header and
was **reconstructed, not copied from the corpus**. Verify it against a real enwik9
before publishing any figure, by running the block below:

```
ID: verify-input
Type: sh

ENWIK9=${ENWIK9:-enwik9}
FIXTURE='<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.3/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLoca'
tmp=${TMPDIR:-/tmp}/p8v2-fixture.$$
trap 'rm -f "$tmp"' EXIT HUP INT TERM
if [ ! -r "$ENWIK9" ]; then
	echo "verify-input: cannot read $ENWIK9 (set ENWIK9=path)" >&2
	exit 2
fi
printf '%s' "$FIXTURE" > "$tmp"
dd if="$ENWIK9" bs=128 count=1 2>/dev/null | cmp - "$tmp" || {
	echo "verify-input: MISMATCH - fixture is not the first 128 bytes of $ENWIK9" >&2
	exit 1
}
echo "verify-input: OK"
```

Run it as:

```sh
cmpr --run verify-input --ofra tests/pprog/p8v2-viz-spec.md
```

Strictly POSIX `sh` — no process substitution, no `local`, no `mktemp`, no
bashisms. Exit status 0 = verified, 1 = mismatch, 2 = enwik9 unavailable.

**Prerequisite:** `--run` does not yet support `Type: sh`; it currently resolves an
entry block and reads `E:` / `T:` / `P:` / `Omega:` headers. Adding `sh` support —
execute the block body with `/bin/sh`, propagate its exit status — is a
precondition of this spec and is not otherwise part of it.

Because verification lives here, **the renderer takes no arguments at all**. That
matters: it means the whole thing ships as files that can be moved with `scp` and
opened locally, which is the working arrangement (development over SSH to an EC2
instance, no browser on the box, no server).

---

## 3. Parameters

Fixed for this specification. They are constants in the program, named as below.

| name | value | meaning |
|---|---|---|
| `N` | 128 | input byte events observed in the learning scan; equals `len(B)` |
| `K` | 8 | maximum antecedent length; levels are `k = 0 … 8` |
| `L_WIN` | 32 | length of the **settling window**: chain cells that carry patterns |
| `L_CHAIN` | 128 | total chain cells drawn; cells beyond `L_WIN` are inert |
| `BUDGET_K` | 256 | max patterns kept per level `k ≥ 2` |
| `S_ITERS` | 24 | settling iterations per settling frame sequence |
| `AND_THETA` | 0.05 | AND-gate firing threshold (placeholder, §4.8) |

`L_WIN = 32` is the design document's own figure ("let's start with 32 bytes as a
reasonable max word length"), and the inert tail beyond it is its own stipulation:
the chain trailing off away from the window "takes on no patterns whatsoever
except the absolute leftward one that defines the chain."

Note for the implementer: with `N = 128` and `BUDGET_K = 256`, **the budget never
binds** — there are at most `N − k` distinct windows per level, and measured
lexicon sizes top out at 112 (§12.3). Pruning is therefore not exercised by this
fixture. That is expected, and §12 makes it an assertion rather than a surprise.

---

## 4. The model the program must compute

All of this is computed once at start-up, deterministically, before any frame is
drawn. No randomness anywhere (§11).

### 4.1 Event spaces (E)

- **`byte`** — 256 atomic events, one per byte value `0x00…0xFF`. This is the input
  ES, `e_0`. *(It is named `byte`, not `input`: on recall the same ES is the
  output, so `input` was a misnomer.)*
- **`mem_cell_i`** for `i = 1 … L_CHAIN−1` — a copy of the `byte` ES holding the
  byte from `i` time steps ago.
- **`token_k`** for `k = 2 … K` — the joint ES at level `k`. Its atomic events are
  joint events over `k` consecutive byte events. Populated by brute force during
  the scan (§4.4). Seeded with the whole `byte` ES at `k = 1`, so every byte event
  is covered by a level-1 token: the byte stream and the token stream are two
  factorings of the same underlying joint event.
- **`enwik9_128`** — the top-level memory ES, a singleton: one atomic event, being
  the entire 128-byte dataset as a single joint event. It is named after the
  dataset it holds, by convention.

### 4.2 Transducers (T)

- **`stdin_single_byte`** — one byte in per tick, clock 200000 Hz.
- **`stdout_single_byte`** — one byte out per tick.

These are drawn (§5.5) but carry no dynamics beyond delivering `B[t]` at step `t`.

### 4.3 Patterns (P) — the chain, and the two interior models

**`memchain`** (absolute, strength 255, not learned). "The input byte is X" ⇒ "the
byte 1 time step ago was X". Concretely: at each tick every cell's content shifts
one position further into the past, `e_i(t) = e_{i−1}(t−1)`, with `e_0(t) = B[t]`.
This is the *memory chain pattern*, and the only pattern the inert tail carries.

**`k0`** — the marginal model, learned by counting:

```
count0[v] = #{ t in [0, N) : B[t] == v }
k0        = argmax_v count0[v]                    # ties: lowest byte value
```

`k0` is the prediction at a position with no prior context, and the fallback
wherever a longer-context model has nothing to say.

**`k1`** — the one-byte model, learned by counting:

```
count1[a][b] = #{ t in [1, N) : B[t-1] == a and B[t] == b }
k1[a]        = argmax_b count1[a][b]              # ties: lowest byte value
```

Tie-breaking matters: in this fixture `h` (0x68) is followed by `t` twice
(`http` ×2) and by `e` twice (`XMLSchema`, `schemaLoca`), a perfect tie, resolved
to `e` (0x65 < 0x74). Ten of the 34 antecedents are ties. Bytes `a` that never
occur as a predecessor have `k1[a]` undefined and fall back to `k0`.

The k=1 model stores **only the argmax**, one byte per antecedent — 256 bytes
total, no strengths. That aggressive truncation is p7's, kept deliberately.

Geometrically, `k0` and `k1` are both *interior* to the `byte` ES — `k0` a
distribution over it, `k1` a relation from it to itself one time step later —
rather than levels above it. §5.1 draws both in the `z = 0` plane accordingly.

### 4.4 Tokens and the AND gate

For each level `k = 2 … K` and each time step `t` with `t ≥ k`:

```
w = (B[t-k], B[t-k+1], …, B[t-1])     # the k bytes at lags k … 1
if w not in lex[k]: append w to lex[k]           # first-occurrence order
occ[k][w]      += 1
follow[k][w][B[t]] += 1
```

`lex[k]` is ordered by first occurrence and never reordered. The consequent of a
kept level-`k` pattern is

```
cons[k][w] = argmax_c follow[k][w][c]        # ties: lowest byte value
```

The **AND gate** is the mechanism that makes `w` a single event: the `k` member
byte events feed one joint node, which fires only when all `k` are active. It is
drawn explicitly (§5.3) because it is new machinery, not a pattern in the existing
sense. Its firing rule is a placeholder (§4.8).

**Budget.** Keep at most `BUDGET_K` patterns per level, ranked by ω's scoring rule.
ω's scoring rule is undecided; see §4.6. As noted in §3 the budget does not bind at
`N = 128`, so *every* window is kept and the ranking is unobservable in these
figures. The program must still print `len(lex[k])` per level so the slack is
visible.

### 4.5 PROVISIONAL — strengths

> **Scaffolding. Not the design.** p8v2 calls for LSA (log stochastic algebra)
> with a full complement of algebraic operations, and for strengths carried as
> single bytes — a map from atomic events to `[0, 255]`. LSA has never been written
> down in this project, so it cannot be implemented from the source material.

Placeholder: strength is an integer occurrence count, and the displayed byte
strength is

```
strength(e) = clamp(round(255 * log(1 + count(e)) / log(1 + max_count_at_level)), 0, 255)
```

where `max_count_at_level` is the largest count at the same level `k`. Every mark
whose size or opacity derives from `strength` is rendered in the provisional style
(§6.3).

### 4.6 PROVISIONAL — ω's keep/prune scoring

> **Scaffolding. Not the design.** ω is to learn the `k ≥ 2` patterns via LSA and
> then score them by a mechanical rule over the patterns themselves — explicitly
> *not* by counting eliminated errors and *not* by replaying the data.

Placeholder: rank by `occ[k][w]` descending, ties by position in `lex[k]`. Since
the budget does not bind at `N = 128` this ranking affects nothing that is drawn;
it exists so the code path exists. The legend still lists it as in force.

### 4.7 PROVISIONAL — composite prediction, and the trace

> **Scaffolding. Not the design.** In p8v2 there is **no strict override** of a
> shorter context by a longer one and no separate arbitration step: competing
> predictions are resolved by settling, inside f. The "longest context wins" rule
> below is exactly the idea the design rejected, retained here only so a trace can
> be computed at all. Furthermore, which bytes enter the memory trace is part of ω,
> and is dual to settling — the two have to be specified together, and neither is.

Placeholder composite prediction at step `t`:

```
for k = K down to 2:
    w = (B[t-k] … B[t-1])
    if t >= k and w in lex[k]:  return (k, cons[k][w])
if t >= 1 and k1[B[t-1]] defined: return (1, k1[B[t-1]])
return (0, k0)
```

`t` is **surprising** iff the returned consequent `≠ B[t]`. Note that with `k0` in
the ladder, position 0 is no longer surprising *by definition* — it is surprising
only because `k0` happens to be wrong there (§12.2). `trace_composite` is the set
of surprising `t`.

The program must **also** compute `trace_k1`, the same predicate using only the
`k ≤ 1` branches. That is p7's trace, and it is the honest baseline. Both are
rendered (§8.4); their difference is the only compression claim the picture can
make, and at this fixture size it is dominated by memorization (§12.6).

### 4.8 PROVISIONAL — settling

> **Scaffolding. Not the design.** p8v2's settling is a continuous, bidirectional,
> frequency-domain process: every pattern is applied at a specific firing rate, the
> minimal wavelength set by the pattern's length, driven by pulses originating from
> the absolutely-activated (255) events, terminating in a rolling window. It also
> requires an abduction step that lifts a synthetic ~1-strength input event to 255.
> None of that is specified in runnable detail. What follows is a plain iterative
> relaxation that produces a similar *picture* by different means.

State: `a[i][v] ∈ [0,1]` for chain cells at lag `i ∈ [0, L_WIN)` and values
`v ∈ [0,256)`.

Initialization: for a **clamped** cell (byte known) `a[i][B_pos(i)] = 1`, all other
values 0. For an **open** cell, `a[i][v] = count0[v] / N` — i.e. the k=0 marginal,
which is what "no information yet" actually means here.

One iteration, applied to open cells only, clamped cells held fixed:

```
m[i][v] = W_FWD * Σ_a  a[i+1][a] * [k1[a] == v]
        + W_BWD * Σ_c  a[i-1][c] * bwd[c][v]
        + W_TOK * Σ_{k, o, w}  fire(k,o,w) * [cons[k][w] == v] * [o-1 == i]
a[i]    = m[i] / Σ_v m[i][v]           # if Σ == 0, fall back to the k=0 marginal
```

with

- `bwd[c][v] = count1[v][c] / Σ_x count1[x][c]` — the k=1 table renormalized to run
  backwards, as the design document suggests ("keep what we currently record but
  renormalize the backwards direction").
- `fire(k,o,w) = Π_{j=0}^{k-1} a[o+k-1-j][w_j]`, i.e. the AND gate is a product
  over the member cells at offset `o`; the token contributes only if
  `fire ≥ AND_THETA`. `o` ranges over **all** offsets in `[1, L_WIN − k]`, not just
  `o = 1` — this is what produces the fan-out of overlapping token alignments that
  the design document describes ("word events lighting up at a range of positions,
  not just starting at `ht` but also ` h` and `tt`").
- Weights `W_FWD = 1.0`, `W_BWD = 0.5`, `W_TOK = 2.0`.

Run `S_ITERS` iterations. The settled byte at an open cell is `argmax_v a[i][v]`.
No convergence guarantee is claimed and none is needed: round-tripping is *not* a
goal of this figure (§12.7).

---

## 5. Geometry

Right-handed axes. All coordinates below are in *data* units; §7 gives the display
scaling. Write `X(i) = L_CHAIN − 1 − i = 127 − i` for the x-coordinate of lag `i`.

### 5.1 The three axes

- **x — the memory chain.** The live input `e_0` sits at the **far right edge**,
  `x = 127`. A cell at lag `i` sits at `X(i) = 127 − i`, so the past extends
  leftward and the filled chain reads **left to right in dataset order** — oldest
  byte leftmost, newest at the right, exactly as the bytes read as text.
- **y — the value axis.** `y = v`, the byte value, `0 … 255`. Each chain cell is
  therefore a vertical column of 256 atomic-event slots, and E is literally drawn
  as a (cell × value) lattice in the `z = 0` plane.
- **z — the level axis.** `z = 6k`. Token level `k` sits at `z = 6k` for
  `k = 2 … 8`, i.e. `z ∈ {12, 18, 24, 30, 36, 42, 48}`.

**`z = 6` is deliberately left empty.** Levels `k = 0` and `k = 1` have no token
nodes: both are interior to the `byte` ES, not levels above it, and both are drawn
in the `z = 0` plane. The gap is meaningful and must be labelled in the figure
("k=0, k=1: interior to the byte ES, drawn in the z=0 plane").

### 5.2 Direction — the two opposing flows

This is the organizing idea of the whole picture and must survive every camera
choice:

- **Content flows right → left.** A byte enters at the right edge and ages
  leftward; each tick, every byte's lag increases by one and it moves one step
  left, so the occupied chain grows leftward from the right edge as the scan
  proceeds. Every `memchain` edge points from `X(i)` to `X(i) − 1`. The activations
  the chain carries move with it. This is the memory-chain pattern — the one
  absolute pattern that defines the chain.
- **Prediction flows left → right.** Every learned pattern — the `k = 1` edge and
  every token edge at every level — has its antecedent strictly to the **left** of
  its consequent, and converges on the live input at the right edge. Past predicts
  future, and on this layout that reads in the natural direction.

Arrowheads are required on both, and the legend names the two flows.

### 5.3 Node placement

| node | position |
|---|---|
| byte event `v` at lag `i` | `(127 − i, v, 0)` |
| token instance: level `k`, window `w`, offset `o` | `(127 − o − (k−1)/2, y_tok(w), 6k)` |
| `enwik9_128` singleton | `(64, 128, 60)` |
| `k0` marginal profile | bars from `x = 128` rightward, one per `y = v` |
| `stdin_single_byte` | `(136, 160, 0)` |
| `stdout_single_byte` | `(136, 96, 0)` |

A level-`k` token at offset `o` covers chain cells at lags `o … o + k − 1`, i.e.
`x ∈ [127 − o − k + 1, 127 − o]`, and predicts the cell at lag `o − 1`, at
`x = 128 − o`. The node is centred over the span it covers. During the learning
scan `o = 1` always, so the span is lags `1 … k` (`x ∈ [128 − k, 126]`) and the
consequent is `e_0` at `x = 127` — which at `k = 1` degenerates exactly to the
one-byte model, confirming the `k`-notation (§14.1).

**`k0`** is drawn as a marginal profile hugging the input: for each byte value `v`,
a horizontal bar from `x = 128` to `x = 128 + 4·count0[v]/max(count0)`, at `y = v`,
in the `z = 0` plane, with the `k0` argmax marked and labelled. It sits immediately
to the right of the live input because that is where a zero-context prediction
applies.

**`y_tok(w)`** — stable for the whole run, computed once after the scan:

```
last = w[k-1]                       # the token's most recent byte, at lag o
r    = rank of w among all level-k tokens sharing that last byte, by lex[k] order
n    = number of level-k tokens sharing that last byte
y_tok(w) = last + (0.8 * (r + 0.5) / n) - 0.4
```

Anchoring `y` to the token's *most recent* byte is not cosmetic: it puts every
token that ends in byte `a` in the same `y` column as the `k = 1` edge leaving
byte `a`, so a single glance along that column shows all the levels that are
predicting from the same immediate context, fanning into different consequents.
That is the "multiple levels of predictions competing" the push exists to expose.
The jitter term only separates coincident nodes; it is deterministic and constant
across frames, so nothing drifts during the animation.

### 5.4 Edge placement

| edge | from | to | class |
|---|---|---|---|
| `memchain` | `(X(i), b_i, 0)` | `(X(i) − 1, b_i, 0)` | structure |
| `k1` | `(126, a, 0)` | `(127, k1[a], 0)` | k-model |
| AND-gate input `j` | `(127 − o − k + 1 + j, w[j], 0)` | token node | token |
| token consequent | token node | `(128 − o, cons[k][w], 0)` | token |
| trace membership | ring at `(X(i), b_i, 0)` | — | secondary encoding |
| top-level | `enwik9_128` | each trace entry | structure, faint |

AND-gate input `j` runs from member byte `w[j]`, which sits at lag `o + k − 1 − j`;
`j = 0` is the oldest member and therefore the leftmost.

Edges are straight segments. Only edges with at least one endpoint at lag
`< L_WIN` (i.e. `x > 95`) are drawn; the inert tail carries `memchain` only.

### 5.5 Transducers

`stdin_single_byte` emits one arrow into `(127, B[t], 0)` on the frame at step `t`;
`stdout_single_byte` receives one arrow from the same node. Both sit to the right
of the input edge, are drawn small and in chrome ink — they are context, not
content.

---

## 6. Visual encoding

### 6.1 Surfaces

Default figure surface is the **dark** one: `#1a1a19`. A `--light` build variant
renders on `#fcfcfb` using the light column below. Chrome ink in both modes:
gridline `#2c2c2a` dark / `#e1e0d9` light; muted labels `#898781`; primary ink
`#ffffff` dark / `#0b0b0b` light.

### 6.2 The categorical palette

Three classes only, in fixed slot order. Do not add a fourth hue; further
distinctions are carried by shape, dash and label.

| class | what it covers | light | dark |
|---|---|---|---|
| 1 | **byte events** — the `byte` ES and every `mem_cell_i` node | `#2a78d6` | `#3987e5` |
| 2 | **k-models** — the `k0` profile and the `k1` pattern edges | `#eb6834` | `#d95926` |
| 3 | **tokens** — token nodes, AND-gate inputs, token consequent edges | `#1baf7a` | `#199e70` |

Validated as a categorical set under the all-pairs rule (which is the right rule
here: a 3-D graph puts arbitrary pairs of marks side by side), in both modes:
worst all-pairs CVD ΔE 9.4 dark / 9.2 light, worst normal-vision ΔE 20.9 dark /
24.0 light. In light mode class 3 sits at 2.74:1 against the surface, below the
3:1 bar, so the **relief rule applies**: the legend's direct labels are mandatory
in light mode, not optional.

`k0` and `k1` share class 2 because they are the same thing at two context
lengths; they are told apart by form (profile bars vs. edges) and by label, never
by hue.

`memchain` is **not** a series. It is inert scaffolding and is drawn in gridline
ink at 1px — present, recessive, never competing with the model for attention.

### 6.3 The provisional style — a first-class requirement

Every mark whose behaviour comes from a placeholder in §4.5–§4.8 is drawn in the
**status `warning`** step `#fab219`, **dashed** (4px on, 3px off), and named in a
persistent on-figure panel headed `PROVISIONAL — f and ω are not specified`. The
panel lists, one line each, the placeholders in force:

```
strengths          = log-scaled occurrence counts        (stands in for LSA)
keep/prune score   = occurrence count, budget never binds (stands in for ω)
composite predict  = longest context wins                (rejected by the design)
trace membership   = composite mispredictions            (belongs to ω, unspecified)
settling           = iterative relaxation, 24 iters      (stands in for f)
```

This is the point of the figure as much as the geometry is. A viewer must never
be able to mistake scaffolding for design, and colour alone must never be what
tells them — hence dash + panel + legend label together. Status colour is
reserved for exactly this role and is never used for a series.

### 6.4 Magnitude

Activation and strength are one hue (blue, class 1), light→dark, plus size:

- radius `∝ sqrt(strength)`, floor 8px so every drawn mark stays visible;
- ramp on the dark surface: low `#184f95` → high `#cde2fb`; on light: low
  `#86b6ef` → high `#0d366b`.

A byte event at 255 (known / absolutely activated) is drawn at maximum size with a
2px surface-coloured ring, which also serves as the overlap separator wherever
marks collide.

### 6.5 Trace membership

A trace entry is marked by a **ring**, not a hue: 2px, class-2 colour for
`trace_k1`, class-3 colour for `trace_composite`, concentric when both. Shape
carries the distinction so the byte-event colour stays free to carry magnitude.

### 6.6 Legend and labels

A legend is always present, listing all three classes plus `memchain`, plus the
provisional style, each with a direct text label. Axis labels: `x — memory chain
(dataset order) →`, `y — byte value`, `z — level k`. Two standing annotations:
`← content` above the chain and `prediction →` below it, so §5.2 is readable
without the legend. Frame caption gives `t`, the current byte (as a character when
printable, else hex), and which level supplied the prediction.

Never label every node. Direct labels only on: the live input `e_0`, the `k0`
argmax, the token node that fired this frame, and the level-axis tick labels.

---

## 7. Cameras

Four named views. Display scaling before projection: `x × 1.0`, `y × 0.12`,
`z × 1.0` — the value axis is compressed because 256 values against 128 cells is
otherwise unreadable.

| name | azimuth | elevation | use |
|---|---|---|---|
| `iso` | −60° | 22° | default; both flows and the level axis all legible |
| `chain` | −90° | 0° | pure side-on: the `z = 0` plane, content vs prediction |
| `levels` | 0° | 8° | down the chain axis: levels stacked, fan-out visible |
| `top` | −90° | 88° | plan view: lexicon occupancy per level |

Axis limits are fixed for every frame — `x ∈ [−4, 142]`, `y ∈ [−8, 264]`,
`z ∈ [−2, 62]` — so nothing jumps between frames or between stills. The `iso`
view crops `x` to `[88, 142]`: the settling window (`x ∈ [96, 127]`) plus the `k0`
profile and the transducers. The inert tail appears only in `chain` and `top`.

---

## 8. Animation

Four phases plus an orbit. Frame rate 12 fps for scan and settling, 20 fps for the
orbit.

### 8.1 Phase A — the scan (`t = 0 … 127`, 128 frames)

Camera `iso`. At each frame: the new byte lands at `e_0` at the right edge
(`x = 127`) at strength 255; every byte already in the chain moves one step left,
so the occupied region grows leftward from the right edge; `memchain` edges
redraw; every level `k ≤ t` shows its one active token node with its `k` AND-gate
inputs and its consequent edge; a token seen for the first time flashes (one frame
at 1.6× radius); the `k = 1` edge for the current predecessor is drawn; the `k0`
profile is present throughout, with its bars growing as counts accumulate. The
frame caption reports the predicted byte and the level that supplied it. Trace
rings accumulate and persist.

Cells fade to gridline ink over 4 frames as they pass lag `L_WIN` and leave the
window.

### 8.2 Phase B — the learned model at rest (1 frame, held 24 frames in the GIF)

Camera `iso`. No time index. Every kept pattern at every level drawn at once, with
`memchain` and the inert tail present, plus `enwik9_128` and its faint edges to
every `trace_composite` entry. This is the "all of P" still.

### 8.3 Phase C — settling on the worked example (24 frames)

Camera `levels`. This reproduces the design document's own worked example. The
first `http://` begins at index 18, so `B[18]='h'`, `B[19]='t'`, `B[20]='t'`,
`B[21]='p'`, `B[22]=':'`.

Clamp cells for indices 18, 19 and 22 (the `ht` prefix and the following `:` are
known); leave 20 and 21 **open**; clamp everything else in the window to its true
value. Run §4.8 for `S_ITERS` iterations, one frame per iteration. Each frame
shows, for the two open cells, the activation distribution over `y` (class-1 ramp,
§6.4), every token instance currently above `AND_THETA` at every offset — this is
where the fan-out over overlapping alignments becomes visible — and the running
`argmax`. Caption: iteration index and the current `argmax` string, e.g. `ht??:`
→ `htt?:` → `http:`.

The whole of Phase C is drawn in the provisional style envelope (§6.3): its
dynamics are placeholder.

### 8.4 Phase D — trace comparison (1 frame, held 24 frames)

Camera `chain`. The full 128-cell chain, `memchain` only, with both trace ring
sets (§6.5) and three figures on the frame: `|trace_k1|`, `|trace_composite|`, and
`model_bytes_naive` (§10). This is the only frame that makes a compression
statement, and §12.6 governs how it must be captioned.

### 8.5 Phase E — orbit (72 frames)

Camera azimuth stepping 5° per frame from −60°, elevation held at 22°, on the
Phase B state. Purely for reading the 3-D structure.

---

## 9. Outputs

Written to `./out/`, created if absent. The renderer takes no arguments (§2.3).

| file | content |
|---|---|
| `frames/scan-###.png` | Phase A, 128 files, zero-padded to 3 digits |
| `frames/settle-##.png` | Phase C, 24 files |
| `frames/orbit-##.png` | Phase E, 72 files |
| `p8v2-scan.gif` | Phase A + Phase B held |
| `p8v2-settle.gif` | Phase C |
| `p8v2-orbit.gif` | Phase E |
| `still-01-first-window.png` | Phase A at `t = 8`, first full `k = 8` window |
| `still-02-http.png` | Phase A at `t = 22`, the `http:` example being learned |
| `still-03-scan-end.png` | Phase A at `t = 127` |
| `still-04-model.png` | Phase B |
| `still-05-settle-00.png` | Phase C iteration 0 |
| `still-06-settle-04.png` | Phase C iteration 4 |
| `still-07-settle-23.png` | Phase C iteration 23 |
| `still-08-levels.png` | Phase B, camera `levels` |
| `still-09-top.png` | Phase B, camera `top` |
| `still-10-traces.png` | Phase D |
| `stats.txt` | §10 |
| `p8v2.html` | §9.1 — the viewer |

Stills at 1920×1080, 150 dpi. GIF frames at 960×540. If `ffmpeg` is available,
additionally write `p8v2-scan.mp4` at the still resolution; its absence is not an
error.

### 9.1 `p8v2.html` — the viewer

The renderer is the program; the viewer is deliberately much simpler. `p8v2.html`
is a **single self-contained file** that displays everything above: the ten stills
in order with their captions, the three GIFs, and `stats.txt` verbatim in a
`<pre>`. Every asset is inlined as a `data:` URI — no `<img src="frames/…">`, no
fetch, no external stylesheet, no CDN. One file, scp'd off the box, opened
locally, works with no server and no network.

It carries no logic beyond that: no re-computation, no 3-D, no interactivity
except a play/pause on the GIFs and anchor links to each section. Colours go in a
`<style>` block as custom properties keyed by role, with the dark values declared
under both `@media (prefers-color-scheme: dark)` and `:root[data-theme="dark"]`,
so a viewer's theme toggle wins in both directions. The provisional panel (§6.3)
is repeated once as page text near the top, not only inside the images.

---

## 10. `stats.txt`

Plain text, one fact per line, machine-checkable. Required contents, with the
values this fixture actually produces:

```
fixture_len              128
fixture_sha256           <hex>            # of the embedded bytes, for cross-run comparison
distinct_bytes           34
k0                       0x2f '/'         # count 9
k1_entries               34
k1_ties_broken           10
k1[h]                    0x65 'e'         # the worked tie, §4.3
lex_2_size               84               # of 126 windows, max_occ 4
lex_3_size               94               # of 125 windows, max_occ 3
lex_4_size               101              # of 124 windows, max_occ 2
lex_5_size               105              # of 123 windows, max_occ 2
lex_6_size               109              # of 122 windows, max_occ 2
lex_7_size               111              # of 121 windows, max_occ 2
lex_8_size               112              # of 120 windows, max_occ 2
lex_k_budget_bound       no               # every k
trace_k1_size            63
trace_composite_size     3                # positions 0, 38, 82
model_bytes_naive        4422             # Σ over k=2..8 of (k+1)·|lex[k]|
settle_final_argmax      <5 chars>        # Phase C result at indices 18..22
placeholders_in_force    strengths,score,composite,trace,settling
```

`model_bytes_naive` is the cost of storing every kept pattern as its antecedent
bytes plus its consequent byte — the accounting the design document itself uses
("each kept rule costs 3 bytes at k=2").

---

## 11. Determinism

- No random number generation anywhere. No wall-clock reads. No hash-order
  iteration: every dictionary is iterated in insertion order, and every `lex[k]`
  is first-occurrence ordered and never re-sorted.
- All ties broken by the documented rule (lowest byte value; then `lex[k]` order).
- Floating point is used only in §4.8 and §6.4; frames must be bit-identical across
  runs on one machine and visually identical across machines. Do not thread the
  settling loop.
- Fonts: `system-ui, -apple-system, "Segoe UI", sans-serif` (or the matplotlib
  default sans if unavailable) — no display or serif face. Tabular figures only in
  the stats panel columns.

---

## 12. Acceptance criteria

A reproduction is correct if all of these hold. Every number below was computed
from the fixture, not estimated.

1. `len(B) == 128` and all thirteen §2.2 anchors match.
2. `k0 == '/'` (0x2f, count 9) and `distinct_bytes == 34`. Position 0 is
   surprising because `k0` predicts `/` and `B[0]` is `<` — not by fiat.
3. `k1['h'] == 'e'` — the documented tie, broken low (§4.3). A reproduction
   yielding `'t'` has its tie-breaking wrong. `k1_entries == 34`,
   `k1_ties_broken == 10`.
4. Lexicon sizes exactly: `[84, 94, 101, 105, 109, 111, 112]` for `k = 2 … 8`,
   against `126 … 120` windows seen. `lex_k_budget_bound == no` for every `k`.
5. Every learned-pattern edge in every frame has its antecedent at strictly
   **lesser** `x` than its consequent (§5.2). Every `memchain` edge has the
   reverse. A single edge violating this invalidates the figure.
6. `trace_k1 == 63` and `trace_composite == 3` (positions 0, 38, 82) — and the
   Phase D frame must caption all three of those numbers **together with
   `model_bytes_naive == 4422`**. The 63→3 drop is not compression: the model
   spends 4422 bytes of stored patterns to remove 60 trace entries from a
   128-byte input, because at `N = 128` almost every 8-gram is unique (112
   distinct of 120) and the placeholder rule is simply reciting the fixture back.
   A viewer who comes away believing this figure demonstrates a compression win
   has been misled, and the figure has failed.
7. Round-tripping is **not** an acceptance criterion. The settled result in Phase C
   may or may not reach `http:`. Either outcome is a valid figure; the caption
   reports what happened. A reproduction that adjusts §4.8's weights to force the
   pretty answer has broken the spec, not satisfied it.
8. The provisional panel (§6.3) is present on every frame of every output,
   including the stills and every GIF frame, listing exactly the five placeholders,
   and once as page text in `p8v2.html`.
9. The legend is present in every frame with direct text labels for all classes;
   in the light variant this is mandatory relief for the sub-3:1 class-3 colour.
10. `z = 6` is empty and labelled; `k = 0` and `k = 1` both appear in the `z = 0`
    plane.
11. `p8v2.html` contains no external reference of any kind: no `http://`,
    `https://`, `src="./`, or `@import` outside `data:` URIs.
12. Two runs of the program produce identical `stats.txt`.

---

## 13. Implementation notes

**The renderer: one Python file, standard library + `numpy` + `matplotlib` only.**
No arguments, no network access, no downloads, no other third-party packages. 3-D
via `mpl_toolkits.mplot3d`; GIF via `matplotlib.animation` with `PillowWriter`.
Draw order matters — matplotlib's 3-D depth sorting is per-artist, so draw in the
order: gridlines, inert tail, `memchain`, byte nodes, `k0` profile, `k = 1` edges,
AND-gate inputs, token nodes, token consequents, rings, labels, panels. Cap drawn
artists per frame at 4000; if a frame would exceed it, drop token instances in
ascending `fire` order and print the count dropped — silent truncation is not
acceptable, since a thinned frame otherwise reads as a sparse model.

**The viewer is a separate, much simpler artifact** (§9.1) emitted by the same
program: a single HTML file that inlines the already-rendered assets. It does no
computation. Keeping the split this way means the heavy 3-D work stays in the
language that has the libraries, and the thing that has to travel over `scp` and
open without a server is a flat page.

Render the output and look at it before calling it done. The palette was validated
by script; layout, label collision and overflow were not.

---

## 14. Ambiguities resolved here

**14.1 The meaning of `k`. — RESOLVED, confirmed.** The source is inconsistent. One
passage says "taking length 8 we have a pattern from joint events over 7 memory
cells to the input ES" (total span 8, antecedent 7); another says "let's say that
`k` is the number of bytes in the word ES already, and the joint event is from `k`
joint to the next byte event. Then we have `k = 1` being exactly the M-1 model."
**The second is the current intention.** So `k = 8` means antecedent = the 8 bytes
at lags 1…8, consequent = `e_0`, total span 9. It is also self-checking: it makes
`k = 1` degenerate to the one-byte model exactly.

The ladder extends down to **`k = 0`**: the frequency distribution over bytes, and
the best available prediction given 0 bytes of prior input. §4.3 learns it, §4.7
uses it as the base of the fallback chain, §5.3 draws it as a marginal profile at
the input edge, and §12.2 pins its value.

**14.2 Chain direction. — RESOLVED, reversed from the first draft.** The input is
on the **far right edge**; when the memory chain is filled it reads from left to
right, in dataset order. The past therefore extends leftward, content ages
right→left along the memory-chain pattern, and learned predictions run left→right
onto the input. §5.1–§5.4 are written to this convention and §12.5 pins it.
*(The first draft of this spec had it the other way round.)*

**14.3 `L_WIN` vs the k=1 model's home. — RESOLVED.** The source says the one-byte
model can be seen either as living at every chain cell or as an interior pattern on
the input ES alone, and prefers the latter as more elegant — but then notes that
settling needs it at every position after all. This spec follows both: `k = 1` is
*drawn* as interior to the `byte` ES in the `z = 0` plane (§5.1), and is *applied*
at every cell inside `L_WIN` during settling (§4.8). That is not a contradiction,
but it is a choice.

**14.4 What is deliberately absent.** No hyperparameter block: max `k`, patterns
per level and scan length `N` are constants of ω, not independently declared
knobs — §3 lists them as program constants for reproducibility, not as a claim
about where they belong in the design. No arbitration block: arbitration is
subsumed by settling. No serialization: this figure never writes a model file.
