On this page

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 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๏ƒ

python UX_and_AIX_experiences/04_expert/14_compositions_a_subsystem_as_one_unit.py
py -3.14t UX_and_AIX_experiences/04_expert/14_compositions_a_subsystem_as_one_unit.py

Download this collection ยท Source on GitHub

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๏ƒ

  1"""
  2TIER: expert (14)
  3GOAL: COMPOSITIONS - naming a SET of spells as one thing, and then
  4      asking questions about the thing instead of about its members.
  5
  6      Expert 03 taught residency: a spell lives in exactly one lane.
  7      That answers "where is this one". It does not answer "what is our
  8      billing subsystem", because a subsystem is not a spell - it is a
  9      SET of them that someone decided to treat as a unit.
 10
 11      THE EIGHT VERBS, AND TWO DIFFERENT SPLITS RUN THROUGH THEM
 12
 13      SPLIT ONE - BY AUTHORITY (which room may call it)
 14
 15        ORGANIZE (codegen rooms only - these WRITE)
 16          research_group_register(member_spell_ids, lane=..., reason=...)
 17          research_group_recompose(previous_group_id, add=..., remove=...)
 18
 19        READ (capability rooms get these too)
 20          research_group_view(group_id)
 21          research_group_diff(left, right, strategy=...)
 22          research_group_impact(group_id)
 23          research_group_footprint(group_id)
 24          research_group_drift(group_id)
 25          research_group_history(group_id, campaign=...)
 26
 27      Six reads, two writes. That is expert 08's law one grain up: a
 28      capability room can read the ENTIRE composition record and cannot
 29      change one thing in it. Only a codegen room may restate what a
 30      subsystem IS - because restating it changes what the next reader
 31      concludes about work nobody has redone.
 32
 33      SPLIT TWO - BY DEPENDENCY (what the read has to reach)
 34      The six reads are NOT the same kind of question, and finding this
 35      out the hard way is the reason this lesson exists in its current
 36      form. Three answer from the RESEARCH RECORD alone:
 37
 38          view        the roster and its lane joins
 39          history     the journal story
 40          diff        two rosters, plus lane-evidenced version moves
 41
 42      Three are FORESIGHT reads that join research truth to CUSTODY
 43      material, and they reach through the Crystallizer to do it:
 44
 45          footprint   the physical module shadow
 46          impact      the union blast radius and closure math
 47          drift       recorded-vs-disk, narrowed to the footprint
 48
 49      A FORESIGHT READ WITH NO LIVE CRYSTALLIZER REFUSES. It does not
 50      return an empty report, and melder says why in the message:
 51      "foresight reads need the record - activate the crystallizer
 52      before asking for source, impact, or module graphs." Never-
 53      substitute, on a read path.
 54
 55      AND THE TWO ABSENCES ARE DIFFERENT, WHICH IS THE SUBTLE PART
 56        instrument OFF   -> refusal      (no crystallizer: cannot answer)
 57        instrument ON,
 58        nothing to read  -> data         (members with no custody crystal
 59                                          come back under
 60                                          `unknown_custody_members`)
 61      "I cannot answer" and "the answer is none" are not the same fact,
 62      and melder refuses to spell them the same way.
 63
 64      THE ID IS THE MEMBERSHIP, NOT A SERIAL NUMBER
 65      `group_id` is a sha256 over the SORTED, DEDUPED member list. Three
 66      consequences follow, and none of them are cosmetic:
 67
 68        - A composition is a SET. Input order and duplicates cannot
 69          change its identity, because they are canonicalised away
 70          before the hash.
 71        - THE SAME ROSTER IS THE SAME COMPOSITION. Not a copy of it -
 72          it. So re-declaring an unchanged set REFUSES as a rediscovery
 73          and the error names the lane already holding it. You cannot
 74          accidentally end up with two names for one subsystem.
 75        - A recompose that resolves back to the previous roster refuses
 76          for exactly the same reason. Remove-then-re-add is a no-op, and
 77          melder will not record a no-op as history.
 78
 79      RECOMPOSE SUCCEEDS THE OLD ONE, IT NEVER EDITS IT
 80      A new node is registered with `parent_group_ids=[previous]`, in the
 81      SAME lane, and the previous composition is untouched. Forward-only:
 82      the old answer stays exactly as true as it was, and the timeline is
 83      walkable instead of lossy.
 84
 85      WHAT COMES BACK IS A PAYLOAD, NOT A NODE
 86      Both writes return `node.describe()` - a plain dict carrying
 87      `node_type`, `group_id`, `member_spell_ids`, `parent_group_ids`,
 88      `author`, `reason`, `campaign`, `created_at`, `metadata`. Detached
 89      and plain-value, so holding one cannot mutate the record.
 90
 91      AND THE JOIN THAT MAKES IT USEFUL
 92        research_group_history(group_id, campaign=...)
 93      History is WHEN. Campaign is WHY. Passing both is the WHERE-by-WHEN
 94      join - "how did this subsystem change during that effort" - which
 95      is the question you actually have during a review, and the one you
 96      cannot ask if a set is only ever a list you kept in your head.
 97
 98      IMPACT IS LIFTED TOO. `research_impact` on a single spell names the
 99      GroupedResearchNode subsystems it touches under
100      `affected_compositions`. Blast radius stopped being a list of files
101      and became a list of things with names.
102SURFACE EXERCISED: the eight research_group_* commands across a codegen
103                   room and a capability room, the read/write line
104                   between them, and the record/foresight line inside the
105                   reads
106VERIFY: RUN GREEN 2026-08-03 on the owner's 3.14t harness.
107"""
108import melder as md
109
110
111WRITES = ("research_group_register", "research_group_recompose")
112# Answered from the research record alone.
113RECORD_READS = (
114    "research_group_view", "research_group_history", "research_group_diff",
115)
116# Joined against custody material - these need a live Crystallizer.
117FORESIGHT_READS = (
118    "research_group_footprint", "research_group_impact",
119    "research_group_drift",
120)
121READS = RECORD_READS + FORESIGHT_READS
122
123
124def _room(nexus, kind, name):
125    """Open one rift of the given kind and hand back its command surface."""
126    configuration = nexus.create_rift_configuration()
127    configuration.with_space_type(kind)
128    rift = nexus.create_rift(configuration=configuration, rift_name=name)
129    rift.mark_active()
130    return rift.space.command_system
131
132
133def main() -> None:
134    nexus = md.Nexus()
135    system_configuration = nexus.create_configuration()
136    system_configuration.with_rift_creation_enabled(True)
137    nexus.activate(system_configuration)
138
139    # The research family reaches the Aether-hosted root through a
140    # NON-CONSTRUCTING peek, so it must be live before any of this answers.
141    research = md.MutationResearch()
142    research_configuration = research.create_configuration()
143    research_configuration.with_defaults().activate()
144    research.activate(research_configuration)
145    print("research root live -", research.activated)
146
147    codegen = _room(nexus, "codegen", "composer")
148    capability = _room(nexus, "capability", "reviewer")
149
150    # THE SPLIT, MEASURED. Six reads in both rooms; two writes in one.
151    print()
152    print("composition surface by room kind:")
153    for verb in READS:
154        assert hasattr(codegen, verb), verb
155        assert hasattr(capability, verb), verb
156    print(f"   reads   codegen {len(READS)}   capability {len(READS)}")
157    for verb in WRITES:
158        assert hasattr(codegen, verb), verb
159        assert not hasattr(capability, verb), (
160            f"capability gained {verb} - the read/write line moved"
161        )
162    print(f"   writes  codegen {len(WRITES)}   capability 0")
163    print()
164    print("a capability room reads the WHOLE record and changes nothing")
165
166    # DECLARE A SUBSYSTEM. Members must already be resident - a composition
167    # PINS declared versions, it does not introduce them.
168    research_set = research.research_set()
169    invoices = "a" * 64
170    payments = "b" * 64
171    research_set.register_spell(invoices)
172    research_set.register_spell(payments)
173    research_set.create_lane("billing", lane_type="production")
174
175    payload = codegen.research_group_register(
176        [invoices, payments], lane="billing", reason="billing subsystem",
177    )
178    # WHAT CAME BACK IS A describe() PAYLOAD - a detached plain dict, not
179    # the node. Holding it cannot reach in and change the record.
180    assert payload["node_type"] == "group"
181    group_id = payload["group_id"]
182    assert payload["member_spell_ids"] == sorted([invoices, payments])
183    assert payload["parent_group_ids"] == []
184    print()
185    print("registered a composition ->", group_id[:12], "...")
186    print("   members come back SORTED - a composition is a SET")
187
188    # THE ID IS THE MEMBERSHIP. Declaring the same roster again is not a
189    # second subsystem, it is the SAME one - so it refuses and says where
190    # the original lives.
191    try:
192        codegen.research_group_register(
193            [payments, invoices], lane="billing", reason="same set, again",
194        )
195        raise AssertionError("expected a rediscovery refusal")
196    except RuntimeError as error:
197        print()
198        print("re-declaring the same roster refused -", error)
199        print("   order reversed, identity identical: it hashes the SET")
200
201    # RECOMPOSE SUCCEEDS THE OLD ONE. New id, previous recorded as parent,
202    # same lane - and the old composition still resolves untouched.
203    refunds = "c" * 64
204    research_set.register_spell(refunds)
205    second = codegen.research_group_recompose(
206        group_id, add=[refunds], reason="refunds joined billing",
207    )
208    second_id = second["group_id"]
209    assert second_id != group_id, (
210        "a recompose must mint a NEW id - overwriting would destroy the "
211        "old answer, and the old answer was never wrong"
212    )
213    assert second["parent_group_ids"] == [group_id], (
214        "the new composition must record what it evolved FROM"
215    )
216    assert len(second["member_spell_ids"]) == 3
217    print()
218    print("recomposed -> new id, parent =", group_id[:12], "...")
219    assert codegen.research_group_view(group_id) is not None
220    print("   the previous composition still resolves, still true")
221
222    # A RECOMPOSE THAT CHANGES NOTHING IS NOT HISTORY. Remove a member and
223    # add it straight back and you are describing the roster you already
224    # have - same content, same identity, nothing to record.
225    try:
226        codegen.research_group_recompose(
227            second_id, add=[refunds], remove=[refunds], reason="no-op",
228        )
229        raise AssertionError("expected a refusal: unchanged roster")
230    except RuntimeError as error:
231        print()
232        print("a no-op recompose refused -", type(error).__name__)
233        print("   melder will not write 'nothing happened' into a timeline")
234
235    # COMPOSITIONS DO NOT NEST. A group id is a real identity in the same
236    # sha namespace, but it names no code - so it cannot be a member.
237    try:
238        codegen.research_group_register(
239            [second_id, invoices], lane="billing", reason="nesting",
240        )
241        raise AssertionError("expected a refusal: composition as member")
242    except ValueError as error:
243        print("a composition cannot be a member of a composition -")
244        print("  ", error)
245
246    # THE RECORD READS, FROM THE ROOM THAT CANNOT WRITE. These need
247    # nothing but the research record, so they answer right now.
248    print()
249    print("capability room, RECORD reads (research record only):")
250    print("   view    ->",
251          type(capability.research_group_view(second_id)).__name__)
252    print("   history ->",
253          type(capability.research_group_history(second_id)).__name__)
254    print("   diff    ->",
255          type(capability.research_group_diff(group_id, second_id)).__name__)
256
257    # THE FORESIGHT READS REFUSE UNTIL CUSTODY IS RECORDING. The
258    # crystallizer exists - Aether built it - but existing is not being
259    # live, and melder will not answer a physical question from an
260    # instrument that is switched off.
261    print()
262    print("capability room, FORESIGHT reads with custody not recording:")
263    for verb in FORESIGHT_READS:
264        try:
265            getattr(capability, verb)(second_id)
266            raise AssertionError(f"{verb} must refuse without custody")
267        except RuntimeError as error:
268            assert "crystallizer" in str(error).lower()
269            print(f"   {verb.split('_')[-1]:<9} -> refused")
270    print("   'cannot answer' is not 'the answer is none' - so it raises")
271    print("   instead of handing back an empty report")
272
273    # SWITCH THE INSTRUMENT ON. Same ladder as everywhere else: the
274    # configuration activates first, then the subsystem.
275    crystallizer = md.Crystallizer()
276    crystallizer.activate(
277        md.CrystallizerConfigurationBuilder().with_defaults().activate(),
278    )
279    assert crystallizer.activated is True
280    print()
281    print("crystallizer activated - custody is recording now")
282
283    print()
284    print("capability room, the SAME three foresight reads:")
285    for verb in FORESIGHT_READS:
286        answer = getattr(capability, verb)(second_id)
287        print(f"   {verb.split('_')[-1]:<9} ->", type(answer).__name__)
288
289    # AND NOW THE OTHER KIND OF ABSENCE. These members were declared by
290    # id and never bound, so no custody crystal exists for any of them.
291    # That is DATA, and the footprint names them rather than pretending.
292    footprint = capability.research_group_footprint(second_id)
293    assert set(footprint["unknown_custody_members"]) == set(
294        second["member_spell_ids"]
295    ), "every member was declared by id and never bound, so all are unknown"
296    print()
297    print("footprint reports", len(footprint["unknown_custody_members"]),
298          "members with no custody crystal, BY NAME")
299    print("   instrument off -> refusal.  nothing to read -> data.")
300    print("   two different absences, spelled two different ways")
301
302    print()
303    print("a subsystem is a SET someone named, not a folder")
304    print("the id IS the membership - so the same roster is the same thing")
305    print("recompose succeeds the old answer instead of erasing it")
306    print("only the room that may write code may restate what a thing IS")
307
308
309if __name__ == "__main__":
310    main()

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 ยท Level guide