docs(visual): close the visual contract with the phase 4c automation, lifecycle, camera, effects, and ceiling rules

Format Specification revision 0.7 adds section 19, the third and final
visual contract section, covering PRD 85-89 and 119-120.

Visual automation reuses the audio track shape of 16.1 and adds only
`loop`. It is declared in two scopes: `visuals.automation`, measured from
activation, and `visuals.systems.<id>.automation`, measured from that
system's instantiation boundary, so a track on a spawned system behaves
identically however late the spawn happens.

Section 8.1 gains exactly four visual rows, all system-level: camera
fields, layer opacity, system visibility, and post-effect parameters. No
per-object geometry, transform, or style property becomes externally
addressable in 0.1, so the statements in 17.14 and trace 19 of 18.10
survive unchanged. This is the decision 17.14 and 18.6 both deferred.

Systems are persistent or spawned by an explicit `lifecycle` enum. A
spawned system is a template, instantiated only by a spawn action, with a
five-state machine plus FAILED, a `0ms` default release, and a spawn
ceiling that refuses rather than evicts.

The camera matrix is normative, including that parallax multiplies the
camera translation only. `projection` is authored once and is neither
automatable nor bindable.

Seven post-effects, applied in normative array order with a stated
approximation boundary. `grain` is explicitly exempt from reproducibility
and consumes no procedural stream.

Section 19.5 is the centralized ceiling table PRD 120 requires. It
separates authoring bounds, which reject an exhibit at import, from
runtime ceilings, which shed work by a documented deterministic rule and
keep the exhibit running, and it restates every ceiling first fixed in
sections 10 and 14 through 18. Its aggregate values are provisional
pending the slice 4h GC6 measurement.

Two diagnostic codes added: ERR_INVALID_EFFECT_TYPE and
WARN_VISUAL_CEILING. ERR_AUTOMATION_CONFLICT widens to cover a visual
scope and the behavior collision rather than minting a second code.

Documentation only: no runtime, schema, fixture, or test file was
touched. The visual contract now contains no unresolved forward
reference, and npm test still passes 102 tests with zero failures.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AzX3848PwqYwzdUCWvjegL
This commit is contained in:
2026-09-06 04:29:40 +00:00
co-authored by Claude Opus 5
parent 2deec016d6
commit d3cd688a3c
4 changed files with 583 additions and 16 deletions
@@ -0,0 +1,207 @@
# Phase 4c evidence — Visual subsystem contract, part III (Format Specification section 19)
**Date:** September 6, 2026
**Slice:** 4c — Contract III (documentation only; no runtime change)
**Baseline:** `main` at `2deec01` (Phase 4a and 4b devlog)
**Specification revision:** 0.6 → 0.7
**PRD coverage:** 85-89, 119-120
## What this slice fixes
Section 19 closes the Visual contract. It covers visual automation and its loop modes, the visual
lifecycle and ownership of persistent and spawned systems, the camera and its two projection modes, the
seven-effect post-processing chain and the exact boundary of permitted renderer approximation, and
visual safety limits together with the centralized runtime ceilings of the whole engine.
With this slice the visual contract contains no unresolved forward reference. Every reference to 19.1,
19.2, and 19.5 left open by sections 17 and 18 now resolves, and the section 11 register's Visuals row
reads **Complete (Rev 0.7 / Phase 4a-4c)**.
## Decisions taken, so 4f does not relitigate them
1. **Automation has two declaration scopes, each with its own time origin.** `visuals.automation` is
measured from activation and reaches scene, layer, camera, and effect properties;
`visuals.systems.<id>.automation` is measured from that system's instantiation boundary and reaches
that system's own properties. The second scope is what makes a track on a spawned system behave
identically however late the spawn happens — the property 16.1 gives an audio track measured from its
sound instance's start. A track naming a target in the other scope is `ERR_INVALID_REFERENCE`.
2. **The track shape is the audio track shape.** `target`, `mode`, `interpolation`, `points`, the three
modes, the four curves, the `exponential` fallback and its `WARN_AUTOMATION_FALLBACK`, the
two-to-`256` point bound, and the strictly-increasing duration-literal `at` are all reused verbatim
from 16.1 rather than restated differently. `loop` is the only field visual automation adds, because
it is the only thing PRD 85 asks for that PRD 54 did not.
3. **`ping-pong` counts complete round trips.** One `count` unit is forward and back, so a finite count
always ends where it began. `repeat` restarts from the first point and may step discontinuously at
the wrap; that is the author's choice, not a defect.
4. **The section 8.1 table gains exactly four visual rows, all system-level:**
`visuals.camera.<field>`, `visuals.layers.<id>.opacity`, `visuals.systems.<id>.visible`, and
`visuals.effects[<index>].<param>`. Nothing per-object becomes externally addressable in 0.1. This is
the decision 17.14 and 18.6 both deferred here, and it is deliberately the smallest surface that
expresses what the reference exhibits need: Exhibit D's instrument displays are state-driven geometry
*inside* a system, which automation and behaviors reach with no external capability at all. A later
revision can widen the surface compatibly; nothing can narrow it.
5. **`visuals.layers.<id>.visible` is deliberately absent** from that table. A layer hides by animating
`opacity` to `0`, which is continuous and needs no second mechanism.
6. **The automatable registry is broader than the section 8.1 rows, and that is the point.** Automation
is declared inside the subsystem that owns the property, so adding it never widens the subsystem's
external surface — the rule 16.2 states for audio. An emitter's `rate` is automatable and is still
`ERR_UNSUPPORTED_TARGET` for a binding, `set`, or `override`, so trace 19 of 18.10 passes unchanged.
7. **A behavior and an automation track on the same object channel is `ERR_AUTOMATION_CONFLICT`.** A
behavior accumulates or multiplies onto a channel while an automation `mode` displaces the base; two
writers with different composition rules on one scalar have no defined answer. Where they do not
collide, the shared pipeline resolves first and behaviors compose over the resolved value in array
order. This is the composition rule 18.6 defers here.
8. **`lifecycle` is an explicit enum, not an inference.** A system is `persistent` unless it says
`spawned`; lifecycle fields on a persistent system are `ERR_UNKNOWN_FIELD`. Making the discriminator
the presence of a field would have been implicit magic that no validator could report clearly.
9. **A spawned system is a template.** It is declared, validated, and counted at import, and is not
instantiated or drawn at activation. Its instantiation boundary is the spawn moment and its stream
child key is `<system-id>#<spawn-ordinal>`, mirroring the particle rule of 18.2.
10. **The visual lifecycle has no `SCHEDULED` state.** The audio machine has one because a voice is
committed to the audio clock before it sounds; a visual system has no second clock to commit to.
Five states plus `FAILED`, with the same terminal and no-op-on-second-stop rules as 16.3.
11. **Visual release defaults to `0ms`, where audio defaults to `50ms`.** An audio release prevents a
click, which is a defect; a visual pop is merely abrupt and is sometimes what the author wants. A
`0ms` release still passes through `RELEASING` for one tick, so a single teardown path handles every
removal — the property 16.4 exists to protect.
12. **A spawn at the ceiling is refused, never evicted** — the opposite of the voice policy of 16.6, and
for a stated reason: an evicted voice fades out in milliseconds and is forgiven, while a visual
system evicted mid-scene disappears in front of the viewer. A refused spawn is not a scenario
failure.
13. **The camera matrix is normative**, including where parallax enters it: parallax multiplies the
camera *translation* only, never its zoom or rotation, so a distant layer drifts more slowly without
also being scaled or tilted differently. The projection center is the center of the display
rectangle after `fit` resolution, and every camera operation and the perspective factor of 17.6 act
about that one point.
14. **`projection` is authored once and is neither automatable nor bindable.** There is no value between
`orthographic` and `perspective`: an interpolating mechanism cannot express the change and a stepping
one would snap the whole scene. PRD 87's automatable camera *properties* are the five numeric ones.
15. **Post-effect order is normative and the runtime does not reorder the chain.** `bloom` before
`color-adjust` blooms authored colors; after it, adjusted ones. These operations do not commute, so
array order is contract rather than convenience.
16. **`grain` is exempt from reproducibility and consumes no procedural stream.** Its per-pixel noise
comes from a frame counter and pixel coordinate, not a seeded stream. Section 9.3 promises identical
*decisions*; grain drives no decision, and pulling per-pixel samples from the exhibit's stream would
make stream position depend on resolution and frame rate — breaking every other reproducibility
guarantee to protect one nobody needs. This is the only such exemption in the visual contract, and
it is stated rather than discovered.
17. **Approximation has a boundary.** `blur` and `bloom` may be computed at reduced resolution and
upsampled, raising `WARN_VISUAL_APPROXIMATION` once per effect instance and never once per frame. An
effect a renderer cannot perform is skipped, never silently substituted with a different one.
18. **Authoring bounds fail; runtime ceilings shed.** This is the dividing line PRD 89 implies and 19.5
states: a value the author wrote that is out of range rejects the exhibit, while an aggregate the
document never named sheds work by a documented rule and keeps running, because PRD 119 requires the
runtime to remain usable when performance degrades. Neither is ever silently transformed into
something materially different.
19. **Every shedding rule is deterministic and normative**, so a degraded frame is the same degraded
frame everywhere: oldest-particle-first from the largest system, the ascending pair order of 18.8
for links, farthest-`z` first for compositing buffers, and the trailing effects of the chain for
passes.
20. **A document can never raise a ceiling.** There is no field, parameter, or action by which an
exhibit requests a larger budget; a runtime may lower any ceiling on a weaker device, exactly as
16.6 permits for voices.
## Amendments to sections 1, 7, 8, 11, 17, and 18
All consequences of this slice rather than corrections. No decision recorded in section 17 or 18 was
reversed.
- **1.3** adds `visuals.layers.*`, `visuals.camera.*`, and `visuals.effects.*` to the canonical namespace
prefixes, and records that an ordered container is indexed rather than keyed — the
`visuals.effects[<index>]` and `points[<index>]` forms, which are the only two in 0.1.
- **7** gains `ERR_INVALID_EFFECT_TYPE` and `WARN_VISUAL_CEILING`; `ERR_AUTOMATION_CONFLICT`'s cause
text widens from "in a recipe instance" to cover a visual scope and the behavior collision;
`ERR_VISUAL_LIMIT_EXCEEDED`'s cause text names automation records and post-effect entries.
- **8.1** gains the four visual rows, and its closing paragraph now states that those four are the whole
visual surface rather than that visual properties are unsupported.
- **11**'s Visuals row reads **Complete (Rev 0.7 / Phase 4a-4c)** and names what section 19 fixes and
what remains provisional.
- **17.7**'s system field table gains `lifecycle` and `automation` rows, and its lifecycle-field
deferral is replaced by the rule that those fields are legal only when `lifecycle` is `spawned`.
- **17.14**'s "Until section 19.1 lands, no visual property is in that table" paragraph is replaced by
the four rows that now exist and the statement that no per-object property is among them.
- **18**'s opening paragraph no longer describes 19.1, 19.2, and 19.5 as unresolved forward references.
## New diagnostic codes
Two, added to the section 7 table, which remains the single authoritative list: `ERR_INVALID_EFFECT_TYPE`
and `WARN_VISUAL_CEILING`. Section 19.6 repeats them for locality, exactly as 15.18, 16.10, 17.15, and
18.9 do.
Eleven existing codes are reused rather than duplicated under visual-specific names:
`ERR_AUTOMATION_CONFLICT`, `WARN_AUTOMATION_FALLBACK`, `ERR_UNSUPPORTED_TARGET`, `ERR_INVALID_REFERENCE`,
`ERR_VISUAL_LIMIT_EXCEEDED`, `ERR_NODE_LIMIT_EXCEEDED`, `ERR_OUT_OF_BOUNDS`, `ERR_UNKNOWN_FIELD`,
`ERR_SCHEMA_VALIDATION`, `WARN_VISUAL_APPROXIMATION`, and `WARN_CLEANUP_FORCED`.
## Limits fixed in this slice
Authoring bounds — checked at import, rejecting the exhibit:
| Limit | Bound | Diagnostic |
| --- | --- | --- |
| Automation tracks per exhibit, both scopes | 128 | `ERR_VISUAL_LIMIT_EXCEEDED` |
| Automation points per exhibit, both scopes | 2048 | `ERR_VISUAL_LIMIT_EXCEEDED` |
| Post-effect entries | 4 | `ERR_VISUAL_LIMIT_EXCEEDED` |
| Blur and bloom radius | 32 scene units | `ERR_OUT_OF_BOUNDS` |
| Camera `zoom` | 0.01 to 100 | `ERR_OUT_OF_BOUNDS` |
| Camera `focalLength` | above 0 | `ERR_OUT_OF_BOUNDS` |
| Spawned `release` | 0ms to 10s | `ERR_OUT_OF_BOUNDS` |
Aggregate runtime ceilings — checked every tick or frame, shedding work and continuing:
| Ceiling | Value | Shed |
| --- | --- | --- |
| Live particles, all systems | 8192 | Oldest in the largest system |
| Live emitted items, all emitters | 2048 | Oldest in the largest emitter |
| Live spawned system instances | 64 | Refuse the spawn |
| Link segments per tick | 4096 | Tail of the ascending pair order |
| Trail history samples | 32768 | Oldest samples of the longest histories |
| Field evaluations per tick | 32768 | Nearest items first; remainder is zero force |
| Offscreen compositing buffers per frame | 16 | Drop the excess objects' buffered appearance, farthest-`z` first |
| Post-effect passes per frame | 8 | Skip trailing effects |
| Device-pixel-ratio multiplier | min(dpr, 2) | Clamp |
| Backing store | 4096 x 4096 device pixels | Lower the multiplier to a floor of 1 |
Section 19.5 also restates, in one table, every ceiling first fixed in sections 10, 14, 15, 16, 17, and
18, which is what PRD 120 means by centralizing limits rather than scattering them. Scenario instances
and timeline expansions are the two entries of PRD 120's list no contract has yet fixed; the gap is named
in the table so it stays a center rather than a snapshot.
**These values are provisional.** Their relationship to slice 4h is the relationship 16.7's
master-protection values have to GC6: the shape is normative now, the numbers are confirmed or replaced
by measurement. No evidence record may describe them as measured until trace 21 of 19.7 runs.
## Verification performed
This slice changes documentation only. No runtime source, schema, fixture, or test file was touched.
- Every diagnostic code used in section 19 (13 distinct codes) resolves to a row in the section 7 table;
zero unresolved, checked programmatically.
- Every numeric cross-reference in the whole specification (107 distinct, 48 of them made by section 19)
resolves to an existing heading. Zero unresolved forward references remain anywhere in the document —
the first time that has been true since section 17 was written.
- All 116 code fences in the document are balanced, 6 of them in section 19, and every table row in the
document is well formed once escaped pipes are discounted.
- `npm test` passes 102 tests with zero failures, confirming the existing suite is unaffected.
## Test boundary
Traces 1-19 of section 19.7 are automated and belong to slice 4f, where automation, lifecycle, camera,
and effects exist to run them. Traces 20 and 21 are user-observed and close slice 4h: the PRD 130 visual
challenge judged on a real display — the same gate as trace 15 of 17.16 and trace 20 of 18.10, restated
for locality rather than added as a third gate — and the early combined GC6 benchmark at `1920 x 1080`,
which is what decides whether the ceilings above are the right ones.
## State at this checkpoint
The visual contract is complete and no visual code exists. `src/runtime` has no renderer module, the JSON
Schema has no visual definitions, `exhibits/` has no visual fixture, and nothing has been drawn from any
build. Slice 4d is the first slice of Phase 4 that writes runtime code.
Phase 3 remains unaccepted while 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 sections 17-19 pass a multi-model review before slice 4d begins remains open, and it is now a
decision with a deadline rather than a standing question: 4d is the next slice, the review's whole value
is in catching contradictions before implementation defends them, and the surface has grown from the
sections 14-16 pass that found thirteen defects to three sections of comparable size.