Skip to content

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_order and c_orderline have 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 next is the next item's before. 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 by PP_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:

  1. Use the AABB as the construction envelope
  2. At each BOM level, select the best-fitting BOM by BOMCategory + SpaceSize ≤ available AABB
  3. Write a co_empty_space_line at EVERY level (= Reprocess Mode as primary mode)
  4. Each line's before/next coordinates 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 findBestFitAnyOwner into CompilationPipeline.java for 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_level ordering rather than explicit next_line_id FK.
  • 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 at BOMTierResolver.java goes 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 to expandBOMNode, 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.javapopulateCoEmptySpace() 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_ownerc_bpartnerdoc_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):

  1. Room context carries BomCategory_ID='LI' (set by user in Bonsai)
  2. Compiler queries BOMCategory for m_bom.doc_sub_type matching C_DocType.DocSubType
  3. If exactly one BOM found → take it in toto, ONE ESLine, done (SH/DX case)
  4. If no match → continue to AABB-based walk (ST case)
  5. (ST/walk only) Compiler reads room AABB from ad_room_boundary (or R*Tree)
  6. (ST/walk only) Looks up M_BomCategory WHERE type='LI' AND aabb ≈ room AABB
  7. (ST/walk only) Reads M_BomCategoryLine slots in Sequence order
  8. (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
  9. 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=BEDROOMassembly_id=BED_SET_MASTER M_BOM WHERE BOMCategory='BD' AND doc_sub_type=C_DocType.DocSubType
room_type=BATHROOMassembly_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:

  1. CO_EmptySpaceLine — structural tiers (4–7 lines per building)
  2. c_orderline — per-element order topics, WHAT to build (62–1,115 per building)
  3. 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:

  1. C_OrderLine references M_Product_ID (a BUILDING product, e.g. BUILDING_SH_STD)
  2. BOMWalker explodes the BOM tree recursively (IsBOM products expand)
  3. PlacementCollectorVisitor resolves world coordinates from the tack chain
  4. 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_NodeProduct tables in output.db. DDL in BIM_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_ID is the primary production→space link. The c_orderline_id FK 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:

  1. User makes changes (edit Orders, add components, modify BOM rules)
  2. User saves / triggers compilation
  3. Pipeline processes all stages end-to-end
  4. 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:

  1. 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.

  2. 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).

  3. Spatial + finishes (6 elements): IfcSpace(3) room volumes + IfcCovering(3) ceiling tiles. SH IFC has no explicit space or covering elements.

  4. 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.

  5. Data issues found:

  6. 2 doors at 199mm width — likely incorrect BOM data (normal door ~800mm+)
  7. 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:

  1. Orientation/rotation drift — piano facing wrong wall, furniture rotated
  2. Spatial distance drift — furniture too close / too far from neighbours
  3. Missing LOD — element has BBox but no real geometry (stub/placeholder)
  4. Missing material — element has geometry but no surface style
  5. 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:

  1. Dining set (highest priority) — ESLine calculates best central spot
  2. Sofa set (next priority) — ESLine finds remaining space that fits sofa BBox
  3. Piano / other (lower priority) — ESLine calculates remaining spot, checks no BBox clash with already-placed sets
  4. 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:

  1. Current state: Hardcoded Java methods (assembler) generate MEP elements. VerbStage parses .bimcobol but doesn't execute.
  2. 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.
  3. 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)