Skip to content

entviz Spec Change Log

This file records what changed in each version of the entviz specification. The current algorithm is defined in spec.md.

Earlier versions (v1–v3) are archived in the project's git history (browse the docs/v1docs/v5 folders, or earlier docs/index.md, at the corresponding release commits on GitHub).


Editorial: Phase-2 structural reorganization (v15, no behavioral change)

2026-07-11. spec.md was reorganized to a pipeline-order spine — Part A Framing (non-normative), Part B Producing an entviz (normative, in pipeline order), Part C Conformance and verification (normative, now last so every quantity it cites is already defined), Part D Appendices. This resolves the accumulated "15-AI-pass" scatter: Conformance no longer precedes the algorithm; input parsing and entropy characterization are unified; large-input handling has a single home; the six channels are named once and specified once; and the Cell Rendering Algorithm (now Rendering one cell) is promoted to where it is invoked. Casual avalanche (v10) folds into that section as a non-normative note. This was a moves-and-merges pass: every normative sentence was carried verbatim and the RFC-2119 requirement set is unchanged (verified line-by-line against reviews/spec-requirements-inventory.md). No requirement was added, dropped, or weakened, and no rendered output changes, so the spec stays v15 (no version bump); the corpus and all five implementations are unaffected. Retired anchor #casual-avalanche-v10 (→ #rendering-one-cell); #entropy-characterization and all Conformance-subsection anchors were preserved. See this.i:sp3cre0rg.

Follow-up clarification (same editorial pass, still v15, still no behavioral change): the Rendering one cell size rule was corrected from "sized by that cell's own token character count" to "sized by the full-token width of the alphabet the cell's text is written in" — the input alphabet for head/tail cells, Crockford base32 for the middle cells. The old per-token wording implied a short final token (e.g. a 4-character trailing hex token) would enlarge to reference size, but the reference implementation and every golden size it per alphabet (a hex cell stays at 0.75× regardless of its length), keeping cells visually consistent. The prose now matches the implemented behavior — no output, golden, or version change. The render-model text was also corrected from a two-class ("full vs. 0.75×") to a three-class size field (full, 0.80×, 0.75×). See this.i:c3llsize.


What's new in v15

v15 makes the top label reconcilable with the pasted value. When entviz strips a leading prefix off a value before visualizing it — 0x off an Ethereum address, cosmos1 off a Cosmos-family bech32 address, the SSH structural header off a key — the cells begin at a character other than the first one the user pasted. Through v14 that stripped prefix was silently absent from the label (only the type name, e.g. ETH or bech32, was shown), so a reader comparing the value against the glyph saw a different first character with no explanation. v15 echoes the literal stripped prefix as a trailing label slot, mirroring the bottom strip's ...<suffix> (which already reconciles the end of the value). This is a spec-observable label change (the Tier-A labels field changes for every prefix-stripping scheme, and the large-input marker text changes), so the version is bumped v14 → v15; but labels are excluded from the Tier-B raster, so there are no golden.png changes — the visible glyphs move, the pixels do not.

  1. Stripped-prefix slot. The grammar gains a trailing PREFIX slot: [+hash ]PRIMARY[, MOD]…[, SIZE][, PREFIX]. PREFIX is the literal front prefix that was stripped from the visualized core — the first parts entry when its bind is none (0x, 1, bc1, ltc1, bitcoincash:, addr1, cosmos1/osmo1, Stellar G/M, the CID multibase b/Qm, the SSH header, …). It is always shown in addition to the type name — redundancy is judged by a naive human reader (addr1 does not obviously imply Cardano), so ADA, addr1, bech32, cosmos1, ETH, 0x never collapse. A folded identity prefix (bind = fold: did/urn/gitoid/swhid) is not repeated — it is already the PRIMARY slot. Examples: ETH, 0x; BTC, 1; bech32, cosmos1; CIDv1, dag-pb, b; XLM, G.
  2. Prefix truncates to fit; nothing else does. The prefix is the only elastic element. It is fit to line_chars = floor(grid_width / (label_font_px × 0.6)), where 0.6 em is a fixed spec constant (not the renderer's real font metric) so all implementations truncate identically and the Tier-A label stays reproducible. A prefix that does not fit is cut to <head>... with the head floored at 4 characters, so a long prefix — in practice only SSH's ~24–52-character structural header — shows a few leading characters (SSH, ed25519, 264-bit, AAAA...) rather than collapsing to a bare ... or disappearing. Every non-SSH prefix (≤ 12 chars) is shown verbatim.
  3. Large-input marker renamed fingerprint of+hash. The bold dark-red marker is now +hash (+hash hex, 1024-bit). The wording is more honest and much terser: the cells still show the value's own head and tail bytes with a hash folded into the middle only for the part too large to display, so it is the value, augmented with a hash (the leading + is additive), not "a hash". At ~9 fewer characters it also fits grids the old marker overran. The loud styling (bold, #a00000) is unchanged.
  4. SSH ECDSA curve shortened in the label: ecdsa-nistp256ecdsa-p256 (and -p384/-p521). p256 is the curve's common short name and there is no rival non-NIST p256, so the standards-body word is dropped; the algorithm word stays (a naive reader needs ecdsap256 is a curve, not a signature scheme). The data-qualifiers.algorithm characterization field keeps the faithful SSH id (ecdsa-nistp256); only the projected label shortens it.

No data-* characterization attribute changes value (the label is a projection of the existing v13/v14 fields — parts already carried the stripped prefix, qualifiers.algorithm is unchanged). v15 changes only the visible top-label text.

Ports. Per the multi-impl roadmap, this change propagates to the four sister-repo ports (entviz-js, -rs, -java, -go), each implementing the trailing prefix slot, the 0.6-em budget truncation, the +hash marker, and the ecdsa-p256 shortening, and reproducing Tier A against the regenerated corpus.


What's new in v14

v14 makes the visible label strips a pure projection of the v13 entropy characterization and closes a real correctness gap: bound checksums were shown in the label but mostly not verified. Both are spec-observable output changes (the Tier-A labels field changes for most vectors, and new error vectors are added), so the version is bumped v13 → v14; but the label strips are excluded from the Tier-B raster, so no golden.png changes as a consequence of the label redesign — the visible glyphs move, the pixels do not. (The lone raster change in this release is an unrelated fixture fix; see below.)

  1. Label grammar = a projection of the characterization. Through v13 the top strip was hand-fused per parser from Parsed.type/prefix (CESR Ed25519 nt pubkey:, hex(64):, txt(56)->b64url:, ETH: 0x...). v14 defines one grammar, [fingerprint of ]PRIMARY[, MOD]…[, SIZE] (comma-space separated, no trailing : or ...), computed by a single render_label(characterization) function over the shared encoding/scheme/role/qualifiers/size_basis/size_bits fields — the same fields already emitted as data-* attributes. Every implementation renders the strips from this one function. Examples: CESR, Ed25519 nt; hex, 256-bit; text, 56-byte; did:key; CIDv1, dag-pb; SSH, ed25519, 264-bit; fingerprint of b64, 712-bit. The bottom strip (...<suffix> (<note>)) is unchanged in shape.
  2. PRIMARY / MOD / SIZE slots. PRIMARY is the self-describing prefix for did/urn/gitoid/swhid (did:key, urn:isbn, swh:1:rev), the short scheme name otherwise (ETH, BTC, UUID, CESR, CIDv0/CIDv1, …), or — for a null scheme — text (UTF-8 fallback) or the encoding name (hex, b64, …). MOD slots follow silent default / loud departure: the CESR primitive (minus a redundant trailing pubkey), the SSH algorithm, the CID codec (always) + hash (on departure from sha2-256), a blockchain testnet network (mainnet silent; the legacy/segwit variant is dropped), and a multihash hash on departure. SIZE shows only for a null scheme (<bits>-bit/<bytes>-byte per size_basis) and for ssh/multihash (<bits>-bit); all other schemes omit it.
  3. Bound checksums are now VERIFIED (reject on mismatch). A parser may surface a checksum suffix only if it verified it. A structural match with a failing checksum is now rejected (an error vector), matching the existing EIP-55 behavior: base58check (Bitcoin/Litecoin legacy) verifies the trailing double-SHA256; bech32/bech32m verifies the BIP-173/BIP-350 polymod on all paths (the specific bc1/ltc1 parsers previously skipped it); CashAddr (Bitcoin Cash) verifies its own 40-bit BCH checksum (a distinct code from the bech32 polymod, folding in the bitcoincash/bchtest prefix); LEI now rejects a bad MOD 97-10 instead of falling through to a generic base36 encoding. The accepted trade-off — a bad-checksum base58/bech32/CashAddr blob is rejected, not rendered bare — is the intended "no entviz from an invalid checksum" behavior. Cardano Byron is the one recognized scheme still unverified (its integrity check is a CRC-32 embedded in its CBOR payload, not a trailing base58 field); v14 accordingly stops peeling Byron's last 6 base58 chars off as a false "suffix" — a Byron address now surfaces no suffix at all (the whole body is its core), so it never displays an unverified checksum. Cardano Shelley (bech32) is fully verified.
  4. New error vectors. err-btc-legacy-bad-checksum, err-btc-segwit-bad-checksum, err-ltc-bad-checksum, err-cosmos-bad-checksum, and err-lei-bad-checksum — each a valid render vector with one checksum character corrupted — join err-eip55-bad-checksum to prove rejection across all five implementations.
  5. One fixture fix (the only raster change). The litecoin render vector's ltc1… address was a placeholder that fails the now-enforced bech32 polymod (it only rendered because the specific parser skipped the checksum); it is replaced with a real, checksum-valid address (the same 20-byte witness program as the canonical BIP-173 bc1qw508… example under the Litecoin HRP). This changes exactly one golden.png (compliance/corpus/litecoin/golden.png) — the sole raster change in v14. The stale placeholder Litecoin/Cardano-Shelley addresses in the gallery and in tests/test_entropy.py are replaced the same way.

The v13 characterization data-* attributes are unchanged — v14 changes only the visible label text projected from them and the checksum-rejection behavior.

Ports. Per the multi-impl roadmap, this change propagates to the four sister-repo ports (entviz-js, -rs, -java, -go), each implementing the same render_label projection and checksum verification and reproducing Tier A + the new error vectors against the regenerated corpus.


What's new in v13

v13 adds a normative, structured entropy characterization to the render output. Through v12 the parser produced a single opaque type-label that fused several orthogonal facts — scheme/namespace, the semantic role of the bits, network/variant, and size — forcing downstream consumers (labels, UI pills, developer APIs) to string-parse the label back apart. v13 re-expresses that same recognition along independent axes and emits them as additive top-level fields in each corpus model.json. This is a model-only, reporting-only change: no rendered pixel, no fingerprint input, and no label string changes, so every previously-conformant SVG is byte-for-byte identical apart from the version stamp. The >512-bit truncation trigger is deliberately left on the existing tokenization byte length and is not re-pointed at the new size_bits.

  1. The characterization model. Every input now carries eight fields: encoding (the declared alphabet), scheme (the recognizer/namespace that fired, or null for a bare encoding / UTF-8 fallback), role (the CLOSED enum {key, signature, digest, address, identifier} or null), qualifiers (independently-varying facets — network/variant/algorithm/version/method/nid), size_basis (decoded|utf8), size_bits (the value size in bits), parts (the ordered {text, bind} decomposition), and entropy_type (the derived convenience scheme ?? encoding). See the new Entropy characterization subsection under Conformance.
  2. size_bits two-branch definition (Resolution A). size_bits is always a whole multiple of 8, computed from the core only. Encoding cores (size_basis = "decoded") use the decoded byte length × 8 — power-of-2 alphabets via floor(chars × bits_per_char / 8) × 8, and the non-power-of-2 alphabets (base58/base36/decimal) by decoding the core to its integer value and taking its minimal byte length (never the token-packing bits_per_char, which overstates density). Text cores (size_basis = "utf8": DID method-specific-ids, URN namespace-specific strings, the UTF-8 fallback) use the UTF-8 byte length × 8. size_basis is scheme-driven, never inferred from the alphabet or the content's appearance (a did:jwk msi is base64url JSON but is still a text core).
  3. Approximation and exclusion caveats (Resolution B). Where an encoding core is a base58check substring whose version/checksum are split off, size_bits is accepted as approximate. A folded identity prefix (bind = "fold") is excluded from size_bits — it binds the fingerprint but is not part of the rendered core.
  4. parts + bind replace the prefix/prefix_semantic overload. Each part declares bind ∈ {none, fold, core}; bind is a property of the part at the recognizer's granularity (the z multibase selector is none bare but core inside a did:key msi), decoupled from whether the part is displayed.
  5. Two principles made normative. role is asserted only from the generic recognizer (SSH → key, CESR Edigest, blockchains → address, gitoid → digest; but did:keyidentifier not key, did:pkhidentifier not address, urn:isbnidentifier), and bind is a property of the part at the recognizer's granularity.

Ports. Per the multi-impl roadmap, this change propagates to the four sister-repo ports (entviz-js, -rs, -java, -go), each emitting the same characterization fields and reproducing Tier A against the regenerated corpus.


What's new in v12

v12 fixes a rendering defect in the outer frame that could clip it at fractional render scales (issue #31). The frame — the gray #808080 rectangle around the whole glyph — is the load-bearing anchor for raster-based comparison/localization: a matcher finds the solid rectangle in a screenshot or photo and derives scale and geometry from it. Through v11 the four border lines were centered on the canvas boundary, so the outer half of each stroke lay on the edge; when the SVG was scaled to a non-integer pixel size (e.g. displayed at a non-default font size in a browser), the default overflow: hidden shaved that outer half and the frame rendered thin or missing along the bottom/right edges, degrading frame detection. This is a breaking change to the rendered output of every input (all geometry shifts), so the whole conformance corpus is regenerated; there are no deployed certified users of v11's exact geometry.

  1. Transparent quiet margin around the frame. A MARGIN = 1 user-unit ring is added on all four sides of the canvas. The frame, the white field fill, and all content are inset by MARGIN, so the frame's outer edge now sits exactly one unit inside the canvas boundary and can never be clipped: the fractional overflow clip removes strictly less than one device pixel, and one user unit is ≥ one device pixel at any scale ≥ 1. MARGIN = 1 is the smallest integer inset with this property. The bounding-rect dimensions grow by 2·MARGIN; the color bar and grid origins shift by +MARGIN.
  2. The margin ring is transparent, not white. The white background fills only the frame rect (inset by MARGIN), leaving the outer ring unpainted. No fill or ink reaches the canvas edge, so the gray frame is the outermost visible pixel on every side and the glyph composites cleanly on any page color (no white halo). Because the raster comparison is RGBA, the transparent ring is a matched property — an implementation that paints it white is correctly rejected at Tier B.
  3. Frame invariant. The spec now states, normatively, that the frame renders as a complete, solid, closed rectangle whose outer edge is exactly MARGIN from the canvas edge and never touches or crosses the boundary at any render scale. A geometric regression test (tests/test_issue31_quiet_margin.py) asserts no ink lands in the quiet ring across representative inputs and font sizes (including the 20 pt repro) — the right place to guard this, since a static raster corpus rendered at integer scale is structurally blind to a fractional-display-scale clip.

Ports. Per the multi-impl roadmap, this change propagates to the four sister-repo ports (entviz-js, -rs, -java, -go), each reproducing Tier A against the regenerated corpus.


What's new in v11

v11 adds first-class handling for DIDs (W3C Decentralized Identifiers) and the closely-related URNs (RFC 8141). The prior treatment recognized only a single-segment DID method-specific-id, tokenized every DID as base64url, stripped the method without binding it, surfaced the DID URL as a suffix, and could not parse a #fragment; URNs were not recognized at all (they fell to the UTF-8 fallback as one opaque blob). v11 replaces this with a single, principled generic path shared by both schemes. No other input type changes; all non-DID/URN corpus vectors are unaffected, and new DID + URN vectors are added to the corpus (so this is a breaking change only for DID/URN inputs, which had no deployed certified users).

  1. Method-specific-id may contain :; the DID URL is dropped as a free annotation. Per the W3C DID Core ABNF, : is a legal segment separator inside the method-specific-id, so the body now ends only at the first /, ?, or #. Everything from that point — path, query (including ION's ?-ion-initial-state=…), and #fragment — is dereferencing context that varies independently of the identifier and is dropped (not entered into the core, not surfaced as a suffix), exactly like SWHID ;… qualifiers. This unbreaks multi-segment DIDs (did:web:example.com:user:alice, did:webvh:<scid>:domain, did:ethr:0x89:0x…) and fragment-bearing DID URLs, which previously fell through to the UTF-8 fallback. See the new Decentralized Identifiers subsection in the normalization step.
  2. The method name is identity and binds the fingerprint by prefix-fold. The same method-specific-id under a different method denotes a different DID (the swap test), so the did:<method>: literal is kept as the prefix and the fingerprint hash input becomes prefix ‖ core, exactly as for SWHID/gitoid. Fixes the prior bug where the method was stripped and not bound, so did:web:X and did:key:X collided in every fingerprint-driven channel.
  3. The method-specific-id is the core, kept verbatim and case-preserved; tokenized as base64url. Internal separators, a leading multibase selector, a network/chain identifier, and any self-certifying hash all stay in the core — bound (via prefix ‖ core) because the fingerprint hashes text, not decoded bytes. DID cores are not case-folded (DIDs are case-sensitive per DID Core; the consequence is fail-safe — case differences only ever produce a false negative). Tokenization is uniformly base64url: every non-hex alphabet in this spec already tokenizes on the same 4-character/24-bit boundary, so a per-method alphabet would change only the nucleus-color hint channel, never boundaries, the fingerprint, or the read-aloud text. The core is not percent-decoded (no parser ahead of the hash).
  4. Label. A DID renders with no type and did:<method>: as its self-describing prefix — the top strip reads did:<method>:... (the no-type rule already used by SWHID/gitoid).
  5. URNs ride the same path (RFC 8141). A URN (urn:<NID>:<NSS>) is the same shape as a DID — NID ↔ method, NSS ↔ method-specific-id, the r-/q-/f-components (?+/?=/#) ↔ the DID URL — so it gets the same generic handling: NID bound by prefix-fold, NSS kept verbatim and base64url-tokenized, components dropped (RFC 8141 says they are not part of URN equivalence), urn:<nid>:... no-type label. Two differences from a DID: the NSS keeps / and ends only at ? or # (not /); and per RFC 8141 the urn:<nid>: prefix is lowercased (NID case-insensitive) while the NSS case is preserved. urn:uuid:, urn:oid:, urn:isbn: etc. are not per-namespace special-cased. See the Uniform Resource Names subsection.
  6. No per-method/per-namespace special-casing (deferred). The generic path binds a DID's or URN's full identity and is encoding-agnostic at the token boundary, so v11 visualizes every well-formed DID/URN — recognized or not — by one path, with no per-method table. Two additive, non-breaking refinements were deliberately deferred: reusing the Ethereum EIP-55 reject for did:ethr/did:erc725, and dropping the long-form initial-state of did:ion/did:prism. See the Decentralized Identifiers subsection and this.i:d1dm3th0.

Ports. Per the multi-impl roadmap, this change must propagate to the four sister-repo ports (entviz-js, -rs, -java, -go) once the Python reference and DID corpus vectors land. Each must reproduce Tier A for the new DID vectors. (Tracked as tick 5yau.)


What's new in v10

v10 ("casual avalanche") closes a measured perception gap. Over 100,000 one-character-neighbour pairs, ≈24% of the background-unchanged cases were casually colour-identical at baseline — concentrated in dense full-grid inputs (a random UUID vs. its one-character neighbour: ~61% within that quarter). v10 injects fingerprint signal into colour as a few rare, discordant singletons the eye catches pre-attentively, taking that quarter to ~0.33% colour-miss. These add casual salience only — the careful-comparison channels (surround pattern, colour bar, ellipse, blank positions, quartile marks) are unchanged, and this is explicitly not a collision-resistance claim. See the Casual avalanche (v10) section.

  1. Fingerprint-sourced surround edge colour on three cells. Three cells override the nearest-palette edge-colour rule and take their edge colour directly from the fingerprint: grid position 0 (top-left, the first-fixation cell) and the 1st/2nd quartile-ftok cells. Each uses edge_palette[q & 0b11], where q is that cell's own used-ftok quant. The nucleus background colour is unchanged (still entropy-derived and lossless); every other filled cell keeps the nearest-weighted_rgb_distance rule. The override is skipped when the cell is blank or its quartile ftok is null, and applies once if position 0 is also a quartile cell. Kept to ≤ 3 cells so the discordant hues stay pre-attentive singletons rather than confetti. See the Fingerprint-edge cells (v10) rule in the cell-rendering step.
  2. Fingerprint blank fill (was fill = none). A blank cell's rounded "pill" now fills its interior from the fingerprint: enumerating the filled blanks in cell-index order, the j-th takes edge_palette[digest[32 + j] & 0b11] (digest bytes 32..59, disjoint from the ellipse's 60..63). The pill outline is retained (it preserves the "gap" reading and keeps a blank distinct from a nucleus). See the Blank fill (v10) rule.
  3. Hybrid map-blank fill. If the map-bearing blank is the only blank, it is filled from the fingerprint like any filled blank — the common small-input case (LEI, small hex, 18-char base36) where casual-avalanche colour is most needed; otherwise it keeps the v9 white/gold anchor fill while its sibling blanks carry fingerprint colour, and the sole-blank map markers may be recoloured to luminance contrast. See the Map rendering rule.
  4. Conformance surface. The three fingerprint-edge cells' edge colours and the blank-pill fills are fingerprint-driven and part of the matched render model and Tier-B raster (edge colour is still exposed via data-edge-color). No new hash is introduced — all of it reuses the primary fingerprint digest.

What's new in v9

  1. Crockford base32 middle cells (was hex). For >512-bit inputs, the 4 middle ("fingerprint") cells now render the second, domain-separated digest as 5 lowercase Crockford base32 characters per cell (the 24-bit big-endian value second[3i]·2¹⁶ + second[3i+1]·2⁸ + second[3i+2], high-order zero-padded, alphabet 0123456789abcdefghjkmnpqrstvwxyz — no i/l/o/u), replacing v6's 6 lowercase hex. It stays injective (32⁵ = 2²⁵ ≥ 2²⁴), so the ≈2⁹⁶ partial-preimage barrier (4 cells × 24 bits) is unchanged. 5 characters is the provable floor for "injective on 24 bits, single letter-case, homoglyph-clean": 4 characters would need a ≥ 64-symbol (6-bit) alphabet, forcing mixed case or punctuation (re-importing read-aloud "cap" syllables + a homoglyph surface), and base58 can't reach 24 bits in 4 chars (58⁴ < 2²⁴). The existing per-cell size rule auto-yields 4/5 = 0.80× (10 pt at the 12 pt reference, up from hex's 0.75×). The "this is a digest, not your data" cue is improved: Crockford differs from a hex input by both alphabet and rendered size (v6 hex was weakest precisely on a hex input). DOMAIN_TAG = "entviz/fingerprint-middle/v6\0" is unchanged (its v6 is the construction version, not the spec version). Breaking change to >512-bit renders; accepted (no deployed users yet). See the Large-input handling subsection, the cell-text rendered-size rule, and this.i:cr0ckmid / this.i:d1scr3t3.
  2. Color-bar band order decoupled from height. The bands' vertical order is now set by each 2-bit pattern's first-appearance order while scanning the digest's 256 disjoint 2-bit slices (tie-break by pattern value, 00<01<10<11), independent of band heights (heights still each count^4 in descending magnitude — only the vertical sequence changes). Through v8 the order was descending count = argsort(heights), carrying no information beyond the heights; decoupling adds a few reliable discrete bits (read the letters top to bottom) at zero glance and zero CVD cost, and does not affect the match task. data-color-bar-rank now reflects the decoupled first-appearance order. See the color-bar step and this.i:b4rm4rks / this.i:d1scr3t3.
  3. Two color-bar markers (new discrete channel, always present). A small filled circle rides each gutter — left and right — in one of K = clamp(floor(bar_height / 12px), 4, 16) equal fixed slots (independent of bands): left at slot second[12] mod K, right at second[13] mod K. Identity is carried by side, not shape — an earlier draft used a square (left) and a triangle (right), but shape discrimination was unreliable at the bar's scale (on a dark band the black halo vanishes, leaving a too-small inner glyph), and the left/right gutters already distinguish the two. The driving digest second = SHA-512(DOMAIN_TAG ‖ core) is now computed for every input (one extra SHA-512), so short inputs gain this channel too — closing the coverage hole where the blank-cell map vanishes on an exactly-filled grid. bar_width is unchanged (markers live in ~4 px gutters inside the existing bar); left/right placement means the two can never overlap. Markers are drawn opaque — white fill + ~0.75 px black halo, not mix-blend-mode/difference compositing — for cross-rasterizer portability (the same reason the blank-cell map avoids blend modes, F-A6) and so a band-straddling marker stays visible (white core on the dark side of the cut, black halo on the light side); the contrast is pure lightness (CVD/grayscale-safe). Painted after the bands and letters, before the final gray border. Honest framing: tripwire-tier (~6 independent hard bits), not the security backbone. See the color-bar step and this.i:b4rm4rks / this.i:d1scr3t3.
  4. Conformance surface for the markers. The render model's color bar field gains the two marker slots and K; the SVG profile adds required attributes data-bar-marker-left, data-bar-marker-right (each the integer slot index) and data-bar-slots (the integer K), and the normative paint-order list records the markers within the color-bar layer (after the band letters). No new hash is introduced — the markers reuse the existing second digest / DOMAIN_TAG. See the SVG profile, render model, and paint-order sections.

What's new in v8

The v8 release (PR #11, "blank-map-and-determinism") collects four conformance and accessibility fixes from the 2026-06-08 review milestone, and re-freezes the golden corpus.

  1. Deterministic snowflake detection — no wall clock (SPEC-F1). Snowflake classification previously read the current time, so an identical boundary decimal could render as snowflake at one moment and hex at another — a violation of the determinism MUST. v8 classifies a 17–20-digit decimal as a snowflake iff its value fits in a signed 64-bit integer (sign bit clear, < 2⁶³) — a property of the bit pattern, not of the date — and pins the epoch, bounds, and predicate in the spec. The comparison is identical either way; only the type label and tokenization differ. See the decimal-alphabet note in the tokenization step and this.i:sn0wfl4k.
  2. Blank-map marker positions as (row,col) attributes (SPEC-F2). The two map markers had emitted boolean data-blank-map-min/max="true", leaving a Tier-A checker to reverse-engineer the marked cells from pixel geometry. v8 carries each marker's position as a literal "row,col" string in data-blank-map-min (min) and data-blank-map-max (max), and flags the map-bearing blank with data-cell-blank-map. See the SVG profile and the blank-cell step.
  3. base32 disproof path canonicalizes to UPPER (SPEC-F3). A bare base32 fragment matched by alphabet-disproof was lowercased, but the spec mandates base32 → UPPER (RFC 4648); the wrong case changes SHA-512(core text) and therefore every channel, so a disproof-matched fragment diverged from the same value parsed by a specific base32 parser. v8 canonicalizes the disproof path to UPPER for per-alphabet consistency. See the alphabet-detection-by-disproof step.
  4. Blank-map max marker is a shape, not just a hue (PSY-F1). The blank-cell map — the landmark a habituated reader checks first — distinguished max from min by colour alone (red vs blue dots), which collapse to near-equal grays under achromatopsia (ΔL* ≈ 7.8), so a reader could see that two cells were marked but not which was which. v8 gives the max marker a distinct shape, a red plus, while the min stays a blue dot, so the distinction survives total colour blindness; colour is retained as a redundant cue. See the blank-cell step and its rationale.

What's new in v7

  1. The fingerprint hashes canonical normalized text, not decoded bytes (now explicit). Earlier spec wording ("SHA-512 of the normalized entropy bytes") was ambiguous; v7 makes it normative (MUST hash the UTF-8 of the normalized core text; MUST NOT decode to raw bytes first). A consequence, now stated as an explicit non-goal: cross-encoding invariance — the same 32 bytes as hex vs base64, or one CID in two multibases, render as different entvizes. This is deliberate and fail-safe: it keeps every channel agreeing on identity (the text channel is verbatim and can't be made encoding-invariant), removes a decoder-malleability collision surface (distinct strings decoding to identical bytes), and keeps the multi-implementation fingerprint byte-reproducible. No behavioral change to the renderer — this ratifies and documents existing behavior. See the new spec subsection Why the fingerprint hashes text, not decoded bytes, threat-model.md, and this.i:h4shtext.
  2. Identity-bearing prefixes bind the fingerprint (and, where possible, the cells). Each part of the input is classified presentation / identity / annotation (the swap test: hold the body fixed; if another legal prefix could sit there and change the value with the body unchanged, it's identity; if the body would have to re-encode, it's presentation — this cleanly separates multibase from multicodec). Identity material binds the fingerprint by one of two mechanisms: (a) kept in the core when it shares the body's alphabet and is contiguous, so it is rendered in the cells and hashed — used by the CESR derivation code (cell 0 now shows the code) and the LEI LOU issuer code (the core is now LOU + "00" + entity, not just the entity body); or (b) prefix-fold (SHA-512(prefix ‖ core)) when it is a different alphabet from the body — used by the SWHID/gitoid object-type and hash-algorithm (without which a SWHID and a gitoid over the same git hash would collide). Multibase is presentation (already stripped); a CID's multicodec is identity but already lives inside the base32 core, so it was already bound. Affects CESR, LEI, SWHID, and gitoid entvizes; all presentation-prefix types (Ethereum, etc.) render identically to v6. See this.i:s3mpr3fx.
  3. CESR code-table corrections. The blinding factor is the lowercase code a (44 chars); it had been mis-entered as capital Z, which is actually Tag11 (12 chars) — so real a… blinding factors failed to parse and a bogus 44-char Z… string would have been accepted. The FN-DSA (FIPS 206) post-quantum codes b/c/d/e and 1AAQ/1AAR were added, and 1AAB/1AAJ relabeled "pub/enc key" to match the spec table. The gallery KERI/CESR taxonomy was corrected: B is a non-transferable AID, the usual transferable AID is the self-addressing E (Blake3-256 of the inception event), and a bare D is a verification key — not "the transferable AID". Authoritative source: the CESR specification master code table.
  4. Conformance formalization (editorial — no output change). The spec gained RFC 2119 requirements language (a Notation and requirements language section; load-bearing instructions re-voiced to MUST/SHOULD, rationale marked Note/Rationale as non-normative) and a normative Conformance section defining: the abstract render model an implementation must compute; the three conformance tiers (A semantic model, B canonical raster as the visual authority, C browser smoke) and conformance levels; the equivalence relation (which SVG-serialization differences a checker ignores — namespace, attribute order, numeric formatting, salted ids, grouping); the SVG profile (required data-* attributes, viewBox, and the normative paint order that fixes layering/occlusion); and the error conditions every implementation MUST enforce (EIP-55 reject, note-sanitization reject, out-of-range parameters). This adds no algorithm and changes no rendered pixel; it backs the cross-language conformance suite (compliance/) and the Rust/TypeScript implementations. See this.i:c0nf0rm1/c0mpsu1t.

What's new in v6

  1. Long-input middle = the fingerprint, not body slices. For >512-bit inputs, v5 filled the 4 middle text cells with body slices sampled at fingerprint-derived offsets. That made the middle text differ between inputs only probabilistically — a low-entropy/structured body could render identical middle cells for two different inputs, so a screen-reader / read-aloud comparison (which can't see the gestalt) could miss a difference. v6 fills the middle from a second, domain-separated fingerprintSHA-512("entviz/fingerprint-middle/v6\0" ‖ core) — rendered as hex (6 chars = 24 bits per cell), so the middle text is injective and guaranteed to avalanche on any input change for every alphabet, and is independent of the primary fingerprint that drives the gestalt. Head and tail stay real entropy (recognition + verification); matching the 4 displayed fingerprint tokens is an injective ≈2⁹⁶ partial preimage, independent of matching the gestalt. The middle nuclei are painted with the entviz background color (neutral, framed with a gold/white border) since they no longer carry entropy in their bg. Blank placement for long inputs also now uses the same median/quartile shift as short inputs — v5's two fixed separator blanks are gone — so a long input's blank layout varies per input instead of being identical for every long input. See the "Large-input handling" subsection in the spec.
  2. Wider color bar. The color bar's width doubles from box_height to bar_width = 2·box_height, so the per-band color letters (w/g/r/b/k) render legibly. The bar width is now a named geometry term. The letters render at the cell-text size, bottom-anchored within each band. See the geometry and color-bar steps in the spec.
  3. Redesigned blank-cell marker. v5's white-disc-with-clock-hands marker is replaced. Every blank cell carries a black-outlined rounded rectangle; the first blank cell additionally becomes a map — a miniature scale model of the grid, filled white (or gold on a white-background entviz), with a red dot at the maxftok cell's position and a blue dot at the minftok cell's. This conveys positions (not directions) and uses no mix-blend-mode, so it renders identically in browsers and non-browser rasterizers (closing adversarial finding F-A6 for this channel). See the blank-cell step in the spec.
  4. Clamped ellipse overlay. The ellipse's rx/ry bounds change from [nucleus_height, d_far − cell_width] to [0.22·d_far, 0.58·d_far], so the overlay covers a noticeable but partial share of the grid on every grid size — never an invisible sliver, never swamping the grid. See the ellipse-overlay step in the spec and reviews/ellipse-audit-2026-06-02.md.
  5. Refreshed font fallback chain. The monospace fallback chain is reordered and extended to cover iOS and Android explicitly (not just Linux/Windows/macOS). See the font-family note in the spec.

What's new in v5

The v5 changes are concentrated in three places:

  1. Head + tail + fingerprint-selected middle slices for long inputs. v4 truncated >512-bit inputs to head-256 + tail-256 only; an attacker who could match those 64 bytes had a byte-identical text channel and only had to grind the fingerprint-driven gestalt channels. v5 reserves four of the 22 cells for middle slices sampled at byte offsets derived from the fingerprint, so two long inputs that share their head and tail still differ in the text channel unless the attacker can also match four specific 3-byte windows inside the body — and those windows move as a function of the full input. See the tokenization step in the algorithm and the "Large-input handling" subsection in the spec.
  2. Loud truncation marker. The quiet ^…$ marker on the top label strip is replaced by a bold red fingerprint of prefix (read as "fingerprint of \<the type>" once the type label follows it). The marker now reads as a warning rather than a footnote. The original entropy byte length is conveyed by the type-label parenthetical (e.g. hex(200), b64(119)) immediately after the marker, so it is not repeated inside the marker itself. See the label-strips step in the spec.
  3. Color-bar letters. Each color-bar band now carries a centered lowercase letter naming its color (w, g, r, b, k). This gives the color bar a verbal label that survives complete color blindness, monochrome display, and CSS color filtering, and makes spot-check verification ("what's the top band?") possible without trusting hue discrimination. See the color-bar step in the spec.

The v4 base — described in the main spec — is otherwise unchanged from the v4 spec. Implementations that supported v4 need only make the three changes above to support v5.


What v4 changed

v4 kept v2's fingerprint, large-input handling skeleton, and overall structure, and kept v3's color bar skew, color bar frame, ellipse overlay rework, and hex font fitting. The substantive v4 changes are concentrated in the edge channel and a couple of geometry/layout choices that follow from it:

  • The v3 edge channel — 6 edge rects per cell, each filled with a cubist or polygon shape using a per-edge XOR rotation through a 4-color edge palette — is replaced by a 24-box surround per cell. Bit i of the ftok's quant (LSB = bit 0) controls whether box i is filled or empty. The surround tiles the entire region around the nucleus with no corner rects.
  • Each cell has a single edge color chosen as the palette entry (one of the 4 non-bg colors) perceptually closest to that cell's nucleus background. Per-edge color rotation, color_shift, and shape_shift are gone.
  • Nucleus geometry decouples from font size in the vertical direction: nucleus_width = 3·font_size_px (unchanged) but nucleus_height = 1.25·font_size_px (was equal to font_size_px). The 25% vertical extra makes room for the glyph descenders of monospace fonts whose bounding boxes extend below the em-box (most of them); v3 had this descender-protrusion latent but it was masked by sparsely-filled edge shapes. With v4's densely solid surround, descenders protruding into the surround region became visible.
  • Cell aspect ratio is therefore 3:2 (not v3's 2:1). cell_width = nucleus_width + 2·box_width = 10·box_width = 3.75·font_size_px; cell_height = nucleus_height + 2·box_height = 4·box_height = 2.5·font_size_px. Surround boxes are no longer square: box_width = nucleus_width/8 = 0.375·font_size_px (derived from the horizontal tiling — 10 top-row boxes span nucleus_width + 2·box_width); box_height = nucleus_height/2 = 0.625·font_size_px (derived from the vertical tiling — 2 side-column boxes stack to nucleus_height).
  • The shape count summary (SCS) is removed entirely; there are no shapes left to count. The bounding rect contracts by (nucleus_height + GM) on the bottom.
  • The color bar's data source changes: v3 tallied per-edge color usage; v4 tallies the four 2-bit patterns (00, 01, 10, 11) across the 256 disjoint 2-bit slices of the SHA-512 digest. The count⁴ skew, descending sort, and rendering geometry are unchanged.
  • A small SVG-portability fix for the ellipse overlay: the clipPath id is salted with the fingerprint and grid dimensions so that multiple entvizes embedded in the same HTML document do not collide on a shared id (which silently makes the browser resolve every url(#…) to the first matching id document-wide).