osrforge.overrides
Overrides application: the human correction channel, applied inside assembly.
Contract models live in contracts/overrides.py; this module is assembly-side
application code — the same split as report contracts vs. report production.
The anti-silent-no-op rule, pinned: every entry in a correction file must take
effect or application fails with OverrideError
naming the entry — a typo'd monster name or area address that silently did
nothing would let a human believe a correction landed when it didn't. The
division of labor: addressing errors are loud; content validity flows to the
report. An override whose template_id doesn't exist in the catalog does
take effect — the dangling id lands in the draft and validate_adventure
reports it in report.json, which is exactly the loop the human is already in.
Every function here is pure: application is part of assembly, and assembly's artifacts stay a deterministic function of the stage caches plus the overrides file.
AREA_OVERRIDE_FIELDS
module-attribute
The replaceable per-area fields, in the pinned overridden vocabulary order.
LevelOverridePlan
dataclass
LevelOverridePlan(
area_overrides: dict[str, AreaOverride] = dict[str, AreaOverride](),
adds: tuple[tuple[str, AreaOverride], ...] = (),
removed: frozenset[str] = frozenset(),
cells: dict[str, tuple[Position, ...]] = dict[str, tuple[Position, ...]](),
edges: dict[str, Edge] = dict[str, Edge](),
entrance_set: bool = False,
entrance: Position | None = None,
transitions_set: bool = False,
transitions: tuple[TransitionSpec, ...] = (),
)
One survey level's resolved override application, addressing-validated.
Attributes:
| Name | Type | Description |
|---|---|---|
area_overrides |
dict[str, AreaOverride]
|
Survey-area key → its field-replacement entry. |
adds |
tuple[tuple[str, AreaOverride], ...]
|
|
removed |
frozenset[str]
|
Survey-area keys whose |
cells |
dict[str, tuple[Position, ...]]
|
Area key → overridden cell cluster (survey areas and adds). |
edges |
dict[str, Edge]
|
The override edge map, canonically keyed. |
entrance_set |
bool
|
Whether the entrance was overridden (a |
entrance |
Position | None
|
The overridden entrance. |
transitions_set |
bool
|
Whether the transitions tuple was overridden. |
transitions |
tuple[TransitionSpec, ...]
|
The overridden transitions. |
area_overrides
class-attribute
instance-attribute
area_overrides: dict[str, AreaOverride] = field(default_factory=dict[str, AreaOverride])
cells
class-attribute
instance-attribute
edges
class-attribute
instance-attribute
OverridePlan
dataclass
OverridePlan(
levels: dict[tuple[str, int], LevelOverridePlan] = dict[tuple[str, int], LevelOverridePlan](),
town: TownOverride | None = None,
module: ModuleOverride | None = None,
)
The whole correction file, resolved against one survey index.
Attributes:
| Name | Type | Description |
|---|---|---|
levels |
dict[tuple[str, int], LevelOverridePlan]
|
|
town |
TownOverride | None
|
The town entry, validated non-empty. |
module |
ModuleOverride | None
|
The module entry, validated non-empty. |
apply_level_overrides
apply_level_overrides(geometry: LevelGeometry, plan: LevelOverridePlan | None) -> LevelGeometry
Apply one level's plan to its synthesized geometry, before LevelSpec construction.
Overridden cells replace an area's cluster wholesale and are kept in the
author's order (the human's word verbatim); a removed area's placed cells
become corridor; added areas append after the survey areas. Override edges
merge over the synthesized map, override winning per canonical key (a
wall entry legitimately seals a synthesized opening), and the merged map
is re-sorted under the pinned key order for byte stability. width and
height are recomputed as the bounding box of the final area and corridor
cells. Synthesis postconditions are not re-asserted — human input is a
runtime condition, not a bug; what a bad geometry override breaks,
validate_adventure and the playability lint report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
geometry
|
LevelGeometry
|
The synthesized level geometry. |
required |
plan
|
LevelOverridePlan | None
|
The level's plan, or |
required |
Returns:
| Type | Description |
|---|---|
LevelGeometry
|
The effective geometry; extraction-side facts (connection ambiguities, |
LevelGeometry
|
disconnected components) carry through unchanged — no override kind |
LevelGeometry
|
touches the extracted connection graph. |
apply_monster_overrides
apply_monster_overrides(resolutions: MonsterResolutions, overrides: Overrides) -> MonsterResolutions
Replace cached monster resolutions with the correction file's remaps.
Override keys match extracted names under the same normalization the
monsters stage uses, so "Hobgoblin Chieftain" still hits the cache's
"hobgoblin chieftain". A matched override replaces the cached resolution
before encounter building: the stand-in (or omission) never happens, no
monster_unresolved flag is emitted, and the report's monsters summary
counts the name resolved. The result is in-memory only — the monsters
cache is never rewritten, so a monster correction needs only assemble.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resolutions
|
MonsterResolutions
|
The monsters-stage cache. |
required |
overrides
|
Overrides
|
The loaded correction file. |
required |
Returns:
| Type | Description |
|---|---|
MonsterResolutions
|
The resolutions with every override applied; the input is unchanged. |
Raises:
| Type | Description |
|---|---|
OverrideError
|
If two override keys normalize to the same name (contradictory corrections), or a key matches no name in the cache (the error lists the cache's unresolved names — the likeliest targets). |
apply_template_overrides
apply_template_overrides(
blocks: dict[str, RawStatBlock | None], entries: dict[str, StatBlockOverride]
) -> dict[str, RawStatBlock | None]
Apply the planned template entries to the cached raw blocks, pre-mapping.
An entry patches its name's cached block field by field — absent leaves
the extracted value, explicit null clears it back to unprinted — and a
name with no cached block (or an absent marker) gets a candidate block
from the entry's fields alone. The result feeds the same refusal ladder
and mapping an extracted block does; the inputs are unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
blocks
|
dict[str, RawStatBlock | None]
|
The stat-block cache's |
required |
entries
|
dict[str, StatBlockOverride]
|
The planned entries
( |
required |
Returns:
| Type | Description |
|---|---|
dict[str, RawStatBlock | None]
|
The effective candidate blocks. |
canonicalize_edge_key
Re-key an override edge key through osrlib's own canonical form.
osrlib stores only north/west keys — a cell's east edge is its eastern
neighbour's west edge, its south edge the southern neighbour's north — and
consults LevelSpec.edges through edge_key, so a south/east key
stored verbatim would never take effect — the silent-no-op hazard this
function defuses: "5,2:east" becomes "6,2:west".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
An override edge key, |
required |
Returns:
| Type | Description |
|---|---|
str
|
The canonical edge key. |
effective_roster
effective_roster(survey_level: SurveyLevel, plan: LevelOverridePlan | None) -> tuple[tuple[str, str], ...]
The draft's ordered area roster for one level: (key, name) pairs.
Survey areas in survey order minus removals, then adds in correction-file
order — the one ordering rule build_draft and render_previews must
agree on, so previews always show the corrected roster.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
survey_level
|
SurveyLevel
|
The survey level. |
required |
plan
|
LevelOverridePlan | None
|
The level's plan, or |
required |
Returns:
| Type | Description |
|---|---|
tuple[str, str]
|
The ordered roster. Names are the survey's for survey areas and the |
...
|
override's for adds (name overrides are content-side and don't affect |
tuple[tuple[str, str], ...]
|
what the preview labels — it renders keys, not names). |
plan_overrides
plan_overrides(index: SurveyIndex, overrides: Overrides) -> OverridePlan
Resolve and validate every non-monster override entry against the survey.
All addressing and contradiction errors surface here, before any stage
tracking or artifact write — a correction file that cannot take effect
fails the command without touching the workdir. Monster overrides validate
separately (they address the monsters cache, not the survey) in
apply_monster_overrides.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index
|
SurveyIndex
|
The survey cache the entries address. |
required |
overrides
|
Overrides
|
The loaded correction file. |
required |
Returns:
| Type | Description |
|---|---|
OverridePlan
|
The resolved plan. |
Raises:
| Type | Description |
|---|---|
OverrideError
|
If an entry addresses no surveyed level or area and is
not a well-formed add (an add needs |
plan_template_overrides
plan_template_overrides(overrides: Overrides, resolutions: MonsterResolutions) -> dict[str, StatBlockOverride]
Resolve and validate the monster_templates: entries against the monsters cache.
Addressing and contradiction errors surface here, before any stage
tracking or artifact write. The cache-state errors (no statblocks.json,
an off knob echo) are assembly's — only assembly knows what caches
exist — so this function validates what the correction file alone can
contradict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
overrides
|
Overrides
|
The loaded correction file. |
required |
resolutions
|
MonsterResolutions
|
The monsters-stage cache (as written — the extracted-name authority the entries address). |
required |
Returns:
| Type | Description |
|---|---|
dict[str, StatBlockOverride]
|
Normalized name → its entry, for every |
Raises:
| Type | Description |
|---|---|
OverrideError
|
If two entry keys normalize to the same name, a name
also appears under |