On this page
Branching the record and joining back๏
๐ต Expert ยท Lesson 23
BRANCHING, AND WHY A JOIN CAN REFUSE. Research lanes are melder's branches: parallel lines of versions over one world, anchored to each other, finished by joining. It looks like git until three places where it deliberately is not.
A BRANCH STARTS EMPTY research_create_lane("try-a", attach_to="default", attach_at_spell_id=v1) Anchoring records ANCESTRY ONLY. No node is copied, no node is moved, and the anchor lane is untouched - the new lane begins with zero versions and a memory of where it came from. That is the opposite of a git branch, which starts AT a commit and carries the whole history with it.
ANCHORING IS ALL OR NOTHING attach_to and attach_at_spell_id must be supplied together; exactly one of them raises before the lock is even taken. There is no "anchor to whatever the tip is" shorthand, because the anchor is the one fact the join later reasons about and a shorthand would let you record a different anchor than you meant.
THE DEFAULT LANE TYPE IS experiment, NOT development Omit lane_type and you get experiment - deliberately NOT the default lane's development. A freshly cut lane is an experiment until someone says otherwise, so the safe classification is the one you get by not thinking about it.
AND A JOIN IS DIVERGENCE-AWARE, WHICH IS THE WHOLE LESSON research_join("try-a", into="default") The CLEAN path requires two things at once: the source is anchored onto the receiver, AND the receiver's tip is STILL AT that anchor. Anything else - the receiver moved on, a foreign anchor, no anchor at all - is a DIVERGENT join and refuses unless you pass force=True. The refusal NAMES BOTH TIPS, so you are told exactly what drifted apart rather than being handed a conflict to hunt.
force=True IS NOT A MERGE. It is an EXPLICIT SUPERSEDE, and melder is blunt about the difference: "Reconciliation-by-content is not a join concern: compose in the codegen workshop, register the multi-parent result, then join." Merging content is work you do with code, in a room, and the record only ever books the outcome. A version-control system that merges FOR you is guessing, and this one refuses to guess about your source.
COLLAPSE CHOOSES HOW MUCH LINE COMES WITH IT collapse=False (default) fold the source's FULL line in collapse=True move only the TIP, leaving the rest readable in the joined source lane Residence transfers with whatever moves, and the source is marked joined - it accepts no further work afterwards. A NEW VERSION IS BOUND THE WAY THE FIRST ONE WAS. The receiver is moved by staging RuleV2 on RuleV1's index, so both are bound under one string category and binding name, ('rule', 'branch'): an index keeps its first version's binding, and bind_inactive refuses a version bound any other way (2026-10-25).
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/23_branching_the_record_and_joining_back.py
py -3.14t UX_and_AIX_experiences/04_expert/23_branching_the_record_and_joining_back.py
Public surface๏
research_create_lane (anchored, half-anchored and refused), research_heads, research_join with and without force, Conduit.bind_inactive to move the receiver, and the lane payload create_lane returns (a lane describe() snapshot: lane_type, anchor, nodes)
Code๏
1"""
2TIER: expert (23)
3GOAL: BRANCHING, AND WHY A JOIN CAN REFUSE. Research lanes are melder's
4 branches: parallel lines of versions over one world, anchored to
5 each other, finished by joining. It looks like git until three
6 places where it deliberately is not.
7
8 A BRANCH STARTS EMPTY
9 research_create_lane("try-a", attach_to="default",
10 attach_at_spell_id=v1)
11 Anchoring records ANCESTRY ONLY. No node is copied, no node is
12 moved, and the anchor lane is untouched - the new lane begins with
13 zero versions and a memory of where it came from. That is the
14 opposite of a git branch, which starts AT a commit and carries the
15 whole history with it.
16
17 ANCHORING IS ALL OR NOTHING
18 `attach_to` and `attach_at_spell_id` must be supplied together;
19 exactly one of them raises before the lock is even taken. There is
20 no "anchor to whatever the tip is" shorthand, because the anchor is
21 the one fact the join later reasons about and a shorthand would let
22 you record a different anchor than you meant.
23
24 THE DEFAULT LANE TYPE IS `experiment`, NOT `development`
25 Omit `lane_type` and you get `experiment` - deliberately NOT the
26 default lane's `development`. A freshly cut lane is an experiment
27 until someone says otherwise, so the safe classification is the one
28 you get by not thinking about it.
29
30 AND A JOIN IS DIVERGENCE-AWARE, WHICH IS THE WHOLE LESSON
31 research_join("try-a", into="default")
32 The CLEAN path requires two things at once: the source is anchored
33 onto the receiver, AND the receiver's tip is STILL AT that anchor.
34 Anything else - the receiver moved on, a foreign anchor, no anchor
35 at all - is a DIVERGENT join and refuses unless you pass
36 `force=True`. The refusal NAMES BOTH TIPS, so you are told exactly
37 what drifted apart rather than being handed a conflict to hunt.
38
39 `force=True` IS NOT A MERGE. It is an EXPLICIT SUPERSEDE, and
40 melder is blunt about the difference: "Reconciliation-by-content is
41 not a join concern: compose in the codegen workshop, register the
42 multi-parent result, then join." Merging content is work you do
43 with code, in a room, and the record only ever books the outcome.
44 A version-control system that merges FOR you is guessing, and this
45 one refuses to guess about your source.
46
47 COLLAPSE CHOOSES HOW MUCH LINE COMES WITH IT
48 collapse=False (default) fold the source's FULL line in
49 collapse=True move only the TIP, leaving the rest
50 readable in the joined source lane
51 Residence transfers with whatever moves, and the source is marked
52 joined - it accepts no further work afterwards.
53 A NEW VERSION IS BOUND THE WAY THE FIRST ONE WAS. The receiver is
54 moved by staging RuleV2 on RuleV1's index, so both are bound
55 under one string category and binding name, ('rule', 'branch'):
56 an index keeps its first version's binding, and bind_inactive
57 refuses a version bound any other way (2026-10-25).
58SURFACE EXERCISED: research_create_lane (anchored, half-anchored and
59 refused), research_heads, research_join with and
60 without force, Conduit.bind_inactive to move the
61 receiver, and the lane payload create_lane returns
62 (a lane describe() snapshot: lane_type, anchor, nodes)
63VERIFY: RUN GREEN 2026-10-25 on 3.14t, GIL off and on; asserts are the
64 contract. The SURFACE line was corrected 2026-08-05, and both
65 versions moved under one binding 2026-10-25.
66"""
67import melder as md
68
69
70FRAME = "branch-world"
71
72
73class RuleV1:
74 def __init__(self) -> None:
75 self.tag = "v1"
76
77
78class RuleV2:
79 def __init__(self) -> None:
80 self.tag = "v2"
81
82
83def main() -> None:
84 # Record and custody live BEFORE the world, so binds auto-record.
85 crystallizer = md.Crystallizer()
86 crystallizer.activate(
87 md.CrystallizerConfigurationBuilder().with_defaults().activate(),
88 )
89 research = md.MutationResearch()
90 configuration = research.create_configuration()
91 configuration.with_defaults().activate()
92 research.activate(configuration)
93
94 # A RECORDED WORLD MUST BE BORN CONFIGURED. With custody active, a
95 # dynamic conjure REFUSES if any bind ran before the configuration
96 # was finalized - the profile record and default bootstrap would
97 # otherwise durably persist binds made against unsettled config.
98 spellbook_configuration = (
99 md.SpellbookConfiguration(FRAME).with_defaults().finalize()
100 )
101 book = md.Spellbook(aetheric_frame=FRAME,
102 configuration=spellbook_configuration)
103 # THE FRAME POSTURE GOES BEFORE THE BIND. A plain bind auto-declares
104 # into research only `if self._is_dynamic_posture()` - a frame-level
105 # question answerable before conjure. Bind into a not-yet-dynamic
106 # frame and nothing is declared, silently, because research
107 # bookkeeping never gates a bind. There would then be no node in the
108 # default lane to anchor a branch at.
109 book.configure_aether_frame(
110 system_state="dynamic",
111 disposal=None,
112 disposal_method_names=None,
113 rift_enabled=True,
114 ai_native=True,
115 )
116 v1 = book.bind(spell=RuleV1, spellframe="rule", existence="unique",
117 permissions="create", binding_name="branch")
118 conduit = book.conjure(name="branch-root")
119 print("v1 recorded on the default lane:", v1[:12], "...")
120
121 nexus = md.Nexus()
122 system_configuration = nexus.create_configuration()
123 system_configuration.with_rift_creation_enabled(True)
124 system_configuration.with_allowed_target_frame_names([FRAME])
125 nexus.activate(system_configuration)
126 rift_configuration = nexus.create_rift_configuration()
127 rift_configuration.with_space_type("codegen")
128 rift = nexus.create_rift(configuration=rift_configuration,
129 rift_name="brancher")
130 rift.mark_active()
131 rift.create_frame_link(FRAME)
132 commands = rift.space.command_system
133
134 # ANCHORING IS ALL OR NOTHING - and it refuses before doing anything.
135 try:
136 commands.research_create_lane("half-anchored", attach_to="default")
137 raise AssertionError("expected a refusal: anchor is all-or-nothing")
138 except ValueError as error:
139 print()
140 print("attach_to without attach_at_spell_id refused -")
141 print(" ", error)
142
143 # CUT A BRANCH, ANCHORED. It starts EMPTY.
144 clean = commands.research_create_lane(
145 "clean-branch",
146 attach_to="default",
147 attach_at_spell_id=v1,
148 reason="a branch that will join cleanly",
149 )
150 print()
151 print("created 'clean-branch':")
152 print(" lane_type :", clean["lane_type"], " (default is experiment)")
153 print(" anchor_spell:", str(clean["anchor_spell_id"])[:12], "...")
154 print(" nodes :", len(clean["nodes"]),
155 " <- EMPTY: ancestry only, nothing copied")
156 assert clean["lane_type"] == "experiment"
157 assert clean["anchor_spell_id"] == v1
158 assert len(clean["nodes"]) == 0
159
160 # A SECOND BRANCH FROM THE SAME POINT. This one will go stale.
161 commands.research_create_lane(
162 "stale-branch",
163 attach_to="default",
164 attach_at_spell_id=v1,
165 reason="a branch the receiver will move away from",
166 )
167 heads = commands.research_heads()
168 print()
169 print("research_heads() ->", len(heads), "open lanes")
170
171 # THE CLEAN JOIN. The receiver's tip is still exactly at the anchor.
172 commands.research_join("clean-branch", into="default",
173 reason="nothing diverged")
174 print()
175 print("join('clean-branch' -> 'default') accepted")
176 print(" source anchored onto the receiver AND the receiver still at")
177 print(" the anchor: the only arrangement that is not a supersede")
178
179 # NOW MOVE THE RECEIVER. A new version lands on the default lane, so
180 # 'stale-branch' is anchored at a point the receiver has left. It is
181 # bound the way v1's index was - ('rule', 'branch') - or it is refused.
182 v2 = conduit.bind_inactive(
183 spell=RuleV2,
184 spell_index=conduit.get_spell_by_spell_id(v1).spell_index,
185 spellframe="rule",
186 binding_name="branch",
187 existence="unique",
188 permissions="create",
189 )
190 print()
191 print("v2 recorded; the default lane moved on:", v2[:12], "...")
192
193 # THE DIVERGENT JOIN REFUSES, AND THE REFUSAL NAMES THE TIPS.
194 try:
195 commands.research_join("stale-branch", into="default")
196 raise AssertionError("expected a refusal: divergent join")
197 except RuntimeError as error:
198 print()
199 print("join('stale-branch' -> 'default') refused -")
200 print(" ", str(error)[:150])
201 print(" it names BOTH tips, so you are told what drifted rather")
202 print(" than handed a conflict to go and find")
203
204 # force=True IS A SUPERSEDE, NOT A MERGE.
205 commands.research_join("stale-branch", into="default", force=True,
206 reason="explicit supersede, content already "
207 "reconciled elsewhere")
208 print()
209 print("join(..., force=True) accepted - and note what it is NOT:")
210 print(" it did not merge any SOURCE. Reconciliation by content is")
211 print(" work you do in the codegen workshop; you register the")
212 print(" multi-parent result and THEN join. The record books an")
213 print(" outcome and never guesses at your code")
214
215 # THE JOINED LANES ARE CLOSED. A join is terminal for the source.
216 final_heads = commands.research_heads()
217 print()
218 print("open lanes now:", len(final_heads), " (joined sources closed)")
219 assert len(final_heads) <= len(heads)
220
221 print()
222 print("a branch starts EMPTY and remembers where it came from")
223 print("a clean join is anchored AND un-drifted; anything else is a")
224 print("supersede you have to ask for by name")
225
226
227if __name__ == "__main__":
228 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.