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, 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)
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. 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. 2. Promotes the parked candidate into the active maps. 3. Repoints the index pointer and the framewide binding signature, 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, 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, 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, and notches back. Everything runs on public calls.
9
10 THE THREE VERBS
11
12 book.bind(...) v1 active on a fresh index
13 conduit.bind_inactive(...) v2 PARKED on the same index;
14 returns v2's spell_id
15 conduit.notch_spell( v2 becomes the active member
16 spell_index=index, spell_id=v2_id)
17
18 NAME THE VERSION BY ITS SPELL_ID. A spell_id names one version,
19 and the public spell_id->object lookups (`get_spell_by_spell_id`,
20 `find_spell_by_spell_id`) answer exactly that version, active or
21 parked (owner ruling 2026-10-17; until 0.2.8253 they answered a
22 parked spell_id with the ACTIVE member). `notch_spell` takes the
23 spell_id too and resolves the exact version inside the call. The
24 Spellbook is still not the surface
25 you notch from - the conduit's verb does the resolving (owner
26 ruling 2026-10-16; until then the verb took only the parked
27 OBJECT, no public door handed one out, and this lesson stopped
28 at staging).
29
30 WHAT STAGING ACTUALLY BUYS YOU
31 `bind_inactive` puts a real, bound, owned spell into the lineage
32 and leaves the selection alone. The candidate is inert and
33 unmeldable until something promotes it. That separation is what
34 lets a swap be PREPARED long before it is TAKEN - the expensive,
35 failure-prone half (compiling and registering a new version)
36 happens while the old version is still serving.
37
38 AND WHAT A NOTCH DOES WHEN IT HAPPENS
39 0. Checks the version: it must belong to the conduit's
40 Spellbook and to the index you passed, or the call raises
41 ValueError before anything moves.
42 1. Parks the outgoing active spell off the four active owned
43 maps, then TEARS DOWN ITS CREATION CONTEXT so the door epoch
44 bumps and the warm fast-door cannot serve the stale spell.
45 2. Promotes the parked candidate into the active maps.
46 3. Repoints the index pointer and the framewide binding
47 signature, old spell_id -> new spell_id.
48 4. Re-registers the index gated + dirty, so revalidation
49 recompiles LAZILY on the next resolve rather than inside the
50 window.
51 All of it happens under one seal, and the sealed conduits' creation
52 gates are quiesced for the duration, so no in-flight meld can
53 straddle the repoint. You do not arrange any of that; the verb
54 does, the same way `link` does in expert 16.
55
56 THE OUTGOING VERSION IS NOT DESTROYED. It stays a member of the
57 lineage, parked, which is why a rollback is structurally the same
58 act as the swap - a notch back by its spell_id - rather than a
59 restore.
60
61 MIND THE LAYER WHEN YOU READ THE SOURCE. `_apply_notch` says
62 contracted borrowers are "NOT yet fanned out HERE (owner-side
63 only)". That HERE is load-bearing: the seam is the local switch,
64 and `Conduit.notch_spell` one layer up does walk the links. A
65 statement read out of its scope becomes a false one.
66SURFACE EXERCISED: Conduit.bind_inactive, Conduit.notch_spell(spell_id=...),
67 Conduit.meld, SpellIndex.selected_spell_id /
68 spells_in_index, Conduit.get_spell_by_spell_id
69VERIFY: RUN GREEN 2026-10-17 on 3.14t, GIL off and on.
70"""
71import melder as md
72
73
74class HotSwapPricingV1:
75 def __init__(self) -> None:
76 self.rate = 100
77
78
79class HotSwapPricingV2:
80 def __init__(self) -> None:
81 self.rate = 250
82
83
84def main() -> None:
85 book = md.Spellbook(aetheric_frame="hotswap-world")
86 v1_id = book.bind(
87 spell=HotSwapPricingV1, existence="unique", permissions="create",
88 )
89 conduit = book.conjure(dynamic=True, name="hotswap-root")
90 print("v1 bound and active")
91
92 # THE INDEX IS PUBLICLY REACHABLE from the active spell, and it is a
93 # LINEAGE: a set of member ids plus exactly one selection.
94 active = conduit.get_spell_by_spell_id(v1_id)
95 assert active is not None
96 index = active.spell_index
97 assert index.selected_spell_id == v1_id
98 assert index.spells_in_index() == {v1_id}
99 print("index: 1 member, selected =", v1_id[:12], "...")
100
101 # v1 RESOLVES. This is the world as it stands.
102 before = conduit.meld("HotSwapPricingV1")
103 assert before.rate == 100
104 print("meld -> rate", before.rate)
105
106 # STAGE A CANDIDATE. It is bound, owned, and a real member of the
107 # lineage - and it changes nothing about what resolves.
108 v2_id = conduit.bind_inactive(
109 spell=HotSwapPricingV2,
110 spell_index=index,
111 existence="unique",
112 permissions="create",
113 )
114 assert index.spells_in_index() == {v1_id, v2_id}
115 assert index.selected_spell_id == v1_id, (
116 "staging must NOT move the selection - preparing a swap and "
117 "taking it are two different acts"
118 )
119 print()
120 print("v2 staged: 2 members, selection UNCHANGED")
121
122 # AND THE PARKED CANDIDATE IS INERT. The expensive half of the swap
123 # has already happened - v2 is compiled, registered and owned - while
124 # the old version is still the one serving traffic.
125 still = conduit.meld("HotSwapPricingV1")
126 assert still.rate == 100
127 print("meld -> rate", still.rate, " (v2 is parked, not resolvable)")
128 print(" the costly half of a swap is done while v1 still serves")
129
130 # A SPELL_ID NAMES ONE VERSION. Looking up the parked spell_id answers
131 # the parked version itself - not the member live for this lineage -
132 # and the notch below is given that same spell_id.
133 resolved_from_parked_id = conduit.get_spell_by_spell_id(v2_id)
134 assert resolved_from_parked_id is not None
135 assert resolved_from_parked_id.spell_id == v2_id
136 print()
137 print("looking up the PARKED spell_id returned the PARKED spell")
138 print(" and the notch is told the same version by the same spell_id")
139
140 # TAKE THE SWAP. The spell_id bind_inactive returned names the version;
141 # the notch resolves it inside the call and checks that it belongs to
142 # this index before anything moves.
143 promoted = conduit.notch_spell(spell_index=index, spell_id=v2_id)
144 assert promoted.spell_id == v2_id
145 assert index.selected_spell_id == v2_id
146 assert index.spells_in_index() == {v1_id, v2_id}, (
147 "a notch moves the selection; it removes no member"
148 )
149 after = conduit.meld(spell_id=v2_id)
150 assert after.rate == 250
151 print()
152 print("notch by spell_id: v2 selected, meld -> rate", after.rate)
153
154 # ROLL BACK: the same verb pointed the other way, because v1 was
155 # parked, not destroyed.
156 conduit.notch_spell(spell_index=index, spell_id=v1_id)
157 assert index.selected_spell_id == v1_id
158 back = conduit.meld(spell_id=v1_id)
159 assert back.rate == 100
160 print("notch back by spell_id: v1 selected, meld -> rate", back.rate)
161
162 print()
163 print("an index is a lineage with one selection")
164 print("staging adds a member; only a notch moves the selection")
165 print("nothing is destroyed by a notch, which is why rolling back")
166 print("is the same act and not a restore")
167
168
169if __name__ == "__main__":
170 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.