Remove three kinds of duplication in the backend

Stage 2, items 1, 4, and 5 of `plan/17-refactor.md`.

**One path resolver in `worldstate`.** `apply_delta` and `apply_override` routed
`flags.<name>`, `milestones.<id>`, `world.<stat>`, `player.<stat>`, and
`npc.<id>.<stat>` with parallel code, about 100 lines each. `_resolve` now says
what a path points at and returns either a target or the rejection to report.
Each function keeps its own write rule, because the rules genuinely differ: an
override sets a number rather than adding to it, ignores `cooldown`,
`max_delta_per_turn`, and the rule that a counter only counts up, and can un-set
a milestone.

A differential check ran both implementations over 3960 payloads: twenty paths,
fourteen values, three starting states, plus every three-path combination. The
results are identical except that 674 rejections from `apply_override` now carry
a `fix` string. `apply_delta` already worded those, and the world-state editor
renders them, so an override that names an unknown flag now explains itself the
way a delta does.

**`sse`, `SSE_HEADERS`, and `turn_error` move to `app/sse.py`.** Two routers
stream, and `chat.py` had to import from `routers.adventures` to reach them.

**`get_adventure_or_404` becomes the `current_adventure` dependency.** All 32
handlers repeated the call as their first statement. The ownership check now
reads in the signature and runs before the body. FastAPI caches a dependency for
one request, so the handler's `db` is the session the adventure came from.

The generated OpenAPI document is byte-identical except on `rename_branch`,
where `branch_id` is now listed before `adventure_id`, because that handler no
longer names `adventure_id` itself. Parameter order in the document is
cosmetic.

Six tests in `test_state_revert.py` call `undo_turn` and `retry_action`
directly rather than over HTTP. They pass the adventure they already hold
instead of an id.

549 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Dix4oGV3njgWRdu7P9t6r
This commit is contained in:
parththakkar106
2026-08-29 02:04:02 +05:30
co-authored by Claude Opus 5
parent 0623dd8b78
commit 2c57b1ceab
16 changed files with 258 additions and 261 deletions
+6 -10
View File
@@ -18,14 +18,15 @@ from ...context import lineage
from ...database import get_db
from . import turns
from .deps import CurrentUser, get_adventure_or_404, router
from .deps import CurrentUser, current_adventure, router
from .nodes import db_tip
from .paging import current_window
@router.get("/{adventure_id}/branches", response_model=list[schemas.BranchOut])
def list_branches(
adventure_id: int, db: Session = Depends(get_db), user: models.User = CurrentUser
db: Session = Depends(get_db),
adventure: models.Adventure = Depends(current_adventure),
):
"""Returns every branch of the adventure and where each one leaves its parent.
@@ -35,7 +36,6 @@ def list_branches(
`actions`, never one query per branch, so a view of a hundred forks does not
cost a hundred round trips.
"""
adventure = get_adventure_or_404(adventure_id, db, user)
branches = (
db.query(models.Branch)
.filter(models.Branch.adventure_id == adventure.id)
@@ -95,11 +95,10 @@ def get_branch_or_404(
"/{adventure_id}/branches/{branch_id}", response_model=schemas.BranchOut
)
def rename_branch(
adventure_id: int,
branch_id: int,
payload: schemas.BranchRename,
db: Session = Depends(get_db),
user: models.User = CurrentUser,
adventure: models.Adventure = Depends(current_adventure),
):
"""Names a branch, or clears the name to leave it unnamed.
@@ -107,7 +106,6 @@ def rename_branch(
name anyone chose, and storing one gives the client an empty label to draw
instead of the fork depth.
"""
adventure = get_adventure_or_404(adventure_id, db, user)
branch = get_branch_or_404(adventure, branch_id, db)
name = (payload.name or "").strip()
branch.name = name or None
@@ -145,7 +143,7 @@ def delete_branch(
adventure_id: int,
branch_id: int,
db: Session = Depends(get_db),
user: models.User = CurrentUser,
adventure: models.Adventure = Depends(current_adventure),
):
"""Deletes a branch and everything forked from it.
@@ -163,7 +161,6 @@ def delete_branch(
cascade on `branches.parent_branch_id`, so the delete is a single statement
however deep the subtree is.
"""
adventure = get_adventure_or_404(adventure_id, db, user)
branch = get_branch_or_404(adventure, branch_id, db)
if branch.parent_branch_id is None:
raise HTTPException(
@@ -238,7 +235,7 @@ def switch_branch(
adventure_id: int,
branch_id: int,
db: Session = Depends(get_db),
user: models.User = CurrentUser,
adventure: models.Adventure = Depends(current_adventure),
):
"""Reads and plays a different branch of the story.
@@ -249,7 +246,6 @@ def switch_branch(
another branch's numbers, including the world-state cooldown clock inside the
snapshot.
"""
adventure = get_adventure_or_404(adventure_id, db, user)
branch = db.get(models.Branch, branch_id)
if branch is None or branch.adventure_id != adventure.id:
raise HTTPException(404, "Branch not found")