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

Download this collection ยท Source on GitHub

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.

More expert examples ยท Level guide

API contracts๏ƒ