osrforge.contracts.stages
Stage-cache contracts: the stages/survey.json, areas.<dungeon>.<level>.json, and monsters.json wire formats.
Stage caches are cross-stage wire formats — content reads survey's cache, and the monsters and assemble stages read both — so their models live here, in the established home for anything serialized between stages. No stage module ever imports another stage module.
Pinned reading: a stage cache holds the extraction stage's
validated-and-normalized output, not the model's literal wire answers.
Canonical ids and keys are normalized at the source (the survey stage), with
the model's original spellings preserved in source_label/name; a host
that wants the literal wire answers wraps its provider in
RecordingProvider.
Caches carry schema_version only, not osrforge_version — the producing
package version is already recorded once per workdir in run.json, and
duplicating it per cache would break golden-file byte-equality on every package
version bump. Usage and model identity likewise live only in run.json:
usage varies with provider retries, so putting it here would let byte-identical
extractions produce differing caches.
AC_NOTATIONS
module-attribute
AC_NOTATIONS: tuple[str, ...] = get_args(AcNotation)
The AcNotation wire values, for building extraction-schema enums.
AREA_KINDS
module-attribute
The AreaKind wire values, for building extraction-schema enums.
CANONICAL_SLUG_PATTERN
module-attribute
The canonical id/key grammar.
The alphabet is the point: no / (the AreaAddress grammar forbids it), no
. (so areas.<dungeon>.<level>.json filenames parse unambiguously and
request tags stay within the tag charset), and lowercase (so a hand-edited
overrides.yaml can't alias 4a against 4A).
CONNECTION_VIAS
module-attribute
CONNECTION_VIAS: tuple[str, ...] = get_args(ConnectionVia)
The ConnectionVia wire values, for building extraction-schema enums.
DICE_PATTERN
module-attribute
osrlib's dice grammar: optional 1-999 count, the allowed die sizes, optional canonical modifier.
The content-batch schema constrains count_dice with this exact pattern, so a
schema-valid dice string is an osrlib-parseable dice string — a looser pattern
like 2d7 or 1d6+07 would pass the cache and fail KeyedMonster validation
at assembly.
DIRECTIONS
module-attribute
The Direction wire values, for building extraction-schema enums.
MAP_DOORS
module-attribute
The MapDoor wire values, for building extraction-schema enums.
AcNotation
module-attribute
AcNotation = Literal['descending', 'ascending', 'dual']
How a printed armour class counts: classic descending, modern ascending, or both (5 [14]).
AreaKind
module-attribute
AreaKind = Literal['room', 'corridor', 'cave', 'landmark', 'other']
A surveyed area's rough kind.
ConnectionVia
module-attribute
ConnectionVia = Literal['passage', 'door', 'secret_door', 'stairs', 'trapdoor', 'chute', 'other']
A connection's stated mechanism; passage when the text names none.
secret_door is its own value, not a modifier: it drives both
DoorSpec(kind="secret") and the playability lint's secret_only_access
warning.
Direction
module-attribute
Direction = Literal['north', 'south', 'east', 'west', 'up', 'down', 'unknown']
A connection's compass or vertical direction.
MapDoor
module-attribute
MapDoor = Literal['none', 'door', 'secret_door']
A map-proposed pair's door reading; none is an assertion, not an absence.
The truth convention's own posture, inverted from prose: a proposed pair's
door reading is complete, so none on a proposed pair disputes a
prose-stated door, while prose with no stated door on that pair agrees
silently whatever its non-door mechanism (the map's vocabulary doesn't carry
mechanisms).
ResolutionMethod
module-attribute
ResolutionMethod = Literal['exact', 'alias', 'fuzzy', 'llm', 'unresolved', 'override', 'custom']
How a name resolved: one of the four resolution tiers, not at all, a human override, or emission.
override and custom appear only in memory, when a monster override
supersedes a cached resolution or template emission gives an unresolved name
the module's own creature during assembly — the monsters.json cache is
written by the monsters stage alone and never contains either.
AreaConnection
Bases: BaseModel
One extracted connection to another area.
to_key is a free string — connections may cross batches or levels; the
prompt instructs canonical keys from the survey excerpt, and dangling
references are assembly's job, surfacing as connection_ambiguous.
to_level is the escape hatch for level-shaped targets ("stairs descend to
the second level" states a level, not a keyed area); the prompt prefers the
keyed target.
The failure posture, pinned: tolerate and flag, never reject. The batch
JSON schema stays flat (no conditional coupling — structured-output
implementations handle it badly), so a schema-valid response can carry
door conditions on a non-door via, or neither target. This model accepts
all of it and consumers discard-with-flag: geometry reads door conditions
only when via is a door kind, and a connection with neither to_key nor
to_level is skipped with connection_ambiguous:no target stated. A
pydantic error mid-stage would be a crash, not defense.
to_key
class-attribute
instance-attribute
to_key: str | None = None
The stated target area key; None when no keyed target was stated.
direction
instance-attribute
direction: Direction
The stated compass or vertical direction; unknown when unstated.
via
class-attribute
instance-attribute
via: ConnectionVia = 'passage'
The stated mechanism; passage when the text names none.
door_stuck
class-attribute
instance-attribute
door_stuck: bool = False
A stated stuck-door condition; meaningful only on a door via.
door_locked
class-attribute
instance-attribute
door_locked: bool = False
A stated locked-door condition; meaningful only on a door via.
to_level
class-attribute
instance-attribute
to_level: int | None = None
A stated target level, for level-shaped targets with no keyed area.
AreaContent
Bases: BaseModel
One keyed area's extracted content.
encounters
instance-attribute
encounters: tuple[AreaEncounter, ...]
The area's extracted encounters.
trap
class-attribute
instance-attribute
trap: str | None = None
The stated trap, verbatim; None when the area states none.
treasure
instance-attribute
The stated treasure strings, verbatim — parsed later by assembly's
treasure grammar (parse_treasure).
features
instance-attribute
Notable stated features, one entry per feature.
connections
instance-attribute
connections: tuple[AreaConnection, ...]
The stated connections out of this area.
source_pages
instance-attribute
The 1-based source pages this content was extracted from.
AreaEncounter
Bases: BaseModel
One extracted encounter: a monster name plus what the module said about count.
The three count fields are independent optionals; the cache stores what the
model said. Assembly's encounter builder owns the mapping onto osrlib's
exactly-one-of rule (prefer dice when both are set; flag when neither is)
and discards in memory any count_dice osrlib's dice parser still rejects,
flagging the area — defense in depth behind the extraction schema's
DICE_PATTERN. The mapping lives
in assembly, not the monsters stage: counts are per-encounter facts, and the
monsters cache is keyed per-name.
count_fixed
class-attribute
instance-attribute
A stated fixed count ("3 orcs").
count_dice
class-attribute
instance-attribute
count_dice: str | None = None
A stated dice count ("1d6 goblins"), within
DICE_PATTERN.
count_note
class-attribute
instance-attribute
count_note: str | None = None
A stated non-numeric count ("one per character"), verbatim.
LevelContent
Bases: BaseModel
The stages/areas.<dungeon>.<level>.json cache: one level's extracted areas.
schema_version
class-attribute
instance-attribute
schema_version: int = SCHEMA_VERSION
The stage-cache schema version this cache was written under.
level_number
class-attribute
instance-attribute
The 1-based level number, matching the survey index.
areas
instance-attribute
areas: tuple[AreaContent, ...]
The level's extracted areas, in survey order.
MapEdgeProposal
Bases: BaseModel
One map-proposed same-level adjacency between two printed keys.
a and b cache as answered, raw — endpoint resolution
(exact-then-slug against the survey's keys) happens at reconcile time in
deterministic code, so a reconciliation-policy fix never strands a
recorded fixture.
MapLevelReading
Bases: BaseModel
One level's map reading: the proposals, or the recorded reason there are none.
level_number
class-attribute
instance-attribute
The 1-based level number, matching the survey index.
map_pages
instance-attribute
The pages actually sent — provenance, not the survey's map_pages
verbatim: blanked renders are excluded, and an unread level sent none.
status
instance-attribute
status: Literal['read', 'unread']
Whether this level's map was read. An unread level produces no
adoptions and no disagreements at reconcile time — the prose-only path,
bit for bit.
unread_reason
class-attribute
instance-attribute
unread_reason: str | None = None
Why the level went unread (no_map_pages, pages_blanked,
no_keyed_areas, empty_reading); None exactly when status is
read.
proposals
class-attribute
instance-attribute
proposals: tuple[MapEdgeProposal, ...] = ()
The proposed adjacencies, as answered.
entrance_key
class-attribute
instance-attribute
entrance_key: str | None = None
The map-labeled way in, as answered; None when the map labels none.
MapReading
Bases: BaseModel
The stages/mapread.json cache: every level's map reading.
Carries the map_reading knob echo (the
StatBlocks.custom_monsters
precedent) so assembly reads the cache, never the knob; under off the
stage writes the echo with zero levels.
schema_version
class-attribute
instance-attribute
schema_version: int = SCHEMA_VERSION
The stage-cache schema version this cache was written under.
map_reading
instance-attribute
map_reading: Literal['read', 'off']
The knob the stage ran under, echoed.
levels
class-attribute
instance-attribute
levels: tuple[MapLevelReading, ...] = ()
One reading per surveyed level, in survey order; empty under off.
MonsterResolution
Bases: BaseModel
One extracted name's resolution against the osrlib monster catalog.
template_id
class-attribute
instance-attribute
template_id: str | None = None
The matched catalog template id; None exactly when unresolved.
vetoed_template_id
class-attribute
instance-attribute
vetoed_template_id: str | None = None
The LLM- or fuzzy-tier pick the stat-block veto
discarded — additive and defaulted, so caches recorded before the veto
existed still load. The cache invariant template_id is None ⟺ method ==
"unresolved" holds; the veto record rides beside it.
veto_detail
class-attribute
instance-attribute
veto_detail: str | None = None
The human-readable both-readings record of the veto
(orc chief → orc, printed HD 2 vs 1).
MonsterResolutions
Bases: BaseModel
The stages/monsters.json cache: every keyed encounter name's resolution.
Keys are normalized names (casefolded, internal whitespace collapsed,
stripped — normalize_monster_name), sorted ascending for byte stability.
Normalization is the point: modules spell the same monster "Zombies" and
"zombies", one resolution must serve both, and override matching
(overrides.yaml monsters: keys) normalizes the same way.
schema_version
class-attribute
instance-attribute
schema_version: int = SCHEMA_VERSION
The stage-cache schema version this cache was written under.
resolutions
instance-attribute
resolutions: dict[str, MonsterResolution]
Normalized name → its resolution, keys sorted ascending.
RawStatBlock
Bases: BaseModel
One creature's printed stat block, transcribed system-neutrally — never converted.
Every field is the page's text or number as printed; the stat-block pass
transcribes and classifies notation, nothing more — every rules judgment
(AC complements, THAC0/saves/XP derivation, movement rates) lives in
assembly's deterministic mapping, where it is testable and correctable.
A value the pages don't print is None.
ac
class-attribute
instance-attribute
ac: str | None = None
The armour-class value exactly as printed ("5", "5 [14]").
ac_notation
class-attribute
instance-attribute
ac_notation: AcNotation | None = None
Which system the printed ac counts in.
thac0
class-attribute
instance-attribute
thac0: str | None = None
The printed to-hit line, keeping its notation ("17", "19 [+0]", "+2").
hit_dice
class-attribute
instance-attribute
hit_dice: str | None = None
The Hit Dice line as printed ("3+1", "1-1", "½", "2d8").
class_level
class-attribute
instance-attribute
class_level: str | None = None
A printed class-and-level designation ("F 3", "3rd-level cleric") —
the leveled-NPC shape that prints no HD line; hit_dice and this are the
two printed forms of the same fact.
hp
class-attribute
instance-attribute
The printed hit points.
attacks
class-attribute
instance-attribute
One entry per printed attack line, counts and damage as printed
("2 claws (1d4 each)").
movement
class-attribute
instance-attribute
movement: str | None = None
The printed movement line ("120' (40')", "Fly 180' (60')").
saves
class-attribute
instance-attribute
saves: str | None = None
The printed saving-throw line, whatever its form ("D12 W13 P14 B15
S16 (2)", "save as F2").
morale
class-attribute
instance-attribute
The printed morale score.
alignment
class-attribute
instance-attribute
alignment: str | None = None
The printed alignment, verbatim.
xp
class-attribute
instance-attribute
The printed XP award.
number_appearing
class-attribute
instance-attribute
number_appearing: str | None = None
The printed number-appearing value ("1d6 (2d6)", "2-8").
special
class-attribute
instance-attribute
One entry per printed special-ability line or note.
confidence
class-attribute
instance-attribute
The transcription self-assessment. Defaults to 1.0 because an override-supplied block is the human's word; the model pass always sets its own.
StatBlocks
Bases: BaseModel
The stages/statblocks.json cache: raw printed stat blocks for the unresolved names.
Assembly is driven purely by this cache's contents — it never reads the
custom_monsters knob itself, only the echo stored here.
schema_version
class-attribute
instance-attribute
schema_version: int = SCHEMA_VERSION
The stage-cache schema version this cache was written under.
custom_monsters
instance-attribute
custom_monsters: Literal['emit', 'off']
The knob the stage ran under, echoed.
blocks
class-attribute
instance-attribute
blocks: dict[str, RawStatBlock | None] = {}
Normalized name → its raw block, keys sorted ascending. Under emit,
an entry for every name in the stat-block population — the union of
unresolved, LLM-resolved, and fuzzy-resolved — a block, or an explicit null absent
marker (the pass ran and found nothing). Under off, empty. Caches
written before the population widened carry unresolved names only;
assembly treats a missing LLM/fuzzy entry as no evidence.
SurveyArea
Bases: BaseModel
One keyed area in the survey index.
source_label
class-attribute
instance-attribute
source_label: str | None = None
The model's original key spelling, preserved wherever the canonical
key differs from a non-empty printed spelling; None when they agree
or the model's spelling was empty.
SurveyDungeon
Bases: BaseModel
One dungeon in the survey index.
id is the canonical slug derived from name; name is the model's
printed name, untouched. There is no source_label here — the id derives
from name, which is preserved on the same model, so a label could never
carry information name doesn't.
SurveyIndex
Bases: BaseModel
The stages/survey.json cache: the index that plans everything downstream.
schema_version
class-attribute
instance-attribute
schema_version: int = SCHEMA_VERSION
The stage-cache schema version this cache was written under.
title
instance-attribute
title: str
The module's printed title; empty when unstated (assembly defaults and flags it).
description
class-attribute
instance-attribute
description: str = ''
The module's own pitch — an excerpt of its printed introduction or back-cover text, never invented, empty when the module has none — defaulted so survey caches recorded before the field existed still load and assemble.
dungeons
instance-attribute
dungeons: tuple[SurveyDungeon, ...]
The surveyed dungeons, in document order.
monster_names
instance-attribute
The document-wide monster-name superset — wandering tables and
townsfolk included. The narrower resolution population is
encounter_names.
census_disputes
class-attribute
instance-attribute
Where the survey census disagreed
with the survey — one stable human-readable entry per disagreement
(census names 'crypt-of-horrors'; survey does not). Additive and
defaulted (the TownInfo.services precedent), so caches recorded before
the census existed still load; assembly turns each entry into a
module-scope survey_disputed flag.
SurveyLevel
Bases: BaseModel
One dungeon level in the survey index.
number
class-attribute
instance-attribute
The 1-based level number, unique within its dungeon.
map_pages
instance-attribute
The pages showing this level's map — load-bearing for the content
stage's direction extraction: the map pages ride along on every content
batch so the model can answer direction when the prose is silent.
TownInfo
Bases: BaseModel
The town or home base — never a dungeon.