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.
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.
53SURFACE EXERCISED: research_create_lane (anchored, half-anchored and
54 refused), research_heads, research_join with and
55 without force, Conduit.bind_inactive to move the
56 receiver, and the lane payload create_lane returns
57 (a lane describe() snapshot: lane_type, anchor, nodes)
58VERIFY: rides the owner's 3.14t harness; asserts are the contract. The
59 SURFACE line was corrected 2026-08-05; executable code unchanged.
60"""
61import melder as md
62
63
64FRAME = "branch-world"
65
66
67class RuleV1:
68 def __init__(self) -> None:
69 self.tag = "v1"
70
71
72class RuleV2:
73 def __init__(self) -> None:
74 self.tag = "v2"
75
76
77def main() -> None:
78 # Record and custody live BEFORE the world, so binds auto-record.
79 crystallizer = md.Crystallizer()
80 crystallizer.activate(
81 md.CrystallizerConfigurationBuilder().with_defaults().activate(),
82 )
83 research = md.MutationResearch()
84 configuration = research.create_configuration()
85 configuration.with_defaults().activate()
86 research.activate(configuration)
87
88 # A RECORDED WORLD MUST BE BORN CONFIGURED. With custody active, a
89 # dynamic conjure REFUSES if any bind ran before the configuration
90 # was finalized - the profile record and default bootstrap would
91 # otherwise durably persist binds made against unsettled config.
92 spellbook_configuration = (
93 md.SpellbookConfiguration(FRAME).with_defaults().finalize()
94 )
95 book = md.Spellbook(aetheric_frame=FRAME,
96 configuration=spellbook_configuration)
97 # THE FRAME POSTURE GOES BEFORE THE BIND. A plain bind auto-declares
98 # into research only `if self._is_dynamic_posture()` - a frame-level
99 # question answerable before conjure. Bind into a not-yet-dynamic
100 # frame and nothing is declared, silently, because research
101 # bookkeeping never gates a bind. There would then be no node in the
102 # default lane to anchor a branch at.
103 book.configure_aether_frame(
104 system_state="dynamic",
105 disposal=None,
106 disposal_method_names=None,
107 rift_enabled=True,
108 ai_native=True,
109 )
110 v1 = book.bind(spell=RuleV1, existence="unique", permissions="create",
111 binding_name="branch-v1")
112 conduit = book.conjure(name="branch-root")
113 print("v1 recorded on the default lane:", v1[:12], "...")
114
115 nexus = md.Nexus()
116 system_configuration = nexus.create_configuration()
117 system_configuration.with_rift_creation_enabled(True)
118 system_configuration.with_allowed_target_frame_names([FRAME])
119 nexus.activate(system_configuration)
120 rift_configuration = nexus.create_rift_configuration()
121 rift_configuration.with_space_type("codegen")
122 rift = nexus.create_rift(configuration=rift_configuration,
123 rift_name="brancher")
124 rift.mark_active()
125 rift.create_frame_link(FRAME)
126 commands = rift.space.command_system
127
128 # ANCHORING IS ALL OR NOTHING - and it refuses before doing anything.
129 try:
130 commands.research_create_lane("half-anchored", attach_to="default")
131 raise AssertionError("expected a refusal: anchor is all-or-nothing")
132 except ValueError as error:
133 print()
134 print("attach_to without attach_at_spell_id refused -")
135 print(" ", error)
136
137 # CUT A BRANCH, ANCHORED. It starts EMPTY.
138 clean = commands.research_create_lane(
139 "clean-branch",
140 attach_to="default",
141 attach_at_spell_id=v1,
142 reason="a branch that will join cleanly",
143 )
144 print()
145 print("created 'clean-branch':")
146 print(" lane_type :", clean["lane_type"], " (default is experiment)")
147 print(" anchor_spell:", str(clean["anchor_spell_id"])[:12], "...")
148 print(" nodes :", len(clean["nodes"]),
149 " <- EMPTY: ancestry only, nothing copied")
150 assert clean["lane_type"] == "experiment"
151 assert clean["anchor_spell_id"] == v1
152 assert len(clean["nodes"]) == 0
153
154 # A SECOND BRANCH FROM THE SAME POINT. This one will go stale.
155 commands.research_create_lane(
156 "stale-branch",
157 attach_to="default",
158 attach_at_spell_id=v1,
159 reason="a branch the receiver will move away from",
160 )
161 heads = commands.research_heads()
162 print()
163 print("research_heads() ->", len(heads), "open lanes")
164
165 # THE CLEAN JOIN. The receiver's tip is still exactly at the anchor.
166 commands.research_join("clean-branch", into="default",
167 reason="nothing diverged")
168 print()
169 print("join('clean-branch' -> 'default') accepted")
170 print(" source anchored onto the receiver AND the receiver still at")
171 print(" the anchor: the only arrangement that is not a supersede")
172
173 # NOW MOVE THE RECEIVER. A new version lands on the default lane, so
174 # 'stale-branch' is anchored at a point the receiver has left.
175 v2 = conduit.bind_inactive(
176 spell=RuleV2,
177 spell_index=conduit.get_spell_by_spell_id(v1).spell_index,
178 existence="unique",
179 permissions="create",
180 )
181 print()
182 print("v2 recorded; the default lane moved on:", v2[:12], "...")
183
184 # THE DIVERGENT JOIN REFUSES, AND THE REFUSAL NAMES THE TIPS.
185 try:
186 commands.research_join("stale-branch", into="default")
187 raise AssertionError("expected a refusal: divergent join")
188 except RuntimeError as error:
189 print()
190 print("join('stale-branch' -> 'default') refused -")
191 print(" ", str(error)[:150])
192 print(" it names BOTH tips, so you are told what drifted rather")
193 print(" than handed a conflict to go and find")
194
195 # force=True IS A SUPERSEDE, NOT A MERGE.
196 commands.research_join("stale-branch", into="default", force=True,
197 reason="explicit supersede, content already "
198 "reconciled elsewhere")
199 print()
200 print("join(..., force=True) accepted - and note what it is NOT:")
201 print(" it did not merge any SOURCE. Reconciliation by content is")
202 print(" work you do in the codegen workshop; you register the")
203 print(" multi-parent result and THEN join. The record books an")
204 print(" outcome and never guesses at your code")
205
206 # THE JOINED LANES ARE CLOSED. A join is terminal for the source.
207 final_heads = commands.research_heads()
208 print()
209 print("open lanes now:", len(final_heads), " (joined sources closed)")
210 assert len(final_heads) <= len(heads)
211
212 print()
213 print("a branch starts EMPTY and remembers where it came from")
214 print("a clean join is anchored AND un-drifted; anything else is a")
215 print("supersede you have to ask for by name")
216
217
218if __name__ == "__main__":
219 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.