# XZBT 0.1 §§17–19 visual triage — fix log (what was applied, and the design chosen for each) Record of edits to `docs/XZBT_0-1_Format_Specification.md` closing the thirty repair packages (V1–V30) and three additional defects (A1–A3) of [01-triage.md](01-triage.md), in the batch order that document's closure table sets. Follows the [04-fix-log.md](04-fix-log.md) pattern for the §§14–16 audio pass. **Standing decision for this pass:** the user directed that the triage's own *Recommended fix* be adopted for every Tier-1/2/3 design decision and logged rather than reviewed individually. Every row below therefore records a **chosen design**, not a deferral. Where the triage offered a recommendation, it was taken; where it named a decision without a recommendation, the choice made is stated with its reason. - Baseline: working tree at commit `527220e`, spec revision **0.7**. - Result: spec revision **0.8**. Doc-only — no schema, runtime, fixture, or test file was touched by this pass. `npm test` unchanged at **102 passing, zero failures**. - Net diff: **704 insertions / 88 deletions**, one file. - New tool: `tools/verify-spec-contract.py`, the contract harness previously run ad hoc, now committed. Post-edit run: 126 headings, 46 declared diagnostic codes with 43 used and 3 declared-only, **107 distinct cross-references all resolving, zero unresolved**, 170 balanced fence markers, 112 well-formed tables. Completion state of every package below is **spec fixed**. None is *implementation verified*: no visual runtime exists yet, and no trace in §§17.16, 18.10, or 19.7 has run. --- ## Batch 1 — contradictory schema surfaces and impossible traces | Package | Chosen design | Sections edited | | --- | --- | --- | | **V1** | `automation` added to the §17.3 allowed-field table as an optional array defaulting to `[]`, with a note that the system-scoped array is a field of the system and not of this container. The two scopes and their time origins are untouched; no capability row was added. | 17.3 | | **V2** | Trace 17.16.14 rewritten around legal *and* illegal capabilities per target family, keeping the per-object rejection case and adding the type-mismatch and undeclared-reference cases. The stale "the table is unchanged by this slice" assertion is deleted, and the trace now says explicitly that accepting a target is not evidence its pipeline runs — that is trace 7 of §19.7. | 17.16.14 | | **V5** | `fill` and `stroke` retyped `ValueSpec`, paint object, or `null`. A new **Color leaves and ValueSpecs** block names the exact set that accepts a ValueSpec — `fill`, `stroke`, `glow.color`, `shadow.color`, and paint `stops[].color` — and fixes three limits: a `ValueSpec` never returns a paint object (`ERR_TYPE_MISMATCH`), a paint's structural fields stay literal, and system-level colors (`scene.background`, `depthFog.color`, effect `color`) stay literal. Once-at-instantiation sampling preserved. | 17.12 | | **V11** | The `step` registry clause is deleted, not rescued. A repeater now has **no** automatable property, with the reason stated (copies are created once at instantiation, so no later change can reach them) and the `ERR_UNSUPPORTED_TARGET` / `ERR_INVALID_REFERENCE` distinction spelled out. No repeater layout language was invented. | 19.1 | | **V12** | One rule replaces "only on a persistent scope": `infinite` is legal in **every** scope, bounded by the lifetime of whatever owns the track. A four-row table covers exhibit scope, persistent, finite-lifetime spawned, and indefinite spawned. No diagnostic; `ping-pong`'s round-trip count unit untouched. | 19.1 | | **V17** | Two conditional rules. *Viewport fit:* absent `fit` is accepted with no mapping effect and the `contain` default is not taken; only an explicitly authored non-`stretch` value is `ERR_SCHEMA_VALIDATION`. *Layer omission:* when `visuals.layers` is present a system's `layer` is required and an omission is `ERR_INVALID_REFERENCE` — no silent first-layer fallback — and a present-but-empty map is `ERR_SCHEMA_VALIDATION`. | 17.4, 17.5, 17.7 | | **V26** | Trace 17.16.9's nonexistent "`9` layer nesting levels" replaced by "`17` layers", with a note that group depth is trace 4's case. | 17.16.9 | | **A1** | **The design decision of this batch.** All five spawned-instance lifecycle fields move into a dedicated **`spawn` object** on the system: `spawn.lifetime`, `spawn.release`, `spawn.ownership`, `spawn.inputs`, `spawn.cancelWithScenario`. A top-level `lifetime` is unambiguously *per item* (particles, emitter) and `spawn.lifetime` is *per instance*, so a spawned emitter expresses both, and a spawned repeater takes `spawn.lifetime` without contradicting §18.5's ban on a repeater `lifetime`. A template's own `inputs.*` reference scope is stated and kept disjoint from a component's. Patching only the persistent-system prohibition — which would have left the spawned ambiguity intact — was rejected as the triage instructs. | 17.7, 19.2, 19.7.8 | | **A2** | `direction` declared on the noise field: a number in degrees, **required** under `mode: "value"` and `ERR_UNKNOWN_FIELD` under `curl`/`gradient`. The conditional `trail` on a particle's `render` object is **removed** rather than declared: one placement (the system's `trail`), no precedence rule, `ERR_UNKNOWN_FIELD` elsewhere. | 18.7, 18.8 | ## Batch 2 — renderer geometry and composition | Package | Chosen design | Sections edited | | --- | --- | --- | | **V3** | A full named-space composition replaces the ambiguous matrix prose. Four spaces (local, scene, CSS, device) are named; the fit matrix `F = T(ox, oy) × S(sx, sy)` carries the centered `contain`/`cover` offsets that were missing; the camera center is mapped through `F` before the translation is formed (`q = F(cameraCenter)`, translation `q − c`), which is the unit mismatch the review found; the normative chain is `x_device = B × P × V × F × M_effective × x_local` with DPR applied once by `B`; perspective is a stage `P` about the projection center, not a vague post-multiplication. Nonuniform `stretch` is stated to produce parallelograms from rotated squares, and axis-less appearance scalars take the single uniform factor `g = zoom · k · sqrt(sx · sy)`. Perspective **does** scale stroke, blur, glow, and shadow. | 17.2, 17.6, 19.3 | | **V4** | Local extents and anchors fixed for all fourteen primitives, with requiredness and defaults given rather than inferred (`rectangle` anchors **top-left**, circular primitives **center**, `ring` angles default `0`/`360`). Arc sweep is directed: `|d| > 360` is `ERR_OUT_OF_BOUNDS` **before** normalization, `|d| == 360` is a full turn, `d == 0` draws nothing. `catmull-rom` is a **uniform cardinal spline** with the cubic Hermite equation and the tension form `m = tension · (p_next − p_prev)` written out, plus the open-duplication and closed-wrap index rules. A `closed` `bezier` spline closes with a **straight line**. Open-subpath fill closure, `point` stroke behavior, and `maxWidth` condensation (`maxWidth / w` about the align anchor) are all fixed; the text-metric device dependence is argued to need no new reproducibility exemption. | 17.9, 17.13 | | **V9** | Sortable units defined for every system, not just particles. A `graphic` system's top-level objects sort individually; each `particles`, `emitter`, and `repeater` is **atomic**, with representative depth = the **lowest** `zEffective` among its live items (generalizing §18.2's existing rule rather than replacing it) and internal order by depth then creation ordinal. Cross-unit ties, empty systems, and links all covered. Per-point `z` is given exactly one effect — per-point perspective under `perspective` — while sorting and fog use the object's own `zEffective`, which keeps the one-object fog rule intact. | 17.6 | | **V10** | An eight-stage normative per-object compositing order plus a layer stage. The effective alpha is own opacity × ancestor opacities × **release factor**, and layer `opacity` is explicitly **not** in it (applied once at the layer stage), removing the §17.5/§17.12 double-application. Buffer accounting covers layer buffers and object buffers in one 16-slot pool, with reuse (concurrent, not cumulative), allocation order (layers first, then draw order), refusal semantics per feature, and farthest-first shedding. Buffer refusal is named a **diagnosed exception** to §17.1's appearance guarantee. `WARN_VISUAL_APPROXIMATION` is reused, not re-minted. | 17.5, 17.12 | | **V18** | Fog arithmetic fixed: per-component blend in **non-premultiplied sRGB**, no linearization, alpha never fogged, fog color's alpha ignored, and **gradient stops fogged individually before the paint is constructed**. Conic fallback axis fixed against the object's local-space AABB, with explicit rules for a center outside the box, a non-intersecting ray, and a degenerate box; stops and offsets preserved exactly, against the reviews' suggestion. | 17.6, 17.12 | | **V20** | `focalLength` range closed at **`1` to `100000`** scene units. Literals outside are `ERR_OUT_OF_BOUNDS` at import; resolved values from `random`, binding, automation, override, or modulation clamp into it as the §8.1 safety-clamp stage, with no implementation epsilon anywhere. | 19.3 | | **V29** | `size` is a **uniform local geometry scale** on the `render` object, applied innermost, with a per-render-type mapping table (including `pointSize`, ellipse radius, text size, and components) and an explicit exclusion of appearance dimensions. Velocity alignment — `emitter.align`, `face-motion`, ribbon perpendicular — uses **XY velocity only**, `atan2(vy, vx)`, with a stated zero-XY-speed rule (hold previous rotation; authored rotation at creation). | 18.2 | | **A3** | The `4096 × 4096` backing store wins over the "floor of `1`" multiplier: on a CSS display larger than 4096 the multiplier goes **below 1** and the frame is upsampled, raising `WARN_VISUAL_APPROXIMATION` once per resolution change; the multiplier is never raised above `min(devicePixelRatio, 2)`. "Pinned to the display" becomes **"pinned against camera translation"**, with zoom and rotation explicitly still applying at `parallax: 0`. | 19.3, 19.5 | ## Batch 3 — procedural evaluation | Package | Chosen design | Sections edited | | --- | --- | --- | | **V6** | A three-class field-ownership table replaces "every ValueSpec above resolves once per particle": **system-instantiation** values (including `count`, `rate`, and burst `count`, so creation counts are known before allocation), **system channels** (`position`, `acceleration`, `drag`, `visible`), and **per-item** values. A second table fixes what a live channel does to existing versus future items — `rate` and `position` reach only new items, `acceleration` and `drag` reach every live item on the next tick — and states that a track composes against the sampled base under its `mode`. Burst-before-continuous creation order and ordinal assignment fixed. Applied to §18.4 with `emit` substituted. | 17.14, 18.2, 18.4 | | **V8** | Coherent noise written out as an algorithm: the twelve gradients in fixed order and unnormalized; the Fisher–Yates permutation with an explicit **255-sample** draw count (correcting "one sample per entry"); the full lattice hash and trilinear/quintic evaluation; `h mod 12` selection defended as deterministic rather than unbiased; octave normalization by accumulated amplitude; sample coordinates; and the three `mode` vector constructions, with **`curl` as the explicit perpendicular of the scalar potential's gradient** by central difference at `h = 1e-3`. Published tolerances: `1e-9` for scalar oracles, `1e-4 · amplitude / scale` for curl divergence, tested at non-lattice as well as lattice points. Behavior noise gets **one shared permutation table per exhibit** plus a per-behavior **offset pair** (2 samples, not 1), with per-behavior coordinate formulas — the shared table chosen so that a large particle system does not derive 255 samples per item. | 18.6, 18.7, 18.10.15, 18.10.16 | | **V13** | `n = 1` placement uses fraction `0`, matching `repeat.fraction`. Even ring/path/line/grid placements, even-mode ring radius (mid-annulus), grid wrap when `n ≠ rows × columns`, the three `depth` inverse functions, ellipse perimeter (uniform in parameter, one sample, stated as the choice), rectangle perimeter (**one** sample, correcting the two-sample assertion), and a normative path arc-length flattening tolerance of `0.1` scene units are all written out. "Field order" is replaced by an **explicit per-distribution draw list**, with declaration order distinguished from variate order and angle-before-radius kept. The `grid`-with-jitter and `depth` sub-block sample costs are reconciled with trace 18.10.10, which is rewritten. Burst creation-index rule fixed. | 18.3, 18.10.10 | | **V14** | A complete field contract table for all seventeen behaviors (type, requiredness, default, range). Waveforms given as equations in cycles with a common phase origin and all four zero-crossing at `phi = 0` where they can be. `pulse` rise/fall envelope and `face-motion` frame-rate-independent smoothing (`1 − smoothing^dt`) fixed. An **accumulating versus fresh** table prevents an orbit being integrated as a per-tick displacement, with `orbit`'s contribution written out. A behavior **channel write-set** table supports conflict detection, and `velocity.*` and `points[*]` are noted as outside the automatable registry, so those behaviors can never conflict — which also disposes of the reviews' vector-track counterexample. `vortex` gets an explicit y-down perpendicular `(r.y, −r.x)` with the note that a force strength is not an angle. | 18.6, 18.7 | | **V15** | Morph restricted to point-list geometry: `polyline`, `polygon`, `spline` (matching `mode`), and `path` only when every command is `move`/`line`/`close` **and** the two op sequences are identical. Everything else is `ERR_MORPH_INCOMPATIBLE`. Target points are read **live**, not snapshotted, with document-key evaluation order making it decidable and a mutual morph pair `ERR_CYCLIC_DEPENDENCY`. | 18.6, 18.10.14 | | **V16** | The distribution `path`'s sibling-key shorthand is **removed** — a system's siblings are systems, so the container never existed — leaving inline `commands` under the full §17.13 contract. The object-attached `follow-path` sibling reference, which does have a container, is preserved explicitly. | 18.3 | | **V21** | Link `style` defaults to the resolved style of the system's own `render`/`repeat` object, naming the exact source. `fadeWithDistance: true` without `maxDistance` is `ERR_SCHEMA_VALIDATION`, with the alternative normalization rejected and the reason given. `index`, `stride`, `closed`, and nearest tie-breaks operate on a **densely re-indexed live ordering**, so deaths close gaps. Nearest ties break by ascending creation ordinal. The `256` pairwise-population bound is stated as **intentionally conservative** (checked against the upper bound, not the live count) and enforced at import for literals and at the instantiation boundary for a procedural `repeater.count`. | 18.8, 18.10.18 | | **V22** | One canonical component-input location: the **`component` object's own `inputs`** (§18.1), everywhere. The system-level `inputs` fields on `emitter` and `repeater` are removed and are now `ERR_UNKNOWN_FIELD`. Chosen over the reverse because it leaves both normative examples valid as written, keeps `component` uniform wherever it appears, and leaves `spawn.inputs` (A1) as the only other `inputs`, in a different container with a different meaning — so there is no precedence rule, no duplicate-key case, and no masked-sampling question. | 18.4, 18.5 | ## Batch 4 — live resources and later execution | Package | Chosen design | Sections edited | | --- | --- | --- | | **V7** | Authored-record validation and live-instance accounting separated into two checks over two populations: an **import authoring bound** counting each declaration once, and a **live budget** counting instantiated records from `CREATED` to `DISPOSED`. A spawn crossing either is refused **atomically** — nothing allocated, no partial instance, **no spawn ordinal consumed**, `WARN_VISUAL_CEILING` under the §19.5 cadence, not a scenario failure. Admitting an instance with tracks silently dropped is explicitly forbidden. Reclamation at `DISPOSED`, not `FINISHED`; `FAILED` reclaims immediately. The §19.5 row is amended from "authoring bound, not a shed" accordingly. | 19.1, 19.5, 19.7.6 | | **V19** | Radius conversion fixed as `r_device = r_scene · zoom · sqrt(sx · sy) · dpr_effective`, with the two terms an effect **cannot** have — object depth `k` and layer `parallax` — excluded explicitly and by name. Reduced-resolution processing multiplies by the resolution factor inside the buffer. Effect ValueSpecs, `enabled` included, resolve once at activation in array-then-field order. `saturation`/`hueRotate` mapped to the §17.12 `saturate`/`hue-rotate` operations without renaming the authored fields. Device-pixel scanline spacing left alone as a valid choice. | 19.4 | | **V23** | `spawn.ownership` and `spawn.cancelWithScenario` are kept as **two different relationships** — resource ownership versus an independently retained originating-scenario relationship — which is what makes the flag non-redundant on a persistent-owned instance; the documented false case is preserved. Idempotent `remove`, the direct `CREATED → FINISHED` and `→ FAILED` paths, and post-`FAILED` accounting are stated in prose, not only in traces. The **release factor** is defined here (separate multiplier, initialized to `1`, monotonic, never writes an authored `opacity`), which disposes of the claimed opacity pop without adding a public system opacity property. | 19.2 | | **V24** | One cadence for every resource diagnostic: keyed by (code, subject), **at most once per logical second**, with the 120-tick sustained report as the *same* code plus a `sustained` detail rather than a second code, not bypassing the rate limit, and both counters resetting after one clear tick. Approximation shedding uses the same cadence; the per-instance capability warnings of §17.12 and §19.4 are explicitly left alone. §19.2's bare "once" per refused spawn is reconciled to it. | 19.2, 19.5, 19.7.18 | | **V25** | *Mechanical:* the central table gains path commands (512), text characters (256), `strokeDash` entries (8), burst entries (16), grid dimensions (256), custom oscillator partials (64), `nearest` links per item (8), pairwise-linked population (256), and visual automation points per track (256); the resonator row is fixed to `16` fixed in §15.10 and de-duplicated. *Design:* the static-draw-load gap is closed with two new authoring bounds — **`64` declared visual systems** and **`16384` expanded static visual objects** after component and repeater expansion, with the counting rule stated — both marked provisional pending 4h in exactly the sense the aggregate values are. | 17.3, 19.5 | ## Batch 5 — contract synchronization | Package | Chosen design | Sections edited | | --- | --- | --- | | **V27** | A single staging table for resolved-value validation: non-integer resolved integers are `ERR_TYPE_MISMATCH` at the instantiation boundary with **no rounding rule** (§2 strictness preserved); over-length resolved `text` is `ERR_OUT_OF_BOUNDS`; out-of-range component input values are `ERR_OUT_OF_BOUNDS` at import or instantiation. The clamp-versus-reject split is explained by whether the value changes the *shape* or the *magnitude* of what is built. `ERR_INVALID_PRIMITIVE_TYPE`'s cause broadened to name the fourteen primitives plus `component` in both §7 and §17.15. Audio/visual diagnostic parity was **not** forced, and `ERR_UNSUPPORTED_TARGET` for unsupported persistent ownership was left alone, as the triage directs. | 7, 17.14, 17.15 | | **V28** | §17.14 now describes a **sampled base** with the effective value produced by behaviors, automation, and the pipeline; its "no visual analogue" prose points at §19.4's grain exception as the one exemption; the `instances.*` citation is corrected to §8.1's action-addressing namespace rather than a §1.3 document namespace; §19.2's `lifecycle` row is marked a restatement of §17.7's. No new live boolean writer, seeded grain stream, or altered lifecycle enum. | 17.14, 19.2 | | **V30** | A cross-slice contract map added to §19.7: what 4d, 4e, and 4f each must *implement* versus *carry as data without executing*. Trace 17.16.6's perspective requirement is explicitly kept in 4d — dropping it would have removed the only automated check on the composition chain where V3's defects lived — and the map ends with "a parsed stub is never a passed runtime trace". | 19.7 | --- ## Findings the triage cleared, and that this pass did **not** act on All twenty-plus cleared findings in §"Findings checked and cleared" of `01-triage.md` were left unimplemented, as instructed. In particular the locked conventions survive unchanged: degrees with positive angles toward `+y`; increasing `z` farther from the camera with greater-`z`-first sorting; the fixed `1000/60` ms tick; seeded decisions with no per-frame sampling; the four-family §8.1 visual capability surface with no per-object row; the non-evicting spawn policy; `ping-pong`'s round-trip count unit; links drawn before their items; and the visual component namespace `components.visual`. ## What is still open after this pass 1. **No implementation exists.** Every package is *spec fixed*. Slice 4d has not started; the JSON Schema still has a Phase 0 `visuals` stub, `validator.js` still has no `validateVisualSubsystem()`, and `resolution.js` still rejects all four visual target families. That is the Stage 0 code-alignment work of [02-application-code-triage.md](02-application-code-triage.md), which this pass deliberately did not touch so that the contract change lands as its own commit, per the project's contract-before-implementation convention. 2. **Every §19.5 value remains provisional**, the two new static bounds included. No evidence record may call any of them measured until trace 21 of §19.7 runs on real hardware in slice 4h. 3. **Traces are not run.** §§17.16, 18.10, and 19.7 are amended contracts, not results.