Let a backup carry a story that went two ways
A bundle had one list and a forked adventure has two stories, so export was emitting every branch's turns interleaved by index — a mangled story rather than lost data, and unreachable only because forking has no UI yet. `ai-dnd-adventure-v2` carries the branches, the depth each one left its parent at, which attempt at every turn is the story, and what each node left behind. That last one is not decoration: the after-snapshots are what a branch switch puts back, and a bundle without them imports a tree nobody can switch inside. `app/bundle.py` owns both formats and nothing else knows either. The v1 reader stays — those files are already on people's disks — and it is now the only place a `variants` array exists anywhere. The rule the module is built on is that a bundle carries what was chosen and never what is derived. The head branch, the fork points, the live flags and the anchors are decisions somebody made. The lineage, the head depth, the legacy `index` and the variant ordinals are computed from those and are rebuilt on the way in, because a bundle is a text file anybody can edit and a derived field shipped beside its source is a chance for the file to disagree with itself where no read would report it. `index` is the one that stops being academic here. It agreed with `depth` until SP5, and this is the first writer that has to fill it for a forked story, where two branches both hold a node at depth 4. It is allocated one per turn instead: siblings share it, no two coordinates do. Everything a hand-edited file can get wrong about the shape of a tree is a 400 raised before the adventure row exists, because a half-applied import is exactly the failure this phase exists to end — a story that goes quiet. A file wrong about which attempt is live is corrected rather than refused; that is an invariant of the database, not of the format. Measured on the 600-action fixture: 587 kB to 911 kB, and all of the increase is the outcomes at 489 B a node — the coordinates themselves save 57.5 B a node against the old turn-and-variants shape. Twenty forks add 660 B. 4.3% of the import body cap. 381 tests green, 16 of them new in test_bundle_v2.py. No migration, no vacuum owed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015H5qiyiR7gtFQaoDphHZ3g
This commit is contained in:
committed by
Parth
co-authored by
Claude Opus 5
parent
d051501517
commit
a7bf47a35e
@@ -451,9 +451,11 @@ def test_memories_are_created_listed_and_deleted(client):
|
||||
def test_export_carries_the_whole_story(client):
|
||||
"""A bundle is a backup: every action, not the window.
|
||||
|
||||
NOTE for SP6 — the `format` assertion below is the one line in this file
|
||||
expected to change, when the bundle becomes `ai-dnd-adventure-v2`. It is
|
||||
correct through SP1-SP5.
|
||||
SP6 changed the format assertion below, as this note said it would. It also
|
||||
changed one more line in this file than the note allowed for — the
|
||||
`variants` array in `test_export_keeps_retry_attempts`, which is the same
|
||||
fact seen from the other side: a bundle that has coordinates has no use for
|
||||
a repeating group. Everything else here still passes unmodified.
|
||||
"""
|
||||
ScriptedProvider.replies = ["One.", "Two."]
|
||||
_play(client, "go north")
|
||||
@@ -462,7 +464,7 @@ def test_export_carries_the_whole_story(client):
|
||||
r = client.get(f"/api/adventures/{client.adv_id}/export")
|
||||
assert r.status_code == 200, r.text
|
||||
bundle = r.json()
|
||||
assert bundle["format"] == "ai-dnd-adventure-v1"
|
||||
assert bundle["format"] == "ai-dnd-adventure-v2"
|
||||
assert bundle["title"] == "Cave"
|
||||
assert [a["text"] for a in bundle["actions"]] == [
|
||||
OPENING, "> You go north.", "One.", "> You go south.", "Two.",
|
||||
@@ -490,8 +492,9 @@ def test_export_keeps_retry_attempts(client):
|
||||
client.post(f"/api/adventures/{client.adv_id}/retry")
|
||||
|
||||
bundle = client.get(f"/api/adventures/{client.adv_id}/export").json()
|
||||
ai = [a for a in bundle["actions"] if a["type"] == "ai"][-1]
|
||||
assert [v["text"] for v in ai["variants"]] == ["Attempt one.", "Attempt two."]
|
||||
ai = [a for a in bundle["actions"] if a["type"] == "ai"]
|
||||
assert [a["text"] for a in ai] == ["Attempt one.", "Attempt two."]
|
||||
assert [a["live"] for a in ai] == [False, True]
|
||||
|
||||
|
||||
# -------------------------------------------------------------- world state
|
||||
|
||||
Reference in New Issue
Block a user