# Compositions a subsystem as one unit **🔵 Expert · Lesson 14** COMPOSITIONS - naming a SET of spells as one thing, and then asking questions about the thing instead of about its members. Expert 03 taught residency: a spell lives in exactly one lane. That answers "where is this one". It does not answer "what is our billing subsystem", because a subsystem is not a spell - it is a SET of them that someone decided to treat as a unit. THE EIGHT VERBS, AND TWO DIFFERENT SPLITS RUN THROUGH THEM SPLIT ONE - BY AUTHORITY (which room may call it) ORGANIZE (codegen rooms only - these WRITE) research_group_register(member_spell_ids, lane=..., reason=...) research_group_recompose(previous_group_id, add=..., remove=...) READ (capability rooms get these too) research_group_view(group_id) research_group_diff(left, right, strategy=...) research_group_impact(group_id) research_group_footprint(group_id) research_group_drift(group_id) research_group_history(group_id, campaign=...) Six reads, two writes. That is expert 08's law one grain up: a capability room can read the ENTIRE composition record and cannot change one thing in it. Only a codegen room may restate what a subsystem IS - because restating it changes what the next reader concludes about work nobody has redone. SPLIT TWO - BY DEPENDENCY (what the read has to reach) The six reads are NOT the same kind of question, and finding this out the hard way is the reason this lesson exists in its current form. Three answer from the RESEARCH RECORD alone: view the roster and its lane joins history the journal story diff two rosters, plus lane-evidenced version moves Three are FORESIGHT reads that join research truth to CUSTODY material, and they reach through the Crystallizer to do it: footprint the physical module shadow impact the union blast radius and closure math drift recorded-vs-disk, narrowed to the footprint A FORESIGHT READ WITH NO LIVE CRYSTALLIZER REFUSES. It does not return an empty report, and melder says why in the message: "foresight reads need the record - activate the crystallizer before asking for source, impact, or module graphs." Never- substitute, on a read path. AND THE TWO ABSENCES ARE DIFFERENT, WHICH IS THE SUBTLE PART instrument OFF -> refusal (no crystallizer: cannot answer) instrument ON, nothing to read -> data (members with no custody crystal come back under `unknown_custody_members`) "I cannot answer" and "the answer is none" are not the same fact, and melder refuses to spell them the same way. THE ID IS THE MEMBERSHIP, NOT A SERIAL NUMBER `group_id` is a sha256 over the SORTED, DEDUPED member list. Three consequences follow, and none of them are cosmetic: - A composition is a SET. Input order and duplicates cannot change its identity, because they are canonicalised away before the hash. - THE SAME ROSTER IS THE SAME COMPOSITION. Not a copy of it - it. So re-declaring an unchanged set REFUSES as a rediscovery and the error names the lane already holding it. You cannot accidentally end up with two names for one subsystem. - A recompose that resolves back to the previous roster refuses for exactly the same reason. Remove-then-re-add is a no-op, and melder will not record a no-op as history. RECOMPOSE SUCCEEDS THE OLD ONE, IT NEVER EDITS IT A new node is registered with `parent_group_ids=[previous]`, in the SAME lane, and the previous composition is untouched. Forward-only: the old answer stays exactly as true as it was, and the timeline is walkable instead of lossy. WHAT COMES BACK IS A PAYLOAD, NOT A NODE Both writes return `node.describe()` - a plain dict carrying `node_type`, `group_id`, `member_spell_ids`, `parent_group_ids`, `author`, `reason`, `campaign`, `created_at`, `metadata`. Detached and plain-value, so holding one cannot mutate the record. AND THE JOIN THAT MAKES IT USEFUL research_group_history(group_id, campaign=...) History is WHEN. Campaign is WHY. Passing both is the WHERE-by-WHEN join - "how did this subsystem change during that effort" - which is the question you actually have during a review, and the one you cannot ask if a set is only ever a list you kept in your head. IMPACT IS LIFTED TOO. `research_impact` on a single spell names the GroupedResearchNode subsystems it touches under `affected_compositions`. Blast radius stopped being a list of files and became a list of things with names. ## Before you run Use the [Expert guide](../../expert/index.md) for prerequisite concepts. Run from a checkout with Melder installed and Python 3.14 free-threading selected. The collection download includes the level's local helper modules. ## Run the saved script ```bash python UX_and_AIX_experiences/04_expert/14_compositions_a_subsystem_as_one_unit.py ``` ```powershell py -3.14t UX_and_AIX_experiences/04_expert/14_compositions_a_subsystem_as_one_unit.py ``` {download}`Download this collection <../../downloads/expert-examples.zip>` · [Source on GitHub](https://github.com/Synaptic724/melder/blob/90d52e0a12cb0743d62568d5a19ce954a54443bc/UX_and_AIX_experiences/04_expert/14_compositions_a_subsystem_as_one_unit.py) ## Public surface the eight research_group_* commands across a codegen room and a capability room, the read/write line between them, and the record/foresight line inside the reads ## Code ```{literalinclude} ../../downloads/04_expert/14_compositions_a_subsystem_as_one_unit.py :language: python :linenos: ``` ## Check the outcome The script contains its own assertions or demonstrated refusal paths. Run it to evaluate those checks against your installed version. The code above is taken directly from the saved file; no run output is invented here. [More expert examples](index.md) · [Level guide](../../expert/index.md) ## Related guides - [Compositions, synthesis, and impact](../../expert/compositions.md)