Skip to content

osrforge.mapread

Stage 4: the map-reading pass — one request per surveyed level over its printed map pages.

The map becomes extraction input here and only here: each request carries a level's keyed-area list (printed forms) plus its unblanked map pages, and the model proposes same-level adjacency pairs, doors on those pairs, and the map-labeled entrance — nothing more. No content-stage output rides the request: the map read is an independent second opinion on the prose's edge facts, and feeding it prose connections would let one instrument contaminate the other. Proposals cache as answered, raw — endpoint resolution happens at reconcile time (osrforge.reconcile) in deterministic code, so a reconciliation-policy fix never strands a recorded fixture.

The stage plans its requests off the survey cache alone (SurveyLevel.map_pages has fed the content pass's compass hint since phase 6); content and monsters never touch the cache, so only a survey re-run clears it. Downstream-most of the model stages by design: rerun mapread re-rolls only itself plus assembly, and a map failure preserves the expensive content and monsters caches.

MAPREAD_SYSTEM module-attribute

MAPREAD_SYSTEM = 'You read one dungeon level\'s printed map from a tabletop adventure module. The user message names the dungeon and level and lists the level\'s keyed areas in printed order, then interleaves each map page\'s extracted text (each headed by a [page N] marker) with that page\'s image — the map is in the images; read them.\n\nRules:\n- Propose adjacency only between the provided printed keys. Two keyed areas are adjacent when the map joins them directly by a door, opening, or passage, or through unkeyed passage in consecutive order along it.\n- Report a door only where the map marks one; use "secret_door" only where the map\'s legend marks the door secret. "none" means the map joins the pair with no door marked.\n- "entrance" is the keyed area holding the way into the level from outside, only where the map labels it (an entrance arrow, a labeled stair from the surface, a marked cave mouth). Answer null when the map labels none.\n- Propose nothing that cannot be read: keys the map does not show, adjacencies you cannot trace, or an entrance the map does not label are omissions, never guesses.\n'

build_mapread_request

build_mapread_request(
    dungeon_id: str, dungeon_name: str, level: SurveyLevel, parts: Sequence[TextPart | ImagePart]
) -> ModelRequest

Build one level's map-reading request.

Public and pure, like build_survey_request — the extraction runner's targeted leg must build fingerprint-identical requests without duplicating prompt code. The keyed-area list rides in survey order as printed forms (source_label or key, the census's printed-form recovery precedent), so the proposal universe and the yardstick agree on spellings.

Parameters:

Name Type Description Default
dungeon_id str

The canonical dungeon id.

required
dungeon_name str

The dungeon's printed name (the id when unnamed).

required
level SurveyLevel

The survey level whose map is being read.

required
parts Sequence[TextPart | ImagePart]

The level's unblanked map pages' interleaved parts (page_request_parts).

required

Returns:

Type Description
ModelRequest

The request, tagged mapread.<dungeon-id>.<level>.

Raises:

Type Description
ValueError

If no parts are given — a level with no sendable pages never builds a request (programmer misuse; the stage records an unread reading instead).

mapread

mapread(workdir: Workdir, provider: ModelProvider) -> MapReading

Run stage 4: read every surveyed level's map; write stages/mapread.json.

Per level, in survey order: pages in blank_page_renders are excluded from the sent set (preprocess emitted them as blank white PNGs — sending one buys nothing, and a blank read as an empty map would dispute every prose edge); a level with no map pages at all records unread:no_map_pages with no request; a nonempty map_pages whose every page is blanked records unread:pages_blanked with no request; a level with no keyed areas records unread:no_keyed_areas with no request (there are no printed keys to propose between); a level with two or more keyed areas whose answer proposes zero adjacencies records unread:empty_reading — a degenerate read must not assert map silence against the whole level — keeping the answer raw but discarding its assertions via the status; a single-area level keeps a zero-pair reading and its entrance proposal.

Under map_reading: off the stage completes writing the knob-echoed cache with zero levels — the prose-only pipeline, bit for bit. The cache is a single atomic artifact; no pre-clearing is needed.

Parameters:

Name Type Description Default
workdir Workdir

A workdir whose monsters stage is completed.

required
provider ModelProvider

The model provider.

required

Returns:

Type Description
MapReading

The reading, as written to the cache.

Raises:

Type Description
ValueError

If the monsters stage is not completed or the survey cache is missing (programmer misuse).

ProviderError

On provider transport, auth, or rate-limit exhaustion.

SchemaValidationError

If the provider exhausts its schema budget.

mapread_schema

mapread_schema() -> dict[str, object]

Build the map-reading JSON Schema: flat adjacencies plus a nullable entrance.

Flat and tolerate-and-flag (the AreaConnection posture): endpoints are free strings — the printed keys ride the request only through the prompt text, because enum-locking the endpoints would reject an answer the reconciler could still resolve by slug, and unresolvable answers drop at reconcile time with a flag, never a crash. Level-independent as a result: one schema serves every request.

Returns:

Type Description
dict[str, object]

The request schema.

mapread_tag

mapread_tag(dungeon_id: str, level_number: int) -> str

Return one level's request tag: mapread.<dungeon-id>.<level> within the tag charset.

The canonical slug alphabet guarantees the tag parses unambiguously — no dots in dungeon ids.

Parameters:

Name Type Description Default
dungeon_id str

The canonical dungeon id.

required
level_number int

The 1-based level number.

required

Returns:

Type Description
str

The request tag.

parse_mapread_response

parse_mapread_response(data: object) -> tuple[tuple[MapEdgeProposal, ...], str | None]

Parse one schema-valid map-reading response into raw proposals plus the entrance answer.

Public for the same reason the request builder is: the extraction runner's targeted leg must produce a cache byte-identical to the stage's without duplicating parsing code.

Parameters:

Name Type Description Default
data object

The response data, already validated against mapread_schema.

required

Returns:

Type Description
tuple[tuple[MapEdgeProposal, ...], str | None]

The proposals as answered, and the entrance key (or None).