On this page
Notch swapping a live object๏
๐ต Expert ยท Lesson 17
LINEAGES, AND A HOT SWAP TAKEN END TO END. An index is a version LINEAGE with exactly one SELECTED member. This lesson builds one, stages a second version onto it, shows that staging changes nothing about what resolves - which is why the swap is a two-step act - then takes the swap by the new version's spell_id, melds it at the index's address, and notches back. Everything runs on public calls.
THE THREE VERBS
book.bind(...) v1 active on a fresh index conduit.bind_inactive(...) v2 PARKED on the same index; returns v2's spell_id conduit.notch_spell( v2 becomes the active member spell_index=index, spell_id=v2_id)
EVERY VERSION IS BOUND THE WAY THE FIRST ONE WAS. An index keeps its first version's binding for life: the kind of spellframe (none, a string category or a Protocol) and the address that gives - the spellframe's name, or a bare class's own name, and the binding name. bind_inactive refuses a version bound any other way before anything is registered, so a retry with the right arguments is admitted. That is why a notch never moves the address, and whoever melds by the address follows the notch. Two classes cannot share a bare address, so v1 and v2 are both bound under one string category, "pricing" (2026-10-25; until then a version could be staged at any address and a notch moved the address with it).
NAME THE VERSION BY ITS SPELL_ID. A spell_id names one version, and the public spell_id->object lookups (get_spell_by_spell_id, find_spell_by_spell_id) answer exactly that version, active or parked (owner ruling 2026-10-17; until 0.2.8253 they answered a parked spell_id with the ACTIVE member). notch_spell takes the spell_id too and resolves the exact version inside the call. The Spellbook is still not the surface you notch from - the conduit's verb does the resolving (owner ruling 2026-10-16; until then the verb took only the parked OBJECT, no public door handed one out, and this lesson stopped at staging).
WHAT STAGING ACTUALLY BUYS YOU bind_inactive puts a real, bound, owned spell into the lineage and leaves the selection alone. The candidate is inert and unmeldable until something promotes it. That separation is what lets a swap be PREPARED long before it is TAKEN - the expensive, failure-prone half (compiling and registering a new version) happens while the old version is still serving.
AND WHAT A NOTCH DOES WHEN IT HAPPENS 0. Checks the version: it must belong to the conduit's Spellbook and to the index you passed, or the call raises ValueError before anything moves. 1. Moves the address's frame claim from the outgoing version to the incoming one in one compare-and-set. When another spell holds the address it raises RuntimeError naming the holder, and nothing moves. 2. Parks the outgoing active spell off the four active owned maps, then TEARS DOWN ITS CREATION CONTEXT so the door epoch bumps and the warm fast-door cannot serve the stale spell. 3. Promotes the parked candidate into the active maps and repoints the index pointer, old spell_id -> new spell_id. 4. Re-registers the index gated + dirty, so revalidation recompiles LAZILY on the next resolve rather than inside the window. All of it happens under one seal, and the sealed conduits' creation gates are quiesced for the duration, so no in-flight meld can straddle the repoint. You do not arrange any of that; the verb does, the same way link does in expert 16.
THE OUTGOING VERSION IS NOT DESTROYED. It stays a member of the lineage, parked, with the object it built, which is why a rollback is structurally the same act as the swap - a notch back by its spell_id - rather than a restore.
MIND THE LAYER WHEN YOU READ THE SOURCE. _apply_notch says contracted borrowers are "NOT yet fanned out HERE (owner-side only)". That HERE is load-bearing: the seam is the local switch, and Conduit.notch_spell one layer up does walk the links. A statement read out of its scope becomes a false one.
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/17_notch_swapping_a_live_object.py
py -3.14t UX_and_AIX_experiences/04_expert/17_notch_swapping_a_live_object.py
Public surface๏
Conduit.bind_inactive, Conduit.notch_spell(spell_id=...), Conduit.meld(spellframe=...), SpellIndex.selected_spell_id / spells_in_index, Conduit.get_spell_by_spell_id
Code๏
1"""
2TIER: expert (17)
3GOAL: LINEAGES, AND A HOT SWAP TAKEN END TO END. An index is a
4 version LINEAGE with exactly one SELECTED member. This lesson
5 builds one, stages a second version onto it, shows that staging
6 changes nothing about what resolves - which is why the swap is a
7 two-step act - then takes the swap by the new version's spell_id,
8 melds it at the index's address, and notches back. Everything
9 runs on public calls.
10
11 THE THREE VERBS
12
13 book.bind(...) v1 active on a fresh index
14 conduit.bind_inactive(...) v2 PARKED on the same index;
15 returns v2's spell_id
16 conduit.notch_spell( v2 becomes the active member
17 spell_index=index, spell_id=v2_id)
18
19 EVERY VERSION IS BOUND THE WAY THE FIRST ONE WAS. An index keeps
20 its first version's binding for life: the kind of spellframe
21 (none, a string category or a Protocol) and the address that
22 gives - the spellframe's name, or a bare class's own name, and
23 the binding name. `bind_inactive` refuses a version bound any
24 other way before anything is registered, so a retry with the
25 right arguments is admitted. That is why a notch never moves the
26 address, and whoever melds by the address follows the notch. Two
27 classes cannot share a bare address, so v1 and v2 are both bound
28 under one string category, "pricing" (2026-10-25; until then a
29 version could be staged at any address and a notch moved the
30 address with it).
31
32 NAME THE VERSION BY ITS SPELL_ID. A spell_id names one version,
33 and the public spell_id->object lookups (`get_spell_by_spell_id`,
34 `find_spell_by_spell_id`) answer exactly that version, active or
35 parked (owner ruling 2026-10-17; until 0.2.8253 they answered a
36 parked spell_id with the ACTIVE member). `notch_spell` takes the
37 spell_id too and resolves the exact version inside the call. The
38 Spellbook is still not the surface
39 you notch from - the conduit's verb does the resolving (owner
40 ruling 2026-10-16; until then the verb took only the parked
41 OBJECT, no public door handed one out, and this lesson stopped
42 at staging).
43
44 WHAT STAGING ACTUALLY BUYS YOU
45 `bind_inactive` puts a real, bound, owned spell into the lineage
46 and leaves the selection alone. The candidate is inert and
47 unmeldable until something promotes it. That separation is what
48 lets a swap be PREPARED long before it is TAKEN - the expensive,
49 failure-prone half (compiling and registering a new version)
50 happens while the old version is still serving.
51
52 AND WHAT A NOTCH DOES WHEN IT HAPPENS
53 0. Checks the version: it must belong to the conduit's
54 Spellbook and to the index you passed, or the call raises
55 ValueError before anything moves.
56 1. Moves the address's frame claim from the outgoing version to
57 the incoming one in one compare-and-set. When another spell
58 holds the address it raises RuntimeError naming the holder,
59 and nothing moves.
60 2. Parks the outgoing active spell off the four active owned
61 maps, then TEARS DOWN ITS CREATION CONTEXT so the door epoch
62 bumps and the warm fast-door cannot serve the stale spell.
63 3. Promotes the parked candidate into the active maps and
64 repoints the index pointer, old spell_id -> new spell_id.
65 4. Re-registers the index gated + dirty, so revalidation
66 recompiles LAZILY on the next resolve rather than inside the
67 window.
68 All of it happens under one seal, and the sealed conduits' creation
69 gates are quiesced for the duration, so no in-flight meld can
70 straddle the repoint. You do not arrange any of that; the verb
71 does, the same way `link` does in expert 16.
72
73 THE OUTGOING VERSION IS NOT DESTROYED. It stays a member of the
74 lineage, parked, with the object it built, which is why a
75 rollback is structurally the same act as the swap - a notch back
76 by its spell_id - rather than a restore.
77
78 MIND THE LAYER WHEN YOU READ THE SOURCE. `_apply_notch` says
79 contracted borrowers are "NOT yet fanned out HERE (owner-side
80 only)". That HERE is load-bearing: the seam is the local switch,
81 and `Conduit.notch_spell` one layer up does walk the links. A
82 statement read out of its scope becomes a false one.
83SURFACE EXERCISED: Conduit.bind_inactive, Conduit.notch_spell(spell_id=...),
84 Conduit.meld(spellframe=...),
85 SpellIndex.selected_spell_id / spells_in_index,
86 Conduit.get_spell_by_spell_id
87VERIFY: RUN GREEN 2026-10-25 on 3.14t, GIL off and on.
88"""
89import melder as md
90
91
92class HotSwapPricingV1:
93 def __init__(self) -> None:
94 self.rate = 100
95
96
97class HotSwapPricingV2:
98 def __init__(self) -> None:
99 self.rate = 250
100
101
102def main() -> None:
103 book = md.Spellbook(aetheric_frame="hotswap-world")
104 v1_id = book.bind(
105 spell=HotSwapPricingV1, spellframe="pricing",
106 existence="unique", permissions="create",
107 )
108 conduit = book.conjure(dynamic=True, name="hotswap-root")
109 print("v1 bound and active under the category 'pricing'")
110
111 # THE INDEX IS PUBLICLY REACHABLE from the active spell, and it is a
112 # LINEAGE: a set of member ids plus exactly one selection.
113 active = conduit.get_spell_by_spell_id(v1_id)
114 assert active is not None
115 index = active.spell_index
116 assert index.selected_spell_id == v1_id
117 assert index.spells_in_index() == {v1_id}
118 print("index: 1 member, selected =", v1_id[:12], "...")
119
120 # v1 RESOLVES at the index's address. This is the world as it stands.
121 before = conduit.meld(spellframe="pricing")
122 assert before.rate == 100
123 print("meld(spellframe='pricing') -> rate", before.rate)
124
125 # A VERSION BOUND ANOTHER WAY IS REFUSED. Bound bare, v2 would be
126 # addressed by its own name - another address than the index's - so
127 # bind_inactive refuses it before anything is registered.
128 try:
129 conduit.bind_inactive(
130 spell=HotSwapPricingV2,
131 spell_index=index,
132 existence="unique",
133 permissions="create",
134 )
135 except ValueError as refusal:
136 assert "keeps the binding its first version" in str(refusal)
137 else:
138 raise AssertionError("a version bound another way must be refused")
139 assert index.spells_in_index() == {v1_id}
140 print()
141 print("bind_inactive bare -> ValueError: the index keeps its first")
142 print(" version's binding, and nothing was registered")
143
144 # STAGE A CANDIDATE bound the way the index's first version was. It is
145 # bound, owned, and a real member of the lineage - and it changes
146 # nothing about what resolves. The refused try left nothing behind, so
147 # this is not a spell_id collision.
148 v2_id = conduit.bind_inactive(
149 spell=HotSwapPricingV2,
150 spell_index=index,
151 spellframe="pricing",
152 existence="unique",
153 permissions="create",
154 )
155 assert index.spells_in_index() == {v1_id, v2_id}
156 assert index.selected_spell_id == v1_id, (
157 "staging must NOT move the selection - preparing a swap and "
158 "taking it are two different acts"
159 )
160 print("v2 staged under 'pricing': 2 members, selection UNCHANGED")
161
162 # AND THE PARKED CANDIDATE IS INERT. The expensive half of the swap
163 # has already happened - v2 is compiled, registered and owned - while
164 # the old version is still the one serving traffic.
165 still = conduit.meld(spellframe="pricing")
166 assert still.rate == 100
167 print("meld -> rate", still.rate, " (v2 is parked, not resolvable)")
168 print(" the costly half of a swap is done while v1 still serves")
169
170 # A SPELL_ID NAMES ONE VERSION. Looking up the parked spell_id answers
171 # the parked version itself - not the member live for this lineage -
172 # and the notch below is given that same spell_id.
173 resolved_from_parked_id = conduit.get_spell_by_spell_id(v2_id)
174 assert resolved_from_parked_id is not None
175 assert resolved_from_parked_id.spell_id == v2_id
176 print()
177 print("looking up the PARKED spell_id returned the PARKED spell")
178 print(" and the notch is told the same version by the same spell_id")
179
180 # TAKE THE SWAP. The spell_id bind_inactive returned names the version;
181 # the notch resolves it inside the call and checks that it belongs to
182 # this index before anything moves. The address does not move: it now
183 # serves v2.
184 promoted = conduit.notch_spell(spell_index=index, spell_id=v2_id)
185 assert promoted.spell_id == v2_id
186 assert index.selected_spell_id == v2_id
187 assert index.spells_in_index() == {v1_id, v2_id}, (
188 "a notch moves the selection; it removes no member"
189 )
190 after = conduit.meld(spellframe="pricing")
191 assert after.rate == 250
192 assert conduit.meld(spell_id=v2_id) is after
193 print()
194 print("notch by spell_id: v2 selected, and the address follows -")
195 print(" meld(spellframe='pricing') -> rate", after.rate)
196
197 # ROLL BACK: the same verb pointed the other way, because v1 was
198 # parked, not destroyed - and its object with it.
199 conduit.notch_spell(spell_index=index, spell_id=v1_id)
200 assert index.selected_spell_id == v1_id
201 back = conduit.meld(spellframe="pricing")
202 assert back.rate == 100
203 assert back is before
204 print("notch back by spell_id: v1 selected, meld -> rate", back.rate,
205 "(v1's own object)")
206
207 print()
208 print("an index is a lineage with one selection and one binding")
209 print("staging adds a member bound the same way; only a notch moves")
210 print("the selection, and the address follows it")
211 print("nothing is destroyed by a notch, which is why rolling back")
212 print("is the same act and not a restore")
213
214
215if __name__ == "__main__":
216 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.