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.yaml —
assembly 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
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.
EmittedTemplate
dataclass
EmittedTemplate(template: MonsterTemplate, derived: tuple[str, ...], source_pages: tuple[int, ...])
One emitted custom template with its review record inputs.
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 |
required |
Returns:
| Type | Description |
|---|---|
AssembleResult
|
The draft adventure and its extraction report, as written to |
AssembleResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the monsters stage is not |
OverrideError
|
If an override entry cannot take effect — raised before
any |
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 ( |
required |
plan
|
OverridePlan | None
|
The resolved override plan; |
None
|
custom_templates
|
Mapping[str, MonsterTemplate] | None
|
Emitted template id → template. Only templates a
built encounter actually references bundle into
|
None
|
suspects
|
Mapping[str, str] | None
|
Name → |
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 ( |
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 |
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 |
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 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
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 |
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 |
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. |