Rewrite Python comments in Google developer documentation style (#12)
* Rewrite comments in Google developer documentation style Rewrite the comments and docstrings across the backend core modules so they read plainly. The previous prose was accurate but dense and figurative, which made it slow to skim. Applies the Google developer documentation style guide: short sentences, active voice, present tense, American spelling, and no metaphors, idioms, or rhetorical asides. Replaces em-dash chains with separate sentences.
This commit is contained in:
+110
-97
@@ -5,24 +5,25 @@ from pydantic import BaseModel, ConfigDict, Field, computed_field
|
||||
|
||||
from . import images
|
||||
|
||||
# Length caps (Phase 9). The VARCHAR ones are correctness, not just abuse
|
||||
# limits: Postgres enforces column lengths (SQLite never did), so anything
|
||||
# longer must be a 422 here rather than a 500 at INSERT. Text-column caps are
|
||||
# generous abuse ceilings a legitimate player won't hit.
|
||||
NAME_MAX = 200 # titles/names — VARCHAR(200)
|
||||
TAGS_MAX = 500 # VARCHAR(500)
|
||||
CARD_TYPE_MAX = 100 # VARCHAR(100)
|
||||
PROSE_MAX = 50_000 # memory, author's note, prompts, entries, notes...
|
||||
SCRIPT_MAX = 200_000 # one JS source
|
||||
ACTION_MAX = 20_000 # one player action
|
||||
# Length caps (Phase 9). The VARCHAR caps are a correctness requirement rather
|
||||
# than only an abuse limit. Postgres enforces column lengths and SQLite never
|
||||
# did, so a longer value has to be a 422 here rather than a 500 at INSERT. The
|
||||
# text-column caps are generous abuse ceilings that a legitimate player does not
|
||||
# reach.
|
||||
NAME_MAX = 200 # Titles and names. VARCHAR(200).
|
||||
TAGS_MAX = 500 # VARCHAR(500).
|
||||
CARD_TYPE_MAX = 100 # VARCHAR(100).
|
||||
PROSE_MAX = 50_000 # Memory, author's note, prompts, entries, and notes.
|
||||
SCRIPT_MAX = 200_000 # One JavaScript source file.
|
||||
ACTION_MAX = 20_000 # One player action.
|
||||
MEMORY_TEXT_MAX = 5_000
|
||||
# A scenario cover image, stored inline as a base64 data URI. 400x300 WebP at
|
||||
# the quality the editor encodes lands around 20-40 KB; 400 KB leaves room for
|
||||
# a client that downscales less aggressively without letting anyone park a
|
||||
# multi-megabyte PNG in a row that gets read on every list request.
|
||||
# A scenario cover image, stored inline as a base64 data URI. A 400x300 WebP at
|
||||
# the quality the editor encodes runs about 20 to 40 kB. A cap of 400 kB leaves
|
||||
# room for a client that downscales less aggressively, and it stops anyone from
|
||||
# storing a multi-megabyte PNG in a row that every list request reads.
|
||||
IMAGE_MAX = 400_000
|
||||
ICON_MAX = 16 # one emoji/glyph — VARCHAR(16)
|
||||
BRANCH_NAME_MAX = 80 # what a player called one line of the story — VARCHAR(80)
|
||||
ICON_MAX = 16 # One emoji or glyph. VARCHAR(16).
|
||||
BRANCH_NAME_MAX = 80 # What a player called one line of the story. VARCHAR(80).
|
||||
|
||||
Name = Annotated[str, Field(max_length=NAME_MAX)]
|
||||
Tags = Annotated[str, Field(max_length=TAGS_MAX)]
|
||||
@@ -77,12 +78,12 @@ class ScenarioBase(BaseModel):
|
||||
authors_note: Prose = ""
|
||||
ai_instructions: Prose = ""
|
||||
tags: Tags = ""
|
||||
# Cover art — an https URL or a base64 data URI. See app/images.py.
|
||||
# Cover art, either an https URL or a base64 data URI. See `app/images.py`.
|
||||
image: Image = ""
|
||||
# Emoji/glyph shown when `image` is empty.
|
||||
# The emoji or glyph shown when `image` is empty.
|
||||
icon: Icon = ""
|
||||
# Phase 12: RPG world-state template (stat defs, bands, rules, milestones).
|
||||
# None means no RPG layer.
|
||||
# Phase 12: the RPG world-state template, holding stat definitions, bands,
|
||||
# rules, and milestones. `None` means the scenario has no RPG layer.
|
||||
stat_schema: dict | None = None
|
||||
|
||||
|
||||
@@ -106,7 +107,7 @@ class ScenarioUpdate(BaseModel):
|
||||
|
||||
class ScenarioOut(ORMModel, ScenarioBase):
|
||||
id: int
|
||||
is_public: bool = False # shared demo content — read-only for everyone
|
||||
is_public: bool = False # Shared demo content, read-only for everyone.
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
story_cards: list[StoryCardOut] = []
|
||||
@@ -120,8 +121,9 @@ class ScenarioListItem(ORMModel):
|
||||
tags: str
|
||||
is_public: bool = False
|
||||
updated_at: datetime
|
||||
# Read off the row so `image_url` can be derived, but excluded from the
|
||||
# response: a list of base64 data URIs would be megabytes of JSON.
|
||||
# Read from the row so that `image_url` can be derived, and excluded from
|
||||
# the response, because a list of base64 data URIs would be megabytes of
|
||||
# JSON.
|
||||
image: str = Field("", exclude=True)
|
||||
icon: str = ""
|
||||
|
||||
@@ -136,7 +138,8 @@ class ScenarioListItem(ORMModel):
|
||||
class AdventureCreate(BaseModel):
|
||||
scenario_id: int | None = None
|
||||
title: Name | None = None
|
||||
# ${Placeholder} values collected from the player at start (AI Dungeon behavior).
|
||||
# The `${Placeholder}` values collected from the player at the start, which
|
||||
# is the AI Dungeon behavior.
|
||||
placeholders: dict[str, str] = {}
|
||||
|
||||
|
||||
@@ -151,27 +154,32 @@ class AdventureUpdate(BaseModel):
|
||||
|
||||
|
||||
class AdventureRefresh(BaseModel):
|
||||
"""Body for "Update from scenario". `placeholders` supplies answers the
|
||||
adventure has no stored value for (see AdventureCreate.placeholders); they
|
||||
are merged over the stored ones and saved."""
|
||||
"""The body for "Update from scenario".
|
||||
|
||||
`placeholders` supplies answers the adventure has no stored value for. See
|
||||
`AdventureCreate.placeholders`. The answers are merged over the stored ones
|
||||
and saved.
|
||||
"""
|
||||
|
||||
placeholders: dict[str, str] = {}
|
||||
|
||||
|
||||
class RefreshPlan(BaseModel):
|
||||
"""What a refresh would change — drives the confirm dialog."""
|
||||
"""What a refresh would change. The confirm dialog is built from this."""
|
||||
|
||||
scenario_id: int
|
||||
scenario_title: str
|
||||
has_changes: bool
|
||||
# field name -> {"old": ..., "new": ...}, only for fields that differ.
|
||||
# Maps a field name to `{"old": ..., "new": ...}`, for differing fields
|
||||
# only.
|
||||
fields: dict[str, dict] = {}
|
||||
# {"added"|"updated"|"removed": [card name, ...]}
|
||||
# Maps "added", "updated", or "removed" to a list of card names.
|
||||
cards: dict[str, list[str]] = {}
|
||||
# {"added"|"removed": [stat path, ...]} — live values are otherwise kept.
|
||||
# Maps "added" or "removed" to a list of stat paths. Live values are
|
||||
# otherwise kept.
|
||||
world_state: dict[str, list[str]] = {}
|
||||
# ${Placeholder} names the scenario asks for that the adventure has no
|
||||
# stored answer to; the client must collect these and send them back.
|
||||
# The `${Placeholder}` names the scenario asks for that the adventure has
|
||||
# no stored answer to. The client collects these and sends them back.
|
||||
placeholders_needed: list[str] = []
|
||||
|
||||
|
||||
@@ -182,42 +190,44 @@ class ActionOut(ORMModel):
|
||||
type: str
|
||||
text: str
|
||||
reasoning: str | None = None
|
||||
# Phase 12: compact RPG state changes for this turn (from the model property).
|
||||
# Phase 12: the compact RPG state changes for this turn, read from the
|
||||
# model property.
|
||||
world_changes: list[dict] = []
|
||||
# Retry history: how many attempts exist for this turn (0 = never retried)
|
||||
# and which one is live. The attempts themselves come from
|
||||
# GET /actions/{id}/variants so this payload stays small.
|
||||
# Retry history: how many attempts exist for this turn, where 0 means the
|
||||
# turn was never retried, and which attempt is live. The attempts themselves
|
||||
# come from `GET /actions/{id}/variants`, so this payload stays small.
|
||||
variant_count: int = 0
|
||||
variant_index: int = 0
|
||||
# SP9: the pager. How many takes this turn has, and which one is on screen —
|
||||
# `2/4`. Keyed on the parent, so it counts the takes of *this* turn and not
|
||||
# every node that happens to share a depth, and so it keeps counting them
|
||||
# after one has been forked onto a branch of its own.
|
||||
# SP9: the pager, such as `2/4`. It reports how many attempts this turn has
|
||||
# and which one is on screen. It is keyed on the parent, so it counts the
|
||||
# attempts of this turn rather than every node that shares a depth, and it
|
||||
# keeps counting them after one has been forked onto its own branch.
|
||||
#
|
||||
# 1/1 for a turn nobody has retaken, which is most of them; the client draws
|
||||
# no pager for a count of one. That is a different convention from
|
||||
# `variant_count`, which says 0 for the same case — those two are the
|
||||
# pre-SP9 pair and SP8 drops them.
|
||||
# A turn nobody has retaken reads 1/1, which is most turns, and the client
|
||||
# draws no pager for a count of one. `variant_count` uses a different
|
||||
# convention and reports 0 for the same case. Those two fields are the
|
||||
# pre-SP9 pair, and SP8 drops them.
|
||||
take_count: int = 1
|
||||
take_index: int = 0
|
||||
# Which line this node is on, so the pager can tell the two kinds of step
|
||||
# apart without asking the server first: a take on this branch is a leaf
|
||||
# with nothing under it, and showing it is a local matter; a take on another
|
||||
# branch has a story of its own, and going there is a branch switch.
|
||||
# Which line this node is on, so the pager can distinguish the two kinds of
|
||||
# step without asking the server. An attempt on this branch is a leaf with
|
||||
# nothing below it, so showing it is a local change. An attempt on another
|
||||
# branch has a story of its own, so moving to it is a branch switch.
|
||||
branch_id: int | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class VariantOut(BaseModel):
|
||||
# Since SP4 every attempt is its own node, so each one has an id — and the
|
||||
# client needs it: forking is addressed by the attempt being taken, not by
|
||||
# its ordinal in a group that renumbers whenever one is added.
|
||||
# Since SP4 every attempt is its own node, so each one has an id, and the
|
||||
# client needs that id. A fork is addressed by the attempt being promoted,
|
||||
# not by its position in a group that renumbers whenever an attempt is
|
||||
# added.
|
||||
id: int
|
||||
index: int
|
||||
text: str
|
||||
reasoning: str | None = None
|
||||
# See ActionOut.branch_id: it decides whether choosing this take is a local
|
||||
# step or a branch switch.
|
||||
# See `ActionOut.branch_id`. It decides whether choosing this attempt is a
|
||||
# local step or a branch switch.
|
||||
branch_id: int | None = None
|
||||
created_at: str | None = None
|
||||
active: bool = False
|
||||
@@ -230,12 +240,11 @@ class VariantSelect(BaseModel):
|
||||
class BranchOut(ORMModel):
|
||||
"""One line through the story tree (Phase 14, SP5).
|
||||
|
||||
Enough to draw the tree and nothing more: `fork_depth` is where this line
|
||||
leaves its parent and `depth` is where it currently ends, so a fork is two
|
||||
numbers rather than a walk. `own_actions` counts the turns played on this
|
||||
branch itself — the rest of its story is borrowed from its ancestors, which
|
||||
is the whole point and also why the number is smaller than the reader
|
||||
expects.
|
||||
This carries enough to draw the tree and nothing more. `fork_depth` is where
|
||||
this line leaves its parent, and `depth` is where it currently ends, so a
|
||||
fork is two numbers rather than a walk. `own_actions` counts the turns played
|
||||
on this branch itself. The rest of its story is borrowed from its ancestors,
|
||||
which is why the number is smaller than a reader expects.
|
||||
"""
|
||||
|
||||
id: int
|
||||
@@ -244,14 +253,14 @@ class BranchOut(ORMModel):
|
||||
depth: int
|
||||
own_actions: int = 0
|
||||
is_head: bool = False
|
||||
# NULL for a branch nobody has named. The client draws those from the fork
|
||||
# depth rather than the server inventing one — see the column comment.
|
||||
# NULL for a branch nobody has named. The client labels those from the fork
|
||||
# depth rather than the server inventing a name. See the column comment.
|
||||
name: str | None = None
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class BranchRename(BaseModel):
|
||||
"""A name a player chose, or `null` to go back to being unnamed."""
|
||||
"""A name a player chose, or `null` to make the branch unnamed again."""
|
||||
|
||||
name: Annotated[str, Field(max_length=BRANCH_NAME_MAX)] | None = None
|
||||
|
||||
@@ -263,23 +272,23 @@ class ActionUpdate(BaseModel):
|
||||
class ActionCreate(BaseModel):
|
||||
type: Literal["do", "say", "story", "continue"]
|
||||
text: ActionText = ""
|
||||
# The node this action is played after (SP9). Omitted means "the tip",
|
||||
# which is every ordinary turn.
|
||||
# The node this action is played after (SP9). Omitting it means the tip,
|
||||
# which is what every ordinary turn uses.
|
||||
#
|
||||
# Naming a take the story moved past is how a branch gets made: stepping
|
||||
# between takes costs nothing and creates nothing, and the fork happens on
|
||||
# the first thing written below one. That is the only moment the player has
|
||||
# said which line they mean — before it, they were reading.
|
||||
# Naming an attempt the story moved past is what creates a branch. Stepping
|
||||
# between attempts costs nothing and creates nothing, and the fork happens
|
||||
# on the first text written below one. That is the first moment the player
|
||||
# states which line they mean. Before it, they were reading.
|
||||
after_id: int | None = None
|
||||
|
||||
|
||||
class TakeCreate(BaseModel):
|
||||
"""Another take of a turn (SP9).
|
||||
"""Another attempt at a turn (SP9).
|
||||
|
||||
`text` is what the player is saying instead, and is theirs to write only
|
||||
when the turn was theirs. An AI turn's other take is generated, so the field
|
||||
is ignored there rather than refused — the client asks the same way for both
|
||||
and the node type decides what happens.
|
||||
`text` is what the player says instead, and it applies only when the turn was
|
||||
the player's. An AI turn's other attempt is generated, so the field is
|
||||
ignored there rather than rejected. The client makes the same request for
|
||||
both, and the node type decides what happens.
|
||||
"""
|
||||
|
||||
text: ActionText = ""
|
||||
@@ -298,9 +307,9 @@ class AdventureOut(ORMModel):
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
story_cards: list[StoryCardOut] = []
|
||||
# The NEWEST window of the story, not all of it — older pages arrive from
|
||||
# GET /{id}/actions as the reader scrolls up. `action_count` is the whole
|
||||
# story's length, which is how the client knows there is more above.
|
||||
# The newest window of the story, not all of it. Older pages arrive from
|
||||
# `GET /{id}/actions` as the reader scrolls up. `action_count` is the whole
|
||||
# story's length, which is how the client knows more actions exist above.
|
||||
actions: list[ActionOut] = []
|
||||
action_count: int = 0
|
||||
|
||||
@@ -310,7 +319,7 @@ class ActionPage(BaseModel):
|
||||
|
||||
actions: list[ActionOut] = []
|
||||
total: int = 0
|
||||
# Whether anything older than this slice exists. Computed server-side so
|
||||
# Whether anything older than this slice exists. The server computes it, so
|
||||
# the client never has to do arithmetic on positions to find the end.
|
||||
has_more: bool = False
|
||||
|
||||
@@ -348,10 +357,10 @@ class AdventureListItem(ORMModel):
|
||||
title: str
|
||||
updated_at: datetime
|
||||
action_count: int = 0
|
||||
# "Where you left off" — the tail of the most recent narrative beat, so a
|
||||
# Continue card can show the story instead of just a turn count.
|
||||
# The end of the most recent narration, so a Continue card can show the
|
||||
# story rather than only a turn count.
|
||||
snippet: str = ""
|
||||
# Cover art inherited from the parent scenario (see app/images.py).
|
||||
# Cover art inherited from the parent scenario. See `app/images.py`.
|
||||
image_url: str = ""
|
||||
icon: str = ""
|
||||
|
||||
@@ -403,8 +412,9 @@ class AdventureScriptOut(ORMModel):
|
||||
input_js: str
|
||||
context_js: str
|
||||
output_js: str
|
||||
# Set by the router (not stored): True when a syncable library version
|
||||
# exists whose code differs from this copy; None when nothing to sync.
|
||||
# The router sets this field, which is not stored. It is `True` when a
|
||||
# syncable library version exists whose code differs from this copy, and
|
||||
# `None` when there is nothing to sync from.
|
||||
out_of_date: bool | None = None
|
||||
|
||||
|
||||
@@ -419,9 +429,9 @@ class AdventureScriptUpdate(BaseModel):
|
||||
# ---------- Auth (Phase 8) ----------
|
||||
|
||||
class AuthCredentials(BaseModel):
|
||||
email: Annotated[str, Field(max_length=320)] # VARCHAR(320)
|
||||
# Upper bound keeps scrypt cost flat — hashing megabyte "passwords" is CPU
|
||||
# an attacker would otherwise get for free.
|
||||
email: Annotated[str, Field(max_length=320)] # VARCHAR(320).
|
||||
# The upper bound keeps the scrypt cost constant. Without it, hashing a
|
||||
# megabyte password would give an attacker free CPU time.
|
||||
password: Annotated[str, Field(max_length=128)]
|
||||
|
||||
|
||||
@@ -429,7 +439,8 @@ class AuthCredentials(BaseModel):
|
||||
|
||||
class SettingsOut(ORMModel):
|
||||
endpoint_url: str
|
||||
# The key itself is never echoed back (encrypted at rest, write-only).
|
||||
# The key itself is never returned. It is encrypted at rest and
|
||||
# write-only.
|
||||
has_api_key: bool
|
||||
model: str
|
||||
api_mode: str
|
||||
@@ -449,12 +460,12 @@ ScenarioOut.model_rebuild()
|
||||
|
||||
|
||||
# ---------- AI Chat (power users) ----------
|
||||
# A scratchpad for talking to a model directly, with no story framing. Nothing
|
||||
# is persisted server-side, so these caps are purely per-request abuse limits.
|
||||
# A scratchpad for talking to a model directly, with no story framing. The
|
||||
# server persists nothing, so these caps are per-request abuse limits only.
|
||||
|
||||
CHAT_MESSAGE_MAX = 100_000 # one message
|
||||
CHAT_TOTAL_MAX = 400_000 # whole conversation sent up per request
|
||||
CHAT_MESSAGES_MAX = 200 # turns per request
|
||||
CHAT_MESSAGE_MAX = 100_000 # One message.
|
||||
CHAT_TOTAL_MAX = 400_000 # The whole conversation sent per request.
|
||||
CHAT_MESSAGES_MAX = 200 # Turns per request.
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
@@ -464,22 +475,24 @@ class ChatMessage(BaseModel):
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
messages: Annotated[list[ChatMessage], Field(min_length=1, max_length=CHAT_MESSAGES_MAX)]
|
||||
# Empty/omitted = fall back to the user's configured model.
|
||||
# If this field is empty or omitted, the user's configured model is used.
|
||||
model: Name | None = None
|
||||
temperature: Annotated[float, Field(ge=0, le=5)] | None = None
|
||||
max_tokens: Annotated[int, Field(ge=1, le=100_000)] | None = None
|
||||
|
||||
|
||||
class SettingsUpdate(BaseModel):
|
||||
endpoint_url: Annotated[str, Field(max_length=500)] | None = None # VARCHAR(500)
|
||||
# Encryption expands the stored value ~4/3 into the same VARCHAR(500):
|
||||
# 256 plaintext chars is the largest safe input ("enc:" + Fernet + base64).
|
||||
endpoint_url: Annotated[str, Field(max_length=500)] | None = None # VARCHAR(500).
|
||||
# Encryption expands the stored value by about four thirds into the same
|
||||
# VARCHAR(500), so 256 plaintext characters is the largest safe input. The
|
||||
# stored form is "enc:" plus Fernet plus base64.
|
||||
api_key: Annotated[str, Field(max_length=256)] | None = None
|
||||
model: Name | None = None
|
||||
api_mode: Annotated[str, Field(max_length=20)] | None = None
|
||||
temperature: Annotated[float, Field(ge=0, le=5)] | None = None
|
||||
max_output_tokens: Annotated[int, Field(ge=1, le=100_000)] | None = None
|
||||
# -1 = explicitly off (sends `reasoning: {effort: none}`); 0 = send nothing.
|
||||
# A value of -1 turns reasoning off explicitly, which sends
|
||||
# `reasoning: {effort: none}`. A value of 0 sends nothing.
|
||||
reasoning_max_tokens: Annotated[int, Field(ge=-1, le=100_000)] | None = None
|
||||
context_token_budget: Annotated[int, Field(ge=256, le=200_000)] | None = None
|
||||
narrator_prompt: Prose | None = None
|
||||
|
||||
Reference in New Issue
Block a user