docs: raise the format specification to revision 0.9 and land the reconciliation
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
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
# Spec Review — Abacus AI Agent
|
||||
|
||||
Source: Abacus AI Agent
|
||||
Scope: sections 14–16 only (sections 1–13 and the PRD read for cross-checking)
|
||||
Received: 2026-09-06
|
||||
Status: independent review — untriaged
|
||||
|
||||
---
|
||||
|
||||
Independent review of sections 14–16 of `docs/XZBT_0-1_Format_Specification.md` (Rev 0.4). Every finding was verified against the current document text. Section 7 and the shared sections were read to check staging and diagnostic codes; the PRD was consulted only to confirm that cited PRD sections and gates exist. Deliberate design decisions listed in the review brief are not reported as defects.
|
||||
|
||||
Findings are ranked by severity: a defect that makes a rule, capability, or algorithm unimplementable outranks an internal inconsistency.
|
||||
|
||||
---
|
||||
|
||||
### 1. `ERR_INVALID_RANGE_ORDER` is staged Semantic-only, but its defining condition is "after resolution" — the third occurrence of the recurring staging class
|
||||
|
||||
**Severity: high (rule unenforceable at its assigned stage)**
|
||||
|
||||
**§7** files `ERR_INVALID_RANGE_ORDER` as **Semantic** only, with the cause "Declared paired bounds (e.g. `sample-hold` `min`/`max`) are not in strictly increasing order **after resolution**."
|
||||
|
||||
**§14.12** declares `min`/`max` as `ValueSpec<number>` in "14.4 scope", and 14.4 fixes resolution "once, at the owning sound instance's instantiation boundary." **§14.5** states the semantic stage's own limits: "Validation never opens an `AudioContext` and never depends on audio hardware" — and it also has no sound instances, so no resolved ValueSpecs.
|
||||
|
||||
The documented invalid case (`min: 1, max: -1`, literals) is checkable at import. A legal document such as `{ "min": { "random": { "min": -1, "max": 1 } }, "max": { "random": { "min": -1, "max": 1 } } }` is not: the resolved pair exists only at instantiation, which §7 does not permit to emit this code. The cause text itself concedes this — "after resolution" — while the stage column forbids it. The document already contains the remedy pattern twice (`ERR_OUT_OF_BOUNDS` is dual-staged "Semantic / Runtime"; the frequency ceiling is split into a 24000 literal tier and an instantiation clamp tier in 14.5); this code did not get it.
|
||||
|
||||
Secondary harm: if an inverted resolved pair reaches instantiation uncaught, 14.12's draw ("one uniform sample in `[min, max]`") is itself undefined for `min > max`, with no stage assigned to stop it. Secondary wording problem: 16.1 uses the same code for automation point ordering, which is a literal-only, genuinely semantic condition that §7's "paired bounds" cause text does not describe.
|
||||
|
||||
**Smallest fix:** In §7, re-stage `ERR_INVALID_RANGE_ORDER` as `Semantic / Runtime` (literal pairs and literal curve order at import; resolved pairs at instantiation), mirroring `ERR_OUT_OF_BOUNDS`, and extend the cause text to name both conditions. Optionally add one clause in 14.12 stating that a resolved-order failure raises at instantiation.
|
||||
|
||||
---
|
||||
|
||||
### 2. Automation point `value` consumes the seeded sound stream with no documented sampling boundary, order, or key
|
||||
|
||||
**Severity: high (reproducibility unimplementable; corrupts the shared stream order)**
|
||||
|
||||
**§16.1** defines each point as `{ "at": <duration literal>, "value": ValueSpec<number> }` and says "A track's *values* remain full ValueSpecs and may be random."
|
||||
|
||||
No section states when or from what stream a point `value` is sampled. §14.4's resolve-once rule covers "numeric and enum fields **on node objects**"; §15.12 had to add its own sentence to fold route `depth` into that rule ("Resolved once at instantiation (14.4)") — the precedent showing that non-node fields do not inherit 14.4 automatically. No equivalent sentence exists for track point values. §9.3 samples random ValueSpecs "once at the containing object's **documented** instantiation or invocation boundary"; a track has no documented boundary, and no domain or child key is documented for its draws.
|
||||
|
||||
An implementer cannot know whether `value` is drawn at instantiation, at first evaluation, or lazily per point, nor its position in the depth-first document-order pass. Because all of a sound instance's ValueSpecs draw from **one** stream (14.4), an unspecified draw position shifts every subsequent node-field and `depth` draw: two implementations can both claim conformance yet produce different values for the same seed — breaking the §9.3 reproducibility promise for the whole instance, not just the track.
|
||||
|
||||
**Smallest fix:** One sentence in 16.1: point `value` ValueSpecs are resolved once at the owning sound instance's instantiation boundary, in the same depth-first property-document pass as node fields and route `depth` (14.4, 15.12, 9.3), with a stated position — e.g. tracks after `nodes` and `routes`, in `automation` array order, points in array order.
|
||||
|
||||
---
|
||||
|
||||
### 3. `audio.buses.<id>.gain` is promised automation and additive modulation, but no legal document can author either
|
||||
|
||||
**Severity: high (normative capability claim with no authoring surface)**
|
||||
|
||||
**§8.1** gives the bus row `Automation: Yes` and `Additive modulation: Yes`. **§15.17** repeats it: bus gain "remains the supported surface for binding, automation, override, and additive modulation of audio level from outside a graph." **§16.2** repeats it again: bus gain "takes binding, automation, override, and modulation as section 8.1 already states."
|
||||
|
||||
No construct can express the middle two:
|
||||
|
||||
- Automation tracks are declared only in the `automation` array of a **graph object** (16.1, 14.3), and graph objects exist at exactly three places (`audio.recipes.<id>`, `sounds.<id>.recipe`, `components.audio.<id>`) — none is a bus. A track's `target` must be `<node-key>.<property>` resolved in the same graph and must be in the 15.13 registry, which lists only node properties. A bus is not a node in any graph.
|
||||
- Modulation routes are graph-internal (15.12–15.13); there is no route mechanism that reaches a bus.
|
||||
- The bus object's allowed-field table (15.17) contains `gain` only.
|
||||
|
||||
Binding and override do have authoring paths (the `bindings` array; actions). Automation and additive modulation are asserted in three places and are unrequestable by any document; the §8.5 required trace "A target has legal additive modulation" likewise has no authorable subject.
|
||||
|
||||
**Smallest fix:** Either add an authoring surface — e.g. an `automation` array on the bus object, target `gain`, governed by the 16.1 track rules — or strike automation and additive modulation from the §8.1 bus row and from the restatements in 15.17 and 16.2. Do not leave the claims standing with no mechanism.
|
||||
|
||||
---
|
||||
|
||||
### 4. "Underlying automation continues while overridden" (16.2) and trace 16.11.8 have no legal subject
|
||||
|
||||
**Severity: high (specified behavior and mandatory trace unpassable)**
|
||||
|
||||
**§14.4** is explicit: "A `BindingSpec`, `set` action, or `override` action addressing a node field … is `ERR_UNSUPPORTED_TARGET`."
|
||||
|
||||
**§16.2** consequence 2 nonetheless narrates override-vs-automation on node properties: "An override masks the automation stage's output for as long as it wins … When the override releases, the property returns to whatever the track has reached by then." **§16.11.8** requires tracing exactly this.
|
||||
|
||||
Every automatable property is a node property in the 15.13 registry (16.1 targets rule), and every node property is an illegal override target per 14.4. The one override-capable audio target, bus gain, has no authorable automation (finding 3). So the behavior 16.2 specifies and the trace 16.11.8 demands cannot be produced by any legal document. Note the asymmetry inside 16.2 itself: the pipeline hedges binding ("where supported") and consequence 1 restates the binding half of 14.4, but "winning override" is unhedged and consequence 1 is silent on the override half, while consequence 3's own principle ("Every stage a target does not expose is absent") makes the override stage absent for node properties.
|
||||
|
||||
**Smallest fix:** In 16.2, state that node properties expose no override stage (14.4) and that override-vs-automation interaction applies only to `audio.buses.<id>.gain` — which requires fixing finding 3 first — and retarget 16.11.8 to bus gain (or drop it).
|
||||
|
||||
---
|
||||
|
||||
### 5. The 16.5 one-shot bound is undefined when a resonator's retained modes are all omitted
|
||||
|
||||
**Severity: high (algorithm undefined for a legal input)**
|
||||
|
||||
**§16.5** defines the resonator contribution as "the longest `decay` among its **retained** modes." **§15.10**: "A mode whose resolved frequency exceeds `audioMaxFrequency` at instantiation is omitted." Per §14.5, `audioMaxFrequency = min(24000, sampleRate × 0.45)` and the sample rate is unknown at import, where the semantic ceiling is the device-independent `24000`.
|
||||
|
||||
Legal input: a resonator with a single mode `{ "frequency": 20000 }` passes import (20000 ≤ 24000). On a 44.1 kHz device the ceiling is 19845, the mode is omitted, the retained set is empty, and "longest decay among retained modes" is a maximum over an empty set — undefined. The one-shot ending bound, which §16.5 requires the runtime to compute at instantiation, has no defined term for this graph, and 15.10's omission rule (matching 14.7) raises no diagnostic, so no error path covers it. The node's own output with zero retained modes is also unspecified.
|
||||
|
||||
**Smallest fix:** One sentence in 16.5 (and optionally 15.10): a resonator with no retained modes contributes `0` to the bound and outputs silence.
|
||||
|
||||
---
|
||||
|
||||
### 6. The bus `gain` base ValueSpec has no documented resolution boundary or stream
|
||||
|
||||
**Severity: medium (seeded draw with no derivation key; legal input, undefined behavior)**
|
||||
|
||||
**§15.17** types the bus field `gain` as `ValueSpec<number>`, and §8.1 makes that ValueSpec the base stage of the bus row.
|
||||
|
||||
No section says when it is resolved or from which stream. §14.4's boundary belongs to sound instances; a bus is not owned by one. §9.3 requires "the containing object's documented instantiation or invocation boundary" — none is documented for buses — and the reserved domains (`cadence`, `scenario`, `visual`, `sound`, `manual-sample`) contain no bus domain, nor is any child key documented for bus gain (14.12 is the only child-key documentation in the audio contract, and it is `sample-hold` only). A random or weighted-choice bus gain is therefore legal to author per the field type, yet its sampling time and derivation key are undefined — the same defect class as finding 2 on a different field.
|
||||
|
||||
**Smallest fix:** One sentence in 15.17 fixing the boundary and stream (e.g., resolved once at performance start from a documented domain with a documented child key), or restrict the bus `gain` base to literals and `ref` forms.
|
||||
|
||||
---
|
||||
|
||||
### 7. The 16.6 eviction policy is not scoped to the request's ceiling and contradicts its own independence rule
|
||||
|
||||
**Severity: medium (policy incoherent as written)**
|
||||
|
||||
**§16.6** steps 1 and 2 are unqualified: "Dispose the oldest instance already in `FINISHED`" and "Evict the oldest instance in `RELEASING`", applied "when a new instance would exceed **its** ceiling", stopping "at the first candidate." Step 3 is explicitly kind-scoped ("For a one-shot request only: evict the oldest `ACTIVE` one-shot"), so the asymmetry is in the text. The ceilings are separate budgets (64 one-shot / 16 continuous) and a voice "counts against its ceiling from `CREATED` until `DISPOSED`."
|
||||
|
||||
Read literally, a one-shot request at its 64 ceiling may stop at step 1 by disposing the oldest `FINISHED` instance — which can be a continuous instance, freeing continuous budget, which does not admit the request; the runtime then either admits the request over its ceiling or has freed nothing. Step 2 likewise lets a one-shot request evict a `RELEASING` continuous sound, directly contradicting the same subsection's "A one-shot request never evicts a continuous sound, and a continuous request never evicts a one-shot."
|
||||
|
||||
**Smallest fix:** Scope steps 1 and 2 to the request's budget: "the oldest same-ceiling instance already in `FINISHED`" / "the oldest same-ceiling instance in `RELEASING`".
|
||||
|
||||
---
|
||||
|
||||
### 8. Automation track/point limits are "runtime ceilings rather than document properties" in 15.14 but semantically enforced in 16.1
|
||||
|
||||
**Severity: medium (self-contradictory staging classification)**
|
||||
|
||||
**§15.14** classifies automation tracks (`64`) and points (`256`) as "runtime ceilings rather than document properties … belong to Phase 3c with the lifecycle contract."
|
||||
|
||||
**§16.1** enforces them as `ERR_NODE_LIMIT_EXCEEDED`, which §7 stages as **Semantic**, and §16.11.6 tests them under a header that says the traces are "executable without an audio device" — i.e., at import.
|
||||
|
||||
Track and point counts per expanded sound are document-determinable (component expansion needs no device), so 16.1's semantic enforcement is correct and 15.14's classification is false — and if 15.14 were taken seriously, the Semantic-staged code would be unenforceable. Additionally, §7's cause text for `ERR_NODE_LIMIT_EXCEEDED` enumerates only "oscillator partials, resonator modes, expanded nodes or routes per sound," omitting the limits 16.1 assigns to it.
|
||||
|
||||
**Smallest fix:** In 15.14, move tracks/points into the authoring-limits table alongside expanded nodes/routes (keep "runtime" only for one-shot voices and continuous sounds), and add tracks/points to §7's `ERR_NODE_LIMIT_EXCEEDED` cause text.
|
||||
|
||||
---
|
||||
|
||||
### 9. `ACTIVE -> FINISHED` is reserved for "a one-shot that has reached its determinable ending," but 16.5 defines that ending to include the release
|
||||
|
||||
**Severity: medium-low (lifecycle trigger undefined; term collision across sections)**
|
||||
|
||||
**§16.3:** "`ACTIVE -> FINISHED` without a release is reserved for a one-shot that has reached its determinable ending, where the envelope has already returned to zero and a further release would be redundant."
|
||||
|
||||
**§16.5:** "The instance's ending is the maximum over all source-to-`output` paths of the sum of contributions along that path on the expanded audio graph, **plus the release duration of 16.4**."
|
||||
|
||||
If the ending includes the release, an instance still `ACTIVE` at that instant never released, so it cannot have reached an ending defined to include one; if it releases at the end of the audible path, it passes through `RELEASING` and the direct transition never fires. The two sections use "determinable ending" for two different instants (path bound vs. path bound plus release), leaving the transition's trigger — the single most-implemented lifecycle edge for one-shots — undefined.
|
||||
|
||||
**Smallest fix:** In 16.3, name the actual trigger: the end of the audible path (the 16.5 bound minus the release duration) with the output already silent — or redefine the 16.5 ending as the path bound with the release as a tail allowance.
|
||||
|
||||
---
|
||||
|
||||
### 10. 14.7's empty-`harmonics` prose and its own invalid-case example assign different codes to the same input
|
||||
|
||||
**Severity: low-medium (prose contradicts a JSON example in the same section)**
|
||||
|
||||
**§14.7** states unconditionally: "An empty `harmonics` array is `ERR_SCHEMA_VALIDATION`." The same section's invalid case is `{ "type": "oscillator", "waveform": "sine", "harmonics": [] }` → `ERR_UNKNOWN_FIELD` on `harmonics`.
|
||||
|
||||
The example input *is* an empty `harmonics` array, so the unconditional sentence assigns `ERR_SCHEMA_VALIDATION` to exactly the shape the example assigns `ERR_UNKNOWN_FIELD` (via the also-matching rule "`harmonics` present while `waveform` is not `custom` is `ERR_UNKNOWN_FIELD`"). No precedence between the two rules is stated, and trace 14.13.1 requires the documented invalid case to emit "exactly the documented code."
|
||||
|
||||
**Smallest fix:** Scope the empty-array rule to the case where the field is declared: "An empty `harmonics` array while `waveform` is `custom` is `ERR_SCHEMA_VALIDATION`" — or state that unknown-field checks precede value checks.
|
||||
|
||||
---
|
||||
|
||||
### 11. `ERR_NO_AUDIBLE_PATH` has two different triggers, and 15.14 rules 5 and 9 overlap on 15.19.3's own fixture
|
||||
|
||||
**Severity: low-medium (inconsistent code definition; trace fixture is dual-code)**
|
||||
|
||||
**§14.3:** "A graph object with no route reaching `output` is `ERR_NO_AUDIBLE_PATH`…" **§15.14** rule 9: "At least one audio route chain **from a non-control source** reaches `output` | `ERR_NO_AUDIBLE_PATH`."
|
||||
|
||||
For a graph whose only output-reaching route starts at a control source (e.g. `lfo` → `output`): 14.3 says a route does reach `output`, so this is not `ERR_NO_AUDIBLE_PATH`; 14.10–14.12 and 15.19.3 ("A graph whose only path to `output` originates at an `lfo`, `constant`, or `sample-hold` **fails rule 5**") assign `ERR_INVALID_ROUTE`; yet rule 9 as worded is also violated (no non-control source exists). The same graph therefore matches two rules with different diagnostics, while 15.19.2 requires each rule's fixture to emit "exactly the named diagnostic."
|
||||
|
||||
**Smallest fix:** Align rule 9 with 14.3's trigger — it fires when no audio route chain reaches `output` at all — leaving control-source chains to rule 5; or state a precedence for overlapping violations.
|
||||
|
||||
---
|
||||
|
||||
### 12. The `exponential` interpolation requirement contradicts its own fallback trigger, leaving same-signed negative segments undefined
|
||||
|
||||
**Severity: low-medium (breached rule with no defined breach behavior)**
|
||||
|
||||
**§16.1:** exponential "Requires both endpoints **strictly positive** and same-signed; a segment with a **zero or sign-crossing** endpoint falls back to `linear` and raises `WARN_AUTOMATION_FALLBACK`."
|
||||
|
||||
"Strictly positive" already implies same-signed, and it conflicts with the fallback trigger: a segment with both endpoints negative (legal — point `value` is `ValueSpec<number>` with no declared range) is neither zero nor sign-crossing, so no fallback applies, yet it breaches the "strictly positive" requirement. A breached requirement with no applicable remedy is undefined behavior. The formula `v0 x (v1 / v0)^t` is in fact well-defined for same-signed negatives, which is evidently the intent.
|
||||
|
||||
**Smallest fix:** "Requires both endpoints nonzero and same-signed" — matching the zero-or-sign-crossing fallback exactly.
|
||||
|
||||
---
|
||||
|
||||
### 13. 14.3's type-specific-field pointer "14.7-14.12 and 15.1-15.8" omits three node types' defining sections
|
||||
|
||||
**Severity: low (wrong cross-reference coverage; no dangling reference)**
|
||||
|
||||
**§14.3's** node-object field table points type-specific fields to "14.7-14.12 and 15.1-15.8." The same table's `type` enum includes `mixer`, `resonator`, and `component`, whose fields and constraints are specified in **15.9, 15.10, and 15.11** — outside the stated range. All referenced sections exist; the range is simply wrong in coverage (it was never extended when Phase 3b grew to 15.11).
|
||||
|
||||
**Smallest fix:** "14.7-14.12 and 15.1-15.11" (or 15.2-15.11).
|
||||
|
||||
---
|
||||
|
||||
### 14. `input` is a "reserved node key" with no reservation rule and no diagnostic
|
||||
|
||||
**Severity: low (legal-per-the-letter graph with ambiguous routing)**
|
||||
|
||||
**§15.15:** when a component declares `input: true`, "the reserved node key `input` is available as a route `from` inside the graph." **§14.3's** reserved-key rule reserves only `output`: "`output` is reserved … and may not be declared in `nodes` (`ERR_INVALID_ID`)."
|
||||
|
||||
`input` matches the identifier regex, and nothing forbids declaring a node keyed `input` in `nodes` of a component graph that declares `input: true`. Such a document is legal per the stated rules, and a route `{ "from": "input" }` is then ambiguous between the reserved passthrough and the author's node — undefined behavior. The word "reserved" implies the prohibition but, unlike `output`, states neither it nor a diagnostic.
|
||||
|
||||
**Smallest fix:** Mirror the `output` rule: in a component graph that declares `input: true`, a node keyed `input` may not be declared in `nodes` (`ERR_INVALID_ID`).
|
||||
|
||||
---
|
||||
|
||||
### Classes with no finding
|
||||
|
||||
- **Prose contradicting a JSON example in the same section:** finding 10 only. All other exhibits and examples in 14–16 (14.3, 14.7–14.12 headers, 15.2–15.10, 15.15, 15.16, 15.17, 16.1) were checked field-by-field against their tables and prose; they match.
|
||||
- **Field introduced in one section but missing from its container's allowed-field table:** none. Every container table in 14–16 (graph object, node object, all sixteen node types, route, component graph, component parameters, component instance, sounds entry, recipe, bus, automation track, automation point, harmonics entry, resonator mode entry) matches every field introduced for it. Finding 3 is the inverse shape — a capability asserted for a container with no authoring field anywhere.
|
||||
- **Rule assigned to a stage that cannot enforce it:** findings 1 and 8. Elsewhere the document applies the correct pattern: `ERR_OUT_OF_BOUNDS` is dual-staged, 14.5 splits the frequency ceiling into a literal/24000 semantic tier and an instantiation clamp tier, and 16.5 correctly restricts the semantic `ERR_INDETERMINATE_ONESHOT` check to the document-decidable unbounded-source case.
|
||||
- **Value consuming a seeded stream without a documented derivation key:** findings 2 and 6. `sample-hold` (14.12) documents its child key; component `values` and route `depth` are folded into 14.4/15.12; node fields are covered by 14.4.
|
||||
- **Rule with no defined behavior on breach:** findings 12 and 14 (finding 7's eviction steps also produce undefined outcomes for a legal input).
|
||||
- **Algorithm that may not terminate or is undefined for a legal input:** findings 5, 7, and 9. No non-termination: the 16.5 longest-path computation runs on the expanded acyclic graph bounded at nesting 8 and terminates; the delay tail formula `time x ceil(log(1/1000)/log(feedback))` is defined on the whole legal feedback range `(0, 0.95]` (as feedback approaches 0 the count ceils to 1, matching the `else time` branch at 0).
|
||||
- **Diagnostic code used in 14–16 but absent from §7, or filed under the wrong stage:** none absent — all 24 codes used in 14–16 appear in §7 with causes matching their 15.18/16.10 restatements. Wrong-stage aspects are findings 1 and 8.
|
||||
- **Cross-references to sections that do not exist:** none. Every internal section reference in 14–16 resolves, as do the PRD references (33–62, 117–120) and gates (GC4, GC6). Finding 13 is a wrong coverage range, not a dangling reference.
|
||||
|
||||
### Deliberate design decisions recognized and not reported
|
||||
|
||||
Node fields not externally bindable with `audio.buses.<id>.gain` as the sole external surface; nodes as a keyed map carrying no `id`; `noise`/`impulse` sample generation outside the reproducibility promise with `sample-hold` inside it; exactly one automation track per property while multiple modulation routes sum; provisional master-protection ceiling, tolerance, and release behavior pending GC6; `audio.master` required to be absent; automation point `at` as duration literals; the GC4 audio long-stall bound as a known omission; bus processing beyond gain and modulation of compressor/reverb/waveshaper properties out of scope.
|
||||
Reference in New Issue
Block a user