Skip to content

osrforge.assemble

Stage 6: assembly — overrides application, the Adventure build, validation, report production, artifact writing.

Assembly is pure: adventure.json, report.json, and the previews are a deterministic function of the cached stage outputs plus overrides.yamlassembly purity, the core guarantee. overrides.yaml is loaded once and threaded through; every override addressing error surfaces before any stage tracking or artifact write, so a correction file that cannot take effect fails the command without touching the workdir.

Two sequential trackings: geometry around synthesis and geometry-override application, then assemble around the build, validation, and artifact writes — so a failure in the second leaves an honest geometry: completed. Neither stage touches a provider; usage stays zero and run.json's provider identity is untouched.

Flags describe the built draft — the unifying rule: because overrides replace inputs to the build, every flag falls out of the path actually taken (an overridden encounter emits no monster or count flags; overridden cells drop geometry_synthesized; an overridden title emits no default flag), while extraction facts — confidence, source_pages, connection flags — persist regardless.

CONFIDENCE_FLOOR module-attribute

CONFIDENCE_FLOOR = 0.6

The self-assessed confidence below which an area gains low_confidence:<value> self-assessed.

An honest uncertainty badge, explicitly not a hallucination guard — the corpus's unmatched extracted areas carry confidences 0.84-0.99, above the workdir median. Pinned against the measured distributions over the ten retained phase 9 workdirs (771 areas): nothing sits below 0.5 anywhere (a 0.5 floor is a no-op), 0.6 fires six times corpus-wide (a genuine rarity badge), and 0.7 fires 44 times on the steep part of between-sweep instability (JN1's under-0.6 count moved 5 → 0 across sweeps). Deliberately a constant, not a settings knob: a knob invites tuning what should be re-pinned against data, and the arc's flags are contracts, not preferences. Revisit if a corpus member's distribution shifts.

AssembleResult dataclass

AssembleResult(adventure: Adventure, report: ExtractionReport)

assemble()'s return: the draft adventure plus its report.

adventure instance-attribute

adventure: Adventure

report instance-attribute

EmittedTemplate dataclass

EmittedTemplate(template: MonsterTemplate, derived: tuple[str, ...], source_pages: tuple[int, ...])

One emitted custom template with its review record inputs.

template instance-attribute

template: MonsterTemplate

derived instance-attribute

derived: tuple[str, ...]

source_pages instance-attribute

source_pages: tuple[int, ...]

assemble

assemble(workdir_path: Path) -> AssembleResult

Run stage 6: overrides application, geometry synthesis, the adventure build, validation, and the artifact writes.

Parameters:

Name Type Description Default
workdir_path Path

The workdir root; its monsters stage must be completed and every stage cache present. overrides.yaml, when present, is applied — a missing file is an empty overrides set.

required

Returns:

Type Description
AssembleResult

The draft adventure and its extraction report, as written to

AssembleResult

adventure.json and report.json (plus one preview per level).

Raises:

Type Description
ValueError

If the monsters stage is not completed, the mapread stage is failed or running (a mid-convert map failure must not silently assemble prose-only — rerun mapread is the named remedy; a pending or absent entry, or an absent cache, is the honest no-proposals state of a pre-phase-11 workdir), a cache is missing, or the monsters or stat-block cache is stale against the upstream caches (programmer misuse — convert's ordering makes these unreachable).

OverrideError

If an override entry cannot take effect — raised before any run.json or artifact write. A monster_templates: entry against a workdir with no statblocks.json (one converted before the stat-block pass existed) or an off knob echo fails here: an explicit correction silently suppressed by a missing cache or a setting would be a silent no-op, the worst outcome.

build_draft

build_draft(
    index: SurveyIndex,
    levels: tuple[LevelContent, ...],
    resolutions: MonsterResolutions,
    geometries: tuple[LevelGeometry, ...],
    settings: ConversionSettings,
    plan: OverridePlan | None = None,
    custom_templates: Mapping[str, MonsterTemplate] | None = None,
    suspects: Mapping[str, str] | None = None,
) -> DraftResult

Build the draft adventure and per-area reports from validated caches plus overrides — pure.

Parameters:

Name Type Description Default
index SurveyIndex

The survey cache.

required
levels tuple[LevelContent, ...]

Every level's content cache, in survey order.

required
resolutions MonsterResolutions

The monsters cache, monster overrides and template emission already applied; every keyed encounter name must have an entry.

required
geometries tuple[LevelGeometry, ...]

The effective geometry (synthesized, overrides applied), in survey order.

required
settings ConversionSettings

The run's settings echo (unresolved_fallback, blank_page_renders).

required
plan OverridePlan | None

The resolved override plan; None means no correction file.

None
custom_templates Mapping[str, MonsterTemplate] | None

Emitted template id → template. Only templates a built encounter actually references bundle into Adventure.monsters (sorted by id) — an emission every reference was remapped away from is dead content and stays out.

None
suspects Mapping[str, str] | None

Name → resolution_suspect detail (resolution_suspects); each area whose built encounter carries the name gains the flag.

None

Returns:

Type Description
DraftResult

The draft, its per-area reports in survey order (removed areas keep a

DraftResult

tombstone entry; added areas append after their level's survey areas),

DraftResult

the module-scope flags, and the sorted unresolved names.

Raises:

Type Description
ValueError

If an encounter name is missing from the resolutions — a stale cache (convert's ordering makes it unreachable).

emit_custom_templates

emit_custom_templates(
    resolutions: MonsterResolutions,
    blocks: Mapping[str, RawStatBlock | None],
    forced: Collection[str],
    levels: Sequence[LevelContent],
    base_ids: Collection[str],
) -> tuple[MonsterResolutions, dict[str, EmittedTemplate]]

Emit custom templates for the unresolved names (plus the forced ones) with usable blocks — pure.

A name with a usable candidate block gets an emitted template and an in-memory method="custom" resolution; a forced name (a monster_templates: entry) enters the population even when the tiers resolved it — forcing emission is the human's remedy for a flagless wrong LLM pick — and if its block is nonetheless unusable, the discarded pick is not restored: the name falls to the stand-in machinery flagged monster_unresolved, exactly as the refusal ladder treats an extracted unusable block, and completing the block is the designed remedy. Ids slug from the name with deterministic numeric suffixes on collision (against the base catalog and sibling emissions alike).

Parameters:

Name Type Description Default
resolutions MonsterResolutions

The effective resolutions (monster remaps applied).

required
blocks Mapping[str, RawStatBlock | None]

The candidate blocks (template overrides applied).

required
forced Collection[str]

Normalized names whose monster_templates: entries force emission.

required
levels Sequence[LevelContent]

The content caches (the unprinted-NA fallback's input).

required
base_ids Collection[str]

The base catalog's template ids.

required

Returns:

Type Description
MonsterResolutions

The resolutions with emissions applied, and normalized name →

dict[str, EmittedTemplate]

emitted-template record.

map_stat_block

map_stat_block(
    template_id: str, name: str, block: RawStatBlock, max_keyed_count: int = 1
) -> tuple[MonsterTemplate, tuple[str, ...]]

Map one usable raw block onto a MonsterTemplate under the pinned anchors — deterministic, total.

Every field is either traceably printed or recorded in the returned derived list: AC complements by 19, THAC0/attack bonus and XP derive from the HD tables when unprinted, saves derive via the printed save-as, the class table, or the monster save bands, morale defaults to 7 (the 2d6 mean), alignment to neutral, number appearing to the maximum fixed keyed count, movement to the B/X human norm, and treasure is always the empty ref — keyed treasure is already the area's, and inventing a treasure type would be invention.

Parameters:

Name Type Description Default
template_id str

The already-allocated template id.

required
name str

The normalized extracted name (the template's display name).

required
block RawStatBlock

A block satisfying usable_stat_block.

required
max_keyed_count int

The unprinted-NA fallback — the maximum fixed keyed count across the name's encounters, floor 1.

1

Returns:

Type Description
tuple[MonsterTemplate, tuple[str, ...]]

The template and the sorted derived-field record.

Raises:

Type Description
ValueError

If the block is not usable (programmer misuse — callers gate on the shared predicate).

parse_treasure

parse_treasure(strings: tuple[str, ...]) -> ParsedTreasure

Parse an area's treasure strings through the pinned, conservative grammar.

Per string, tried in order, first match wins: dice notation → unparsed (a dice quantity is per-monster or conditional treasure and cannot be a fixed cache); the two quantified-each shapes — <N> <things> worth <V> gp each and <N> <things> each worth <V> gp — → N valuables of the stated value; any other each and every per → unparsed (still conditional treasure); <thing> worth <N> gp → a valuable (gem exactly when the thing names a gem, else jewellery — both kinds carry identical value and XP semantics, so the narrow lexicon errs harmlessly); money references <N> <cp|sp|ep|gp|pp> with no digits outside them → coins summed per denomination; treasure type <A-V> → a generated-treasure letter. Numbers accept comma thousands-separators (1,000 cp), and a comma-grouped number counts as one coin match under the no-stray-digits guard. Anything else is unparsed — assembly flags it and (under best-effort) compensates with an unguarded-treasure roll. A string that is empty after stripping is skipped outright: it carries no information to flag, and the frozen stage-cache schema does not forbid it, so it must not crash assembly.

Parameters:

Name Type Description Default
strings tuple[str, ...]

The area's cached treasure strings.

required

Returns:

Type Description
ParsedTreasure

The parsed pieces; letters and unparsed strings keep derivation order

ParsedTreasure

(letters deduplicated on first occurrence).

render_previews

render_previews(workdir_path: Path) -> tuple[Path, ...]

Regenerate the SVG previews alone — osrforge preview.

Re-runs geometry synthesis and override application over the survey and content caches plus overrides.yaml and rewrites previews/ only — the level SVGs and index.html — touching neither the other artifacts nor run.json. The rendered bytes are identical to assembly's — previews follow the draft, so corrected cells and override-authored doors render here too.

Parameters:

Name Type Description Default
workdir_path Path

The workdir root; the survey and content caches must be present.

required

Returns:

Type Description
tuple[Path, ...]

The written preview paths — the SVGs in survey order, then index.html.

Raises:

Type Description
ValueError

If the survey or a level's content cache is missing.

OverrideError

If an override entry cannot take effect.

resolution_suspects

resolution_suspects(
    resolutions: MonsterResolutions, blocks: Mapping[str, RawStatBlock | None], templates: Mapping[str, MonsterTemplate]
) -> dict[str, str]

Judge the veto's survivors: normalized name → the resolution_suspect detail — pure.

Per surviving non-exact pick (a cached llm or fuzzy resolution — the veto already flipped the contradicted ones), the printed block is compared to the picked template on the axes that flag but never veto:

  • an HD-modifier-only difference (counts equal, modifiers differ);
  • an AC mismatch when both values are printed (dual notation);
  • a comparison that rests on a derived AC complement and disagrees — the direction hazard, named as such in the detail.

A pick with no usable printed block stays unflagged — no evidence, no badge — which also covers caches recorded before the stat-block pass widened (their llm/fuzzy names have no entries at all). The detail names both readings; multiple axes join into one detail per name.

Parameters:

Name Type Description Default
resolutions MonsterResolutions

The effective resolutions (overrides and emission applied — an overridden or emitted name is no longer llm/fuzzy and takes no badge).

required
blocks Mapping[str, RawStatBlock | None]

The cached raw blocks, as extracted.

required
templates Mapping[str, MonsterTemplate]

Catalog template id → template.

required

Returns:

Type Description
dict[str, str]

Name → detail for exactly the names meeting a condition.

usable_stat_block

usable_stat_block(block: RawStatBlock | None) -> bool

The refusal ladder's eligibility predicate: an AC plus an HD line or a class-level notation.

A block failing this refuses emission — there is nothing to derive combat math from, and emit-with-invented-combat-math would be invention. Shared verbatim with the eval scorer's custom-assertion match signal, so the metric can never score an emission assembly would refuse. The monsters stage's stat_block_veto composes the identical gate from the shared parsers (it cannot import this module), so an edit here must visit the veto too.

Parameters:

Name Type Description Default
block RawStatBlock | None

A cached raw block, or the absent marker.

required

Returns:

Type Description
bool

Whether mapping would emit a template from this block.