docs: the complete domain model design - Part I structure + Part II behavior (Stories #3, #4) - #6
docs: the complete domain model design - Part I structure + Part II behavior (Stories #3, #4)#6jonathanstokes wants to merge 4 commits into
Conversation
The object hierarchy designed from the manual: Device, Directory entries as a type family, the Preset/Scene grid duality, polymorphic blocks, generic typed parameters, and a full manual-feature appendix mapping every feature to the model or an explicit omission with reason. ADR-0006: at M1 the model takes the top-level namespace and the protocol layer moves to pyquadcortex.protocol.
Fills Part II of docs/domain-model.md, the behavioral half of the M0 design. Every number in it was measured on hardware (d14e / CorOS 4.0.1), not read off the schema. The design: - state tracking (§9): the unit's pushes are applied as data, not used as "something changed" triggers - the metronome alone streams a pair of messages per beat, which would have made an invalidation-based cache thrash. A push naming a field the model does not represent falls back to a re-read, because applying half a message is the one failure that leaves the cache confidently wrong. - write verification (§10): a write updates the cached copy immediately and the unit's echo confirms it. The echo is a sparse keyed delta (23 bytes, explicit row and column - unlike a recalled preset) at 113-116 ms for a parameter and 290-420 ms for a placement. Placement is the one write that blocks, because DSP headroom is unreadable so refusal can only be observed. - save lifecycle (§11): save vs save_as, factory presets read-only by type, edits abandoned silently on recall (matching the unit, made safe by recall always being explicit), and the capture-bypass save dance absorbed. - disconnect (§12): a HID read raising means the device is gone; a write raising means nothing (0 read vs 91 write exceptions over a 145 s healthy session). Standby does not disconnect at all and announces itself, so asleep and gone are distinguishable. Reconnect retries the handshake, not just the open. Both of the Epic's empirical questions were answered rather than carried as M1 risks: unsaved-change detection is readable and pushed, and device loss is free to detect. Also resolves three Part I forward references: DeviceLostError replaces the placeholder NotConnectedError in §8, writes to an inactive scene are refused, and the appendix's "Part II" rows now point at §13 or the section that answers them. The breadth the session did not reach is named in §13 rather than left implied. Protocol-layer findings from the session were handed over separately and have already shipped; they are not duplicated here. Story: #4
Part II described save_as() as returning the stored NAME; Part I's signature returns a UserPresetEntry. Points at entry.name instead, and distinguishes re-saving the same name to the same slot (not a collision, no _2 suffix) from a genuine save_as collision (the unit renames).
Rebasing onto main picked up caf6fae, which traced the unit's MUTE button to tempo parameter 4. That confirms the transport always runs - there is no start/stop control at all - but it also means what silences the metronome is a real mute, not the volume control's -60 dB floor this section cited. Conflating the two would leave a reader thinking they could turn the volume up and hear it. The design point is unchanged and now better supported: GlobalTempo streams a pair of messages per beat on every connection, so inbound traffic cannot be used as a "something changed" trigger.
0ffe767 to
2064f88
Compare
jonathanstokes
left a comment
There was a problem hiding this comment.
The following review is from a Claude Opus 5 review with no previous context of either the manual or the domain model.
Independent review — "the hierarchy is recognizable straight from the manual, no mapping table needed"
Subject: docs/domain-model.md @ 2064f88
Against: Quad Cortex User Manual 4.0.0 (CorOS 4.0.0), https://neuraldsp.com/manual/quad-cortex
Criterion under review: issue #3 — "Hierarchy mirrors manual structure … Naming and indexing match the touchscreen interface"
Verdict
The spine holds. The vocabulary does not.
Device → Setlist → Preset → Scene → Row → Block → Parameter, plus routing and assignments, is genuinely
recognizable — I could predict most of the model's shape from the manual before reading it, and a large
majority of leaf names (LANE OUTPUT CONTROL → row.output.lane, has_unsaved_changes ← the italic name,
SCENE BYPASS BEHAVIOR, HOLD TIMING, MIN/MAX RANGE, the splitter/mixer parameter lists, ModeSlot,
Library) land exactly on the manual's own words. That work is real and it is the bulk of the document.
But the claim as written — no mapping table needed — does not survive contact with the manual, for three
separate reasons, in increasing order of seriousness:
- Four of the model's most-used identifiers replace manual vocabulary, and two of them collide with
it rather than merely renaming it (§A). - Five structural placements disagree with the manual's own nesting (§B).
- Six statements in the document assert manual support that the manual contradicts or does not
contain (§C). This is a different and more serious class than naming: principle 3 promises no API
ships with a caveat, and §"Status" promises the manual wins on disagreement. These are places where
the design silently overrode the manual without recording it.
Separately, the appendix's own claim — "Every feature the manual describes, mapped to the model or
explicitly omitted" — has five genuine gaps and three rows that describe features the manual does
not contain (§D).
The sharpest evidence against the claim is structural, not any single finding: the appendix is the
mapping table. Roughly fifteen of its rows exist precisely to explain a model name the manual never
uses. If the hierarchy needed no mapping table, most of those Notes cells would be empty.
Recommendation: the acceptance criterion "Hierarchy mirrors manual structure" — passes. The
criterion "Naming and indexing match the touchscreen interface" — does not pass yet. §A and §B are
cheap to fix (renames and re-parents, no wire work). §C should be fixed before this is called done,
because those are the ones that will mislead a reader who trusts the doc.
§A — Vocabulary the manual doesn't use
Verified by exhaustive grep of the full manual text (2,325 lines, all 12 chapters).
A1. column — the manual never numbers the slots, and never says "column"
- Manual L508: "The Grid consists of four rows, each containing eight device block slots."
- Zero occurrences of
column/Columnin the manual. - Model L18–20 (principle 1): "Rows are 1-4 and columns 1-8 as on the touchscreen … A user who
knows the unit recognizes the API without a mapping table." - Model L253, L281:
def __getitem__(self, column: int),column: int # 1..8
Row is manual vocabulary (L584: "routing to other Rows on The Grid"). column is not, and the
manual never assigns numbers to the eight slots at all. The principle asserts screen-faithfulness for a
term the screen documentation doesn't contain.
A2. Slot and column are crossed — the manual's word applied to the other thing
Every "slot" in the manual is either a grid cell (L508, L514, L522, L618, L1165, L1773, and L2001
"GRID SLOT NAVIGATION") or a Mode Slot (L303–309, L373, L381). It is never a preset address, and the
"28C" notation appears nowhere in the manual.
- Model L119–124:
class Slot: """A preset's address as the Directory shows it: bank number + position letter."""
So the manual's primary "slot" became column, and Slot became the one thing the manual never calls a
slot. A reader who knows the unit will reach for row.slots[3] and get a preset address instead. Note the
model does keep ModeSlot (L454) and IRSlot (L294) faithful, which makes the bare Slot more confusing,
not less.
A3. Catalog / CatalogModel — the manual says Virtual Device List / virtual device
- Zero occurrences of
catalogin the manual. - Manual L512, L518: "VIRTUAL DEVICE LIST" / "The Virtual Device List provides access to all
virtual devices available that can be placed on The Grid." - Manual L666: the parameter editor's own label is "VIRTUAL DEVICE NAME".
- Model L90–91:
# the Virtual Device List/catalog: Catalog— the comment names the manual term,
then the identifier doesn't. - Model L254, L284:
place(self, column: int, model: CatalogModel | ...),model: CatalogModel
This is the model's most-used core noun after Block, and model is doubly unfortunate — it's the word
this very document uses for the domain model itself. Suggest VirtualDeviceList / VirtualDevice, and
row.place(slot, device).
A4. Entry — the Directory's word is "Item"
- Manual: "Item Categories" (L865), "Items can be sorted, favorited…" (L861), "Item Limit"
(L875), "SORTING ITEMS" (L977), "SEARCHING ITEMS" (L989), "Uploading Items to Cortex Cloud"
(L1021) — 27 uses. - Model L148:
class Entry:→PresetEntry,CaptureEntry,IREntry,PluginPresetEntry.
Item / PresetItem would be free and exact. The type-family design itself is good; only the noun is off.
A5. Smaller renames of labelled controls
| Manual label | Model | Line |
|---|---|---|
STOMP MODE BYPASS (L1647) |
settings.stomp_mode_auto_assign |
L472 |
EQ BAND BYPASS (L493) |
EQBand.enabled — polarity inverted |
L429 |
| "Scene assignment" / tap-and-hold (L335–339) | param.follows_scenes, comment "promote/demote" |
L342 |
MY PRESETS (L889) vs "User Setlist" ×10 (L891) |
setlists.user = My Presets, not the user setlists |
L106 |
| footswitch rows "A to D and E to H" (L379) | HybridMode(top=…, bottom=…) |
L459 |
PLAYBACK (L267) |
Metronome.playing |
L445 |
EQBand.enabled is the one worth fixing regardless of this review: five lines above it sits
GlobalEQ.bypassed (L420), and DeviceBlock.bypassed (L285) is the model's convention everywhere else.
eq.bypassed = True and eq.bands[0].enabled = True currently mean opposite things from the same
on-screen word.
setlists.user is the most likely to cause a real bug: anyone who has read chapter 5 reads it as "the ten
user setlists".
§B — Structure that disagrees with the manual's nesting
B1. Global EQ is a sub-screen of I/O Settings, not a device-level feature
- Manual L477–479: "The I/O Settings menu includes a 5-Band Parametric EQ … Tap GLOBAL EQ at the
top of the I/O Settings to access its interface." - Model L96–97:
io: IOandglobal_eq: GlobalEQas siblingDeviceattributes. - Model L395:
class IO: # the I/O Settings menu (swipe down)— containsinputs,returns,
outputs,usb; no EQ.
§6's own promise is "Each feature object mirrors one screen or menu on the unit" — which makes
device.io.global_eq the guessable form and device.global_eq the one needing a lookup.
B2. IO is missing two controls that are on that screen
MIDI THRU— Manual L463–473 lists it as the fourth row of I/O Settings' "USB AUDIO & MIDI
PORTS" alongside USB LEVEL / HP SOURCE / DRY-WET.USBPorts(L414–417) has the first three; thru is only
atsettings.midi(L477). Defensible (the manual also lists it at L1194 under Device → MIDI), but the
appendix's ch3 I/O rows (L888–894) list every sibling and silently drop this one.EXP 1/EXP 2— Manual L702–710: "access the I/O Settings menu … Tap the EXP 1 input
currently in use. Tap RECALIBRATE … Verify that the POSITION indicator responds correctly." A tappable
port class on that screen with no representative on the object that claims to be that screen. (The
calibration omission is documented at L928 — the missing port is not.)
B3. Splitter/Mixer belong to a Row pair; the model puts them on a Row
- Manual L618: "insert a Splitter or Mixer for the corresponding pair of Rows."
- Manual L614–616: "Route audio from Rows 1 or 3 (Path A) to Rows 2 or 4 (Path B)."
- Manual L608: even the side-chain limit is scoped "per pair of Rows".
- Model L255–257:
split: Split | None,create_split(at, rejoin_at),clear_split()onclass Row.
row2.create_split(...) and row4.create_split(...) have no meaning, and row1.split vs row2.split is
undefined. There is no pair object anywhere in the model.
B4. A split need not rejoin — the model requires it
- Manual L616: "MIXER BLOCKS: Route parallel paths on Rows 2 or 4 (Path B) to different output
blocks or merge them back into Rows 1 or 3 (Path A)." - Manual L618/L620: "insert a Splitter or a Mixer" … "The (S) and (M) tokens can then be dragged and
placed next to input or output blocks" — two independent inserts, independently placed. - Model L318–319:
splitter: SplitterBlockandmixer: MixerBlock, both non-optional. - Model L256:
create_split(self, at: int, rejoin_at: int)— no way to express a splitter whose Path B
goes to its own output.
This is a representational gap, not just naming: a preset the manual describes as buildable cannot be
described by Split.
B5. Path A / Path B has no structural presence
The manual's load-bearing term for the two halves of a split (L612, L614, L616, L618, L628, L636) survives
only inside parameter-name comments (LEVEL TO A/B, PAN A/B). The row↔path mapping (odd = A, even = B)
appears nowhere, so nothing in the model says which row is which path.
Also note lane is overloaded: L317's docstring uses it for the parallel path, while L307
lane: LaneOutput | None uses it for the manual's LANE OUTPUT CONTROL (L592). Neither is the manual's
"Path".
B6. Settings flattens two of chapter 10's four sections
Chapter 10 has four named subsections: Account (L1607), System (L1615), Device (L1633),
Support (L1663).
- Model L469:
class Settings: # the Device Settings menu, eponymous rows - Model L478–479:
brightness: Brightness,storage: Storage
But Manual L1623 puts BRIGHTNESS and L1629 puts DEVICE STORAGE under System Settings, not
Device Settings. The other eight members do match L1637–1661 exactly, so "eponymous rows" is true for
eight of ten and false for two. The appendix's ch10 table has no subsection column either.
§C — Statements the manual contradicts
These are the findings I'd hold the PR on. Each one puts a confident assertion in the document that a
reader can falsify from the manual in under a minute.
C1. "ONE control on the unit" — the manual documents two MUTEs
- Manual L638, under
SPLITTER PARAMETERS: "MUTE: When enabled, mutes the Splitter block." - Manual L654, under
MIXER PARAMETERS: "MUTE: When enabled, mutes the Mixer block." - Model L320:
muted: bool # ONE control on the unit; the wire confirms it is shared - Appendix L921: "one shared control - the wire confirms it"
Two menus, two rows, two different objects muted. The wire may well share one field — but "ONE control on
the unit" is the claim the manual refutes, and per the doc's own precedence rule the manual wins over the
design here. Aggravating: L311 and L314 both drop MUTE from the splitter and mixer parameter lists to
make room for the hoist, so a reader who looks for split.splitter.params["MUTE"] finds nothing.
If the wire truly shares it, say so as a wire fact and keep both screen paths, or record it as a
manual-vs-touchscreen conflict in §13.
C2. "the unit has no start/stop control at all" for the metronome
- Manual L267: "PLAYBACK: Toggles the metronome playback on or off."
- Manual L840 (Looper X): "METRONOME PLAYBACK: Toggles the metronome playback on or off."
- Model L620–621: "The metronome transport always runs - the unit has no start/stop control at all,
and what silences it is a separate MUTE"
Contradicted three ways: by L267, by the model's own Metronome.playing (L445), and by its own appendix row
L874 which maps metronome playback to device.tempo.metronome.* as yes. The "separate MUTE" also has no
referent — the metronome's parameter list (L265–277) is VOLUME, PLAYBACK, PAN, T/SIGNATURE, SUBDIVISIONS,
SOUND, ROUTING, and every MUTE in the manual belongs to the tuner, an I/O port, output pairing, a lane, a
splitter or a mixer.
The §9 conclusion (a continuous GlobalTempo stream is not a change signal) is fine and well-evidenced.
Only the justifying sentence is wrong, and it's the sentence a reader would cite.
C3. "only presets can be favorited"
- Manual L1173: "Plugin Presets can be added to Favorites, sorted by Name or Preferred Instrument."
- Manual L861: "Items can be sorted, favorited, uploaded…"
- Manual L973: multi-select can "copy, paste, move, favorite, upload, download, edit, and delete
multiple items" - Model L86, L155:
favorites: Sequence[PresetEntry],favorite: bool # only presets can be favorited - Appendix L940: "presets only, as on the unit"
Only L871/L877 support presets-only, and they're the narrower statement. PluginPresetEntry is missing a
capability the manual grants it, and Device.favorites is typed too narrowly.
C4. PresetEntry.instrument with values Guitar/Bass/Synth/Vocal/Other
"Preferred Instrument" occurs exactly twice in the manual, and never on a Preset:
- Manual L1138 (ch6, saving a capture): "you can add two types of metadata: Capture Type and Preferred
Instrument" → Neural Captures. - Manual L1173 (ch7): "Plugin Presets … sorted by Name or Preferred Instrument" → Plugin Presets, as
a sort key. Guitar/Bass/Synth/Vocalas a value set: zero hits anywhere in the manual.- Model L154, L192, L200:
instrument: Instrument # Guitar / Bass / Synth / Vocal / Otheron
PresetEntry, onPreset, and as asave_asargument. - Model L167:
class CaptureEntry(Entry): ...— the one entity the manual does give this to has no
instrument field. - No appendix row exists for it in ch5 or ch6.
Part II L741–742 defends it on wire grounds ("it lives on the directory listing rather than in the
preset") — which is principle 1 inverted: a wire fact used to justify a screen-facing name. If the
touchscreen does show a preset instrument category, that's fine and the manual is simply behind — but then
say so, and put the capture's Capture Type + Preferred Instrument somewhere too.
C5. stomp.momentary is filed as a chapter 4 manual feature
All seven "momentary" mentions in the manual: L754/L766 (expression toe switch, LATCH EMULATION) and
L824/L826/L830 plus L1410/L1418 (Looper X RECORD / OVERDUB / PUNCH MODE). The stomp-assignment passages
— L355–361 ("FOOTSWITCH ASSIGNMENTS … A single footswitch can be assigned to multiple device blocks"),
L668, and L413 ("EDIT STOMP: Customizes the selected Stomp name and device block assignments") —
document no momentary option.
- Model L489, L493:
momentary: bool,momentary: bool = False - Appendix L924:
| Stomp momentary + label | stomp.momentary, stomp.label | yes | |
Either it's a touchscreen-only control (say so) or the row is wrong.
C6. on_load accepts a message type the unit can't send there
- Manual L1224 (FOOTSWITCH & EXPRESSION MESSAGES): "TYPE: … (CC, CC Toggle, or PC)"
- Manual L1252 (ON PRESET LOAD MESSAGES): "TYPE: … (CC or PC)"
- The asymmetry is reinforced:
MIN/MAX VALUE(L1236), the CC-Toggle-only field, appears only in the
footswitch/expression block. - Model L508–511:
on_load: Sequence[MidiMessage]— one union for all three, including
ControlChangeToggle. - Appendix L983: "CC, CC Toggle, and PC message types modeled" repeats the flattening.
This is exactly the case principle 2 exists for ("Capability differences are type differences"). A
separate OnLoadMessage = ControlChange | ProgramChange union costs one line.
C7. (lower confidence, worth a look) MasterVolume.level read-only
- Model L464:
level: float # 0-100 as displayed; READ-ONLY (the wire cannot write it - it is a separate gain stage) - Manual L1811: "The Master Volume Knob in Cortex Control adjusts the overall output level of the
connected device." - Manual L1815: "the volume level displayed in Cortex Control may not always match the physical Master
Volume … the hardware volume wheel is temporarily deactivated."
Cortex Control is a USB host client doing precisely what the doc says the wire cannot do, and the
mismatch/deactivation behaviour implies a host-side write path. This may be a different mechanism than the
one probed. Worth one line in §13 rather than a flat "the wire cannot write it". Also, the 0-100 display
range is asserted nowhere in the manual.
§D — Appendix completeness
The appendix header (L843) claims "Every feature the manual describes, mapped to the model or explicitly
omitted." Documented omissions with reasons are exactly right and I'm not counting them. These are features
with no row at all:
| Missing row | Manual | Note |
|---|---|---|
| Rename a preset | L947 "You can store, edit, rename, and move Presets"; L1783 | The model ships UserPresetEntry.rename() (L159). Ch5 has rows for move, copy, delete — not rename. |
| Assign Looper X Actions | L782–784 "To customize the footswitch layout, tap the Looper X contextual menu and select Assign Looper X Actions" | A distinct named menu operation; not the same as preset.stomps. Ch4's Looper rows cover place / parameters / transport only. |
| The footswitch HOLD action | L1649 "how long a footswitch must be held to trigger its assigned HOLD action" | The model takes the timing (hold_timing_ms) but neither models nor audits the action being timed. |
| I/O Settings device variants | L453–461 "First-generation … display TYPE switches"; "devices equipped with 'ESS Codec' do not display TYPE switches" | InputPort.input_type (L404) is unconditional and Device has no variant attribute, so on an ESS-codec unit the API names a control the screen doesn't show. |
| Bank View / List View | L987 "When Presets are sorted by Banks, they will display in Bank View. Sorting by any other option displays presets in List View." | Two named Directory views; the Sorting row (L956) covers sorting, not the views. |
Rows that describe something other than what the manual says:
- L924
stomp.momentary— see C5. - L921 splitter/mixer MUTE as one control — see C1.
- L861 "Master Volume knob function" placed in the Chapter 3 table. Ch3's Master Volume section
(L167–176) documents only the knob and the output checkboxes; "MASTER VOLUME KNOB: Set the function …
(Global or Output specific)" appears only at L1627, in ch10 System Settings. (L994 then
annotates the ch10 row "also in ch. 3".) - L992
settings.brightness— the manual's row is "BRIGHTNESS: Adjust screen and LED brightness"
(L1623). "dimmed LED" has zero occurrences in the manual; it's a wire-derived third field presented
in a manual-audit row. - L949 "Preset descriptive tags" is audited as a ch5 manual feature; "tag" has zero occurrences in the
manual. (Harmless, but it's not a manual feature.)
Two smaller modelling notes surfaced by the same pass:
SwitchParam(Parameter[bool])(L349) vs Manual L680: "SWITCHES: Tap to toggle between two or
more discrete states." A three-state switch is neither a boolSwitchParamnor obviously a
ChoiceParam(documented as "dropdowns", L353).TextParam's example (L351) is "a Cab's microphone name field", but Manual L546 describes cabs
as having "selectable microphones" and the control list (L676–682) has no text-entry kind. May be
screen-true; the manual doesn't support the example.
§E — On the bank-size contradiction (§13 item)
The doc flags this (L126–130) and handles it honourably. Two refinements:
- The two chapters are internally consistent but differ by a factor of two throughout:
ch3 says banks of eight (L319) and four in a PRESET-containing HYBRID ("either A-D or E-H",
L385); ch5 says four by default and two in HYBRID (L893). So it's not one stray sentence — it's
two coherent accounts, one of which is scaled 2×. - Given that,
Slot.position: str # "A".."H"(L122) does take a position — the one L126–130 says it
doesn't. A–H is only reachable under ch3. Either widen the comment to"A".."H" under ch3's reading; see §13or drop the annotation until it's resolved.
§F — Reachability nits (doc hygiene, not hierarchy)
Six types are referenced on primary access paths but never declared, and two are declared but unreachable.
None of these change the design; they'd each cost a line and they make the doc harder to review as a
hierarchy.
- Declared, unreachable:
OutputPair(L411) —IO(L395–399) exposes no pair collection, yet appendix
L891 citesOutputPair.linked.PluginPresetEntry(L169) —Device(L74–102) has no plugin-presets
container, yet appendix L965 cites it. - Referenced, never declared:
Scenes(L196),Rows(L197),BlockGrid(L198),Params(L286),
Setlists(L84, prose only),Library(L88, prose only). Preset(L189–201) never declaresstompsormidi_out, though §7 documentspreset.stomps(L491)
andpreset.midi_out(L507).- There is no
Directoryobject; its five on-screen categories are flattened ontoDeviceunder a
comment (L83). Defensible — issue #3's own AC spine omits Directory too — but a manual-only reader looks
fordevice.directory.captures.
§G — The per-Preset tempo question
Not a naming problem, and the omissions are documented — but the combination has a sharp edge worth
naming in §13.
- Manual L251–255: "MODE: Toggles between GLOBAL and PRESET tempo modes … PRESET MODE: Tempo and
Metronome settings are stored within each Preset. Each Preset retains its own Tempo and Metronome
configuration." - Manual L1795 (ch11): "You can set tempo values per Scene, Preset, or globally." (A third manual
self-contradiction, since ch3 offers only Global/Preset — the appendix L875 catches the per-Scene half
and resolves it correctly in the unit's favour.) - Model L97:
tempo: TempoonDeviceonly. L443: MODE omitted, "not on the wire at all".
The omission of the control is properly declared. The consequence isn't: with the unit in PRESET MODE,
device.tempo.bpm and all seven Metronome fields denote per-preset state read through a
device-global object. That's a value that can be silently wrong depending on a mode the model can't see —
which is the situation principle 3 exists to prevent. Recommend an explicit §13 entry.
Method
- Domain model read in full at
2064f88(1,045 lines). - Manual scraped in full via Playwright — all 12 chapters, 2,325 lines / 95 KB — and checked chapter by
chapter rather than through page summaries, so every quote above is the manual's own wording. Line
numbers refer to that capture; the scrape is at~/Downloads/qc-manual-4.0.0.txtif you want to check
quotes yourself (safe to delete). - Every §A–§C finding was then re-verified against the manual text by two independent adversarial passes
instructed to refute it. Findings that didn't survive were dropped; two were downgraded to PARTIAL and
are marked as such (C7, and the tempo item now in §G). - Not reviewed: protocol reachability claims, Part II's hardware measurements, and anything requiring the
unit. Where the model says "the wire confirms" I took that as given and only checked it against the
manual's own description.
Summary
The complete M0 design of the domain model:
docs/domain-model.md, both halves. Part I designs the object hierarchy from the Quad Cortex manual (Story #3). Part II designs the model's behavior - state tracking, write verification, the save lifecycle, and disconnect/reconnect - grounded in a hardware session rather than schema inference (Story #4). Also records ADR-0006 (the model takes the top-level namespace at M1).This supersedes #5. Same design content, but rebuilt on current
mainso the diff is docs-only. #5's branch carried a duplicate of the state-tracking protocol commit that has since shipped tomainwith corrections, so that PR's diff included protocol code it did not mean to change. Nothing was lost in the rebuild - every line unique to the duplicate was an older version of somethingmainalready has, including the pre-correctionDeviceLostErrordocstring and theFileMessage.typeopen question we have since answered.Changes
docs/domain-model.md(new): Part I (hierarchy, typing, full manual-feature appendix) + Part II (§9 state tracking, §10 write verification, §11 save lifecycle, §12 disconnect/standby/reconnect, §13 what is still open)docs/ADR.md: ADR-0006docs/STEERING.md: ADR table row, design-doc pointer, change log entries for both storiesWhat Part II decides
Epic acceptance
Both of Epic #2's empirical questions were answered on hardware rather than carried as M1 risks, which was the stated fallback:
PresetDirty{READ}answers in 2-11 ms and is pushed unsolicited, sohas_unsaved_changesships.Test plan
docs/- verified no code, test, or protocol file is modifiedmain(0001-0005 there, 0006 added here)Known limitations
§13 names what the hardware session did not reach, rather than leaving it implied: the device-wide broadcast sweep, echo behavior for write types beyond parameters and placement, whether writes are honoured during standby, bank size, scene copy/swap, and the expression-bypass fields. None of these changes a decision in the design, because the read-path column covers anything the push column misses. They are the natural first jobs for ADR-0005's hardware suite.
Linked issues