Revision 0.9 adds section 20, the cadence and event subsystems contract, and carries two corrections the implementation forced. Section 6.1 now states that a duration is the authored literal or a non-negative finite number already in milliseconds, since a DurationSpec may be the resolved output of a ValueSpec or a bounded TimeSpec, with the one documented exception of an automation track's `at`, which 19.1 keeps literal-only so that point ordering stays decidable at import. Section 20.11 documents the rejection of an undeclared input name in an event action's `with` map as ERR_UNKNOWN_FIELD — the section's own convention for that shape of error, replacing an invented code that appeared nowhere in the registry. The review record is committed with the code it describes: the two code triages that found these defects, the reconciliation plan that sequenced the fixes, and a follow-up debt record listing what was deliberately left open — the unchecked JSON Schema artifact, degenerate path arcs, post-effect transient allocation, the window-traffic fixture's per-copy wrap bounds, and the unstated `ownership: "persistent"` value on a sound action. None of the five blocks phase 6; all five are written down rather than dropped. Devlog entries are backfilled for the two milestones that had none: phase 3c slice 2, the audio lifecycle and voice ceilings, and slice 4d, the renderer core. The implementation status summary now reflects the reconciled state rather than the in-flight one. 231 tests pass. tools/verify-spec-contract.py reports 46 declared diagnostic codes with every used code resolving and its two long-standing unresolved cross-references unchanged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01ShxxFqFmCUDQnQvFNm4TKy
22 KiB
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, in the batch order that document's closure table sets.
Follows the 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 testunchanged 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<color>, 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<color> 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 |
| 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
- No implementation exists. Every package is spec fixed. Slice 4d has not started;
the JSON Schema still has a Phase 0
visualsstub,validator.jsstill has novalidateVisualSubsystem(), andresolution.jsstill rejects all four visual target families. That is the Stage 0 code-alignment work of 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. - 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.
- Traces are not run. §§17.16, 18.10, and 19.7 are amended contracts, not results.