Let a branch be named, and thrown away

SP7 needs three branch operations and SP5 built one. Switching exists;
naming and deleting had no column and no route between them.

A name is stored because a player chose it. An unnamed branch keeps NULL
rather than a generated "branch 4" — a generated label is derived, and it
would go stale the moment a branch before it is deleted and the ordinals
shift underneath. The client draws those from the fork depth, which
nothing can shift. The v2 bundle carries the name for the same reason it
carries the fork points and leaves `lineage` out: it is a decision, not
something computed from one.

Delete is what stands between a tree and unbounded growth, since nothing
prunes one on its own. It refuses two branches: the root, which holds the
turns every other branch borrows, and the one being read — including any
branch the head was forked from, which is the same mistake in disguise and
the one that would cascade the head away and leave head_branch_id pointing
at nothing. The nodes, memories and descendants go through the foreign
keys that already cascade.

A cursor standing on a deleted branch is cleared. On Postgres a stale
branch id would simply never resolve; SQLite hands the freed id to the
next fork, and then the anchor resolves onto a branch it has never seen
and calls a stretch of story already summarized.

396 tests, 15 new.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015H5qiyiR7gtFQaoDphHZ3g
This commit is contained in:
parththakkar106
2026-08-18 19:14:07 +05:30
committed by Parth
co-authored by Claude Opus 5
parent a7bf47a35e
commit cf3d52171e
7 changed files with 606 additions and 6 deletions
+7
View File
@@ -211,6 +211,13 @@ class Branch(Base):
# beneath it, inclusive. Computed once at fork from the parent's lineage
# plus one entry, so no read ever reconstructs it.
lineage: Mapped[list] = mapped_column(JSON, default=list)
# What the player called this line of the story, or NULL for one nobody has
# named. NULL rather than a generated "branch 4", because a generated name
# is derived and this column is for what was chosen — the same rule the v2
# bundle is built on. A stored default would also become a lie the moment a
# branch before it is deleted and the ordinals shift under it; an unnamed
# branch is drawn from its fork depth instead, which nothing can shift.
name: Mapped[str | None] = mapped_column(String(80), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow)