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:
co-authored by
Claude Opus 5
parent
0623dd8b78
commit
2c57b1ceab
@@ -16,12 +16,12 @@ from ...context import cursors
|
||||
from ...context import lineage
|
||||
from ...database import get_db
|
||||
from ...scripting import ScriptPipeline
|
||||
from ...sse import SSE_HEADERS
|
||||
|
||||
from . import turns
|
||||
from .deps import CurrentUser, get_adventure_or_404, router
|
||||
from .deps import CurrentUser, current_adventure, router
|
||||
from .nodes import delete_turn, last_action, stand_on
|
||||
from .paging import action_window, annotate_takes, current_window
|
||||
from .turns import SSE_HEADERS
|
||||
|
||||
|
||||
@router.post("/{adventure_id}/retry")
|
||||
@@ -30,6 +30,7 @@ def retry_action(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = CurrentUser,
|
||||
adventure: models.Adventure = Depends(current_adventure),
|
||||
):
|
||||
"""Regenerates the last AI action and keeps the discarded attempt.
|
||||
|
||||
@@ -38,7 +39,6 @@ def retry_action(
|
||||
attempt is stored as a sibling at the same coordinate. No text the AI wrote
|
||||
is rewritten or deleted.
|
||||
"""
|
||||
adventure = get_adventure_or_404(adventure_id, db, user)
|
||||
limits.rate_limit("turn", request, user)
|
||||
turns.check_demo_cap(db, user)
|
||||
turns.acquire_turn_lock(adventure_id)
|
||||
@@ -79,7 +79,7 @@ def list_variants(
|
||||
adventure_id: int,
|
||||
action_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = CurrentUser,
|
||||
adventure: models.Adventure = Depends(current_adventure),
|
||||
):
|
||||
"""Returns every attempt made for one AI turn.
|
||||
|
||||
@@ -90,7 +90,6 @@ def list_variants(
|
||||
Switching changes which row the story tells, and a client that holds an id it
|
||||
received a moment ago still has to be able to ask about the same turn.
|
||||
"""
|
||||
get_adventure_or_404(adventure_id, db, user)
|
||||
action = db.get(models.Action, action_id)
|
||||
if action is None or action.adventure_id != adventure_id:
|
||||
raise HTTPException(404, "Action not found")
|
||||
@@ -119,7 +118,7 @@ def select_variant(
|
||||
action_id: int,
|
||||
payload: schemas.VariantSelect,
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = CurrentUser,
|
||||
adventure: models.Adventure = Depends(current_adventure),
|
||||
):
|
||||
"""Makes an earlier attempt live again and restores the state it produced.
|
||||
|
||||
@@ -130,7 +129,6 @@ def select_variant(
|
||||
would leave the story contradicting itself. The attempts of earlier turns
|
||||
stay readable through `list_variants`.
|
||||
"""
|
||||
adventure = get_adventure_or_404(adventure_id, db, user)
|
||||
action = db.get(models.Action, action_id)
|
||||
if action is None or action.adventure_id != adventure_id:
|
||||
raise HTTPException(404, "Action not found")
|
||||
@@ -172,7 +170,7 @@ def fork_from_attempt(
|
||||
adventure_id: int,
|
||||
action_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = CurrentUser,
|
||||
adventure: models.Adventure = Depends(current_adventure),
|
||||
):
|
||||
"""Continues the story from this attempt, forking a branch if one is needed.
|
||||
|
||||
@@ -184,7 +182,6 @@ def fork_from_attempt(
|
||||
* The story has moved past its turn, so the endpoint forks. The attempt gets
|
||||
a branch of its own, and the line it leaves keeps every turn it has.
|
||||
"""
|
||||
adventure = get_adventure_or_404(adventure_id, db, user)
|
||||
action = db.get(models.Action, action_id)
|
||||
if action is None or action.adventure_id != adventure_id:
|
||||
raise HTTPException(404, "Action not found")
|
||||
@@ -236,6 +233,7 @@ def add_take(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: models.User = CurrentUser,
|
||||
adventure: models.Adventure = Depends(current_adventure),
|
||||
):
|
||||
"""Plays a turn again, whoever wrote it.
|
||||
|
||||
@@ -256,7 +254,6 @@ def add_take(
|
||||
turn, so the new attempt is written at the same depth under the same parent,
|
||||
and the line it leaves is unchanged. No node below is copied.
|
||||
"""
|
||||
adventure = get_adventure_or_404(adventure_id, db, user)
|
||||
limits.rate_limit("turn", request, user)
|
||||
limits.check_row_cap("actions", db, user, adventure=adventure)
|
||||
turns.check_demo_cap(db, user)
|
||||
@@ -317,7 +314,9 @@ def add_take(
|
||||
|
||||
@router.post("/{adventure_id}/undo", response_model=schemas.ActionPage)
|
||||
def undo_turn(
|
||||
adventure_id: int, db: Session = Depends(get_db), user: models.User = CurrentUser
|
||||
adventure_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
adventure: models.Adventure = Depends(current_adventure),
|
||||
):
|
||||
"""Deletes the last turn: the trailing AI action and its player action, if any.
|
||||
|
||||
@@ -325,7 +324,6 @@ def undo_turn(
|
||||
ran, and it prunes any memory that summarized the removed actions. The turn
|
||||
lock prevents an undo while a turn is still generating.
|
||||
"""
|
||||
adventure = get_adventure_or_404(adventure_id, db, user)
|
||||
turns.acquire_turn_lock(adventure_id)
|
||||
try:
|
||||
# Only the last turn is removed, so fetch the two actions it can
|
||||
|
||||
Reference in New Issue
Block a user