# Phase 4a evidence — Visual subsystem contract, part I (Format Specification section 17) **Date:** September 6, 2026 **Slice:** 4a — Contract I (documentation only; no runtime change) **Baseline:** `main` at `6aab663` (Phase 3c slice 4) **Specification revision:** 0.4 → 0.5 **PRD coverage:** 69-76 ## What this slice fixes Section 17 opens the Visual contract required by section 11. It covers the visual pipeline and its renderer-neutral scope, canonical units, the `visuals` container, layers, the scene model and the three coordinate spaces, 2.5D depth, the fourteen geometry primitives of Visual Primitive Set 0.1, common visual properties, the transform model, appearance and paint, paths and splines and text, and the resolution boundary for visual ValueSpec fields. Sections 18 (Phase 4b) and 19 (Phase 4c) are named throughout as the owners of procedural systems and of automation, lifecycle, camera, post-effects, and the centralized ceilings. Those forward references are deliberate and mirror how section 14 referenced sections 15 and 16 before they were written; they are the only unresolved numeric references in section 17 and they close when 4b and 4c land. ## Decisions taken, so 4d does not relitigate them 1. **Angles are degrees everywhere**, `0` along `+x`, positive turning toward `+y`. No visual field is authored in radians or turns. This matches the audio contract's `phase` field (14.7). 2. **Increasing `z` is farther from the camera.** Depth sorting draws greater `z` first and breaks ties by document key order, using the same document-order rule sections 9.3 and 14.4 already rely on. Perspective scaling is `focalLength / (focalLength + z)`; `orthographic` leaves `z` affecting sorting, parallax, and fog but never scale. 3. **Visual objects are keyed, not listed.** `content` and `children` are objects keyed by ID and a visual object carries no `id` field. This is the same node-identity rule section 14.3 adopted after the contradictory version was one of the four blocking defects the Phase 3a review closed; adopting it up front here avoids repeating that defect in a second subsystem. 4. **Transform composition order is normative:** `M_local = T(position + translate) x T(origin) x R(rotation) x K(skew) x S(scale) x T(-origin)`, with `M_parent x M_local` down a group hierarchy. Trace 3 pins it with a case where rotation and non-uniform scale do not commute, so a renderer cannot fold the order differently and pass. 5. **Visual ValueSpec fields resolve once, at their object's instantiation boundary**, exactly as section 14.4 fixes for audio node fields. Rendering consumes no procedural stream, so procedural decisions stay identical across render rates and machines. Time variation comes from behaviors (18.6), visual automation (19.1), and the section 8.1 rows that 19.1 adds — from nothing else. 6. **No visual property is externally addressable in this slice.** A binding, `set`, or `override` naming one is `ERR_UNSUPPORTED_TARGET`, and the section 8.1 table is unchanged. Section 1.3 reserves the `visuals.systems.*` namespace; reserving a namespace does not grant a capability. 7. **`layer` is a system property, not an object property.** Letting siblings inside one transform hierarchy composite into different layers would make the hierarchical transform meaningless. 8. **Depth fog is per object, not per pixel**, so it is exact on any renderer and raises no diagnostic. The conic gradient is the *only* appearance fallback in 0.1, and it raises `WARN_VISUAL_APPROXIMATION` once per paint instance rather than once per frame. 9. **`font` is restricted to the three generic CSS families.** Section 13.3 requires the standalone artifact to make no external request, and 0.1 ships no font file. 10. **Degenerate geometry fails validation rather than being transformed.** A zero-radius `arc` is `ERR_OUT_OF_BOUNDS`, not a silently substituted line, per PRD 89. ## Deliberate scope deferral **Live numeric readouts are not in 0.1.** Because `text` resolves once at instantiation, it renders authored strings and static labels only. The instrument and radar displays of PRD 130 item 7 and Exhibit D are to be built from generic geometry driven by state. The reason is recorded in 17.13: a live readout needs a number-to-string formatting contract (precision, rounding, locale, unit suffixes) that PRD 72 does not specify and that would introduce the first subject-shaped construct into the visual set. **Slice 4g revisits this** if the Exhibit D visuals cannot be built without it. This is the one place in section 17 where a later slice may have to reopen the contract. ## New diagnostic codes Five codes were added to the section 7 table, which remains the single authoritative list: `ERR_INVALID_SYSTEM_TYPE`, `ERR_INVALID_PRIMITIVE_TYPE`, `ERR_INVALID_PATH`, `ERR_VISUAL_LIMIT_EXCEEDED`, and `WARN_VISUAL_APPROXIMATION`. Section 17.15 repeats them for locality, exactly as 15.18 and 16.10 do. `ERR_INVALID_RANGE_ORDER`, `ERR_OUT_OF_BOUNDS`, `ERR_UNKNOWN_FIELD`, `ERR_SCHEMA_VALIDATION`, `ERR_INVALID_REFERENCE`, `ERR_INVALID_ID`, and `ERR_UNSUPPORTED_TARGET` are reused rather than duplicated under visual-specific names. ## Authoring limits fixed in this slice | Limit | Bound | Diagnostic | | --- | --- | --- | | Layers per exhibit | 16 | `ERR_VISUAL_LIMIT_EXCEEDED` | | Group nesting depth | 8 (matches audio component nesting, 15.15) | `ERR_VISUAL_LIMIT_EXCEEDED` | | `polyline` / `polygon` vertices | 512 | `ERR_VISUAL_LIMIT_EXCEEDED` | | `path` commands | 512 | `ERR_VISUAL_LIMIT_EXCEEDED` | | `spline` points | 256 | `ERR_VISUAL_LIMIT_EXCEEDED` | | Gradient stops | 16 | `ERR_VISUAL_LIMIT_EXCEEDED` | | `filters` entries | 4 | `ERR_VISUAL_LIMIT_EXCEEDED` | | `strokeDash` entries | 8 | `ERR_SCHEMA_VALIDATION` | | `text` length | 256 characters | `ERR_SCHEMA_VALIDATION` | Runtime ceilings — active particles, emission rate, active spawned systems, link count, trail history, blur radius, post-processing passes, and rendering resolution (PRD 89, 120) — belong to slice 4c and are not set here. ## Verification performed This slice changes documentation only. No runtime source, schema, fixture, or test file was touched. - Every diagnostic code used in section 17 resolves to a row in the section 7 table (checked programmatically; zero unresolved). - Every numeric cross-reference in section 17 resolves to an existing heading, except the intended forward references to 18.1, 18.2, 18.4, 18.5, 18.6, 18.8, 19.1, 19.2, 19.3, 19.4, and 19.5. - `npm test` was run to confirm the existing suite is unaffected; the result is recorded in the commit. - The section 11 contract register's Visuals row now reads **In progress (Rev 0.5 / Phase 4a)** and names what remains for 4b and 4c. ## Test boundary Traces 1-14 of section 17.16 are automated and belong to slice 4d, where the renderer exists to run them. Traces 15 and 16 — the PRD 130 visual challenge judged on a real display, and the early combined GC6 benchmark at `1920 x 1080` — are user-observed and close slice 4h. Phase 4 is not accepted until both do. ## State at this checkpoint Section 17 is contract only. No visual runtime code exists; `src/runtime` has no renderer module and the JSON Schema has no visual definitions. Nothing has been drawn from any build. Phase 3 also remains unaccepted while its own user-observed gates (traces 15-17 of section 16.11) and the Phase 1 direct-file restart observation are open; no Phase 4 slice depends on them, and no Phase 4 slice can close them. Whether section 17-19 passes a multi-model review before slice 4d begins — the pass that found thirteen defects in sections 14-16, nine of which any single reviewer would have missed — remains an open decision.