Construction as ERP¶
Foundation: BBC · DATA_MODEL · BIM_COBOL · TestArchitecture · ACTION_ROADMAP · SourceCodeGuide
How BIM compilation maps to iDempiere C_Order → BOM explosion → spatial resolution
Governing principle: A construction project is a C_Order. There is ONE document type: "Construction Order." C_OrderLine references an M_Product (which may be a BOM product with IsBOM=Y). BOM Drop explodes the product's recipe into child OrderLines. The user edits lines (swap products, add disciplines). completeIt triggers compilation from the OrderLine tree. M_Product_Category classifies products — no separate M_BomCategory needed. PP_Order_Node records HOW (verb operations). CO_EmptySpace tracks WHERE things sit.
What changed (S60): C_DocType no longer drives compilation — it is order metadata only. M_BomCategory replaced by M_Product_Category. Products with IsBOM=Y ARE BOMs. The compiler walks C_OrderLine, not m_bom directly. run_RosettaStones.sh creates C_Order + bomDrop per building, then compiles via completeIt — same path as Designer UI.
1. Three Databases — Separation of Concerns¶
1.1 component_library.db — LOD Geometry Store¶
LOD mesh geometry + materials + intrinsic component orientation. No BOM assembly
logic, no config. Since Phase E, all ad_* working tables moved to {PREFIX}_BOM.db.
Tables staying here use the lod_ prefix to signal their geometry role.
| Table | iDempiere | Content |
|---|---|---|
component_geometries |
— | Vertex/face geometry BLOBs (deduplicated by hash) |
component_definitions |
— | Component metadata + local bounds + up/forward axis + attachment face |
component_types |
— | IFC class taxonomy |
placement_rules |
— | Host-relative placement: host type, offset, spacing, clearance |
lod_geometry_map |
— | Element → geometry hash mapping |
lod_element_placement |
— | Compiled LOD element instances (with orientation NS/EW/POINT) |
lod_parametric_mesh |
M_Product (parametric) | Generator class + params for procedural geometry |
lod_parametric_mesh_param |
AD_Parm | Shape generator parameters |
lod_roof_preset |
— | Roof presets |
surface_styles |
M_Product_Acct (material) | Material name, RGBA colour per product |
material_layers |
— | Layer compositions |
What it is: The product image set — a geometry warehouse with intrinsic
orientation. Every mesh has vertices, faces, a colour, and knows its own
up-axis, forward-axis, and attachment face. placement_rules records
host-relative constraints (ceiling vs wall vs floor, offset, clearance).
Nothing here knows about assemblies, buildings, or BOM-level placement —
that lives in {PREFIX}_BOM.db.
In iDempiere MM terms, this is the product image catalog: the photos and
technical drawings you attach to an M_Product record so buyers can see what
they're ordering. {PREFIX}_BOM.db holds the product definition (M_Product: what it is,
what it costs, how it assembles). component_library.db holds the product image
(how it looks in 3D, its mesh, its material colour). The split is the same
reason ERP systems store product images on a file server, not in the transaction
database — geometry BLOBs are large, immutable, and shared across orders.
Since P0.1-DEDUP (§11.38), M_BOM_Line links each placed instance to its
M_Product in {PREFIX}_BOM.db via child_product_id, completing the chain:
{PREFIX}_BOM.db defines WHAT → component_library.db shows HOW IT LOOKS → output.db records WHERE IT GOES.
1.2 {PREFIX}_BOM.db — Unified Working Database (M_BOM + AD Config)¶
All working tables: BOM assembly recipes, config rules, product catalog, placement rules. Since Phase E, this is the primary database (~73 tables). Rich spatial info: SpaceSize (AABB), orientation rules, locator references.
| Table | iDempiere | Content |
|---|---|---|
m_bom |
M_Product + M_BOM | Assembly definition: BOMCategory (WHAT), doc_sub_type (WHICH variant) |
m_bom_line |
M_BOM_Line | Child placement: dx/dy/dz (parent-relative per tack convention, BOMBasedCompilation.md §4), rotation_rule, locator_ref, allocated_*_mm |
m_attribute |
M_Attribute | Leaf attributes: ports, clearances, UBBL rules |
M_BomCategory |
M_Product_Category | Functional type: RE, LI, BD, KT, FR, ST, SL, L1, L2, GF, RF, PR, HU |
M_Product |
M_Product | Product catalog: intrinsic geometry + Name + M_AttributeSet_ID (§11.38) |
M_AttributeSet |
M_AttributeSet | Attribute templates: BIM_Pipe, BIM_Wall, BIM_Slab, BIM_Conduit, BIM_Component (§11.38) |
C_DocType |
C_DocType | Building type classification: DocBaseType (RE/CO/IN) + DocSubType (SH/DX/TB/TE/ST) + domain config |
C_BPartner |
C_BPartner | Business partner lookup (future: real vendor/customer) |
ad_* (60+ tables) |
AD config | Space types, wall types, opening families, MEP, structural, etc. |
Note (2026-03-04):
c_orderandc_orderlinehave been dropped from {PREFIX}_BOM.db. C_DocType absorbs the domain config (DSL template, output paths, reference AABB). C_Order and C_OrderLine are created fresh in output.db at compile time. See §11.37.
What it is: A pure dictionary — an assembly manual with no transaction data. "A Duplex Unit contains Level 1 + Level 2. Level 1 contains Living Room + Kitchen + Bathroom. Living Room contains Piano + Sofa Set + Buffer Space." Every construct carries its AABB so the parent=SUM(children) invariant holds. C_DocType defines building types (RE_SH, RE_DX, etc.) with domain config. No C_Order, no C_OrderLine — those are compile-time output (§11.18, §11.29).
CHEATING MAXIM (Rule 8, RosettaStoneStrategy): M_BOM_Line dx/dy/dz MUST be
parent-relative offsets — the position of the child within its parent assembly's
AABB. They MUST NOT be world-space centroids copied from IFC extraction.
Enforced by X_M_BOMLine.validateParentRelative() and by verifying both _s
and _e independently match the reference in run_RosettaStones.sh. See Rule 8.
Status (2026-03-07): Tack convention migration DONE (
migration_tack_origin.sql). All dx/dy/dz on EXTRACTED and structured BOMs are parent-relative. ✓X_M_BOMLine.setDx()rejects negative values at write time. ✓ PlacementLoader.loadFromBOM() computes world coords from BOM offsets. ✓Single compilation path: C_OrderLine → M_Product → BOM explosion. Output verified against reference via Rosetta Stone contract tests (G1-G6).
Buffer space (BOMCategory='ST') is part of the BOM construct. Buffer children are explicit M_BOM_Lines in {PREFIX}_BOM.db — not computed at compile time, not inferred from gaps. They exist as named M_BOM_Line records with variable SpaceSize. Without them the parent's AABB cannot equal the sum of its children. The BOM is incomplete without its buffers, just as a bill of materials is incomplete without its spacers.
Relationship to component_library.db: Leaf M_BOM items reference M_Product (NORM-1 renamed
from ad_product_dim, now with component_id FK to component_definitions) for intrinsic
dimensions. LOD geometry is resolved via
lod_geometry_map in component_library.db. The BOM is the recipe; the LOD store has the meshes.
1.3 output.db — the Compiled Result (C_Order output)¶
The work order's compiled output. IFC-compatible elements with world coordinates.
| Table | Content |
|---|---|
elements_meta |
Compiled elements (guid, ifc_class, storey, world xyz) |
element_instances |
Geometry instances (transform matrix, material) |
element_assemblies |
Assembly grouping (parent-child in output) |
co_empty_space |
Construction space header (per C_Order); is_available = quality gate |
co_empty_space_line |
Spatial resolution per BOMLine (before/next, orientation); c_orderline_id → output.db c_orderline (logical FK, same-DB, NORM-0b) |
Table prefix rule — never use ad_ for construction models:
| Prefix | Domain | Database | Examples |
|---|---|---|---|
ad_* |
Application Dictionary — system config, product catalog | {PREFIX}_BOM.db | M_Product (NORM-1 renamed from ad_product_dim) |
m_* |
Master data — BOM assembly recipes, attributes, categories | {PREFIX}_BOM.db | m_bom, m_bom_line, m_attribute, M_BomCategory |
lod_* |
LOD geometry — extracted meshes, element placement, parametric meshes | component_library.db | lod_geometry_map, lod_element_placement, lod_parametric_mesh |
c_* |
Construction order — document types + compiled order | {PREFIX}_BOM.db (C_DocType), output.db (C_Order, C_OrderLine) | C_DocType (domain config, {PREFIX}_BOM.db). C_Order + C_OrderLine (WHAT-only, output.db — created fresh each compile) |
pp_* |
Production operations — HOW to place elements | output.db | PP_Order_Node, PP_Order_NodeProduct |
co_* |
Construction output — compiled spatial resolution | output.db, compile DB | co_empty_space, co_empty_space_line |
w_* |
~~Work-output-specific~~ REMOVED (S61) — see §1.4 | ~~work_output.db~~ | ~~W_BuildingConfig, W_Variant, W_Validation_Result~~ |
The ad_ prefix is iDempiere's system dictionary namespace. Using it for working
construction data (BOM trees, spatial output) conflates configuration with runtime
state. Historical mistake (ad_bom, ad_bom_child, ad_bom_child_param) corrected
in the BOM Dimension migration to m_bom, m_bom_line, m_attribute.
1.4 ~~work_output.db~~ — REMOVED (S61)¶
Superseded. work_output.db is removed from the architecture. The BOM is read-only (EntityType D). Save = Blender native save (.blend file). There are no user edits to buffer — the BOM is the source of truth, the viewport is purely visual confirmation.
4-DB architecture: component_library / {PREFIX}_BOM / output / validation.
C_OrderLine tree (from BOM Drop) lives in the compile DB. ASI overrides, validation results, and verb audit trail also go to the compile DB. CompleteIt compiles from compile DB → output.db (path editable by user).
See DocAction_SRS.md §1.10 for the simplified lifecycle.
Promote conditions: New BOMs must declare parent category, qualified name, author/version, and have all child references validated. Without these, promote is blocked — the selection list stays organized.
2. C_Order — the Construction Order¶
The building project IS a C_Order. Not BIM, not DSL — C_Order directly.
Updated 2026-03-04: C_Order lives in output.db only — created fresh each compile from C_DocType config in {PREFIX}_BOM.db. C_DocType.DocSubType (not C_BPartner) identifies the building pattern. See §11.36–11.37 for migration details.
C_DocType ({PREFIX}_BOM.db — constant domain config)
│ C_DocType_ID = 'RE_DX'
│ DocBaseType = 'RE' (Residential) ← WHAT TYPE (drives template selection)
│ DocSubType = 'DX' ← WHICH VARIANT (BOM scoping)
│ DSLContent, OutputDbPath, ReferenceDbPath ← domain config (absorbed from old c_order)
│ AABB = width/depth/height_mm ← HOW BIG (reference envelope)
C_Order (output.db — created fresh each compile from C_DocType)
│ C_Order_ID = building_id ('Ifc2x3_Duplex')
│ C_DocType_ID = 'RE_DX' ← FK → C_DocType
│ Site_AABB = aabb_width/depth/height_mm ← HOW BIG (construction envelope)
│ Description = 'Duplex residential unit'
│ DocStatus = 'DR' → 'IP' → 'AP' → 'CO'
│
│ DocAction lifecycle for BOM Drop (BIM_Designer_SRS.md §28.11):
│ bomDrop() → DR (draft, BOM tree auto-exploded into C_OrderLine)
│ Save → IP (backend validates, adjusts, returns errors)
│ Approve → AP (save as new product configuration / promote)
│ Complete → CO (full compile into output.db, load viewport)
│
│ These two fields — DocSubType + AABB — ARE the building definition.
│ Everything else on C_Order is administrative (lifecycle, audit, digest).
│ The entire BOM explosion tree derives from WHICH VARIANT + HOW BIG.
│
├── Tab: C_OrderLine (= WHAT to build — output.db, WHAT-only)
│ │ Selects M_BOMs from {PREFIX}_BOM.db catalog
│ │ No placement columns (HOW is in PP_Order_Node, WHERE is in CO_EmptySpaceLine)
│ │ M_AttributeSetInstance_ID = ordering-time customer preference only
│ │
│ │ *** NOTE: work_output.db C_OrderLine is DIFFERENT (§1.4). ***
│ │ It carries dx/dy/dz tack columns (WHAT + WHERE) because the BIM
│ │ Designer's three views (BBox/ORDER/Outliner) edit spatial positions
│ │ directly. output.db C_OrderLine remains WHAT-only — compilation output.
│ │ The two share a name but serve different roles in different databases.
│ │ (e.g., "cherry wood variant" — NOT engineering geometry instructions)
│ │
│ │ *** ENGINEERING ATTRIBUTES LIVE ON M_BOM_Line ({PREFIX}_BOM.db) ***
│ │ Cutting shapes, anchor faces, placement modes, span rules = BOM modelling.
│ │ C_OrderLine is merely the ordering process with customer options.
│ │ Confirmed from iDempiere docker postgres: PP_Product_BOMLine carries
│ │ M_AttributeSetInstance_ID for engineering spec; C_OrderLine carries
│ │ M_AttributeSetInstance_ID for customer choice. Different concerns.
│ │ See m_bom_line columns: anchor_face, layout_strategy, rotation_rule,
│ │ z_rule, locator_ref. And m_attribute (FK→m_bom_line) for overflow.
│ │
│ └── Sub-tab: BOM (read-only reference from {PREFIX}_BOM.db)
│ │ The selected M_BOM tree, referenced from {PREFIX}_BOM.db
│ │ ALL children intact: fixed items, sub-BOMs, AND buffer (ST) children
│ │ SpaceSize, dx/dy/dz, rotation_rule — everything intact
│ │
│ └── Sub-tab: BOMLine
│ Roof, Slab, L1, L2, rooms, furniture, buffers — expanded children
│ Each child is itself an M_BOM with its own BOMLines
│ Buffer children included — the BOM construct is complete as-is
│
├── Tab: PP_Order_Node (= HOW to build — production operations)
│ │ One row per verb invocation (TILE SURFACE, ARRAY, ROUTE, etc.)
│ │ FK: S_Resource_ID → CO_EmptySpaceLine (WHERE)
│ │ FK: C_Order_ID → C_Order
│ │ DocStatus: DR → IP → CO → VO
│ │
│ └── Sub-tab: PP_Order_NodeProduct (structured parameters)
│ Name/Value/ValueType per verb parameter
│
└── Tab: CO_EmptySpace (= WHERE things go — spatial audit trail)
│ Construction site information
│ FK: C_Order_ID → C_Order
│ AABB of whole intended construction space
│ IsAvailable = Y (start + during processing + on reprocess)
│ → N (only after translation to output DB + tests GREEN)
│
└── Sub-tab: CO_EmptySpaceLine
Alignment record: WHERE the BOM box sits + orientation
Does NOT repeat the BOM (that's intact on C_OrderLine.BOM.BOMLine)
Says: "this BOM construct goes HERE, facing THIS way"
Translation to output DB uses this alignment + BOM offsets → world coords
Can hold MEP spatial refs separately from BOM leaf items
2.1 Why C_Order?¶
| Concern | iDempiere | BIM (Construction Order) |
|---|---|---|
| "I want to build a Duplex" | Raise C_Order | C_DocType RE_DX defines the type; compile creates C_Order in output.db |
| "Use DX variant's catalog" | Set C_DocType | C_DocType.DocSubType = 'DX' (BOM scoping) |
| "How big is the site?" | Set dimensions | AABB on C_DocType (reference) or C_Order (compiled) |
| "Include this BOM" | Add C_OrderLine | C_OrderLine generated in output.db at compile time |
| "What fits where?" | Check availability | Query CO_EmptySpace/Line |
| "Build it" | Process Order | ./scripts/run_tests.sh (compile) |
| "Edit the spec" | Modify C_DocType | Update DSLContent, AABB, or BOM rules in {PREFIX}_BOM.db |
The simplest possible building definition is two fields on C_DocType:
DocSubType (WHICH variant) + AABB (HOW BIG). Every downstream decision
cascades from these. A C_DocType entry with these two fields is sufficient to
compile — the BOM explosion engine selects the right UNIT, the right floors,
the right rooms, the right furniture, all from BOMCategory + SpaceSize ≤ AABB.
2.2 C_OrderLine — what gets built (output.db, WHAT-only)¶
Each C_OrderLine selects an M_BOM from {PREFIX}_BOM.db. C_OrderLine lives in output.db only (generated at compile time). No placement columns — HOW is in PP_Order_Node, WHERE is in CO_EmptySpaceLine (§11.9).
C_OrderLine #1: family_ref = 'BUILDING_DX_STD' host_type = BUILDING
C_OrderLine #2: family_ref = 'FLOOR_DX_L1_STD' host_type = BUILDING
C_OrderLine #3: family_ref = 'LIVING_SET' host_type = ROOM, room_ref = 'Rm_Living_1'
C_OrderLine #4: family_ref = 'BED_SET_MASTER' host_type = ROOM, room_ref = 'Rm_Bedroom_1'
The BOM sub-tab on each C_OrderLine shows the M_BOM tree copied verbatim from {PREFIX}_BOM.db. This is the product spec — immutable reference. All information transfers intact: fixed children with their SpaceSize, sub-BOMs with their recursive trees, AND buffer children (BOMCategory='ST') with their variable SpaceSize. The BOM construct in {PREFIX}_BOM.db is complete — it includes every spacer, every gap, every arrangement relationship. The copy to C_OrderLine.BOM preserves this completeness. The compiler reads this reference, not {PREFIX}_BOM.db directly, so the scope is locked to what was ordered.
The user edits C_OrderLines: swap a sofa set, remove the piano, add a dining chair. The compiler reads the final C_OrderLines and resolves.
3. CO_EmptySpace — Construction Space Tracking¶
3.1 CO_EmptySpace (header)¶
One record per C_Order. The post-compile construction site envelope.
Distinction: The C_Order's AABB (aabb_*_mm on C_Order) is the
pre-compile input — "I want to build in this envelope." CO_EmptySpace's AABB
is the post-compile output — "the compiler measured this envelope from the
compiled R*Tree." For owner-matched builds (SH/DX), these are numerically equal.
For ST-mode builds, the output AABB may be smaller (not all space consumed).
Design decision (NORM-3b, 2026-02-28): CO_EmptySpace is deliberately kept as
a separate table in output.db. Collapse into C_Order was assessed and rejected:
- origin_x/y/z_mm (RTREE-measured actual origin) is compile-time data; it does
not belong in C_Order (design-time). IFC models are not always at world origin.
- The quality-gate state machine (is_available, doc_status) is written by
ProveStage using the output.db connection. Moving it to {PREFIX}_BOM.db would require
a cross-DB write at prove time — more complex, not simpler.
- co_empty_space_line.co_emptyspace_id is a clean local SQLite FK within
output.db. Collapse turns it into a logical cross-DB reference — weaker.
- EmptySpaceChecksum reads only co_empty_space_line, not the header; witness
complexity does not decrease if the header is removed.
CREATE TABLE co_empty_space (
co_emptyspace_id INTEGER PRIMARY KEY AUTOINCREMENT,
c_order_id TEXT NOT NULL, -- FK → Construction Order
origin_x_mm REAL NOT NULL DEFAULT 0,
origin_y_mm REAL NOT NULL DEFAULT 0,
origin_z_mm REAL NOT NULL DEFAULT 0,
aabb_width_mm REAL NOT NULL, -- total construction space X
aabb_depth_mm REAL NOT NULL, -- total construction space Y
aabb_height_mm REAL NOT NULL, -- total construction space Z
is_available INTEGER NOT NULL DEFAULT 1, -- Y=available/unproven, N=consumed+tests GREEN
doc_status TEXT NOT NULL DEFAULT 'DR',
created TEXT NOT NULL DEFAULT (datetime('now')),
updated TEXT NOT NULL DEFAULT (datetime('now'))
);
IsAvailable lifecycle:
1. Project start: is_available = 1 (Y), AABB = full intended construction space.
2. During processing: stays is_available = 1. The compiler translates the BOM
construct into the output DB (Blender viewport / IFC export).
3. After clean output + tests GREEN: is_available = 0 (N). The space is confirmed
consumed — the translation passed all witness gates.
4. Reprocess: is_available reset to 1 (Y). Processing begins again from scratch.
5. If is_available remains 1 after processing completes: the build did not pass.
The space was not successfully consumed. Tests failed or translation aborted.
The flag is a quality gate, not a progress marker. It only goes to N when the
output is proven correct. A CO_EmptySpace stuck at is_available = 1 after
processing means the construction did not compile clean.
3.2 CO_EmptySpaceLine (detail)¶
An alignment record — where the whole box model sits. CO_EmptySpaceLine tells the compiler WHERE the BOM construct (the entire box, including its buffers) is aligned in construction space and at what orientation. It does not repeat the BOM — that is already intact on C_OrderLine.BOM.BOMLine. It says: "this BOM box goes HERE, facing THIS way."
A CO_EmptySpaceLine record is created at each structural tier — unit, slab, floor, roof, pair container. For 1:1 extracted buildings (SH, DX), this produces a sparse ledger: SH has 4 lines, DX has 7 lines (see Appendix E). For variant-driven buildings (TB-LKTN), additional lines record selection decisions.
CREATE TABLE co_empty_space_line (
line_id INTEGER PRIMARY KEY AUTOINCREMENT,
co_emptyspace_id INTEGER NOT NULL, -- FK → co_empty_space
bom_line_seq INTEGER NOT NULL, -- sequence from M_BOM_Line
bom_id TEXT NOT NULL, -- which M_BOM this line accepts
bom_line_role TEXT, -- role from M_BOM_Line (WALL_EXT, FURNITURE, etc.)
bom_level INTEGER DEFAULT 0, -- depth in BOM tree (0=top, 1=floor, 2=room, etc.)
-- Spatial translation: {PREFIX}_BOM.db construct → construction space
before_x_mm REAL, -- anchor point BEFORE this item (connecting to previous)
before_y_mm REAL,
before_z_mm REAL,
next_x_mm REAL, -- anchor point AFTER this item (connecting to next)
next_y_mm REAL,
next_z_mm REAL,
orientation_rad REAL DEFAULT 0, -- resolved orientation in radians
-- Space accounting
capacity_mm REAL, -- locator extent (from room boundary)
filled_mm REAL DEFAULT 0,
remaining_mm REAL, -- available space at this locator
-- Locator reference
storey TEXT,
room_name TEXT,
locator_ref TEXT, -- NORTH_WALL, CENTRE, FLOAT...
-- Extensible (not yet in schema): MEP spatial refs, 7D IoT refs, etc.
-- mep_ref TEXT, -- MEP connection point (future)
doc_status TEXT NOT NULL DEFAULT 'DR',
created TEXT NOT NULL DEFAULT (datetime('now')),
updated TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (co_emptyspace_id) REFERENCES co_empty_space(co_emptyspace_id)
);
3.3 When a CO_EmptySpaceLine is created¶
A new line is spawned at a decision point — when the translation engine encounters a BOM level that requires spatial guidance:
| Trigger | What happens | Example |
|---|---|---|
| Acceptance | BOM fits the available space; record the translation | DX: BUILDING_DX_STD accepted into full AABB |
| Variant selection | Multiple M_BOMs compete; record which one won | TB-LKTN: choose smaller LIVING_SET variant |
| Space conflict | BOM peer competes for same zone; record partition | Two furniture sets for one room |
| Orientation change | Room shape differs from BOM assumption; record resolved radians | Rotated L2 rooms (180° vs L1) |
For SH/DX, lines are sparse. The pipeline writes one line per structural tier: SH produces 4 lines (UNIT, GROUND_SLAB, GROUND_FLOOR, ROOF); DX produces 7 lines (UNIT, GROUND_SLAB, LEVEL_1, UPPER_SLAB, LEVEL_2, ROOF, PAIR). There is only ONE variant at every BOM level — no variant to compare, no space conflict. The BOM tree unfolds deterministically. Everything translates 1:1 because it was extracted from the exact geometry. See Appendix E for the full ledger dumps.
For TB-LKTN, lines are dense. TopologyMaker creates room variants. The furniture sets need SpaceSize matching. Each selection decision spawns a CO_EmptySpaceLine, recording which variant won, what orientation was resolved, what space remains.
BOM tree processing — standard ERP manufacturing pattern:
All buildings go through the same BOM tree explosion. There is no exclusive treatment. C_OrderLine references a BOM product. Compile explodes the tree.
| Pattern | iDempiere parallel | How it works |
|---|---|---|
| Instant Drop | Instant BOM Drop plugin | 1 C_OrderLine → compile explodes full BOM tree. No user modifications. The quickest hello world test. |
| BOM Drop | BOMDrop Configurator plugin | User navigates BOM tree, swaps products (same bom_category), adds/removes lines. Compile processes modified order. |
Instant Drop (Rosetta Stones): C_OrderLine references BUILDING_SH_STD. Compile walks the BOM tree, accumulates tack offsets (dx/dy/dz) at every level, resolves leaves to M_Product geometry. ONE C_OrderLine, ONE CO_EmptySpaceLine. SH (55), DX (1099), TE (48,428) all compile this way.
BOM Drop (generative/custom): User starts with a base building product, opens the BOM tree, navigates to the child they want to change, and swaps it with another product in the same bom_category. The chooser filters by category + AABB fit. User can also add new C_OrderLines for additional products (e.g. fire protection discipline). Validation rules govern placement.
AABB as natural discriminant: Each building's room dimensions are unique — SH's living room (4645×2145mm) differs from DX's (3332×3943mm). When the BOM chooser filters by bom_category + AABB fit, the results naturally reflect what fits. This is not a code path — it's geometry.
Rosetta Stone proof — spatial digest verification: The spatial digest hash comparison proves that the BOM tree explosion produces correct element positions. The digest filters to visible, geometry-bearing elements only — same IFC classes on both sides.
Rosetta Stone digest filter (Q&A1, 2026-03-02): SH EXTRACTED has 56 elements; ST_SH GENERATIVE currently produces 123. The 67 extra elements are compilation artifacts (structural stubs, PHANTOM buffers, props). The SpatialDigest comparison must filter to visible, geometry-bearing elements only — same IFC classes on both sides. Investigation required to identify exactly which element classes to include/exclude. The filter is a prerequisite for the Rosetta Stone gate.
TB-LKTN is NOT a Rosetta Stone. TB-LKTN has no reference IFC to compare a spatial digest against. Its reference is a 2D layout (building grid lines set by the user). Furniture placement for TB-LKTN is "last-mile" — the BOMCategory walk places items into real rooms not pre-matched to any template. Test criteria when implemented: - No furniture strewn or sunken (items sit within room boundaries, valid Z) - At minimum a dining set fits in the living room (smallest viable placement) - A small bedroom gets a bed — same walk, result driven by what fits in that AABB
Invention stops (critical for TB-LKTN): The main failure mode for TB-LKTN is invention — the compiler generating walls, openings, or furniture for which no LOD mesh exists in component_library. The code must STOP with an explicit error when a component_library lookup returns nothing. It must not create a placeholder geometry. The user must: 1. Create the mesh/LOD in component_library 2. Link it to M_Product 3. Re-run the test
An earlier TB-LKTN attempt with 2D layout (building grid lines) produced correct room placements but caused invention for objects not yet in component_library. This led to the Rosetta Stone Strategy — prove the placement process on SH/DX where the expected output is fully known, then apply to TB-LKTN with confidence. Once SH/DX Rosetta tests pass, TB-LKTN has no reason to fail. The same unified process applies — only the reference validation method differs.
3.4 What CO_EmptySpaceLine holds¶
The BOMLine tab is the WHAT (complete BOM construct copied from {PREFIX}_BOM.db — items, sub-BOMs, buffers, SpaceSize — all intact and unchanged). CO_EmptySpaceLine is the WHERE (alignment: box origin + orientation in construction space).
Each line records the guidance that translates abstract BOM info into concrete construction coordinates:
- before/next — the GPD anchor chain in mm. Each item's
nextis the next item'sbefore. This is the spatial connecting info that turns abstract dx/dy/dz offsets into construction-space positions. - orientation_rad — the resolved orientation. {PREFIX}_BOM.db stores abstract rules
(
FACE_INTO_ROOM,PARALLEL_TO_WALL); the line stores the concrete radians for this particular room shape. - remaining_mm — buffer space still available. Visible for fit queries ("can a lampshade fit here?").
- c_orderline_id (NORM-0b) — logical FK → output.db
c_orderline(id). The iDempiere fulfillment link: C_OrderLine = what was requested; CO_EmptySpaceLine = where it was delivered. Same-DB within output.db. Being superseded byPP_Order_Node.S_Resource_ID(§11.9) — the primary production→space link. - mep_ref (future column, not yet in schema) — MEP connection point. Separate from the BOM leaf item. The BOM leaf stays pure product data; the MEP spatial reference is a construction concern tracked on the EmptySpaceLine.
3.5 Translation to Output DB — When Coordinate Work Happens¶
The BOM tab on C_OrderLine holds the WHAT — all children, buffers, SpaceSize, intact from {PREFIX}_BOM.db. CO_EmptySpaceLine holds the WHERE — alignment and orientation in construction space. The actual coordinate translation happens when the compiler writes to the output DB (elements_meta, element_instances) for Blender viewport or IFC export:
Single compilation path (S54b): C_OrderLine references M_Product_ID. IsBOM products explode recursively via BOMWalker. One path for all buildings — no mode selection.
{PREFIX}_BOM.db construct (abstract)
M_BOM: LIVING_SET
M_BOM_Line: Piano dx=0 dy=0 rotation_rule=PARALLEL_TO_WALL
M_BOM_Line: Sofa dx=1500 dy=0 rotation_rule=FACE_INTO_ROOM
M_BOM_Line: Buffer_NW (variable, fills remainder)
C_OrderLine: M_Product_ID = BUILDING_SH_STD
→ BOMWalker explodes tree: BUILDING → FLOOR → SET → LEAF elements
→ PlacementCollectorVisitor resolves world coordinates from tack chain
Translation to output DB (concrete world coordinates)
Piano: world_xyz = origin + rotated(dx=0, dy=0) = (208, -5246, 0) orient=π
Sofa: world_xyz = origin + rotated(dx=1500, dy=0) = (-1292, -5246, 0) orient=π
Buffer_NW: (no geometry — spatial placeholder, but space accounted for)
The {PREFIX}_BOM.db dx/dy/dz offsets — including buffer gaps — are rotated and translated relative to the parent BOM origin via the tack chain (§4). This is the step where abstract BOM offsets become world coordinates.
After this translation, tests run. If all witness gates pass (G8 centroids, F4 edges, W-SPACESIZE-1, etc.), the CO_EmptySpace.is_available is set to N — confirmed consumed. If tests fail, is_available stays Y — the space was not successfully filled.
3.6 Reprocess Mode¶
A compile flag --reprocess-all forces the full layer-by-layer walk even for
SH/DX. In this mode, a CO_EmptySpaceLine is written for every BOM level and
every child — tedious but systematic:
Reprocess mode — DX (verbose, one line per BOM level):
Line #1: BUILDING_DX_STD level=0 accepted into full AABB
Line #2: FLOOR_SLAB_GF level=1 before=(0,0,0) next=(0,0,0) ← ground slab
Line #3: FLOOR_DX_L1_STD level=1 before=(0,0,0) next=(0,0,3000) ← L1 contents
Line #4: FLOOR_SLAB_L2 level=1 before=(0,0,3000) next=(0,0,3000) ← upper slab
Line #5: FLOOR_DX_L2_STD level=1 before=(0,0,3000) next=(0,0,6000) ← L2 contents
Line #6: ROOF_ASSEMBLY level=1 before=(0,0,6000) ← roof
Line #7: Rm_Living_1/LIVING level=2 before=(208,-5246,0) orient=0
Line #8: Rm_Dining_1/DINING level=2 before=(208,-8554,0) orient=0
...
Line #15: Piano level=3 before=(1620,3308,0) next=(3120,3308,0) orient=π
Line #16: Buffer_NW level=3 remaining=254mm
Line #17: Sofa_3Seat level=3 before=(3374,3308,0) next=(5374,3308,0) orient=π
Why this matters: - For SH/DX it is pure verification — if any CO_EmptySpaceLine shows a translation error (wrong orientation, misaligned before/next), the bug is pinpointed to that exact BOM level. - For TB-LKTN it is the actual working mode — every level has real decisions. - The same code path handles both. The difference is only in how many decisions are non-trivial (zero for SH/DX, many for TB-LKTN).
3.7 The 1D Intent — Two Fields Drive Everything¶
The DSL (formerly a complex building description language) collapses to exactly two fields on C_DocType:
| Field | Column | Meaning |
|---|---|---|
| WHICH | DocSubType |
Building variant — which BOM trees are visible (SH/DX/TB/TE/ST) |
| HOW BIG | AABB (width × depth × height mm) | Construction site envelope dimensions |
These two fields are the root of the entire BOM explosion tree. Every downstream
decision — which UNIT BOM, which floor template, which room set, which furniture
leaf, which buffer gap — derives from DocSubType + AABB. Nothing else
influences compilation output.
Current mode (owner-matched): SH/DX/TB/TE each have an exact DocSubType
value that maps to exactly one UNIT BOM. The compilation is deterministic — no
spatial selection is needed. Think of it as a completed Lego set placed on the
board exactly where it is marked.
Standard mode (DocSubType='ST'): When DocSubType='ST', the compiler has no
pre-matched BOM set. It must:
- Use the AABB as the construction envelope
- At each BOM level, select the best-fitting BOM by
BOMCategory+SpaceSize ≤ available AABB - Write a
co_empty_space_lineat EVERY level (= Reprocess Mode as primary mode) - Each line's
before/nextcoordinates ARE the spatial audit trail
This is the layer-by-layer BOM selection engine: the compiler walks the BOM tree top-down, and at each node asks "what fits in the remaining space?" instead of "what does this variant's catalog say?"
POC strategy: Create ST_SH and ST_DX registry entries that compile the
same buildings through the full layer-by-layer selection process. Success
criterion: SpatialDigest(ST_SH) == SpatialDigest(SH). Stay within the
RosettaStone confine — systematic POC on known-good buildings before unlocking
TB-LKTN, where the answer is not yet known.
Why not TB-LKTN yet: TB-LKTN has no complete topology set pre-extracted. The layer-by-layer engine must first be proven on RosettaStone buildings (SH, DX) where the expected output is already known and can be compared via SpatialDigest.
ST vs ST disambiguation:
| Abbreviation | Context | Meaning |
|---|---|---|
DocSubType='ST' |
C_DocType (ST_SH, ST_DX) | Standard/Template mode — no direct BUILDING BOM, uses M_BomCategory AABB match. DocBaseType='RE'. |
BOMCategory='ST' |
M_BomCategory ({PREFIX}_BOM.db) |
Buffer/spacer — empty space child within a BOM assembly |
Different concepts, same abbreviation. DocSubType='ST' is a compilation mode
(ST is a DocSubType, NOT DocBaseType — DocBaseType values are RE/CO/IN only).
BOMCategory='ST' is a spatial placeholder. They coexist: an ST-mode
compilation will encounter ST-category buffer children during BOM explosion.
3.7.1 Implementation Gaps (TODO)¶
Seven concrete gaps between the current compiler and full ST mode:
~~TODO-ST-1: Add AABB to C_Order (Construction Order)~~ IMPLEMENTED (Phase ST-0)
AABB columns (aabb_width_mm, aabb_depth_mm, aabb_height_mm) added to
c_order. Backfilled from compiled output for SH/DX/TB/TE. X_C_Order PO
classes updated in ORMSandbox + TopologyMaker.
Code/model impact (see full list at end of §3.7.1):
TODO-ST-2: ST DocSubType selection logic — PARTIALLY IMPLEMENTED
- Decision resolved: ST mode sees ALL BOMs (all variants). The composition
proof (Phase ST-1b) demonstrates this:
MBOM.findBestFitAnyOwner()queries all active BOMs in a category with no doc_sub_type filter. AABB constraint + template branching drive selection — variant-specific BOMs self-select when they are the only candidate in their category. - Remaining: Wire
findBestFitAnyOwnerintoCompilationPipeline.javafor actual ST-mode compilation (currently only used in composition proof). - File:
CompilationPipeline.java,MBOM.java
TODO-ST-3: CO_EmptySpaceLine L2–L3 population
- Gap: Current code writes L0 (UNIT acceptance) + L1 (per-storey) only. No L2 (rooms) or L3 (items).
- Fix: Recursive walk from L1 floor children → L2 room lines → L3 item lines.
- Each line records: before/next anchor, orientation_rad, capacity_mm, filled_mm, remaining_mm, storey, room_name.
- Buffer (ST) children: Create lines with
remaining_mm > 0, no geometry output. - File:
CompilationPipeline.java— extend the L1 loop to recurse into children.
TODO-ST-4: Document sequencing on CO_EmptySpaceLine
- Gap: Table has spatial before/next but no document sequence fields (prefix/suffix/nextID).
- Evaluate: The before/next xyz columns already provide spatial chaining.
Document sequencing may map to
bom_line_seq+bom_levelordering rather than explicitnext_line_idFK. - File:
BuildingWriter.java(DDL),M_CO_EmptySpaceLine.java(PO)
~~TODO-ST-5: SpaceSize-based BOM variant selection~~ IMPLEMENTED
- Implemented:
MBOM.findNextFitSpace()(Phase F3) — queries m_bom by category + doc_sub_type scope, filters by AABB fit, returns largest volume match. - Pre-compilation gate:
BomTemplateContract.check()(Phase ST-1a) validates BOM catalog completeness against M_BomCategoryLine template with MinQty/MaxQty. - File:
ORMSandbox/.../MBOM.java,ORMSandbox/.../BomTemplateContract.java
TODO-ST-6: Structured translation logging → CO_EmptySpaceLine
- Gap:
[TRANSLATE]printf atBOMTierResolver.javagoes to stdout only. Not queryable. - Fix: Each BOM child expansion writes a CO_EmptySpaceLine L3 record with the exact anchor→world translation.
- Post-compile query:
SELECT * FROM co_empty_space_line WHERE room_name = ? ORDER BY bom_level, bom_line_seq - File:
BOMTierResolver.java— pass Connection toexpandBOMNode, write L3 lines.
~~TODO-ST-7: Product orientation invariants~~ — RESOLVED
component_definitions now carries up_axis, forward_axis, attachment_face,
and orientation (PENDANT/UPRIGHT/WALL_MOUNT). placement_rules adds host-relative
orientation (ALONG_HOST/PERPENDICULAR/NS/EW). lod_element_placement stores the
resolved per-instance orientation. The lod_product_geometry view joins these for
rendering. The M_Product → component_definitions → orientation chain is complete.
rotation_rule on m_bom_line still governs BOM-level rotation; the component-level
intrinsic orientation is now in the LOD geometry store where it belongs.
3.7.2 AABB on C_Order — Code/Model Impact Inventory (DONE, Phase ST-0)¶
AABB columns added to C_Order in Phase ST-0. Impact list (all completed):
Schema (1 migration script):
| Change | File |
|---|---|
ALTER TABLE C_Order ADD COLUMN aabb_width_mm REAL (×3) |
migration/migration_st_aabb_registry.sql |
Backfill from compiled output: UPDATE ... SET aabb_width_mm = (SELECT aabb_width_mm FROM co_empty_space WHERE c_order_id = building_id) |
Same migration |
PO classes (2 modules, 2 files each = 4 files):
| File | Changes |
|---|---|
ORMSandbox/.../po/X_C_Order.java |
+3 COLUMNNAME constants, +3 getters, +3 setters |
ORMSandbox/.../po/MOrder.java |
Inherit new accessors (no logic change) |
TopologyMaker/.../po/X_C_Order.java |
Same 3+3+3 |
TopologyMaker/.../po/MOrder.java |
Inherit |
Registry reader (1 file):
| File | Change |
|---|---|
DAGCompiler/.../dsl/BuildingRegistry.java:18-31 |
Add 3 fields to BuildingEntry record |
DAGCompiler/.../dsl/BuildingRegistry.java:98-102 |
Add 3 columns to SELECT query |
Compilation pipeline (1 file, 2 sites):
| File:Site | Change |
|---|---|
CompilationPipeline.java — UNIT BOM selection |
For ST mode: use registry AABB as envelope constraint instead of R*Tree post-hoc |
CompilationPipeline.java — populateCoEmptySpace() |
For owner-matched: continue computing from R*Tree. For ST: use registry AABB as authoritative input |
BuildingInspector preflight (1 file):
| File | Change |
|---|---|
ORMSandbox/.../BuildingInspector.java |
New check: AABB present and non-zero for ST-mode buildings. Optional warning for owner-matched buildings with NULL AABB. |
Tests (witness additions):
| Test | Assertion |
|---|---|
BuildingRegistryTest |
AABB columns populated for SH/DX after backfill migration |
CompilerContractTest |
ST-mode POC: SpatialDigest(ST_SH) == SpatialDigest(SH) (future) |
NO impact on: - BOMTierResolver (reads BOM, not registry) - FurnitureWorker (dispatches to BOMTierResolver, not registry) - FloorPlateBOMResolver (reads BOM tree, not registry) - StoreyCompiler (receives BuildingEntry, but doesn't use AABB yet — future ST mode) - MEPWriter, BuildingWriter, StructuralWriter (downstream of placement) - TopologyBatchProcess (writes registry, would need to SET AABB on new entries)
Summary: 1 migration + 4 PO files + 2 Java files + 1 inspector check + 2 tests. The AABB columns are NULL-safe — owner-matched builds (SH/DX/TB/TE) continue to work unchanged with NULL AABB. ST-mode compilation requires non-NULL AABB.
3.8 Template-Driven Decomposition (ST Mode)¶
Phase ST-0 adds the schema foundation for Standard Mode — a template-driven compilation path where no pre-built BOM tree exists for the building.
C_DocType — Building Type Classification¶
Updated 2026-03-04: C_BPartner lookup replaced by C_DocType (§11.36). m_bom.c_bpartner renamed to
doc_sub_type. c_order dropped from {PREFIX}_BOM.db.
C_DocType ({PREFIX}_BOM.db) classifies building types. Seed data:
| C_DocType_ID | DocBaseType | DocSubType | Name |
|---|---|---|---|
| RE_SH | RE | SH | Sample House |
| RE_DX | RE | DX | Duplex |
| RE_TB | RE | TB | Terrace Block |
| CO_TE | CO | TE | Airport Terminal |
| ST_SH | RE | ST | Standard Sample House |
| ST_DX | RE | ST | Standard Duplex |
DocSubType 'ST' = template path (no direct BUILDING BOM match → M_BomCategory AABB match).
DocBaseType values: RE, CO, IN only. ST is a DocSubType.
C_BPartner table retained for future real business partners (vendor/customer).
The bom_owner → c_bpartner → doc_sub_type rename chain is complete (§11.37).
M_BomCategoryLine — Recursive Decomposition Recipe¶
New master-detail table on M_BomCategory. Each line maps a parent category to
a child category, forming a recursive template tree. Three aspect columns
enable parametric branching:
| Column | Semantics |
|---|---|
num_units |
0=universal (always active), 1=single-household (GF path), 2=dual-household (PR path) |
storey_count |
Informational: how many storeys this subtree spans |
mirroring_rule |
'NONE' or 'PARTY_WALL_PI' (mirrored pair injection) |
RE (Residential Template, DocSubType='ST')
├── SL (Floor Slab) seq=10 num_units=0 ← universal
├── PR (Duplex Pair) seq=15 num_units=2 ← DX path (2-storey body)
│ ├── HU (Unit A) seq=10 mirror=NONE
│ │ ├── L1 (Ground) Z=0.0–0.5
│ │ │ ├── LI (min=1), DN, KT, BT
│ │ └── L2 (Upper) Z=0.5–1.0
│ │ ├── BD (min=1, max=3), KT, BT
│ └── HU (Unit B) seq=20 mirror=PARTY_WALL_PI
│ └── [same L1/L2 structure]
├── GF (Ground Floor Body) seq=20 num_units=1 ← SH/MY/TB path
│ ├── LI (min=1), BD (min=1), DN, KT, BT
└── RF (Roof Assembly) seq=30 num_units=0 ← universal
Z_Offset_Ratio and Z_Extent_Ratio encode vertical proportions as fractions
of the parent AABB height (e.g. 0.836 = 83.6% of total height for the body).
MinQty/MaxQty constrain required vs optional categories per template level.
Template-Driven ESL Flow (Phase ST-1, not yet implemented)¶
DocSubType='ST' → No variant-matched M_BOM
→ Look up M_BomCategory WHERE doc_type='RE' → finds RE template
→ Load M_BomCategoryLine children: SL(10), GF(20), RF(30)
→ Create 3 CO_EmptySpaceLines from template (Z from ratios × AABB height)
→ For each ESL, find best-fit M_BOM via MBOM.findNextFitSpace()
→ Recurse: GF has sub-lines → create room-level ESLs → find SET BOMs
→ Leaf BOMs: walk BOM children as furniture items (template stops, BOM takes over)
POC Strategy¶
ST_SH is a dormant C_Order (is_active=0) with SH's exact AABB (16867.5 ×
8667.5 × 3945.2 mm). When Phase ST-1 adds the template walker, it must select
SH's BOMs and produce SpatialDigest(ST_SH) == SpatialDigest(SH).
DAO Classes¶
| Class | Table | Purpose |
|---|---|---|
X_CBPartner / MCBPartner |
C_BPartner | Building pattern owner lookup |
X_MBomCategoryLine / MBomCategoryLine |
M_BomCategoryLine | Template decomposition recipe |
X_M_BomCategory (updated) |
M_BomCategory | +Value, +C_BPartner_ID columns |
3.9 M_BomCategory — AABB Template Registry¶
M_BomCategory is a universal construct dictionary — a semantic type
descriptor for ANY building construct, not just rooms. It has no DocSubType
and no building identity. It is indexed by functional type + AABB dimensions.
Scope (Q&A1, 2026-03-02): BomCategory covers everything — rooms (LI, BD,
KT, BT, DN), structural tiers (SL, L1, L2, GF, RF, PR, HU, MP), and
eventually walls, MEP runs, roof assemblies, openings. Every construct type in
component_library.db gets a BomCategory "passport" — its semantic identity.
This is the Semantic IFC/BIM vision: if a shape has no BomCategory definition,
it does not exist in the compiler's vocabulary. Like XML to HTML — adding
structure and meaning to raw geometry.
Why no DocSubType on M_BomCategory? Because the template is not owned by a
building variant. It is a geometric category: "a living room of these dimensions."
The user sets a room's category (LI, BD, KT) in the Bonsai Editor when
configuring the build. The compiler then looks up M_BomCategory by functional
type + AABB to find the matching template. Which SH or DX variant that room
belongs to is irrelevant at this lookup step — only the dimensions matter.
Current templates (Living Room):
| M_BomCategory_ID | Name | AABB (W×D mm) | Source |
|---|---|---|---|
LI_SH |
Living Room SH | 8869 × 4690 | SH reference IFC, ROOM_Ground_Floor_1 |
LI_DX |
Living Room DX | 3332 × 3943 | DX reference IFC, ROOM_A102/B102 |
Two templates because the AABB differs — different rooms, different slot sets. Named descriptively (room type + distinguishing suffix), not by building.
M_BomCategoryLine — Slot Descriptors¶
M_BomCategoryLine is the slot list for a given M_BomCategory. Each line is
a placeholder — it says "this room type at this AABB has a slot for this
furniture-set type, of this size, at this priority." No BOM identity, no
building identity. Purely: what slot, what AABB, what sequence.
| Column | Semantics |
|---|---|
M_BomCategory_ID |
Parent template (e.g. LI_SH) |
Child_BomCategory_ID |
Functional type of the expected child (DN, FR, etc.) |
Sequence |
Priority — lower = placed first (Dining=10, Sofa=20, Piano=30) |
aabb_width_mm |
Slot space width for this furniture set |
aabb_depth_mm |
Slot space depth |
aabb_height_mm |
Slot space height |
What M_BomCategoryLine does NOT hold:
- Buffer/filler space — buffers live as BOMCategory='ST' PHANTOM entries in
m_bom_line within the BOM tree itself. They are part of the WHAT, not the
slot template. M_BomCategoryLine is only slot holders.
- BOM identity — no family_ref, no FK to m_bom. The compiler looks up the
actual BOM via MBOM.findNextFitSpace() at runtime.
- Building scoping — no SH/DX references. Same slot list applies to any room
that matches the parent M_BomCategory AABB.
Example — LI_SH slot list (Sequence order):
M_BomCategory: LI_SH (8869×4690mm)
Seq=10 Child=DN slot=2000×1000mm ← dining set fits here first
Seq=20 Child=FR slot=2500×900mm ← sofa set second
Seq=30 Child=FR slot=1371×600mm ← piano third (dropped if room too small)
How the compiler uses this (unified process):
- Room context carries
BomCategory_ID='LI'(set by user in Bonsai) - Compiler queries BOMCategory for m_bom.doc_sub_type matching C_DocType.DocSubType
- If exactly one BOM found → take it in toto, ONE ESLine, done (SH/DX case)
- If no match → continue to AABB-based walk (ST case)
- (ST/walk only) Compiler reads room AABB from
ad_room_boundary(or R*Tree) - (ST/walk only) Looks up M_BomCategory WHERE type='LI' AND aabb ≈ room AABB
- (ST/walk only) Reads M_BomCategoryLine slots in Sequence order
- (ST/walk only) For each slot: find best-fit BOM via
MBOM.findNextFitSpace(), write new C_OrderLine + ESLine to output.db, advance cursor by slot AABB - Acceptance criterion: spatial digest == reference building's digest (Rosetta Stone)
Key distinction: M_BomCategory/Line are just lists of holders, not a BOM tree. They describe what slots exist and at what priority. The actual BOMs (with their children, buffers, dx/dy/dz offsets) are found in {PREFIX}_BOM.db separately during compilation. The BomCategory/Line is the recipe template; the BOM is the assembly.
4. BOM Explosion Process¶
4.1 The trigger¶
C_DocType 'RE_DX' (DocSubType='DX') defines the building type in {PREFIX}_BOM.db.
Process button (DAGCompiler / run_tests.sh) creates C_Order in output.db
and fires the explosion.
4.2 The chain — DX example¶
Step 1: C_DocType selects top-level M_BOM
M_BOM = BUILDING_DX_STD (BOMCategory='RE', doc_sub_type='DX')
Step 2: Explode BOMLines (first generation — the building's direct children)
BUILDING_DX_STD → M_BOM_Lines:
seq=1 FLOOR_SLAB_GF (BOMCategory='SL') dZ=0 ← ground floor slab
seq=2 FLOOR_DX_L1_STD (BOMCategory='L1') dZ=0 ← Level 1 contents
seq=3 FLOOR_SLAB_L2 (BOMCategory='SL') dZ=3000mm ← upper floor slab
seq=4 FLOOR_DX_L2_STD (BOMCategory='L2') dZ=3000mm ← Level 2 contents
seq=5 ROOF_ASSEMBLY (BOMCategory='RF') dZ=6000mm ← roof
Every physical layer is explicit: slab, contents, slab, contents, roof.
Nothing implied. The unit IS its slabs + floors + roof.
Step 3: Explode each child (second generation)
FLOOR_DX_L1_STD → M_BOM_Lines:
seq=1 LIVING_SET → Rm_Living_1 (BOMCategory='LI')
seq=2 DINING_SET → Rm_Dining_1 (BOMCategory='DN')
seq=3 KITCHEN_CABINET_SET → Rm_Kitchen_1 (BOMCategory='KT')
seq=4 TOILET_BLOCK_FIXTURES → Rm_Bath_L1 (BOMCategory='BT')
ROOF_ASSEMBLY → M_BOM_Lines:
seq=1 ROOF_STRUCTURE (BOMCategory='FR', leaf — trusses/rafters)
seq=2 ROOF_COVERING (BOMCategory='FR', leaf — tiles/membrane)
Step 4: Explode room sets (third generation)
LIVING_SET → M_BOM_Lines:
seq=1 Piano (BOMCategory='FR', leaf) space=1500×600mm
seq=2 SOFA_AREA (BOMCategory='FR', sub-BOM) space=2000×800mm
seq=3 Loveseat (BOMCategory='FR', leaf) space=1600×800mm
seq=4 Buffer_NW (BOMCategory='ST', variable)
seq=5 Buffer_NE (BOMCategory='ST', variable)
Step 5: Explode sub-BOMs (fourth generation)
SOFA_AREA → M_BOM_Lines:
seq=1 Sofa_3Seat (leaf)
seq=2 Coffee_Table (leaf)
seq=3 Side_Tables (leaf)
4.3 Normal mode — SH/DX (sparse CO_EmptySpaceLine, IsAvailable as quality gate)¶
Before explosion:
CO_EmptySpace: AABB = 12372×26730×7884mm, is_available = Y
After explosion + translation to output DB (normal mode):
CO_EmptySpaceLine #1: BUILDING_DX_STD level=0 full AABB
CO_EmptySpaceLine #2: FLOOR_SLAB_GF level=1 ground slab plane
CO_EmptySpaceLine #3: FLOOR_DX_L1_STD level=1 Level 1 body
CO_EmptySpaceLine #4: FLOOR_SLAB_L2 level=1 upper slab plane
CO_EmptySpaceLine #5: FLOOR_DX_L2_STD level=1 Level 2 body
CO_EmptySpaceLine #6: ROOF_ASSEMBLY level=1 roof plane
CO_EmptySpaceLine #7: DUPLEX_SET_STD level=1 pair container
After tests GREEN:
CO_EmptySpace: is_available = N (confirmed: space consumed, output proven correct)
After tests FAIL (or reprocess):
CO_EmptySpace: is_available = Y (space not confirmed — build needs attention)
Why only structural-tier lines? At every BOM level there is exactly ONE candidate — no variant selection needed. The lines track structural capacity (unit, slabs, floors, roof, pair), not individual furniture items. SH produces 4 lines; DX produces 7 (see Appendix E for full dumps).
The BOM tree unfolds deterministically. The translation from {PREFIX}_BOM.db's abstract offsets (dx/dy/dz, rotation_rule) to construction coordinates is a pure function of the accepted structural tiers. No branching, no fallthrough, no iteration.
Furniture sets are BOM-exploded from the parent SET BOM. When the compiler reaches a room-level BOM (LIVING_SET, BED_SET, etc.), BOMWalker recurses into the SET's children and uses the BOM's dx/dy/dz values to position each item. This is why the SH/DX spatial digest is stable: the same BOM offsets produce the same world coordinates on every compile.
Nothing should be amiss for SH/DX. If placement errors occur, the bug is in the translation function itself (BOM offset → world coordinate), not in variant selection or space fitting. Reprocess mode (§3.6) pinpoints these.
4.4 Why extracted buildings always fit¶
For extracted buildings (DX, SH), there is only one record at each BOM layer. One ground slab. One L1. One upper slab. One L2. One roof. In L1, one of each room. In each room, one set of items. The whole construction — slabs, floors, roof, rooms, furniture — equals the original extracted model. It fits by construction because it was extracted from a model that already fit.
The reason they all fit: the original Duplex or SampleHouse IFC file had exactly these elements at exactly these positions. The BOM in {PREFIX}_BOM.db was extracted from that geometry. Replaying the BOM into an identically-sized CO_EmptySpace AABB produces the original result. There is no spatial conflict because the source had no spatial conflict.
4.5 Variant mode — TB-LKTN (dense CO_EmptySpaceLine)¶
TB-LKTN has no complete topology set pre-extracted. TopologyMaker creates room variants. The BOM explosion must iterate:
CO_EmptySpace: AABB = 9900×8500×3000mm (TERRACE_MY_1S), is_available = Y
Explosion with variant selection:
CO_EmptySpaceLine #1: BUILDING_TBLKTN_STD level=0 accepted
CO_EmptySpaceLine #2: FLOOR_SLAB_MY level=1 accepted (slab — one variant)
CO_EmptySpaceLine #3: FLOOR_TBLKTN_GF_STD level=1 accepted (floor contents)
CO_EmptySpaceLine #4: ROOF_PORCH_MY level=1 accepted (porch roof variant)
CO_EmptySpaceLine #5: BEDROOM zone → ? level=2 SpaceSize match:
candidate: BEDROOM_PREFAB_MY_3100 (3100×3100mm) — fits zone 3134×3105mm ✓
CO_EmptySpaceLine #6: COMMON zone → ? level=2 SpaceSize match:
candidate: LIVING_PREFAB_MY — fits zone 3700×6195mm ✓
CO_EmptySpaceLine #7: BATHROOM zone → ? level=2 SpaceSize match:
candidate: BATHROOM_PREFAB_MY — fits zone 1307×2125mm ✓
...
Each line records a real decision. If the CO_EmptySpace AABB is larger or smaller than the extracted template:
- Larger → look for bigger variant BOMs
- Smaller → look for smaller variant BOMs (SpaceSize fallthrough)
- No variant exists → TopologyMaker must create it first
TB-LKTN cannot fulfil its construction unless Topology exists for its unit and floor levels. TopologyMaker creates those constructs. The unit itself can be another BOM_Category (e.g. an 'SH' type unit reused in a different construction).
For furniture, {PREFIX}_BOM.db already has all sorts of smaller furnishing constructs — TB-LKTN can reuse them. Roof, porch, and outer perimeter are also done. What remains is parametric items (mesh floor, etc.).
5. Analysis — Translation and Error Diagnosis¶
5.1 The two classes of error¶
| Error class | Cause | Where it shows | Buildings affected |
|---|---|---|---|
| Translation bug | BOM offset → world coordinate math is wrong | Reprocess mode: CO_EmptySpaceLine has wrong before/next/orient | SH, DX (and all others) |
| Variant selection bug | Wrong M_BOM chosen for available space | Normal mode: CO_EmptySpaceLine records a candidate that doesn't fit | TB-LKTN only |
For SH/DX, only translation bugs are possible. There is no variant selection. The BOM tree is deterministic. If a piano ends up in the wrong place, the translation function ({PREFIX}_BOM.db abstract offset → construction mm) is broken. The BOM itself is correct (extracted from a model that worked).
For TB-LKTN, both classes are possible. A wrong variant selection (too-large BED_SET for a small room) shows up as an overflow in CO_EmptySpaceLine.remaining_mm. A translation bug shows up as misaligned before/next coordinates.
5.2 The recurring placement errors — root cause¶
The stubborn placement errors we've seen at furniture level (Piano off-centre, Sofa rotated wrong) are translation bugs. They will replicate upward when unit and floor levels enter the stack — same math, different scale.
CO_EmptySpaceLine in reprocess mode pinpoints exactly where the translation breaks:
Expected: Line #12: Piano before=(1620,3308,0) orient=π
Actual: Line #12: Piano before=(1620,3308,0) orient=0 ← rotation not translated
The {PREFIX}_BOM.db data says rotation_rule=PARALLEL_TO_WALL. The CO_EmptySpaceLine
should show orientation_rad=π (north wall). If it shows 0, the translation
function failed to resolve the wall rule.
5.3 CO_EmptySpaceLine as the single translation checkpoint¶
The BOM tab (on C_OrderLine) is unchanged — all of {PREFIX}_BOM.db copied verbatim, buffers included. CO_EmptySpaceLine is the ONLY place where the compiler records alignment decisions. The translation to output DB (world coordinates for Blender viewport / IFC export) uses BOM offsets + CO_EmptySpaceLine alignment. Every orientation error, every misaligned position, every space overflow traces back to either the BOM data (intact reference) or the alignment record (CO_EmptySpaceLine).
The IsAvailable gate confirms the translation. If CO_EmptySpace.is_available remains Y after processing, the output did not pass tests — the translation failed somewhere. Reprocess mode (§3.6) then writes verbose CO_EmptySpaceLines at every level to pinpoint exactly where.
Diagnosis pattern:
1. Run in reprocess mode (--reprocess-all)
2. Query co_empty_space_line for the failing element
3. Compare before/next/orientation_rad against expected values
4. The delta IS the bug — trace back to the translation function
5.4 MEP and future extensions¶
CO_EmptySpaceLine can hold additional spatial references per function:
| Extension | CO_EmptySpaceLine field | Purpose |
|---|---|---|
| MEP connections | mep_ref (future) |
Riser point, pipe junction coordinates |
| UBBL clearance | (via M_Attribute on leaf) | Minimum distances from walls/openings |
| 7D IoT | iot_ref (future) |
Sensor placement, conduit routing |
Each is a separate spatial concern tracked on the EmptySpaceLine. The BOM leaf stays clean — it holds WHAT the item is. The EmptySpaceLine holds WHERE it goes in this particular construction and WHAT connects to it. This separation means future spatial concerns (IoT sensors, conduit routing) can be added as new columns on CO_EmptySpaceLine without touching the BOM catalog.
5.5 The 6-Layer Geometry Verification Chain¶
The full data chain from reference IFC to output world coordinates has exactly six layers. The ST POC must prove correctness at every layer.
Layer 1: Extracted IFC (input/extracted.db)
Source of truth. Pristine geometry from Bonsai/BlenderBIM export.
Verify: Bonsai viewport visual match.
Layer 2: {PREFIX}_BOM.db (m_bom_line dx/dy/dz)
Relative spatial arrangement between siblings.
Verify: W-SPACESIZE-1 (children SUM ≤ parent AABB)
Verify: Every leaf child_product_id → valid M_Product
Layer 3: {PREFIX}_BOM.db — M_Product (width/depth/height)
Intrinsic product geometry in meters.
Verify: Dimensions match extracted IFC bounding boxes.
Layer 4: C_DocType: DocSubType + AABB
The 1D Intent. Two fields drive everything (see §3.7).
Verify: AABB ≥ UNIT BOM SpaceSize (site fits building)
Layer 5: CO_EmptySpace/Line (output.db)
Spatial anchoring at each decision point.
Verify: before/next chain continuity (Line N.next = Line N+1.before)
Verify: orientation_rad matches wall assignment
Layer 6: elements_meta / elements_rtree (output.db)
Final world coordinates. One math operation:
world_xyz = anchor + rotate(dx, dy, dz, orient)
Defined in LocalCoord.toWorld() — the ONLY WorldCoord constructor.
Verify: G8 centroid < 500mm, F4 edge < 10mm, SpatialDigest stable
Key insight: The geometry math is trivially correct — it is a single
rotate+translate in LocalCoord.toWorld(), enforced by the D8 ArchUnit gate.
When placement is wrong, walk the chain backwards:
- Layer 6 wrong → check Layer 5 anchor (is the anchor at the right wall face?)
- Layer 5 wrong → check Layer 2 offsets (are dx/dy/dz correct in the BOM?)
- Layer 2 wrong → check Layer 1 reference (does the extracted IFC match?)
Errors are always data (Layers 2–5), never math (Layer 6). This is why the PRIME RULE is "EXTRACT OR COMPILE ONLY" — if the data chain is correct, the geometry is correct by construction.
6. ad_room_slot Deprecation¶
ad_room_slot mapped room_type → assembly_id (BOM dispatch per room type).
With BOMCategory on M_BOM, this dispatch becomes implicit:
| Old (ad_room_slot) | New (BOMCategory) |
|---|---|
room_type=BEDROOM → assembly_id=BED_SET_MASTER |
M_BOM WHERE BOMCategory='BD' AND doc_sub_type=C_DocType.DocSubType |
room_type=BATHROOM → assembly_id=BATHROOM_SET |
M_BOM WHERE BOMCategory='BT' AND doc_sub_type=C_DocType.DocSubType |
The BOM_Category + DocSubType scoping replaces the explicit slot dispatch.
ad_room_slot remains in the database but is no longer the primary dispatch
mechanism — it can serve as a compatibility/override layer during migration.
7. Availability Query — "Can a Lampshade Fit?"¶
CREATE VIEW v_co_available_space AS
SELECT
esl.line_id,
es.c_order_id,
esl.storey,
esl.room_name,
esl.locator_ref,
esl.remaining_mm,
esl.next_x_mm,
esl.next_y_mm,
esl.next_z_mm,
esl.orientation_rad
FROM co_empty_space_line esl
JOIN co_empty_space es ON esl.co_emptyspace_id = es.co_emptyspace_id
WHERE es.doc_status = 'CO'
AND esl.remaining_mm > 0;
Query: "Find space for a 300mm-wide lampshade in the Living Room"
SELECT * FROM v_co_available_space
WHERE c_order_id = 'Ifc2x3_Duplex'
AND room_name = 'Rm_Living_1'
AND remaining_mm >= 300
ORDER BY remaining_mm ASC; -- tightest fit first
This finds both: - Buffer lines (BOMCategory='ST') with leftover space - Locator lines where fixed items didn't fill the wall
8. Complete ERD¶
See also:
docs/bim_architecture_viz.html— interactive ERD with clickable table details, FK relationships, and compilation pipeline visualization.Terminal-specific:
docs/terminal_erd.html— discipline hierarchy, verb→ERP mapping, M_BomCategory scoping, ROUTE-as-BOM tree with M_AttributeSetInstance.
component_library.db (LOD Geometry Store)
┌─────────────────────────┐
│ component_definitions │ Component metadata + up/forward axis + attachment face
│ component_geometries │ Vertex/face BLOBs (deduplicated by hash)
│ component_types │ IFC class taxonomy
│ placement_rules │ Host-relative component constraints
│ lod_geometry_map │ Element → geometry hash mapping
│ lod_parametric_mesh │ Procedural shape generators
│ surface_styles │ Material colours
└─────────────────────────┘
│
│ geometry_hash FK (lod_geometry_map → component_geometries)
│ component_id FK (M_Product → component_definitions)
│
{PREFIX}_BOM.db (Pure Dictionary — No Transaction Data)
┌─────────────────────────┐
│ C_DocType │ Building type: DocBaseType (RE/CO/IN) + DocSubType (SH/DX/TB/TE)
│ │ + domain config (DSL, output path, reference AABB)
│ C_BPartner │ Business partner lookup (future: real vendor/customer)
│ M_Product │ Intrinsic dims (m) + component_id + bom_id (NORM-1+2)
│ m_bom │ Assembly: BOMCategory + doc_sub_type + SpaceSize
│ └── m_bom_line │ Children: dx/dy/dz, rotation, locator, allocated_*_mm
│ └── m_bom (child) │ Recursive: child_product_id → M_Product (MAKE → bom_id)
│ m_attribute │ Leaf attributes: ports, clearances
│ M_BomCategory │ Lookup: RE, LI, BD, KT, FR, ST, SL, L1, L2, GF, RF + ARC/STR/FP (CO)
│ M_BomCategoryLine │ Template decomposition recipe (slot descriptors)
│ ad_* (60+ tables) │ Config, rules, spatial, MEP
└─────────────────────────┘
│
│ C_DocType_ID FK (C_Order → C_DocType, cross-DB)
│ family_ref FK (C_OrderLine → M_BOM.bom_id, cross-DB)
▼
output.db (Self-Contained: Orders + Production + Spatial + Compiled)
┌─────────────────────────┐
│ ORDER (WHAT) │
│ C_Order │ Construction Order (created from C_DocType at compile time)
│ ├── C_OrderLine │ WHAT to build (WHAT-only — no placement columns)
│ │ │ family_ref → M_BOM.bom_id (cross-DB)
│ │ │
│ PRODUCTION (HOW) │
│ ├── PP_Order_Node │ Verb invocations (TILE, ARRAY, ROUTE, etc.)
│ │ └── PP_Order_ │ Structured params (Name/Value/ValueType)
│ │ NodeProduct │
│ │ │
│ SPATIAL (WHERE) │
│ └── CO_EmptySpace │ Construction space header (AABB, IsAvailable)
│ └── CO_EmptySpace │ Spatial translation per BOMLine
│ Line │ (before/next, orient, remaining)
│ │
│ COMPILED OUTPUT │
│ elements_meta │ Final IFC elements (guid, class, xyz)
│ element_instances │ Geometry transforms + materials
│ element_assemblies │ Parent-child grouping in output
└─────────────────────────┘
9. Process Summary¶
1. User: Define building via C_DocType (DocSubType='DX') + AABB in {PREFIX}_BOM.db
2. User: Optionally configure DSL or BOM rules in {PREFIX}_BOM.db
3. Compiler: Read C_DocType → create C_Order + C_OrderLines in output.db → walk M_BOM trees from {PREFIX}_BOM.db
The BOM copy is COMPLETE: fixed items, sub-BOMs, AND buffer (ST) children
with all SpaceSize, dx/dy/dz, rotation_rule intact as reference
4. Compiler: Create CO_EmptySpace (AABB from building footprint, is_available=Y)
5. Compiler: Accept top-level M_BOM into CO_EmptySpace
→ Write CO_EmptySpaceLine #1 (alignment: UNIT box → full AABB, orient=0)
→ is_available stays Y (not yet proven)
6. Compiler: Explode M_BOM recursively: UNIT → SLAB → FLOOR → ROOM → SET → ITEM
Buffer children walk with their parents — the BOM construct is complete
Normal mode: children translate deterministically from acceptance
(no further CO_EmptySpaceLines unless decision point)
Reprocess: write CO_EmptySpaceLine at EVERY level (verbose audit)
Variant mode: write CO_EmptySpaceLine at each selection/conflict
7. Compiler: At each decision point:
a. Read SpaceSize from M_BOM_Line (including buffer SpaceSize)
b. If multiple candidates: select by SpaceSize fit (fallthrough)
c. Translate abstract BOM info → construction coordinates
d. Write CO_EmptySpaceLine (alignment: box origin + orient)
e. Check: remaining >= 0? (overflow = GIC violation)
8. Compiler: Translate to output DB — BOM offsets + CO_EmptySpaceLine alignment
→ world coordinates (elements_meta, element_instances for Blender/IFC)
This is where abstract BOM becomes concrete geometry
9. Compiler: Run tests (G8 centroids, F4 edges, W-SPACESIZE-1, etc.)
Tests GREEN → set is_available=N, DocStatus DR→CO (confirmed consumed)
Tests FAIL → is_available stays Y (space not successfully filled)
Reprocess → reset is_available=Y, process again from step 5
10. User: Query v_co_available_space for remaining capacity
is_available=Y after processing = build did NOT pass
10. First-Level BOMs in {PREFIX}_BOM.db (Residential Catalog)¶
These are the top-level M_BOMs — the "cars on the lot" that a C_Order can select:
| bom_id | BOMCategory | doc_sub_type | Description |
|---|---|---|---|
BUILDING_DX_STD |
RE | DX | Duplex residential building (2 floors) |
BUILDING_SH_STD |
RE | SH | Sample House residential building (1 floor) |
BUILDING_TBLKTN_STD |
RE | TB | TB-LKTN terrace residential building (1 floor) |
FLOOR_DX_L1_STD |
L1 | DX | Duplex Level 1 |
FLOOR_DX_L2_STD |
L2 | DX | Duplex Level 2 |
FLOOR_SH_GF_STD |
GF | SH | SH Ground Floor |
FLOOR_TBLKTN_GF_STD |
L1 | TB | TB-LKTN Ground Floor |
A build with DocSubType='DX' sees BUILDING_DX_STD and its descendants.
A build with DocSubType='TB' sees BUILDING_TBLKTN_STD — and can also
see generic BOMs (doc_sub_type IS NULL) like TOILET_BLOCK_FIXTURES.
Appendix A — Code Advice¶
Note: Appendix B (Compiler Pipeline Changes), Appendix C (Migration State), and Appendix D (Assessment) follow. Appendix E contains the live CO_EmptySpace ledger dumps for SH and DX.
A.1 DocStatus Lifecycle on CO_EmptySpace¶
CO_EmptySpace.doc_status follows iDempiere document lifecycle:
| Status | Meaning | When |
|---|---|---|
DR |
Draft | Project created, is_available=Y |
IP |
In Process | Compiler running, translation in progress |
CO |
Complete | Tests GREEN, is_available=N — construction confirmed |
RE |
Rejected | BOM falls outside CO_EmptySpace AABB — size mismatch, no fit |
// On MCOEmptySpace
public void startProcessing() { setDocStatus("IP"); setIsAvailable(1); }
public void confirmConsumed() { setDocStatus("CO"); setIsAvailable(0); }
public void reject(String reason) { setDocStatus("RE"); setIsAvailable(1); /* log reason */ }
public void resetForReprocess() { setDocStatus("DR"); setIsAvailable(1); }
public boolean isPassing() { return "CO".equals(getDocStatus()) && getIsAvailable() == 0; }
public boolean isRejected() { return "RE".equals(getDocStatus()); }
DR → IP → CO is the happy path. DR → IP → RE means the BOM construct does not fit the construction AABB no matter how the CO_EmptySpaceLines are arranged. The user must either resize the CO_EmptySpace or select different BOMs.
A.2 IsSpaceSizeValid — Per-Locator-Strip Invariant (W-SPACESIZE-1)¶
{PREFIX}_BOM.db has the full chaining info on m_bom_line: dx/dy/dz, rotation_rule,
locator_ref, AND SpaceSize. The invariant check is per-locator-strip, not a naive
global SUM — because children are grouped by locator (wall/zone) and arranged
linearly within each strip (GPD walk).
// On MBOM — validates the BOM construct in {PREFIX}_BOM.db
public boolean isSpaceSizeValid() {
List<MBOMLine> children = getLines();
if (children.isEmpty()) return true; // leaf — no children to sum
// Group by locator_ref (NORTH_WALL, EAST_WALL, CENTRE, FLOAT...)
Map<String, List<MBOMLine>> strips = children.stream()
.collect(Collectors.groupingBy(MBOMLine::getLocatorRef));
for (var entry : strips.entrySet()) {
String locator = entry.getKey();
List<MBOMLine> strip = entry.getValue();
// Along each strip's primary axis: SUM(child.space) must equal strip length
int stripAxisLength = getStripLength(locator); // from parent AABB + locator
int sumAlongAxis = strip.stream()
.mapToInt(line -> line.getSpaceAlongAxis(locator))
.sum();
// SUM includes buffer (ST) children — they fill the gap
if (sumAlongAxis != stripAxisLength) return false;
}
return true;
}
This check runs in {PREFIX}_BOM.db — it validates the assembly design itself. The same data is then copied verbatim to C_OrderLine.BOM.BOMLine, so the invariant holds there too without re-checking.
A.3 IsConstructionValid — BOM Tree vs CO_EmptySpace AABB (W-CONSTRUCT-1)¶
After CO_EmptySpaceLine alignment, walk the BOM tree level-by-level and verify that every resolved position stays within the CO_EmptySpace parent AABB. This is the construction-level witness — it catches translation errors that the BOM-level invariant (A.2) cannot see (because {PREFIX}_BOM.db knows nothing about the construction site).
// On MCOEmptySpace — validates the construction output
public boolean isConstructionValid() {
AABB site = getAABB(); // the CO_EmptySpace envelope
// Walk BOM tree from top-level acceptance
for (MCOEmptySpaceLine line : getLines()) {
MBOM bom = line.getBOM();
// Resolve world position: CO_EmptySpaceLine alignment + BOM dx/dy/dz
AABB resolved = resolveWorldAABB(line, bom);
if (!site.contains(resolved)) {
// BOM construct falls outside construction AABB → reject
reject("BOM " + bom.getBomId() + " at level " + line.getBomLevel()
+ " extends beyond site AABB");
return false;
}
}
return true;
}
CO_EmptySpaceLine traversal: when there is only ONE CO_EmptySpaceLine (normal
mode for SH/DX), the check optimistically traverses the entire BOM tree from that
single alignment record. No other CO_EmptySpaceLine to consult — it walks level by
level using the BOM's own dx/dy/dz chaining, checking each resolved position against
the site AABB. If any child falls out → RE (Rejected).
In reprocess mode, there is a CO_EmptySpaceLine at every level — each can be checked individually against the site AABB.
A.4 fillSpaceBufferChildren — Buffer Computation per Strip¶
// On MBOM — compute buffer SpaceSize for each locator strip
public void fillSpaceBufferChildren() {
Map<String, List<MBOMLine>> strips = getLines().stream()
.collect(Collectors.groupingBy(MBOMLine::getLocatorRef));
for (var entry : strips.entrySet()) {
String locator = entry.getKey();
List<MBOMLine> strip = entry.getValue();
int stripLength = getStripLength(locator);
// Sum fixed children along strip axis
int fixedSum = strip.stream()
.filter(line -> !"ST".equals(line.getBomCategory()))
.mapToInt(line -> line.getSpaceAlongAxis(locator))
.sum();
// Distribute remaining space to buffer (ST) children
List<MBOMLine> buffers = strip.stream()
.filter(line -> "ST".equals(line.getBomCategory()))
.toList();
if (!buffers.isEmpty()) {
int remaining = stripLength - fixedSum;
int perBuffer = remaining / buffers.size(); // equal split
for (MBOMLine buf : buffers) {
buf.setSpaceAlongAxis(locator, perBuffer);
// depth + height = same as parent (buffer fills the full cross-section)
buf.setSpaceDepthMm(getSpaceDepthMm());
buf.setSpaceHeightMm(getSpaceHeightMm());
}
}
}
}
A.5 findNextFitSpace — Variant Selection (TB-LKTN)¶
// Select M_BOM from {PREFIX}_BOM.db that fits available SpaceSize (fallthrough to smaller)
public MBOM findNextFitSpace(int widthMm, int depthMm, int heightMm,
String bomCategory, String docSubType) {
// SELECT FROM m_bom
// WHERE BOMCategory = ?
// AND (doc_sub_type = ? OR doc_sub_type IS NULL)
// AND allocated_width_mm <= ?
// AND allocated_depth_mm <= ?
// AND allocated_height_mm <= ?
// ORDER BY (allocated_width_mm * allocated_depth_mm * allocated_height_mm) DESC
// LIMIT 1
// → largest BOM that fits the available space
}
A.6 findAvailableSubSpace — Ad-hoc Item Placement¶
For placing an ad-hoc item (potted plant, lampshade) into remaining space within an already-compiled room:
// Find available sub-space within a room for an item's AABB
public Optional<MCOEmptySpaceLine> findAvailableSubSpace(
String cOrderId, String roomName,
int itemWidthMm, int itemDepthMm, int itemHeightMm) {
// Query co_empty_space_line for this room's locator strips
// WHERE remaining_mm >= itemWidthMm (along strip axis)
// AND parent depth >= itemDepthMm
// AND parent height >= itemHeightMm
// ORDER BY remaining_mm ASC -- tightest fit first
// Returns the CO_EmptySpaceLine where the item can be placed
// The item gets a new CO_EmptySpaceLine record (decision: ad-hoc placement)
// Buffer in that strip shrinks accordingly
}
This enables the "can a potted plant fit in that corner?" query. The buffer children in the BOM construct are the available sub-spaces. After placement, the buffer's SpaceSize shrinks (or a new buffer is created for the remainder), and a new CO_EmptySpaceLine records the decision.
A.7 Test Gates — Witness Registry¶
| Gate | Level | What it checks | Pass condition |
|---|---|---|---|
| W-SPACESIZE-1 | {PREFIX}_BOM.db | Per-locator-strip: SUM(children) = strip length | Zero violations across all active M_BOMs |
| W-CONSTRUCT-1 | CO_EmptySpace | BOM tree walk stays within site AABB | Every resolved child inside CO_EmptySpace envelope |
| W-PHANTOM-1 | EmptySpace | capacity - used = remaining, no overflow | Already in EmptySpaceTest (3 tests) |
| W-OWNER-1 | C_DocType→M_BOM | No build references BOM with wrong doc_sub_type | Zero cross-variant refs (unless doc_sub_type IS NULL) |
| W-CATEGORY-1 | M_BOM | BOMCategory is functional (LI/BD/KT), never building (SH/DX) | Zero building codes in BOMCategory column |
| W-ISAVAIL-1 | CO_EmptySpace | After full compile, is_available=N for every C_Order | Zero is_available=Y after successful processing |
| W-VERBATIM-1 | C_OrderLine→{PREFIX}_BOM.db | BOMLine copy matches {PREFIX}_BOM.db source | Hash/checksum match on all copied BOM trees |
| W-DOCSTATUS-1 | CO_EmptySpace | DocStatus consistent with is_available | CO→is_available=0, RE→is_available=1, no contradictions |
| G8 | Output DB | Centroid proximity vs reference | < 500mm per element (RosettaPlacementTest) |
| F4 | Output DB | Edge-level bbox proof | Edges match IFC reference within 10mm |
| F5 | Output DB | Glass transparency + staircase Z-span | alpha < 0.5, staircase spans floor-to-floor |
Appendix B — Compiler Pipeline Changes (BOM + EmptySpace → output.db)¶
The current compiler goes straight from BOM → PlacedElement → output DB with no CO_EmptySpace involvement. The pipeline must change to route through CO_EmptySpace alignment and track the IsAvailable/DocStatus quality gate.
B.1 Current Pipeline (9 stages — updated 2026-03-04)¶
CompilationPipeline.java — 9 stages:
1. MetadataValidator → BuildingRegistry reads C_DocType (not c_order)
2. ParseStage → BuildingParser.parse() → BuildingDefinition
3. CompileStage → BuildingCompiler.compileWithValidation() → BuildingSpec
4. TemplateStage → ST-mode only (DocSubType='ST'): BomTemplateComposer
5. WriteStage → BuildingWriter.initSchema() + write(spec)
Creates C_Order in output.db from C_DocType config
6. VerbStage (SPI) → VerbExecutor dispatch (BIM COBOL verbs → PP_Order_Node)
7. DigestStage → SpatialDigest.computeWithReport()
8. GeometryStage → GeometryIntegrityChecker.check()
9. ProveStage → PlacementProver.proveFromDB()
BOM resolution path (inside CompileStage, post-G-1):
StoreyCompiler.placeFixturesAndFurniture(ctx) [line 1333]
→ WorkerRegistry → FurnitureWorker.execute(envelope, placementCtx)
→ BOMTierResolver.resolveForRoom() [three-way dispatch]
→ walks m_bom → m_bom_line recursively (fixture params / GPD / FLOAT)
→ returns List<PlacedFurniture> with world xyz + rotation radians
→ addPlacedElementsToCtx(ctx, roomName, elements)
→ PlacedElement → FixtureSpec(x, y, z, rotation, geoHash, w, d, h)
Output write path (inside WriteStage):
BuildingWriter.write(spec)
→ MEPWriter.writeFixture(fixture, storeyName) [line 548]
→ compute rotated bbox (halfW*|cos|+halfD*|sin|) [line 569-581]
→ get/generate geometry (LOD400 mesh or fallback box)
→ ElementPersistence.writeElementMeta(guid, bbox, material) [→ elements_meta + elements_rtree]
→ ElementPersistence.writeInstance(guid, geoHash) [→ element_instances]
Current state (post-Phase 4): CO_EmptySpace + CO_EmptySpaceLine written at
L0+L1 levels. IsAvailable quality gate operational. wm_empty_storage_line
deprecated — superseded by co_empty_space_line.
B.2 New Pipeline (9 steps, EmptySpace integrated)¶
CompilationPipeline.java — 9 stages:
1. MetadataValidator
2. ParseStage → BuildingParser.parse() → BuildingDefinition
3. EmptySpaceStage → NEW: create CO_EmptySpace (AABB from building footprint)
Set is_available=Y, doc_status='DR'
4. BOMCopyStage → NEW: copy M_BOM tree verbatim from {PREFIX}_BOM.db to C_OrderLine.BOM
ALL children intact: fixed items, sub-BOMs, AND buffers (ST)
SpaceSize, dx/dy/dz, rotation_rule — everything transfers
5. CompileStage → CHANGED: resolve through CO_EmptySpaceLine alignment
Set doc_status='IP'
6. WriteStage → BuildingWriter writes elements_meta + element_instances
AND writes co_empty_space + co_empty_space_line to output.db
7. ValidateStage → NEW: isConstructionValid() — walk BOM tree vs site AABB
Tests GREEN → set is_available=N, doc_status='CO'
Tests FAIL → is_available stays Y, doc_status='RE' if outside AABB
8. DigestStage → SpatialDigest (unchanged)
9. GeometryStage → GeometryIntegrityChecker (unchanged)
B.3 Stage 3 — EmptySpaceStage (NEW)¶
// CompilationPipeline — new stage between Parse and Compile
class EmptySpaceStage implements PipelineStage {
void execute(PipelineContext ctx) {
// 1. Read building footprint from C_Order
// AABB = building envelope (width × depth × height in mm)
// 2. Create CO_EmptySpace record in output.db
// origin = (0,0,0), AABB from footprint
// is_available = 1, doc_status = 'DR'
// 3. Store co_emptyspace_id in ctx for downstream stages
}
}
Output.db schema addition:
-- co_empty_space and co_empty_space_line tables created in output.db
-- (DDL already specified in §3.1 and §3.2 of this document)
B.4 Stage 4 — BOMCopyStage (NEW)¶
// Copy M_BOM tree from {PREFIX}_BOM.db to C_OrderLine.BOM (verbatim)
class BOMCopyStage implements PipelineStage {
void execute(PipelineContext ctx) {
// 1. For each C_OrderLine (Construction Order Details) with family_ref:
// a. Load M_BOM tree from {PREFIX}_BOM.db (m_bom → m_bom_line, recursive)
// b. Copy verbatim to C_OrderLine.BOM tab in output.db
// Including ALL buffer (ST) children + SpaceSize
// c. Store checksum for W-VERBATIM-1 verification
// 2. The compiler reads from this copy, not {PREFIX}_BOM.db directly
// Scope is locked to what was ordered
}
}
B.5 Stage 5 — CompileStage (CHANGED)¶
The BOM resolution path changes from direct world-coordinate computation to CO_EmptySpaceLine-mediated alignment:
CURRENT (post-G-1):
BOMTierResolver.resolveForRoom(room, bomId)
→ walks m_bom_line recursively (three-way dispatch)
→ computes world xyz directly (room anchor + dx/dy/dz + rotation)
→ returns PlacedFurniture(worldX, worldY, worldZ, rotation)
NEW (ST mode):
BOMTierResolver.resolveForRoom(room, bomId, coEmptySpaceId)
→ walks m_bom_line from C_OrderLine.BOM copy (not {PREFIX}_BOM.db)
→ at decision points: writes CO_EmptySpaceLine
(alignment: box origin + orientation in construction space)
→ translates: BOM dx/dy/dz + CO_EmptySpaceLine alignment → world coords
→ buffer (ST) children: no geometry, but space tracked in CO_EmptySpaceLine.remaining_mm
→ returns PlacedFurniture(worldX, worldY, worldZ, rotation)
Specific method changes:
| Method | File:Line | Current | New |
|---|---|---|---|
placeFixturesAndFurniture |
StoreyCompiler:1333 | No EmptySpace | Accept coEmptySpaceId, pass to workers |
worker.execute |
BundleWorker | Returns PlacedElement directly | Also writes CO_EmptySpaceLine at decision points |
resolveForRoom |
BOMTierResolver | Reads m_bom_line from library DB | Reads from C_OrderLine.BOM copy in output.db |
computeBomAnchorForRoom |
BOMTierResolver | Computes anchor from room bounds | Uses CO_EmptySpaceLine alignment as anchor |
expandBOMNode |
BOMTierResolver | Walks m_bom_line, skips buffers | Walks m_bom_line, tracks buffer space in CO_EmptySpaceLine |
CO_EmptySpaceLine write points (normal mode):
For SH/DX:
1 line: top-level BOM accepted into full AABB
(all children translate deterministically — same as current code, just routed through alignment)
For TB-LKTN (or --reprocess-all):
1 line per decision point: variant selection, space conflict, orientation change
Buffer space tracked via remaining_mm on each line
Buffer handling in resolver:
// In expandBOMNode or resolveWithGPD:
for (MBOMLine child : bomLines) {
if ("ST".equals(child.getBomCategory())) {
// Buffer child — no geometry, no PlacedElement
// But track in CO_EmptySpaceLine: remaining_mm -= 0 (buffer IS the remaining)
continue; // skip geometry output
}
// Fixed child — resolve position, generate PlacedElement
// Track: remaining_mm -= child.getSpaceAlongAxis()
}
B.6 Stage 6 — WriteStage (CHANGED)¶
In addition to current elements_meta + element_instances writes:
// BuildingWriter.write(spec) — additional writes
// 1. Write co_empty_space record (from EmptySpaceStage)
// 2. Write all co_empty_space_line records (from CompileStage)
// 3. Set doc_status = 'IP' on co_empty_space (processing complete, awaiting validation)
ElementPersistence additions:
public void writeCOEmptySpace(MCOEmptySpace es) {
// INSERT INTO co_empty_space VALUES (...)
}
public void writeCOEmptySpaceLine(MCOEmptySpaceLine line) {
// INSERT INTO co_empty_space_line VALUES (...)
}
B.7 Stage 7 — ValidateStage (NEW)¶
class ValidateStage implements PipelineStage {
void execute(PipelineContext ctx) {
MCOEmptySpace es = ctx.getEmptySpace();
// 1. isConstructionValid() — walk BOM tree vs site AABB (W-CONSTRUCT-1)
if (!es.isConstructionValid()) {
es.reject("BOM construct falls outside site AABB");
return; // doc_status='RE', is_available stays Y
}
// 2. isSpaceSizeValid() — per-locator-strip check on BOM copy (W-SPACESIZE-1)
// (validates the copied BOM, not {PREFIX}_BOM.db — should be identical)
// 3. Run existing gates: G8 centroids, F4 edges, F5 glass
// PlacementProver.proveFromDB()
// GeometryIntegrityChecker.check()
// 4. ALL GREEN → confirm consumed
es.confirmConsumed(); // doc_status='CO', is_available=N
}
}
B.8 Reprocess Mode (--reprocess-all flag)¶
// CompilationPipeline — accept CLI flag
boolean reprocessAll = args.contains("--reprocess-all");
// In CompileStage:
if (reprocessAll) {
// Reset: co_empty_space.is_available = Y, doc_status = 'DR'
// Delete existing co_empty_space_line records
// Re-resolve: write CO_EmptySpaceLine at EVERY BOM level (verbose audit)
// For SH/DX: same result, more lines (pure verification)
// For TB-LKTN: actual working mode (real decisions at each level)
}
B.9 World Coordinate Flow — Before vs After¶
BEFORE (current, post-G-1):
m_bom_line ({PREFIX}_BOM.db) → BOMTierResolver → world xyz directly
(room anchor + dx/dy + rotation around centroid)
→ PlacedFurniture(worldX, worldY, worldZ, rot)
→ FixtureSpec → MEPWriter → elements_meta
AFTER (ST mode):
C_OrderLine.BOM copy → BOMTierResolver → CO_EmptySpaceLine (alignment: origin + orient)
→ BOM dx/dy/dz + alignment → world xyz
→ PlacedFurniture(worldX, worldY, worldZ, rot)
→ FixtureSpec → MEPWriter → elements_meta
+ co_empty_space_line (output.db)
Key difference: the resolver reads from the C_OrderLine.BOM copy (not {PREFIX}_BOM.db directly), and the alignment step is explicit via CO_EmptySpaceLine. The world coordinate computation is the same math — but the intermediate alignment record makes the translation auditable.
B.10 Output.db Schema Summary (after changes)¶
| Table | Status | Written by |
|---|---|---|
elements_meta |
Existing | ElementPersistence.writeElementMeta() |
element_instances |
Existing | ElementPersistence.writeInstance() |
base_geometries |
Existing | ElementPersistence.writeGeometry() |
elements_rtree |
Existing | ElementPersistence.writeElementMeta() |
element_transforms |
Existing | ElementPersistence (spatial index) |
co_empty_space |
Existing (Phase 4) | CompilationPipeline + ValidateStage |
co_empty_space_line |
Existing (Phase 4) | CompilationPipeline (structural tiers) |
Appendix C — Migration State & Remaining Work¶
C.1 Migration State (updated 2026-02-26)¶
migration_bom_dimension_model.sql (8 parts) + Phase 1 records/SpaceSize scripts — ALL COMPLETE.
| Part | What | Status |
|---|---|---|
| 0 | Table renames (ad_bom→m_bom, etc.) | DONE |
| 1 | M_BomCategory lookup (RE/LI/BD/KT/FR/ST/SL/L1/L2/GF/RF/PR/HU + more) | DONE |
| 2 | C_BPartner column on m_bom (now renamed to doc_sub_type) |
DONE |
| 3 | space_width/depth/height_mm on m_bom_line |
DONE |
| 4 | C_BPartner column on C_Order (now C_DocType_ID) |
DONE |
| 5 | Seed on buildings (SH/DX/TB/TE) | DONE |
| 6 | Copy old BOMCategory → doc_sub_type | DONE |
| 7 | Repurpose BOMCategory to functional codes | DONE |
Additional BOM Dimension Phase 1 migrations (2026-02-25):
- migration_bom_dimension_phase1_records.sql — 14 BOMCategory codes, UNIT/FLOOR/ROOM BOM trees for DX with slab + roof children
- migration_bom_dimension_phase1_spacesize.sql — SpaceSize seeded on all m_bom_line from ad_product_dim + computed aggregates
C.2 BOM Data Completeness (updated 2026-02-26)¶
| Item | Status |
|---|---|
| FLOOR_SLAB_GF / FLOOR_SLAB_L2 BOMs | DONE — UNIT children with dZ offsets |
| ROOF_ASSEMBLY children | DONE — structural + covering children |
| SpaceSize on all m_bom_line | DONE — seeded from product dims |
| Functional BOMCategory (LI/BD/KT etc.) | DONE — 14 codes |
| doc_sub_type scoping on BOMs | DONE — SH/DX/TB/TE |
| Buffer (ST) children on room BOMs | PARTIAL — schema ready, records pending for some rooms |
C.3 Remaining Work — Phase ST¶
Phase ST-1b complete (2026-02-27): Schema foundation (ST-0), BOM template contract with MinQty/MaxQty (ST-1a), aspect columns + DX composition proof (ST-1b). Test gate: 207 PASS / 1 intentional RED / 1 SKIP.
Completed:
- bom_owner→c_bpartner→doc_sub_type rename chain complete, C_DocType replaces scoping role
- AABB on C_DocType (was on c_order), ST_SH dormant entry
- BomTemplateContract.check() — catalog completeness validation
- Aspect columns (num_units, storey_count, mirroring_rule) on M_BomCategoryLine
- DX template branch (RE→PR→HU→{L1,L2}→rooms)
- BomTemplateComposer.compose() — composition proof (W-COMPOSE-DX)
- MBOM.findBestFitAnyOwner() — catalog-wide BOM selection
Remaining pipeline gaps for ST mode:
| Gap | What | Phase |
|---|---|---|
| Template-driven compilation | Wire composer into CompilationPipeline, create ESLs | ST-1c |
| CO_EmptySpaceLine L2–L3 | Room-level + item-level spatial records | ST-1c |
| BOMCopyStage | Verbatim copy M_BOM tree to C_OrderLine.BOM | Appendix B.4 design |
| ValidateStage | isConstructionValid + IsAvailable quality gate | Appendix B.7 design |
| Reprocess mode flag | --reprocess-all verbose audit |
Appendix B.8 design |
POC gate: SpatialDigest(ST_SH) == SpatialDigest(SH) — proves the engine
before unlocking TB-LKTN.
C.4 DAO ORM — Operational¶
All resolver code uses DAO pattern (ModelQuery<X_M_BOMLine>, X_M_BOM, etc.).
Raw JDBC only for single-consumer AD tables (ad_building_grid, ad_wall_face,
ad_room_slot). The X_/M_ classes reference the renamed tables.
Key DAO classes:
- X_M_BOM / MBOM — assembly definition (Table_Name = "m_bom")
- X_M_BOMLine / MBOMLine — child placement + SpaceSize (Table_Name = "m_bom_line")
- X_M_Attribute / MAttribute — leaf attributes (Table_Name = "m_attribute")
- X_M_BomCategory / MBomCategory — functional type lookup (+Value, +C_BPartner_ID)
- X_CBPartner / MCBPartner — building pattern owner lookup (NEW in ST-0)
- X_MBomCategoryLine / MBomCategoryLine — template decomposition recipe (NEW in ST-0)
- X_CO_EmptySpace / M_CO_EmptySpace — construction site header
- X_CO_EmptySpaceLine / M_CO_EmptySpaceLine — spatial alignment record
Pattern: docs/SourceCodeGuide.md — DAO Pattern section.
Working example: BOMTierResolver.resolveForRoom() (Phase G-1).
Appendix D — Assessment of Concept¶
D.1 Highlights¶
1. The 1D Intent — radical simplification.
The entire building definition collapses to two fields: DocSubType (WHICH
variant) + AABB (HOW BIG). Every downstream decision — which unit, which
floor, which room set, which furniture, which buffer — derives from these two
roots. This is a genuinely novel framing: a building is not a geometric model
but a construction order with a bill of materials. The DSL, the grid, the room
boundaries — all become implementation details of the BOM explosion, not
first-class inputs.
2. ERP as the domain model, not a bolt-on. By mapping directly onto iDempiere entities (C_Order, C_OrderLine, M_BOM/M_BOM_Line, CO_EmptySpace), the system inherits a battle-tested transactional framework. DocStatus lifecycle (DR→IP→CO→VO), IsAvailable quality gates, and reprocess semantics come for free. The domain language is procurement and logistics, not geometry — which turns out to be the right abstraction for prefab construction where the question is "what fits where?" not "what shape is this?"
3. Three orthogonal dimensions. Category (WHAT) × DocSubType (WHICH variant) × SpaceSize (HOW MUCH) give a clean factorization of the BOM catalog. A bedroom set is a bedroom set regardless of which building variant defines it (category). The same variant can have multiple room sizes (SpaceSize). Variant-neutral selection (ST mode) falls out naturally by relaxing the WHICH constraint.
4. CO_EmptySpace as spatial audit trail. Every BOM placement gets a before/next coordinate record. This is not geometry — it is a ledger entry. The translation from BOM offsets to world coordinates happens exactly once, in one place, and the CO_EmptySpaceLine records are the single checkpoint. Debugging spatial errors reduces to reading a table, not replaying a geometric algorithm.
5. Deterministic replay from extracted buildings. Extracted buildings (SH, DX) fit by construction — the BOM was reverse- engineered from a model that already fit. This gives a ground-truth baseline for any algorithmic changes: SpatialDigest comparison proves the engine has not regressed. The Rosetta Stone discipline (prove on known-good before attempting unknown) is a strong engineering methodology.
6. Template-driven decomposition (M_BomCategoryLine). The recursive category template (RE→{SL,GF,RF}, GF→{LI,BD,...}) separates the decomposition recipe from the BOM catalog. New building patterns can be defined by adding template rows — no Java code changes. The Z-ratio encoding (Z_Offset_Ratio, Z_Extent_Ratio) keeps vertical proportions data-driven.
D.2 Potential Shortcomings¶
1. 1D strip packing is a simplification, not reality. The axis model (Width=SUM, Depth=MAX, Height=MAX) reduces 3D spatial arrangement to 1D strip packing along the width axis. Real rooms are not arranged in a single strip — they tile in 2D. A living room beside a bedroom beside a kitchen is a 2D floor plan, not a 1D sequence. The current model works for SH (3 rooms in a row) and DX (rooms in L-shaped floors), but will hit limits for complex floor plans with corridors, T-junctions, or irregular footprints. The 2D tiling problem is fundamentally harder than strip packing.
2. AABB is a coarse envelope. Real buildings have L-shaped, T-shaped, or irregular footprints. An axis-aligned bounding box wastes space on non-rectangular plans and provides no mechanism for concavities. The gap between AABB and actual usable floor area grows with plan complexity. For POC with rectangular SH this is fine; for real-world buildings it may become a blocking limitation.
3. Template granularity vs. real diversity. The M_BomCategoryLine template assumes a fixed decomposition recipe: every residential building has exactly {slab, ground floor, roof}, and every ground floor has exactly {living, bedroom, dining, kitchen, bathroom}. Real buildings vary: a studio apartment has no separate bedroom; a 4-bedroom house has multiple bedrooms with different sizes; a split-level house has fractional storeys. The template must either enumerate all variants (combinatorial explosion) or accept that some buildings don't fit the template (requiring manual override or new templates).
4. Best-fit selection assumes a populated catalog.
findNextFitSpace() selects the largest BOM that fits within available space.
This requires a catalog of pre-built room BOMs at various sizes. If the catalog
has only one bedroom size and the available space is significantly larger or
smaller, the selection either wastes space or fails. Catalog density directly
limits the utility of ST mode. Generating BOM variants automatically (parametric
rooms) is not yet addressed.
5. No rotation or orientation algebra. The current model stores orientation as a scalar (radians) and rotation_rule as a string. There is no formal algebra for composing rotations across BOM levels, handling mirroring (DX Unit B), or resolving orientation conflicts when template-selected BOMs face different directions than the parent expects. The DX duplex already requires π rotation for the mirrored unit — this is handled as a special case, not a general mechanism.
6. CO_EmptySpaceLine count explosion in ST mode. Owner-matched builds need ~1 CO_EmptySpaceLine (deterministic, single path). ST mode needs one line per BOM node at every level — potentially hundreds for a moderately complex building. The current schema handles this, but query performance, debugging clarity, and reprocess cost scale linearly with line count. The conceptual elegance of "one ledger entry per placement" becomes a practical burden if the tree is deep and wide.
7. Two-database coordination. {PREFIX}_BOM.db holds the master data; output.db holds the compiled result. The compilation pipeline reads from one and writes to the other, with no transactional guarantee across the two SQLite databases. A crash mid-pipeline can leave output.db in an inconsistent state. The reprocess mechanism (§3.6) mitigates this but does not eliminate it. A single-database design with views/triggers would be more robust but would conflate master data with output.
8. Gap between ERP metaphor and construction reality. The iDempiere mapping is intellectually elegant but can confuse domain experts. A structural engineer thinks in beams and columns, not in C_Orders and M_BOM_Lines. The abstraction helps the developer but hinders communication with the construction industry. Documentation must bridge this gap — the concept is sound but the vocabulary barrier is real.
D.3 Overall Assessment¶
The Construction-as-ERP concept is a strong architectural foundation. The 1D Intent (WHO + HOW BIG) is a genuine insight — most BIM systems over-specify the input when two fields suffice. The three-dimension BOM model, CO_EmptySpace audit trail, and template decomposition are well-designed and data-driven.
The primary risk is the gap between the 1D strip model and real 2D/3D spatial arrangement. The POC strategy (prove on SH, then DX, then TB-LKTN) is the right approach — each building type stress-tests a progressively harder spatial constraint. If ST_SH reproduces SH's SpatialDigest, the engine is sound for rectangular single-storey buildings. The harder tests (multi-unit, multi-storey, irregular plans) will reveal whether the AABB/strip model generalizes or needs extension to 2D bin packing.
The ERP metaphor is not a limitation — it is a discipline. By forcing every placement decision through a ledger (CO_EmptySpaceLine), the system gains auditability that pure-geometry BIM systems lack. The question is not whether the metaphor holds, but whether the spatial model beneath it is rich enough for the target building types.
D.4 Catalog Cart Model & Aspect Injection¶
The 2D→3D lesson. Extracting structure from 2D drawings is lossy and labour-intensive. The alternative: a catalog of ready-made BOM artifacts that already encode correct IFC geometry. Selection replaces extraction.
Standard mould. The catalog provides a validated framework of prefab BOMs. A future Bonsai GUI enables constraint editing — swap room variants, adjust quantities — not geometry redrawing. The user fills a cart from the catalog; the compiler assembles the building.
Aspect injection. Three columns on M_BomCategoryLine parametrically
branch the template tree:
| Column | Semantics |
|---|---|
num_units |
0=universal (SL, RF, room-level), 1=single-household (GF), 2=dual-household (PR) |
storey_count |
Informational: how many storeys this subtree spans |
mirroring_rule |
'NONE' or 'PARTY_WALL_PI' — aspect injection for mirrored pairs |
When num_units=2, the RE template activates the PR→HU→{L1,L2}→rooms branch
and skips GF (single-household). When num_units=1, the reverse. Universal
nodes (num_units=0) like SL (slab) and RF (roof) appear in all configurations.
Composition proof. Pass DX's AABB (12372×26730×7884) + num_units=2 using
generic residential mode. The system walks the RE template, branches to the
duplex path, and at each leaf finds the best-fitting BOM from the entire
catalog (all variants). Since only DX has PR and HU category BOMs (doc_sub_type='DX'),
those self-select without ever specifying doc_sub_type='DX'. Room-level BOMs (LI,
BD, KT, BT, DN) select from generic NULL-variant BOMs — shared parts.
The AABB constraint + template branching naturally produces DX structure without ever saying "build a duplex." This proves the catalog cart mechanism.
Forward challenges. L-shaped rooms, adjacency constraints, structural grid
alignment, MEP proximity — each becomes a template constraint or AD rule, not a
drawing operation. The existing AD infrastructure (ad_typology_template,
ad_unit_type, ad_spatial_rule, ad_check_threshold) provides injection
points for future enrichment without changing the composition engine.
D.5 Universal Configurator — Beyond Construction¶
The engine is domain-agnostic. It knows: product → BOM → children with offsets → library geometry → gate verification. Swap the library and rules, the domain changes.
| Construction | Infrastructure | Automotive | |
|---|---|---|---|
| Product | BUILDING_SH_STD | BRIDGE_TYPE_A | VEHICLE_SEDAN_STD |
| Hierarchy | Storey → Room | FacilityPart → Segment | Assembly → SubAssembly |
| Swap | Flat → pitched roof | Steel → precast deck | ICE → EV powertrain |
| Add discipline | FP sprinklers | Drainage | ADAS sensors |
| Validation | UBBL, NFPA 13 | Road geometry | Crash/weight |
bomDrop, swapProduct, addDiscipline, and AABB fit checks work identically across all three. 4D/5D/6D/7D are columns on the order line — inherent, not bolted on.
Bonsai as visual workbench: Browse 2,459+ catalog products (G-5 BOM Chooser), place into BOM slots with fit validation, modify per-instance via ASI, compile, and the 6 gates + 28 proofs re-verify automatically. The 3D viewport IS the digital twin — every element traces to a catalog product.
Freeform prototyping: Start from an empty C_Order. Take FK's timber frame, SH's curtain wall, DX's staircase — place them in a new arrangement via BOM tree editing. Compile, iterate until gates pass. Cross-domain: bridge piers as structural columns, automotive parts if the library has them. The domain boundary is the library, not the code.
Appendix E: CO_EmptySpace Ledger — SH vs DX¶
The CO_EmptySpace model is the audit trail of compilation. Every BOM placement is a ledger line — an ERP sales-order line that tracks what was placed, where, and how much capacity was consumed.
The pipeline does NOT descend to furniture level in CO_EmptySpaceLines.
Each line represents a structural tier (unit, slab, floor, roof, pair).
The furniture placement happens inside the compiler when it walks the
floor BOM's children (room SETs → furniture items), but those individual
items are written to c_orderline, not to CO_EmptySpaceLine.
Data flow¶
C_DocType ({PREFIX}_BOM.db domain config) → C_Order (output.db, fresh each compile) — WHICH + HOW BIG
└─ c_orderline (output.db) — WHAT to build (order topics)
│ SH: 62 items, DX: 1,115 items
│
▼ [Compiler runs]
│
co_empty_space (output DB) — envelope AABB (1 per building)
└─ co_empty_space_line — structural tier ledger
│ SH: 4 lines, DX: 7 lines
│
▼ [Compiler writes elements]
│
element_instances (output DB) — final IFC elements
SH: 56, DX: 1,089
A.1 SH — Ifc4_SampleHouse (single-storey, 4 lines)¶
c_order (output.db — created from C_DocType RE_SH):
| Field | Value |
|---|---|
| building_id | Ifc4_SampleHouse |
| building_name | IFC4 Sample House |
| C_DocType_ID | RE_SH |
| aabb_width_mm | 16867.5 |
| aabb_depth_mm | 8667.5 |
| aabb_height_mm | 3945.2 |
| doc_status | CO |
co_empty_space (1 row — the envelope):
| Field | Value |
|---|---|
| origin_x/y/z_mm | −9234.9 / −2746.4 / −470.0 |
| aabb_width/depth/height_mm | 16867.5 / 8667.5 / 3945.2 |
| is_available | 0 (fully consumed) |
| doc_status | CO |
co_empty_space_line (4 rows):
| seq | bom_id | role | level | storey | Z range (mm) |
|---|---|---|---|---|---|
| 0 | BUILDING_SH_STD | BUILDING | 0 | — | −470 → 3475 |
| 5 | FLOOR_SLAB_GF | GROUND_SLAB | 1 | — | −470 (slab plane) |
| 10 | FLOOR_SH_GF_STD | GROUND_FLOOR | 1 | Ground Floor | −470 → 2830 |
| 15 | ROOF_ASSEMBLY | ROOF | 1 | — | 2530 (roof plane) |
4 lines. Single-storey: one slab, one floor, one roof, one unit container. The FLOOR_SH_GF_STD line is where the compiler walks into room SETs (LI→LIVING_SET, BD→BED_SET, etc.) and writes furniture to c_orderline.
A.2 DX — Ifc2x3_Duplex (two-storey duplex, 7 lines)¶
c_order (output.db — created from C_DocType RE_DX):
| Field | Value |
|---|---|
| building_id | Ifc2x3_Duplex |
| building_name | IFC2x3 Duplex |
| C_DocType_ID | RE_DX |
| aabb_width_mm | 12372.7 |
| aabb_depth_mm | 26730.8 |
| aabb_height_mm | 7884.8 |
| doc_status | CO |
co_empty_space (1 row — the envelope):
| Field | Value |
|---|---|
| origin_x/y/z_mm | −3147.7 / −22182.7 / −1250.0 |
| aabb_width/depth/height_mm | 12372.7 / 26730.8 / 7884.8 |
| is_available | 0 (fully consumed) |
| doc_status | CO |
co_empty_space_line (7 rows):
| seq | bom_id | role | level | storey | Z range (mm) |
|---|---|---|---|---|---|
| 0 | BUILDING_DX_STD | BUILDING | 0 | — | −1250 → 6635 |
| 5 | FLOOR_SLAB_GF | GROUND_SLAB | 1 | — | −1250 (slab plane) |
| 10 | FLOOR_DX_L1_STD | LEVEL_1 | 1 | Ground | −1250 → 1850 |
| 15 | FLOOR_SLAB_L2 | UPPER_SLAB | 1 | — | 1750 (slab plane) |
| 20 | FLOOR_DX_L2_STD | LEVEL_2 | 1 | Upper | 1750 → 4650 |
| 25 | ROOF_ASSEMBLY | ROOF | 1 | — | 4750 (roof plane) |
| 100 | DUPLEX_SET_STD | PAIR | 1 | — | 4750 (pair container) |
7 lines. Two-storey: two slabs (GF + L2 interfloor), two floors (L1 + L2), one roof, one unit, one pair container. The DUPLEX_SET_STD (PAIR) line is the mirrored half-unit structure (Unit A + Unit B at π rotation).
A.3 Why not furniture-level lines?¶
CO_EmptySpaceLine records structural capacity — each line is an AABB reservation that the compiler fills with BOM children. The furniture items (individual IfcFurnishingElements) are too numerous (56 for SH, 1,089 for DX) and belong to the element output layer, not the capacity-tracking ledger.
The layered process:
- CO_EmptySpaceLine — structural tiers (4–7 lines per building)
- c_orderline — per-element order topics, WHAT to build (62–1,115 per building)
- element_instances — final IFC geometry output
A flat single-tier building (like SH) needs only 4 ledger lines because there is one of everything: one slab, one floor body, one roof. A multi-storey building (like DX) adds lines for each additional slab, floor, and structural container (PAIR). The line count scales with structural complexity, not element count.
11. Design Decisions — Q&A1 Consolidation (2026-03-02)¶
Compilation methodology: Single path — C_OrderLine → BOM explosion → elements. See BOMBasedCompilation.md for the full spec.
Decisions confirmed through structured Q&A. Each resolves a model ambiguity.
11.1 Single Compilation Path (S54b)¶
Historical note: Prior to S54b, two compilation modes existed: EN-BLOC (singularity — one BOM taken whole) and WALK THRU (progressive slot filling). These were unified into a single path: C_OrderLine → BOM explosion → elements. The distinction was unnecessary — BOM Drop (§11.1.1) handles both cases.
The compilation path is now uniform for all buildings:
- C_OrderLine references
M_Product_ID(a BUILDING product, e.g.BUILDING_SH_STD) - BOMWalker explodes the BOM tree recursively (IsBOM products expand)
- PlacementCollectorVisitor resolves world coordinates from the tack chain
- BuildingWriter emits elements with geometry from
component_library.db
The user creates or modifies C_OrderLines via BOM Drop in the Bonsai Designer (see BIM_Designer_SRS.md §28). The compiler compiles whatever the order contains. No mode selection, no DocSubType routing.
- {PREFIX}_BOM.db has no c_orderline (dropped 2026-03-04 — redundant with M_BOM + M_Product)
- output.db c_orderline = what the compiler produced (compile-time, from BOM explosion)
- BOM Drop configurator: user navigates the BOM tree, swaps products by ProductCategory, adds/removes OrderLines. See GENERATIVE_HOUSE_SRS.md TC-1..TC-8.
11.2 C_OrderLine generation in output.db¶
Compiler-generated C_OrderLines are transactional instance data — they go to
output.db, not {PREFIX}_BOM.db. {PREFIX}_BOM.db holds only the BOM recipe
(m_bom + m_bom_line). The compiler expands the BOM tree into C_OrderLines
and writes them alongside CO_EmptySpaceLines.
11.2.1 The M_BomCategoryLine → C_OrderLine generation mechanism¶
When DocSubType differs from any BOM variant (ST mode), the system uses the
M_BomCategoryLine template to generate C_OrderLines.
Template lookup is by DocBaseType, not DocSubType. RE (Residential Template) is
a generic template with C_BPartner_ID = NULL. ST is a test/demo type (like
iDempiere's GardenWorld), not a template owner. The template defines structural
grammar (slots); DocSubType influences which M_BOM fills each slot, not which
template structure is used.
Selection cascade for BOM fitting: 1. AABB fit (primary): SpaceSize must fit within the slot's allocated AABB 2. Largest volume (secondary): maximize space usage among fitting candidates 3. seq_no (tiebreaker): lower = preferred. Owner-specific BOMs default to seq_no=10, generic BOMs to seq_no=20, so owner-specific naturally wins ties.
Template walk steps:
1. Look up M_BomCategory WHERE doc_type='RE' AND C_BPartner_ID IS NULL
→ finds RE (Residential Template)
2. Load RE's M_BomCategoryLine children, filtered by num_units:
- num_units=1 (ST_SH): SL(seq=10), GF(seq=20), RF(seq=30)
- num_units=2 (ST_DX): SL(seq=10), PR(seq=15), RF(seq=30) → PR→2×HU→L1/L2
3. Create one C_OrderLine per template slot, with AABB derived from parent AABB × Z ratios
4. For each C_OrderLine, select best-fit M_BOM via selection cascade above
5. GF recurses: GF's M_BomCategoryLine children (LI, BD, DN, KT, BT) → room-level C_OrderLines
6. Leaf BOMs: walk BOM children → actual elements (doors, furniture, etc.) from component_library.db
Already implemented (partial): BomTemplateComposer in TemplateStage performs
steps 1–4, producing NodeSelection records. Currently these only feed
CO_EmptySpaceLines (WriteStage). The missing link: NodeSelection → C_OrderLine
in output.db, and using those C_OrderLines (not the DSL compiled path) for
element generation.
Why this solves the door invention problem: The BOM tree for SH contains the exact 3 SH doors (from component_library.db). When the compiler walks the BOM tree instead of generating doors from DSL heuristics + DoorWindowLibraryMapper nearest-match, it produces the correct 3 doors with exact dimensions. No invention.
User override: If the user specifies preferences via DSL rules in {PREFIX}_BOM.db, those take priority over generated defaults ("user intent"). Generated C_OrderLines are default holders — friendly defaults when the user didn't specify. All C_OrderLines live in output.db (generated at compile time).
11.3 CO_EmptySpaceLine = WHERE (measurement), C_OrderLine = WHAT (intent)¶
Clean separation confirmed: - C_OrderLine = "I want a living room set in this room" (WHAT the user ordered) - CO_EmptySpaceLine = "this BOM box sits at (x,y,z) facing north" (WHERE it goes) - L2 ESLines = available room space (design-time AABB from ad_room_boundary), not occupied space. Buffer filler concept: habitable rooms must have explicit empty space. Furniture cannot be crammed; items are arranged in corners or evenly central.
11.4 Rosetta Stone: zero tolerance, filter required¶
Deterministic DAG compiler. First principle. No relaxation of digest comparison.
The 123 vs 56 gap (ST_SH vs SH) is because ST_SH generates phantom/stub/prop elements that SH's extraction does not include. The extracted DB is faithful to the IFC — those 56 are the real visible elements. The 67 extras are compilation artifacts (structural stubs, PHANTOM buffers, props).
Action: Investigate which element classes comprise the 67 extras. Build a digest filter that compares only geometry-bearing visible elements. Both sides must hash the same classes. This is prerequisite for the Rosetta Stone gate.
11.5 BomCategory = universal semantic dictionary¶
Not just rooms. Everything gets a BomCategory passport. Walls, slabs, MEP runs, roof assemblies, openings — if it exists in component_library.db, it has a BomCategory definition. Without one, the construct does not exist in the compiler's vocabulary.
Vision: Semantic IFC/BIM. Like XML to HTML — adding structure and meaning to raw geometry. The library will have many components, all with their semantic "passports" in BomCategory.
11.6 TB-LKTN: rule-following, enrichable¶
No Rosetta Stone possible (no reference IFC). Acceptance = architect expectations against 2D grid layout + UBBL rules. The expected_elements count (currently 139) is enrichable as more components are added to the library.
INVENTION STOP is part of the BIM development cycle: team populates library with LOD meshes and M_Product entries. Compiler halts with explicit error on missing component. User creates mesh (Mesh2Library), links to M_Product, re-runs. May need BIM COBOL 2D verbs for layout compliance.
11.7 VerbStage: direct integration, evolving language¶
BIM COBOL should replace hardcoded MEP placement (placeMEPSprinklers, placeHVAC, placeElectrical) — COBOL over assembler. It is part of the compilation pipeline, not an external tool. The language evolves continuously like component_library — team work.
Integration pattern needed: Break circular dependency (DAGCompiler cannot depend on BIM_COBOL) while allowing direct execution. SPI/plugin pattern or verb interface in DAGCompiler with BIM_COBOL as runtime provider. TBD.
Verb storage model (2026-03-04): Structured tables following iDempiere Manufacturing PP_Order_Node + PP_Order_NodeProduct pattern:
PP_Order_Node— one row per verb invocation. iDempiere columns:SeqNo(execution order),Name(verb keyword),Description(COBOL source),S_Resource_ID(FK → ESLine spatial slot),M_Product_ID(FK → BOM recipe),DocStatus(DR→IP→CO→VO lifecycle). BIM-specific:last_result(JSON proof),element_count.PP_Order_NodeProduct— structured parameters per verb. iDempiere columns:Name(param key),Value,ValueType(TEXT/REAL/INTEGER).
Both tables live in output.db (transaction data). Description (COBOL source
text) and NodeProduct rows stay in sync — text is what ScriptRunner parses; params
are the form fields that Bonsai GUI edits. Full schema in BIM_COBOL.md §15.6.
Historical lineage in ADHistory.md §Manufacturing Workflow.
11.8 Terminal: third Rosetta Stone, discipline-driven hierarchy¶
See:
docs/TerminalAnalysis.md§ERP Model Architecture for full design.
48,428 active elements (2,660 REBAR deferred to IfcOpenShell Python). DocBaseType=CO determines the hierarchy shape: BUILDING → STOREY → DISCIPLINE → ASSEMBLY → COMPONENT. This differs from RE (room-oriented at L2).
Key ERP model decisions for Terminal:
- M_BomCategory doc_type='CO' scopes discipline codes (ARC, STR, FP, etc.)
- M_AttributeSetInstance used for ROUTE verbs (varying pipe lengths), not TILE
- C_OrderLine stays WHAT-only — ~40-50 lines (storey × discipline grid)
- PP_Order_Node carries verb parameters (TILE grid, ROUTE path, FRAME bay)
- Val_Rule via domain AD tables (ad_fp_coverage, ad_acmv_sizing) — not generic SQL
- ROUTE segments are sub-BOMs with per-segment M_AttributeSetInstance (BIM_Pipe)
11.9 C_OrderLine separation: order topics vs production detail (DECIDED 2026-03-04)¶
Architectural invariant: c_orderline is WHAT-only. No placement columns. Schema enforces shape. Java PO interface enforces access. Javadoc enforces intent. If placement data can't be written, flat-data shortcuts are impossible by construction.
The PP_ model (§11.7) exposes that c_orderline is overloaded. It currently mixes three concerns that iDempiere keeps in separate tables:
| iDempiere table | Concern | Current c_orderline columns |
|---|---|---|
| C_OrderLine | WHAT to build (order topics) | building_type, element_ref, ifc_class, discipline, family_ref, is_active |
| PP_Order_BOMLine | WITH WHAT materials | width_mm, height_extent_mm, depth_mm, material_name, material_rgba, geometry_hash (vestigial) |
| PP_Order_Node | HOW to place | host_type, host_ref, position_rule, position_value ×3, height_mm, orientation |
In iDempiere: C_OrderLine says "customer wants 100 chairs." PP_Order says "cut → assemble → paint." Our c_orderline currently says both "this building needs IfcPlate_0042" AND "put it at FRACTION 0.35 on NORTH_WALL of LIVING."
Column split destination:
STAYS in c_orderline (order topics — WHAT):
building_type, element_ref, ifc_class, discipline,
family_ref, is_active
= "This building needs these elements from this product catalog"
MOVES to PP_Order_NodeProduct (production — HOW):
host_type, host_ref, position_rule,
position_value, position_value_2, position_value_3,
height_mm, orientation
= "Place via TILE/ROUTE/ARRAY verb with these parameters"
ALREADY in M_Product (material — WITH WHAT):
width_mm, height_extent_mm, depth_mm → already in m_product
material_name, material_rgba → product appearance
geometry_hash → vestigial (0 active rows use it)
Row reduction by discipline (1,206 active rows):
| Discipline | Rows | Verb replacement | Stays in c_orderline? |
|---|---|---|---|
| MEP (913) | pipes, fittings, terminals | ROUTE/CONNECT/WIRE verbs → ~15 verb lines | No — verb-generated |
| ARC (226) | doors, windows, walls, plates | Doors/windows = order items (stay). Walls/plates = TILE | ~100 stay, ~126 → verbs |
| FURN (34) | BOM dispatch anchors (host_type=UNIT) | Order-level anchors | Yes — stay as-is |
| STR (32) | slabs, beams, rebar | ARRAY verbs → ~5 verb lines | No — verb-generated |
| PLB (1) | plumbing | ROUTE verb | No — verb-generated |
Net: 1,206 rows → ~134 slim order lines + ~25 verb lines with structured params.
CO_EmptySpaceLine: NOT redundant — promoted.
ESLine = spatial container (WHERE). Verb = production operation (HOW). These are
orthogonal concerns. In iDempiere terms, ESLine ≈ S_Resource (workstation) and
PP_Order_Node (operation on that workstation). Multiple verbs
target the same ESLine (TILE floor + ARRAY rebar + ROUTE sprinklers on one slab).
The S_Resource_ID FK on PP_Order_Node is the primary link from
production to space. See ADHistory.md §S_Resource parallel.
BomCategory: unchanged but better positioned. BomCategory drives template composition (WHAT rooms a building needs), not placement mechanics (HOW to fill them). With verbs, the cascade is cleaner: BomCategory.Sequence → BomTemplateComposer → creates L2 ESLines → each L2 gets PP_Order_Node rows (HOW to fill this room).
RelationalResolver: @Deprecated, replaced by VerbStage.
Currently reads c_orderline's placement columns to compute coordinates. With
verbs, VerbStage reads PP_Order_Node by SeqNo, dispatches to
VerbRegistry, and verb results carry positions directly. The deprecation path
(NORM-3a Phase D→E) aligns with this migration.
Migration phases:
| Phase | What happens | Breaks existing? |
|---|---|---|
| Phase 1 (current) | PP_Order_Node + PP_Order_NodeProduct tables created in output.db. New/generative buildings use verbs. Extracted buildings (SH, DX) keep flat c_orderline. Both paths coexist. | No — additive |
| Phase 2 | VerbStage reads PP_Order_Node for buildings that have them, falls back to RelationalResolver for legacy. Slim c_orderline schema (keep old columns nullable). | No — fallback path |
| Phase 3 | Python extractor writes PP_Order_Node rows instead of flat c_orderline. Delete RelationalResolver. Drop placement columns from c_orderline. | Yes — migration SQL |
{PREFIX}_BOM.db is a pure model dictionary — BOM definitions, M_Product, BomCategory, C_DocType, C_BPartner. ALL transaction data (c_order, c_orderline, PP_Order_Node, PP_Order_NodeProduct, co_empty_space, compiled elements) lives in output.db. output.db is self-contained: orders + production + spatial + compiled result.
DECIDED (2026-03-04): c_orderline becomes WHAT-only. No placement columns. The Java PO/DAO class will have no position setters — the compiler itself is the structural guard. If placement data can't be written, flat-data shortcuts are impossible by construction.
Phase 1 — CURRENT: Create
PP_Order_Node+PP_Order_NodeProducttables in output.db. DDL inBIM_COBOL.md§15.6. Additive, no breakage.Phase 2: VerbStage fallback — PP_Order_Node rows present → VerbRegistry dispatch; absent → RelationalResolver fallback (deprecated path).
Phase 3: Drop placement columns from c_orderline (position_rule, position_value ×3, host_type, host_ref, height_mm, orientation). Remove RelationalResolver. SH/DX extracted data migrated to verb recipes.
RESOLVED: ESLine FK direction —
PP_Order_Node.S_Resource_IDis the primary production→space link. Thec_orderline_idFK on ESLine (NORM-0b) is superseded and will be dropped in Phase 3.
11.10 BomCategory = UPC/EAN material management codes¶
BomCategory codes are fine-grained, like Materials Management UPC/EAN barcodes. They qualify a construct down to its operational details:
- A wall with openings has its own BomCategory distinct from a solid wall
- A door BomCategory captures finer details beyond component_library basics
- Fitting categories can encode: curtains, priority, replacement schedules, attending prerequisites, material specs
This is the path toward a rich MM (Materials Management) layer within the ERP model. Every construct gets a semantic passport that enables downstream operations: procurement, scheduling, maintenance, replacement planning.
11.11 VerbStage: verbs generate real geometry¶
BIM COBOL verbs produce real elements in the build, not read-only reports. ROUTE SPRINKLERS placements become actual sprinkler elements. WIRE LIGHTING produces actual circuit elements. The language is Java-to-native-code — high-level intent compiled to low-level geometry.
The target user is the architect, not the programmer. Architects cannot understand code but can understand Excel-like or SQL-like English declarative statements. BIM COBOL bridges the gap between domain intent and compiled output.
11.12 Single-user standalone (multi-user deferred)¶
Current tooling targets a single standalone user. No collaboration, locking, or merge logic needed. Multi-user is a future concern that can be layered on later without affecting the core compilation model.
11.13 Batch/process compilation (not live interactive)¶
Compilation is batch/process-based, like COBOL compilation or ERP document processing. The workflow cycle:
- User makes changes (edit Orders, add components, modify BOM rules)
- User saves / triggers compilation
- Pipeline processes all stages end-to-end
- Results refresh in output.db (and Bonsai GUI reloads)
No live incremental recompile per keystroke. The run_tests.sh gate is the
current batch trigger. Bonsai GUI will have a "Process" button (iDempiere
DocAction pattern: Draft → Process → Complete).
11.14 Rosetta Stone gap investigation — 67-element decomposition (2026-03-02)¶
Comparing ifc4_samplehouse.db (SH, EXTRACTED, 56 elements) against
st_sh.db (ST_SH, GENERATIVE, 123 elements). Gap = 67 elements.
Full class distribution:
| ifc_class | SH | ST_SH | Delta | Category |
|---|---|---|---|---|
| IfcFurniture | 15 | 15 | 0 | Shell — count match, dims differ |
| IfcMember | 20 | 22 | +2 | Shell — semantics differ (SH=curtain wall mullions, ST_SH=steel frame) |
| IfcPlate | 6 | 7 | +1 | Shell — semantics differ (SH=glazed panels, ST_SH=wall cladding) |
| IfcWall | 5 | 0 | -5 | Decomposed → Plate+Column+Beam |
| IfcWindow | 4 | 4 | 0 | Shell — match |
| IfcDoor | 3 | 5 | +2 | Shell — extra doors (narrow 199mm — data issue?) |
| IfcSlab | 2 | 4 | +2 | Shell — SH=foundation+roof floor, ST_SH=foundation+3 finish floors |
| IfcRoof | 1 | 1 | 0 | Shell — match |
| IfcBeam | — | 9 | +9 | Structural: lintels above openings |
| IfcColumn | — | 7 | +7 | Structural: 4 CORNER + 3 T_JUNCTION |
| IfcPipeSegment | — | 9 | +9 | MEP: fire protection pipe runs |
| IfcPipeFitting | — | 6 | +6 | MEP: fire protection fittings |
| IfcAirTerminal | — | 6 | +6 | MEP: HVAC supply/return |
| IfcOutlet | — | 5 | +5 | MEP: electrical outlets |
| IfcSwitchingDevice | — | 3 | +3 | MEP: light switches |
| IfcLightFixture | — | 3 | +3 | MEP: lights |
| IfcFireSuppressionTerminal | — | 3 | +3 | MEP: sprinklers |
| IfcFan | — | 3 | +3 | MEP: exhaust fans |
| IfcAlarm | — | 3 | +3 | MEP: smoke detectors |
| IfcFlowTerminal | — | 2 | +2 | Misclassified: "tall_cabinet" = furniture |
| IfcCovering | — | 3 | +3 | Finishes: ceiling tiles |
| IfcSpace | — | 3 | +3 | Spatial: room volumes |
| Total | 56 | 123 | +67 |
Gap breakdown by category:
-
MEP systems (43 elements): Fire protection(18) + HVAC(9) + Electrical(14) + FlowTerminal(2). SH IFC has no MEP. ST_SH generates them via placeMEPSprinklers/placeHVAC/placeElectrical. These are legitimate compilation additions.
-
Structural framing (16 elements): IfcBeam(9) lintels + IfcColumn(7) posts. SH IFC uses monolithic IfcWall. ST_SH decomposes walls into structural members. The 5 IfcWall → 7 IfcPlate(cladding) + 7 IfcColumn + 9 IfcBeam + 3 IfcCovering(ceiling).
-
Spatial + finishes (6 elements): IfcSpace(3) room volumes + IfcCovering(3) ceiling tiles. SH IFC has no explicit space or covering elements.
-
Shared class deltas (net +2): IfcMember(+2) + IfcPlate(+1) + IfcDoor(+2) + IfcSlab(+2) - IfcWall(-5) = +2. Critical finding: even "matching" classes have different semantics — SH IfcMember = curtain wall mullions, ST_SH IfcMember = steel framing. SH IfcPlate = glazed panels, ST_SH IfcPlate = wall cladding.
-
Data issues found:
- 2 doors at 199mm width — likely incorrect BOM data (normal door ~800mm+)
- 2 IfcFlowTerminal "tall_cabinet" — furniture misclassified as MEP
Implications for Rosetta Stone filter: The gap is NOT just phantom/stub elements. It reflects a fundamentally different decomposition model. The SH IFC was modelled by an architect (monolithic walls, curtain wall members, glazed plates). ST_SH is compiled from BOM rules (structural framing, wall cladding, explicit MEP, room spaces).
A class+count digest filter alone will not achieve equality. The Rosetta Stone proof requires one of: - (A) Align the BOM model to produce the EXACT same elements as SH (same classes, same dims, same count). This means the BOM for SH must map 1:1 to the IFC extraction — walls stay as IfcWall, not decomposed. Curtain wall stays as IfcMember+IfcPlate(glazed), not steel framing. - (B) Define equivalence at a higher semantic level: same rooms, same furniture types, same AABB, same functional purpose — even if the structural decomposition differs. This requires a "semantic digest" that abstracts away structural representation choices.
Furniture detail (15 vs 15 — count matches, dimensions don't):
SH furniture comes from the IFC (architect-chosen items). ST_SH furniture comes from
component_library.db (closest matching BOM items). Example: SH has bed_2032x1980x500mm,
ST_SH has bed_1980x2032x634mm — same type, different dims (swapped width/depth, different height).
This delta exists because the compiler picks from the library catalog, not from the IFC.
199mm door resolution (Q5): The 199mm is the door depth (wall thickness), not width.
RTREE BBox confirms actual width=1860mm. The element_name encodes depth×height instead of
width×height — a naming convention bug, not a geometry error. Component_library has the
exact SH door BBoxes: c5357415(178×880×2145mm), 33e1931b(178×880×2145mm),
f0181ba2(1860×199×2110mm). However, ST_SH generates 5 doors where SH has 3 — the
extra 2 (D7, D2) are BOM modeling errors to be fixed.
11.15 Rosetta Stone: Element Identity + BBox envelope (Round 4, 2026-03-02)¶
Element Identity is required — not semantic equivalence. The digest must catch compilation drift deterministically. "Otherwise we have no reference of non-drift in compilation — the big pain."
However, structural decomposition (walls → column+beam+plate) is accepted IF it maintains visual exactness and IFC integrity for downstream 4D→7D. The proof changes from element-count hash to BBox vertex hash: the decomposed wall assembly's combined BBox must equal the monolithic wall's BBox.
Two-layer digest: 1. Furniture/openings (BUY leaves): exact element identity — same product from component_library, same dimensions, same count 2. Structure (MAKE assemblies): BBox envelope equivalence — decomposed walls must occupy the same spatial envelope as monolithic walls
This means the SpatialDigest formula must evolve: instead of COUNT per ifc_class +
total AABB, it becomes product_id hash per BUY leaf + BBox vertex hash per
structural zone. MEP elements are excluded from the digest entirely.
11.16 Furniture: exact match required, drift is catastrophic¶
Component_library must contain the exact IFC furniture items for Rosetta Stone buildings. Drift at the reference stage is "disastrous downstream." The compiler must pick the same product_id that the IFC extraction recorded — not the closest match.
For non-Rosetta buildings (TB-LKTN, future projects), narrower drift is tolerable during compilation since there is no reference to compare against. But the BOM rules and library must still be deterministic.
Confirmed: SH doors exist in component_library with exact BBox dims:
IfcDoor_Ifc4_SampleHouse_c5357415 (178×880×2145mm),
IfcDoor_Ifc4_SampleHouse_f0181ba2 (1860×199×2110mm). The extraction pipeline
correctly populated these.
11.17 MEP excluded from Rosetta Stone digest¶
The digest/hash-total is for visual confirmation only. MEP elements (pipes, fittings, terminals, alarms, fans, lights, outlets, switches) are excluded because: - The SH reference IFC has zero MEP elements - MEP is generated by the compiler (placeMEPSprinklers, placeHVAC, placeElectrical) - The goal is "maths to nail it" — not manual Bonsai inspection cycles
MEP correctness is verified separately by BIM COBOL verbs (CHECK PLACEMENT, VERIFY PLACEMENT) and ProveStage quality gates.
11.18 C_Order + C_OrderLine → output.db (DONE 2026-03-04)¶
Both the C_Order header and C_OrderLine detail are now in output.db only. c_order and c_orderline dropped from {PREFIX}_BOM.db (2026-03-04). {PREFIX}_BOM.db is a pure model dictionary — BOM definitions, M_Product catalog, BomCategory taxonomy, C_DocType config, placement rules, and attribute specs. No transactional data.
This completes the 4-DB separation: - {PREFIX}_BOM.db = model dictionary (what CAN be built — rules, recipes, catalog) - component_library.db = geometry (what things LOOK LIKE — LOD meshes, shapes) - output.db = compiled result (what WAS built — orders, placements, spaces, elements)
11.19 Structural decomposition enriches BIM value¶
The STR discipline (wall → column+beam+plate decomposition) is a feature, not a bug. It enriches the BIM model with structural engineering detail that monolithic IfcWall cannot express. The architectural IFC (SH) has no structural framing; the compiled BIM (ST_SH) adds it.
The BBox vertex hash proves visual equivalence without element-count matching. If the combined BBox of columns+beams+plates equals the original IfcWall BBox, the building looks the same — and the structural detail is available for downstream 4D→7D analysis.
"By such check, you may reiterate right away what broke the placement."
11.20 Digest verification: LOD-to-LOD cross-sampling (Round 5, 2026-03-02)¶
The digest is not exhaustive element-by-element comparison. It is strategic cross-sampling that catches the known failure modes:
- Orientation/rotation drift — piano facing wrong wall, furniture rotated
- Spatial distance drift — furniture too close / too far from neighbours
- Missing LOD — element has BBox but no real geometry (stub/placeholder)
- Missing material — element has geometry but no surface style
- Invented drawing — element not in component_library, compiler fabricated it
LOD-to-LOD check: compare the extracted DB's actual geometry (LOD mesh, material, BBox) against the compiled DB's geometry for each matched element. If extracted has real LOD and compiled has only BBox, that's a failure. If both have LOD but positions differ beyond ε, that's a failure.
The sampling approach uses BOM lineage (m_bom_line parent→children) for grouping, not spatial proximity. Each BOM assembly's children form one verification group.
11.21 Furniture placement: parent AABB fit = wholesale port¶
Critical algorithm for furniture set placement:
When a parent room's AABB exactly fits a BOM template (M_BomCategory AABB), the compiler takes ALL furniture sets wholesale — one ESLine for the entire room. This is the wholesale port optimisation. No individual furniture placement calculation needed.
- SH living room: AABB exact match → BOM explosion takes dining set, sofa set, piano set with their buffer fillers as one BOM tree. One C_OrderLine, BOMWalker recurses.
- This is not hardcoded — it's a feature: parent AABB fit triggers wholesale, non-fit triggers individual placement.
When the parent room AABB does NOT fit (TB-LKTN case), the compiler spawns multiple C_OrderLines with priority-based ESLine placement:
- Dining set (highest priority) — ESLine calculates best central spot
- Sofa set (next priority) — ESLine finds remaining space that fits sofa BBox
- Piano / other (lower priority) — ESLine calculates remaining spot, checks no BBox clash with already-placed sets
- If a set cannot fit → skip (or INVENTION STOP if configured)
Each set gets its own C_OrderLine + ESLine pair. The ESLine provides the calculated remaining space after each placement. Buffer fillers are added between sets.
BOM commit option: If the TB-LKTN living room arrangement is satisfactory, it can be committed as a NEW BOM to {PREFIX}_BOM.db — creating a reusable template for future rooms of similar size. Fillers are included in the committed BOM.
11.22 Extra doors and inventions: remove for Rosetta Stone¶
ST_SH produces 5 doors where SH has 3. The extra D7 and D2 are "unknown inventions" from the BOM model that don't correspond to the reference IFC. Remove them. The Rosetta Stone BOM must produce only elements that exist in the reference.
General rule: for Rosetta Stone buildings, the compiler must NOT invent elements beyond what the reference IFC contains (excluding MEP, which is separately verified).
11.23 output.db C_Order schema: ERP transactional with tracking¶
The output.db C_Order/C_OrderLine schema retains ERP transactional information for downstream tracking:
- Date — when the order was compiled/processed
- Description — human-readable compilation context
- Source — USER (from Bonsai GUI) vs COMPILED (compiler-generated)
- compilation_id — links to the compilation run that produced the line
This is not a minimal "compiled order" schema — it's a proper ERP document that supports audit trail, version history, and reprocessing.
11.24 IfcFlowTerminal misclassification: m_bom_line data error¶
Root cause traced: KITCHEN_CABINET_SET and KITCHEN_CABINET_SET_DX_A/B have
child_product_id='IfcFlowTerminal' in m_bom_line. This references the M_Product
stub IfcFlowTerminal (product_type=STRUCTURAL, dims=0.001×0.001×0.001) instead of
Tall_Cabinet (product_type=FURNITURE, dims=0.6×0.6×2.0).
The component_library correctly classifies Tall_Cabinet as IfcFurniture/FURNITURE.
The bug is in m_bom_line.child_product_id — it should reference Tall_Cabinet,
not the IfcFlowTerminal structural stub.
Fix: Migration to update m_bom_line rows where bom_id IN ('KITCHEN_CABINET_SET', 'KITCHEN_CABINET_SET_DX_A', 'KITCHEN_CABINET_SET_DX_B') AND child_product_id='IfcFlowTerminal' → SET child_product_id='Tall_Cabinet'.
Also: TOILET_BLOCK_FIXTURES has child_product_id='IfcFlowTerminal' — reclassify
to IfcSanitaryTerminal (basin, toilet, bidet) per Round 6 Q4.
11.25 AABB matching: exact, no tolerance (Round 6, 2026-03-02)¶
Room AABB vs BomCategory AABB comparison is exact to the millimetre. No ε tolerance. This ensures: - Singularity/explosion logic is deterministic - Rosetta Stone replication is precise - Engineering precision is the hallmark — not approximation
If a real room is 4695mm and the template is 4690mm, that is NOT a match. The BOM template must be updated to match the actual room, or the cascade selects a different BOM that fits.
11.26 Priority ordering: M_BomCategoryLine.Sequence (user-defined)¶
Furniture set priority (dining→sofa→piano) is encoded in M_BomCategoryLine.Sequence — user-defined defaults that the user can override. The Sequence column already exists; it controls BOM explosion order within each level.
Priority is per-room-type (via BomCategory). Different room types can have different ordering: a bedroom prioritises bed→wardrobe→desk; a kitchen prioritises counter→cabinet→appliance.
11.27 BOM commit: auto-commit to {PREFIX}_BOM.db¶
When a non-reference room arrangement (TB-LKTN) produces a stable result, it auto-commits as a new BOM to {PREFIX}_BOM.db (same database). This creates a reusable template for future rooms of similar size. Explicit user-triggered "Save as BOM" via Bonsai GUI is deferred — for now, the compiler auto-commits if the arrangement is valid (no INVENTION STOP, no clash violations).
11.28 Furniture matching: AABB + Category DocType¶
Product matching uses exact AABB + inferred BomCategory. Not product name, not building source prefix. The BomCategory carries a DocType (or equivalent classifier) that scopes the search:
- Residential — SH, DX, TB-LKTN BOMs
- Commercial — Terminal BOMs (future)
- Industrial — (future)
This prevents cross-category collisions: a vehicle AABB (same dimensions as a room) won't match residential furniture because the DocType filters it out.
Schema addition needed: M_BomCategory.doc_type TEXT (or similar) to hold the
building-type classifier. Current BomCategory codes (LI_SH, LI_DX, BD_SH, etc.)
already embed the building owner (SH/DX) — the DocType generalises this to a
category level (Residential/Commercial).
At singularity stage (SH/DX), AABB + Category is sufficient to select the exact product. As the catalog grows, additional DSL constraints from C_OrderLine further narrow the match (§11.1 selection cascade).
11.29 C_Order: C_DocType in {PREFIX}_BOM.db, C_Order in output.db (Round 7, 2026-03-02; updated 2026-03-04)¶
C_DocType ({PREFIX}_BOM.db) holds the building type definition — domain config that the compiler reads to know what to compile. Each compilation run creates a fresh output.db with C_Order (from C_DocType) + C_OrderLine + elements + CO_EmptySpace, then writes compile-time results (spatial_digest, expected_elements, empty_space_checksum) into the output.db C_Order.
{PREFIX}_BOM.db is never written to during compilation. It is a pure dictionary.
IMPLEMENTED (2026-03-02→03-04): BuildingWriter.initSchema() creates c_order +
c_orderline tables in output.db. CompilationPipeline creates C_Order from
C_DocType config at compile time. DigestStage writes computed spatial_digest,
expected_elements, empty_space_checksum into output.db c_order and promotes
doc_status IP → CO. c_order and c_orderline dropped from {PREFIX}_BOM.db (2026-03-04).
11.30 No invention: every element must trace to component_library¶
First principle. Every element the compiler produces MUST trace back to an LOD entry in component_library.db that was extracted from the reference IFC. If the structural pipeline generates doors, columns, beams, or plates that don't exist in the extraction, that is cheating — the extraction-to-compilation chain is broken.
All drift blocks (extra doors, wrong dimensions, invented elements) must be traced back to their source and locked down. The compiler is a faithful reproducer, not an inventor.
11.31 AABB matching: 3D exact, mismatches are data errors¶
AABB comparison is 3D exact. If SH bed = 2032×1980×500mm and ST_SH bed = 1980×2032×634mm, the height difference (500 vs 634) is a data error, not an ambiguity to resolve with tolerance. The flow is:
IFC → component_library.db (extraction) → {PREFIX}_BOM.db (exact AABB copy) → output.db
If any step introduces drift, that step is broken. Investigate and fix.
11.32 Digest: mathematical output=input proof (implementation proposed)¶
Requirement: Mathematical proof that compiled output equals extracted input. No visual checks. No approximation. "100% it is GIGO. output = input. Without cheating."
Proposed algorithm: Compare elements_meta + RTREE positions directly between extracted DB and compiled DB. For each element class present in both: 1. Sort elements by (ifc_class, storey, minX, minY, minZ) 2. Hash the sorted BBox vertex tuples (minX, maxX, minY, maxY, minZ, maxZ) 3. Any mismatch = compilation error, report which element drifted
Structural decomposition (IfcWall → column+beam+plate): hash the union BBox of the decomposed group and compare against the monolithic element's BBox. Grouping on the extracted side: by ifc_class + spatial overlap (elements whose BBoxes intersect or abut within 1mm form one structural group).
MEP excluded. Furniture matched by sorted BBox vertices (orientation-independent).
11.33 Wholesale port: trust BOM, digest checks elements_meta¶
BOM explosion applies the same spatial transformation to all children — like transporting a prefab room to the right spot facing correctly. Individual furniture positions are computed from BOM-relative offsets (m_bom_line dx/dy/dz) and written to elements_meta/RTREE.
Trust is total: if the BOM layout is wrong, it's a BOM data error (GIGO). The digest verifies by checking output.db element positions directly (elements_meta + RTREE), not ESLines. ESLines are intermediate placement holders, not verification targets.
11.34 expected_elements: auto-calculated for GENERATIVE¶
- EXTRACTED buildings (SH, DX, Terminal): expected_elements is truth from IFC. Fixed. The compiler must match it exactly.
- GENERATIVE buildings (ST_SH, TB-LKTN): the compiler determines the count. expected_elements is auto-calculated after each successful compilation and written to output.db. Data fixes that change element count don't need manual gate updates.
11.35 VerbStage: gradual takeover (COBOL replacing assembler)¶
Following the COBOL-over-assembler evolution pattern:
- Current state: Hardcoded Java methods (assembler) generate MEP elements. VerbStage parses .bimcobol but doesn't execute.
- Next step: VerbStage executes verbs that produce PlacedElements. Each verb replaces one hardcoded method. Verb runs before Write (option a) so all elements — both Java-generated and verb-generated — flow through the same Write → Prove → Digest pipeline.
- End state: All element generation is verb-driven. The hardcoded Java methods are deleted. The pipeline becomes: Compile → Verb → Write → Prove → Digest.
The SPI interface in DAGCompiler defines VerbExecutor.execute(). BIM_COBOL provides
the runtime implementation. DAGCompiler depends on the interface, not BIM_COBOL.
Circular dependency broken.
11.36 C_DocType: document classification (borrowed from iDempiere)¶
IMPLEMENTED 2026-03-03; extended 2026-03-04 with domain config columns. Table created, PO classes (X_C_DocType, MCDocType) live.
iDempiere's C_DocType classifies documents by DocBaseType (3-char category) + DocSubType (variant). We adopt this for construction orders. Domain config columns absorbed from c_order when it was dropped from {PREFIX}_BOM.db:
C_DocType ({PREFIX}_BOM.db — constant domain config)
├── C_DocType_ID TEXT PK -- 'RE_SH', 'RE_DX', 'CO_TE'
├── Name TEXT NOT NULL -- 'Sample House', 'Duplex'
├── DocBaseType TEXT NOT NULL -- RE (Residential), CO (Commercial), IN (Industrial)
├── DocSubType TEXT -- SH, DX, TB, TE, ST (NULL = generic)
├── DSLContent TEXT -- DSL template content (absorbed from c_order)
├── OutputDbPath TEXT -- output DB path
├── ReferenceDbPath TEXT -- reference DB path
├── aabb_width_mm REAL -- reference AABB (absorbed from c_order)
├── aabb_depth_mm REAL
├── aabb_height_mm REAL
├── ExpectedElements INTEGER -- fixed for EXTRACTED, auto for GENERATIVE
├── SeqNo INTEGER -- compilation sequence
├── ProjectName TEXT -- project/building name
├── IsDefault INTEGER -- default for this DocBaseType
├── IsActive INTEGER
└── Description TEXT
Why this matters:
| Old model | Problem | New model |
|---|---|---|
| c_order.building_type = 'RESIDENTIAL' | Separate column, string matching | C_DocType.DocBaseType = 'RE' |
| c_order.c_bpartner = 'SH' | SH/DX are pattern types, not business partners | C_DocType.DocSubType = 'SH' |
| m_bom.c_bpartner = 'SH' | Same misnaming | m_bom.doc_sub_type = 'SH' |
| C_BPartner table = | These aren't business partners | C_DocType table replaces this role |
| Real vendor/customer | No column exists | c_order.C_BPartner_ID (future: real business partner) |
DocBaseType drives template selection: - RE → M_BomCategory WHERE doc_type='RE' → RE template (SL→GF→RF) - CO → M_BomCategory WHERE doc_type='CO' → Commercial template (future)
DocSubType drives BOM scoping (replaces c_bpartner on m_bom): - SH → owner-specific BOMs (SH_BED_SET, SH_LIVING_SET) preferred over generic - NULL on m_bom = generic BOM, usable by any DocSubType
IsDefault enables smart defaults: - ST_SH / ST_DX are the template-path entries (DocSubType='ST', DocBaseType='RE') - When no specific building variant exists, ST + AABB selects from M_BomCategory catalog
Selection cascade (unchanged logic, cleaner naming): 1. AABB fit (primary) — SpaceSize must fit within slot's allocated AABB 2. Largest volume (secondary) — maximize space usage 3. seq_no (tiebreaker) — lower = preferred; owner-specific (10) beats generic (20)
Seed data:
| C_DocType_ID | Name | DocBaseType | DocSubType | IsDefault |
|---|---|---|---|---|
| RE_SH | Sample House | RE | SH | 0 |
| RE_DX | Duplex | RE | DX | 0 |
| RE_TB | Terrace Block | RE | TB | 0 |
| CO_TE | Airport Terminal | CO | TE | 0 |
| ST_SH | Standard Sample House | ST | SH | 0 |
| ST_DX | Standard Duplex | ST | DX | 0 |
11.37 Migration: c_bpartner → C_DocType + doc_sub_type — COMPLETE¶
Status: M1–M2 DONE (2026-03-03), Phase 3–4 DONE (2026-03-04).
Phase M1 (2026-03-03): m_bom.c_bpartner → doc_sub_type (column renamed in DB + all Java PO/queries). c_order.C_DocType_ID FK added + backfilled (RE_SH, RE_DX, RE_TB, CO_TE). ST_SH/ST_DX added later (Prime Rule alignment). WriteStage copies C_DocType_ID to output.db; BuildingWriter DDL updated. Witnesses: W-OWNER-1/2 use doc_sub_type/C_DocType_ID, W-DOCTYPE-2 new.
Phase M2 (2026-03-03): c_order.c_bpartner kept (future: repurpose for real vendor/customer).
Phase 3 — C_Order model cleanup (2026-03-04):
1. c_order column renames to iDempiere CamelCase. building_type DROPPED (redundant with DocBaseType).
2. c_orderline three-concern separation — 14 placement+material columns DROPPED (§11.9). WHAT-only.
3. c_order → C_DocType merge — domain config columns absorbed into C_DocType. c_order DROPPED from {PREFIX}_BOM.db.
4. c_orderline DROPPED from {PREFIX}_BOM.db — 1330 rows redundant with M_BOM + M_Product + component_library.
5. Pipeline updated: BuildingRegistry reads C_DocType. CompilationPipeline creates C_Order in output.db.
6. .cbpartner() → .docSubType() throughout Java code.
Phase 4 — Witness tests: W-OWNER-1/2, W-DOCTYPE-2 use doc_sub_type/C_DocType_ID.
Phase 5 — Documentation: ConstructionAsERP.md updated (this document). Other docs pending.
Phase 6 — C_BPartner table disposition: - Table retained for future real business partners (vendor/customer) - c_order.C_BPartner_ID future: real business partner FK
11.38 Product Catalog Normalisation — M_AttributeSet + M_Product dedup (P0.1-DEDUP, 2026-03-05)¶
Problem: The extraction pipeline dumped every IFC element as a separate row in
I_Element_Extraction (component_library.db, renamed from ad_element_placement 2026-03-07). 6 smoke detectors = 6 rows with baked-in XYZ.
This conflates product identity (WHAT) with instance placement (WHERE). A product catalog
should have 1 "Smoke Detector" product placed 6 times, not 6 independent items.
iDempiere MM resonance: The entire BIM compiler's 4-DB split mirrors iDempiere's Material Management module. In MM, the product master lives in the dictionary (M_Product, M_BOM, M_AttributeSet), the product images live on the file server, and transactions (Sales Orders, Manufacturing Orders, Inventory Moves) are generated per-run. Our mapping:
iDempiere MM BIM Compiler
─────────────────────────────────────────────────────────────────
M_Product (master data) → {PREFIX}_BOM.db: M_Product (187 rows)
M_BOM + M_BOM_Line (assembly) → {PREFIX}_BOM.db: m_bom + m_bom_line
M_AttributeSet (attribute defs) → {PREFIX}_BOM.db: M_AttributeSet (5 rows)
M_AttributeSetInstance (per-item) → (future P0.1-BOM: pipe length, wall height)
M_Product_Category → {PREFIX}_BOM.db: M_BomCategory (LI, BD, KT, FR...)
Product Image (file server) → component_library.db (meshes, materials, placement rules)
C_Order + C_OrderLine (SO) → output.db: C_Order + C_OrderLine (compile-time)
PP_Order + PP_Order_Node (MO) → output.db: PP_Order_Node (verb invocations)
M_InOut + M_InOutLine (receipt) → (future: as-built verification)
The key insight: component_library.db is the product image set, not the product master.
It holds geometry BLOBs, materials, and placement rules — the visual representation.
The product identity (what it is, how it assembles, what attributes it has) lives in
{PREFIX}_BOM.db alongside the BOM recipes. This is why M_Product lives in {PREFIX}_BOM.db and not in
component_library.db — same reason iDempiere stores M_Product in the dictionary, not
on the image server. The component_id FK on M_Product and the M_Product_ID FK on
I_Element_Extraction (renamed from ad_element_placement) are the bridge between identity and image.
In iDempiere's MM module, M_Product defines the abstract product type
(what you sell), and M_AttributeSetInstance tracks each physical item (serial number, lot,
expiry). M_AttributeSet defines which attributes vary per instance vs. which are fixed per
product. This three-table pattern separates identity from instantiation:
M_AttributeSet defines attribute templates
│
├── M_Product abstract product type (one per unique item)
│ │
│ └── M_AttributeSetInstance each physical item (future: P0.1-BOM)
│
└── M_Attribute + M_AttributeUse + M_AttributeValue (future: P0.1-BOM)
BIM mapping:
| iDempiere Concept | BIM Equivalent | Example |
|---|---|---|
| M_AttributeSet | Attribute template | BIM_Pipe (IsInstanceAttribute=1: length varies per instance) |
| M_Product | Product type | PIPE_COLD_WATER_25MM (cross-section stamp: 25×25×25mm) |
| M_AttributeSetInstance | Physical instance | One specific 3.2m cold water pipe at (2.5, -8.1, 1.2) |
| IsInstanceAttribute=1 | Instance varies | Pipes, walls, slabs — length/height/area differ per placement |
| IsInstanceAttribute=0 | Product identical | Smoke detectors, receptacles, furniture — every instance is the same |
Five attribute sets:
| M_AttributeSet_ID | IsInstanceAttribute | Dimension convention | Example |
|---|---|---|---|
BIM_Pipe |
1 | w=d=h=diameter (cross-section stamp) | PIPE_WASTE_48MM: 48×48×48mm. Length = instance attribute. |
BIM_Conduit |
1 | w=d=h=diameter | CONDUIT_EMT_30MM: 30×30×30mm. Length varies. |
BIM_Wall |
1 | w=thickness, d=1.0, h=1.0 | WALL_EXT_BRICK_BLOCK: w=417mm. Length/height = instance. |
BIM_Slab |
1 | w=thickness, d=1.0, h=1.0 | SLAB_GRADE_127: w=127mm. Area = instance attribute. |
BIM_Component |
0 | canonical sorted (small, mid, large) | SMOKE_DETECTOR: 102×140×140mm. All identical. |
Dedup result (DX): 1099 instance rows → 79 unique M_Product entries (14:1 ratio). 65 new M_Product rows inserted, 14 existing rows updated with Name/Description/M_AttributeSet_ID. Total M_Product: 187 rows.
Column additions to M_Product:
| Column | Type | Purpose |
|---|---|---|
M_AttributeSet_ID |
TEXT FK | Links to M_AttributeSet — determines instance-vs-product behavior |
Name |
TEXT | Plain English name for Bonsai GUI display ("Cold Water Pipe 25mm") |
Description |
TEXT | Original Revit element_ref string for traceability |
Column addition to I_Element_Extraction (component_library.db, was ad_element_placement):
| Column | Type | Purpose |
|---|---|---|
M_Product_ID |
TEXT FK | Links each placement instance to its M_Product in {PREFIX}_BOM.db |
Cross-DB FK: I_Element_Extraction.M_Product_ID (component_library.db) → M_Product.product_id
({PREFIX}_BOM.db). Verified: 0 orphans, 0 NULLs for active DX rows.
Naming convention:
- product_id: PIPE_COLD_WATER_25MM, WALL_EXT_BRICK_BLOCK, SMOKE_DETECTOR (global, no building prefix)
- Name: "Cold Water Pipe 25mm", "Exterior Brick on Block", "Smoke Detector"
- Description: Original Revit family string (e.g., M_Smoke Detector:Smoke Detector:Smoke Detector)
- extracted_from: Ifc2x3_Duplex for DX-sourced products
What this does NOT change (boundaries):
- No Java PO changes — X_MProduct.java untouched. New columns unused by pipeline until P0.1-BOM.
- No pipeline changes — PlacementLoader, StoreyCompiler, BOMWalker unchanged.
- No output.db changes — M_AttributeSetInstance tables come in P0.1-BOM.
- No m_bom_line creation — BOM assembly recipes come in P0.1-BOM.
- ~~No ad_element_placement rename~~ — DONE (P0.2: renamed to I_Element_Extraction).
Next steps (P0.1 continued):
- ~~P0.1-ORIENT: Intrinsic orientation per M_Product from component_definitions.~~ — DONE (component_definitions.up_axis/forward_axis/attachment_face/orientation + placement_rules.orientation + lod_element_placement.orientation)
- P0.1-BOM: M_AttributeSetInstance for per-instance attributes (pipe length, wall height).
M_Attribute + M_AttributeUse + M_AttributeValue seeded. m_bom_line entries reproduce all instances.
- P0.1-RENAME: ~~ad_element_placement → archive~~ — DONE (P0.2: I_Element_Extraction). New data flows through M_Product + m_bom_line.
- P0.1-VERIFY: SpatialDigest(BOM walk) == SpatialDigest(PlacementLoader) for SH and DX.
Migration: migration/migration_P01_product_catalog.sql ({PREFIX}_BOM.db),
migration/migration_P01_placement_product_link.sql (component_library.db).
Appendix A — BOM Dimension Model (merged from BIMasBOMConcept.md)¶
Category + Owner + SpaceSize: three orthogonal dimensions on M_BOM
A.1 ERD — Flattened for BIM¶
In iDempiere, M_Product sits between M_BOM and everything else. In a BIM compiler, M_Product is flattened into M_BOM. A leaf item is an M_BOM with no M_BOM_Line children.
M_BomCategory ──────┐
▼
C_DocType ────► M_BOM ──► M_BOM_Line ──► M_BOM (child, recursive)
(WHO=DocSubType) (= M_Product + M_BOM merged)
▲
BIM ─────────────────┘ ──► BIMLine ──────► PP_Order_Node ──► PP_Order_NodeProduct
(= C_Order) │ (WHAT) (HOW) (params)
│ │
└──► CO_EmptySpaceLine ◄──┘
(WHERE = S_Resource, spatial workstation)
Three-concern separation: C_OrderLine=WHAT, PP_Order_Node=HOW, CO_EmptySpaceLine=WHERE, M_Product=WITH WHAT dimensions.
A.2 M_BomCategory Lookup Table¶
| Code | Name | BOM Level | Example |
|---|---|---|---|
LI |
Living | ROOM | Piano + Sofa arrangement + buffers |
BD |
Bedroom | ROOM | Bed + SideTables + Wardrobe + buffers |
KT |
Kitchen | ROOM | Cabinets + Counter + Sink |
BT |
Bathroom | ROOM | Toilet + Basin + Shower |
DN |
Dining | ROOM | Table + Chairs |
FR |
Furniture | SET/ITEM | Individual piece at ~4th BOM layer |
L1 |
Level 1 | FLOOR | Ground floor assembly |
L2 |
Level 2 | FLOOR | Upper floor assembly |
ST |
Space | any | Buffer/empty space (variable AABB) |
UN |
Unit | UNIT | Complete building unit |
CREATE TABLE M_BomCategory (
M_BomCategory_ID TEXT PRIMARY KEY,
Name TEXT NOT NULL,
Description TEXT,
IsActive INTEGER DEFAULT 1
);
INSERT INTO M_BomCategory VALUES ('LI', 'Living', 'Living room settings', 1);
INSERT INTO M_BomCategory VALUES ('BD', 'Bedroom', 'Bedroom settings', 1);
INSERT INTO M_BomCategory VALUES ('KT', 'Kitchen', 'Kitchen settings', 1);
INSERT INTO M_BomCategory VALUES ('BT', 'Bathroom', 'Bathroom/toilet settings', 1);
INSERT INTO M_BomCategory VALUES ('DN', 'Dining', 'Dining settings', 1);
INSERT INTO M_BomCategory VALUES ('FR', 'Furniture', 'Leaf furniture items (~4th BOM layer)', 1);
INSERT INTO M_BomCategory VALUES ('ST', 'Space', 'Buffer/empty space (variable AABB)', 1);
INSERT INTO M_BomCategory VALUES ('L1', 'Level 1', 'Ground floor assembly', 1);
INSERT INTO M_BomCategory VALUES ('L2', 'Level 2', 'Upper floor assembly', 1);
INSERT INTO M_BomCategory VALUES ('UN', 'Unit', 'Complete building unit', 1);
A.3 Axis Model Invariant¶
| Axis | Aggregation | Meaning |
|---|---|---|
Width (space_width_mm) |
SUM | Items + fillers tile the parent |
Depth (space_depth_mm) |
MAX | Deepest child defines the envelope |
Height (space_height_mm) |
MAX | Tallest child defines the envelope |
Width: parent.space_width_mm == SUM(child.space_width_mm) -- must equal
Depth: MAX(child.space_depth_mm) <= parent.space_depth_mm -- must fit
Height: MAX(child.space_height_mm) <= parent.space_height_mm -- must fit
Verified by W-SPACESIZE-1 witness gate at every BOM level.
Filler distribution: filler.space_width_mm = (parent - SUM(fixed)) / N_fillers. Fillers have depth/height = 0.
Cross-references¶
- METADATA_DRIVEN_ARCHITECTURE.md — domain architecture, phase roadmap, abstract compilation engine vision
- Appendix A (above) — the three-dimension model (Category + Owner + SpaceSize), merged from archived BIMasBOMConcept.md
- PREFAB_ARCHITECTURE.md — 6-level assembly hierarchy + MRP BOM Drop chain
- RELATIONAL_PLACEMENT_SPEC.md — C_OrderLine placement rules
- TerminalAnalysis.md — Terminal forensics + §ERP Model Architecture (discipline hierarchy, verb-to-AttributeSet, Val_Rule, ROUTE as BOM tree)
- ProjectOrderBlueprint.md — Exception-based ordering (§1), C_Project site-as-BOM (§2), abstract category tree (§3), BOM mining via Approve (§4), nD as queries (§5), order inheritance (§6), rule-pack compliance library (§12)