Skip to content

BOM-Based Compilation — The Master Spec

If you can draw it, you can build it. If you can BOM it, you can compile it.

Everything is a BOM. Houses, terminals, bridges, ships — same pipeline, different data.

A Bill of Materials is a recipe: one parent product, N child products, each with a quantity. Each child can itself be a BOM — a building contains floors, a floor contains rooms, a room contains furniture, recursively. Each level is atomic and self-contained.

This document specifies how that recursive BOM model compiles a building. Read the MANIFESTO first for the ERP world view.

Quick navigation:

Section What it covers
§1 Entity Mapping iDempiere tables → BIM Compiler equivalents
§2 Compilation Model The Application Dictionary and BOM contract
§3 Compilation BOM explosion, placement, selection cascade, ASI
§4 Tack Convention The dx/dy/dz spatial offset model (LBD)
§5 Pipeline 12-stage compilation pipeline
§6 Verbs 77 domain verbs (TILE, ROUTE, FRAME, CLUSTER)
§7 Verification 6 mathematical gates
§9 Data Flywheel How 35 buildings teach the compiler
§10 End State What the compiled output looks like

Related specs: DATA_MODEL · BIM_COBOL · TestArchitecture · ACTION_ROADMAP · SourceCodeGuide


1. iDempiere Entity Mapping

Manufacturing concept Construction equivalent
M_Product (product catalog) Building element or assembly. Defined in ERP.db. getChildren() → recurse. Empty → leaf. getParentBOM() → null means root
M_Product_Category Product grouping for substitution — same Category = interchangeable. getProductCategory() returns the shelf
M_BOM (bill of materials) Assembly recipe attached to a product. A product IS a BOM when it has children
M_BOM_Line (BOM child) Recipe line: child product + qty + relative offset (dx/dy/dz). NOT a per-instance placement
AD_Org (organization) Discipline partition (ARC, STR, FP, ACMV, ELEC, SP, CW, LPG). See DISC_VALIDATION_DB_SRS.md §10.4
C_Order (work order) Construction project for a specific building. ONE doc type: "Construction Order"
C_OrderLine (order line) Instance of a product in this project. Compilation explodes parent → child lines. Exception lines name only deviations
C_DocType (document type) "Construction Order" — one only. Classification metadata, NOT a compilation driver
AD_ChangeLog (audit trail) Full provenance — every PLACE, DELETE, MOVE, RESIZE logged with before/after state. UNDO/REDO stack
C_Campaign (marketing) Design theme (Bali, Scandinavian, Industrial)
C_Project (project) Site development — groups C_Orders under one project (budget, schedule, milestones)
C_Location (address/coordinates) Plot location — independent dimension linking site coordinates to C_Order or C_Project
M_Locator (aisle/lot/bin) Plots akin to warehousing ALB (Aisle/Lot/Bin) but in terrain context — with 2D layout capability
EntityType (D/U/A) Dictionary=shipped catalog, User=verb-created, Application=custom

A BOM is self-describing. No level labels needed:

  • getParentBOM() returns null → root
  • getChildren() returns empty → leaf
  • getProductCategory() → what group it belongs to (substitution shelf)

The hierarchy is whatever the products make it. A building, a bridge, a ship — same three methods, different products. No UNIT/FLOOR/ROOM/SET/ITEM vocabulary.

1.1 Disciplines

Discipline = AD_Org. The BOM walker does not know what ARC or FP means — it just recurses. See DISC_VALIDATION_DB_SRS.md §10.4 for the full discipline model (DocEvent per Org → ASI → AD_Val_Rule, shared recipes).


2. The Compilation Model

The BOM databases, product catalog, and validation rules are the Application Dictionary — like iDempiere's AD_Table through AD_Column. They exist. This document describes how the compiler uses them, not how they were created. For BOM creation, see the Rosetta Stone pipeline and building analysis docs.

Three databases, three concerns:

Database Concern iDempiere parallel
ERP.db Product master (M_Product, M_Product_Category, attributes) Product catalog
component_library.db Leaf geometry (LOD meshes, orientation metadata) M_Product_Image
*_BOM.db Assembly recipes (M_BOM, M_BOM_Line with qty + dx/dy/dz) Bill of Materials

EntityType enforcement: Dictionary records (entity_type='D') are read-only at the PO layer. Verbs create new records as entity_type='U'. The guard is in code (MBOM.beforeSave / MBOMLine.beforeSave), not documentation.

No Parametric Mesh in Pipeline. All geometry comes from component_library.db (LOD_ hash prefix). ASI controls per-instance sizing; the compiler scales library LODs, never creates geometry. G5-PROVENANCE Check 6 enforces zero GEO_ hashes.

2.1 Recipe vs Placement — The BOM Contract

The BOM is a recipe. Each m_bom_line is a type line — one row per unique product within its parent BOM, with a qty and verb formula reference. Qty is expressed in the product's cost_uom: EA for discrete items (doors, fittings), M for linear (pipe segments, beams), M2 for area (walls, slabs). The compiler expands type lines into placement instances at compile time.

output.db holds placements. Each row in c_orderline / elements_meta is one physical element at its world-space coordinate. The compiler produces these by expanding BOM recipes through verb formulas (TILE grid, ROUTE path, FRAME bay, etc.) or flat placement (qty=1 for irregular elements).

BOM.db (RECIPE — factored)                  output.db (PLACEMENT — expanded)
┌─────────────────────────────────┐         ┌──────────────────────────────────┐
│ m_bom_line                      │         │ elements_meta / c_orderline      │
│  product_id=PLATE_500x150       │  ──→    │  PLATE_500x150 @ (92.5, -42, 19)│
│  qty=4410                       │ expand  │  PLATE_500x150 @ (93.0, -42, 19)│
│  verb_ref=TILE(15×294, 495mm)   │         │  PLATE_500x150 @ (93.5, -42, 19)│
│                                 │         │  ... (4,410 rows)               │
└─────────────────────────────────┘         └──────────────────────────────────┘

Invariant: SUM(m_bom_line.qty) across all non-PHANTOM leaf lines in the BOM equals the element count in output.db. PHANTOM lines represent spatial availability — they carry qty but do not expand to output elements. The BOM is the recipe; the output is the cooked meal.

TE factorization: 48,485 instances → 1,131 recipe lines (42.8:1). 505 unique products → 48,428 active elements. VerbDetector mines 4 verb patterns from extraction centroids:

Verb Recipe Lines Instances Fidelity What it detects
CLUSTER 354 47,607 approximate (29.1m max, 3.7m avg) Semi-regular offset-table grouping
TILE 3 12 PASS (0.0m) 2D uniform grid (roof plates)
FRAME 2 78 PASS (1.08m max) Grid intersections (structural bays)
ROUTE 2 18 PASS (0.32m max) Axis-aligned uniform-step runs
(UOM=MM/M) Variable-length: InterimWorkshop recomputes primitive mesh (§6.12.2)
flat 770 770 exact Irregular placements

Step-uniformity guard (R8): each ROUTE leg's consecutive gaps must be within ±20% of the average step. Non-uniform groups fall through to CLUSTER or flat writes. See BIM_COBOL.md §19 for verb taxonomy, data flow, and fidelity details.

Pipeline phases (self-contained): 1. BOM pipeline (IFCtoBOMPipeline.run()): reads IFC extraction + YAML Order input → populates catalog (idempotent) → writes {PREFIX}_BOM.db 2. Compile (DAGCompiler): reads {PREFIX}_BOM.db + component_library.db → produces output.db (Authoritative output DDL: Java BuildingWriter.initSchema(), not Python output_schema.sql.)

2.1.8 IFCtoBOM — BOM Recipe Builder

IFCtoBOM produces the BOM recipe ({PREFIX}_BOM.db): m_bom + m_bom_line tables only. No C_Order, no C_OrderLine, no callouts. Orders are created downstream during compilation (processIt()) — the ERP transaction. See DocAction_SRS.md §1.

Two inputs:

  1. IFC extraction DB (*_extracted.db) — elements, IFC family types (IfcWall, IfcFurniture, IfcSpace), spatial containment (IfcRelContainedInSpatialStructure), host relationships (IfcRelFillsElement). The IFC file is the authoritative source of WHAT exists and WHERE it belongs. Classification is IFC-driven — no YAML involvement.

  2. Classification YAML (classify_*.yaml) — the Order input: a human-readable stand-in for C_Order + C_OrderLine. Defines building identity, storey mapping, BOM template references, discipline routing, and static children. YAML tells the pipeline HOW to organise the extracted elements into a BOM tree, not what elements exist.

Separation of concerns:

Concern Source NOT from
What elements exist IFC extraction DB YAML
Which room an element belongs to IFC rel_contained_in_space YAML scope boxes
Element IFC class/family type IFC extraction DB YAML
BOM tree shape (BUILDING → FLOOR → ROOM → SET) YAML Order config IFC
Discipline routing (ARC/STR/FP/ELEC...) YAML disciplines: IFC
Static children (slabs, roof) YAML static_children: IFC
Space-to-template mapping YAML ifc_space:template_bom: IFC

See DISC_VALIDATION_DB_SRS.md §6.13.

IFCtoBOM does NOT:

  • Create C_Order or C_OrderLine (that is processIt() during compilation)
  • Fire callouts (OrderLineProductCallout fires during compilation, not BOM building)
  • Invent elements (every leaf traces to I_Element_Extraction)
  • Use YAML scope boxes for room assignment (IFC spatial containment; scope boxes are Order processing)
  • Validate against regulations (see DocValidate.md)
  • Fill missing pipes or correct gaps (WYSIWYG)

2.1.9 BOM Write Path

Problem: 18 INSERT statements (7 m_bom, 11 m_bom_line) spread across 6 builder files, each with a different column subset. DDL changes require N-file edits — P92 (AD_Org_ID) proved the drift.

Solution: BomWriter — a static utility (same pattern as ProductRegistrar) with two core methods: insertBom(conn, BomRow) and insertBomLine(conn, BomLineRow). Builder-pattern row objects (BomRow, BomLineRow) carry all columns with defaults so callers only set the fields they need.

Invariant: Every m_bom and m_bom_line write in IFCtoBOM goes through BomWriter. No builder owns raw SQL for these tables.

Column truth: BomRow mirrors the m_bom DDL in IFCtoBOMPipeline.java. BomLineRow mirrors the m_bom_line DDL. Adding a column = add to DDL + add to row class + add to BomWriter bind list. One place, not six.


2.2 BOM Compilation Model — Recursive Placement

2.2.1 The Rule

Every m_bom_line is a placement instruction: "place this child's LBD at offset (dx, dy, dz) from my LBD." The compiler walks the BOM tree by one rule: if child_product_id resolves to another m_bom, recurse into it. If it resolves to an M_Product in ERP.db (product catalog migrated S65; geometry meshes remain in component_library.db via MeshBinder), it is a leaf — emit the element at the accumulated world position.

component_type does not exist in compilation. The iDempiere Manufacturing BOM column (BUY/MAKE/PHANTOM) has no role. The walker decides BOM-vs-leaf purely by whether child_product_id has a matching m_bom row. The column remains in the schema for iDempiere compatibility; code must never branch on it.

2.2.2 BOM-to-BOM Recursion

BUILDING BOM  (LBD = world origin, stored in m_bom.origin_x/y/z)
  └─ (dx,dy,dz) = floor LBD position in building  →  FLOOR BOM
       └─ (dx,dy,dz) = room LBD position in floor   →  ROOM BOM (e.g. SH_LIVING_SET)
            ├─ (dx,dy,dz) = piano position in room   →  Piano (leaf in library)
            ├─ (dx,dy,dz) = sofa position in room    →  SOFA BOM (has children)
            │    ├─ (dx,dy,dz) = couch position in sofa  →  Couch (leaf)
            │    └─ (dx,dy,dz) = table position in sofa  →  Table (leaf)
            └─ (dx,dy,dz) = dining position in room  →  DINING BOM (has children)
                 └─ ...

The walker recurses until it hits a leaf (depth capped at 20; practical buildings use 4-5).

2.2.3 Library Population (Outside Compilation)

New leaf products are created outside compilation: TopologyMaker generates mesh geometry, Mesh2Library registers it into component_library.db. After population, the compiler treats it identically to extracted products.

Orientation metadata: Each leaf in component_definitions carries attachment_face, up_axis, forward_axis, orientation, default_rotation — polarity markers for correct mesh orientation at the tack position. See BIM_Designer.md §8.3 (ASI overrides) and DocValidate.md M16/M17.


3. Compilation

3.1 Terms

Term iDempiere parallel Definition
Compilation PP_Product_BOM explosion The compiler explodes the BOM tree from root to leaves, accumulating tack offsets into world coordinates
PHANTOM Phantom BOM line Spatial availability marker — represents remaining capacity in a parent. Walker skips it (no output element). Enables "where can I place this?" queries
Tack Origin datum Left-Back-Down (LBD) corner = (minX, minY, minZ) = (0,0,0) in own frame. All offsets are parent-LBD to child-LBD
BOM Bill of Materials A product with children (getChildren() non-empty). The walker recurses into it
Leaf Purchased item A product with no children (getChildren() empty). The compiler emits the element here. Geometry from component_library.db

3.2 Placement via M_BOM_Line — Parent Owns the Attachment Point

The child BOM does not know its parent. A child BOM does not know which parent hosts it. A leaf product does not know which BOM contains it. This is by design — a child can be reused in any parent that has a slot for it.

The parent M_BOM_Line's dx/dy/dz IS the attachment point:

Parent BOM (e.g. FLOOR_TE_GF)
  │
  └─ m_bom_line (children)                            ← parent defines WHAT + WHERE
       │
       ├─ child 1: AD_Org=ARC, dx=0, dy=0, dz=0        ← architecture assembly at origin
       ├─ child 2: AD_Org=STR, dx=500, dy=0, dz=0       ← structural assembly offset 500mm
       └─ ...                                            ← selection cascade (§3.5) picks child

The BOM walker accumulates offsets: world_pos = parent_LBD + (dx, dy, dz). One addition per BOM level — that's the entire placement mechanism. When the selection cascade picks a child BOM, the child's LBD is placed at the parent's offset position. The child never looks up.

When BOM Drop explodes the tree, C_OrderLine inherits these offsets. No intermediate table, no slot abstraction — the LBD tack convention (§4) handles everything.

Generative consequence: The child BOM can be swapped, resized, or replaced without touching any other BOM — the parent owns the attachment point, not the child.

3.3 The Order Is Minimal

One C_OrderLine referencing a root product compiles an entire building. The compiler explodes the full BOM tree — accumulating tack offsets, resolving leaves to geometry. SH, DX, TE all compile from a single root line.

Exception lines name only deviations. The order says "build me this, but change that child." Product_ID traces directly to the BOM family — no ambiguity. See ProjectOrderBlueprint.md §1 for the exception algebra (Replace, Remove, Compress, Add).

Interactive modification follows the iDempiere BOMDrop Configurator pattern: expand BOM one level, Swap (same Category), Add (new line), or Remove (deactivate). The BOM tree stays navigable at any scale — TE has only 58 BOM nodes and 1,572 lines for 48,428 elements.

3.5 Selection Cascade

The BOM chooser (used during modification to find replacement products) filters by M_Product_Category — same Category means interchangeable. Swap a roof → see only products in the same roof Category. This is the same browse/filter pattern as iDempiere's product lookup: filter by Category, pick from the shelf.

Beyond compilation: Exception-based ordering, order inheritance, reference class compression, and the full Order model are specified in ProjectOrderBlueprint.md.

3.5.1 AttributeSetInstance — Per-Instance Customization

The Selection Cascade picks the BOM recipe (WHAT). The M_AttributeSetInstance (ASI) customizes each instance (HOW). See MANIFESTO.md §M_AttributeSet for the shirt-size analogy and ERP pattern.

Three-table pattern (identical to iDempiere manufacturing):

Table Role Example
M_Product Catalog product (canonical dimensions) WALL_EXT_150: 150mm exterior wall
M_AttributeSetInstance Per-instance header (FK from C_OrderLine) Instance #47: wall on grid A-1
M_AttributeInstance Individual name/value overrides length_mm=12500, material=BrickPlaster

Resolution rule: effective_dimension = ASI_override ?? catalog_default

The compiler resolves effective dimensions from ASI (if customized) or catalog default. Library LOD is scaled by the effective dimension — never remodeled, never generated.

What varies per instance (derived from extracted Rosetta Stones):

Product Type Varying (IsInstanceAttribute=1) Fixed (defines product type)
Wall length_mm, height_mm, material, finish thickness
Pipe length_mm, angle_deg, elevation diameter
Door swing_side, face_anchor (INT/EXT) width, height
Window sill_height_mm, face_anchor width, height
Slab area_m2, thickness_mm material
Furniture — (IsInstanceAttribute=0) All dims fixed
MEP terminal — (IsInstanceAttribute=0) Identical everywhere

IsInstanceAttribute=0 products (furniture, smoke detectors) need no ASI — every instance is identical. IsInstanceAttribute=1 products (walls, pipes, slabs) vary per instance and carry ASI overrides.

Generative flow: User defines room size → cascade finds matching SET → ASI overrides stretch walls to new dimensions → compiler resolves effective = ASI ?? catalog. Zero new code — just data on the OrderLine.

ASI matrix: BIM_Designer.md §8. Schema: output.db. Generative: GENERATIVE_ROOM_SRS.md §6.

3.5.2 Product → Verb Routing via ASI

In iDempiere Manufacturing, each product has a routing (PP_Product_Planning → operations → sequences). We removed PP_Order as too heavy (S73). The lighter equivalent uses the existing ASI chain to carry verb parameters as structured per-instance data.

The Chain

M_Product
  └─ M_AttributeSet_ID → 'BIM_Wall'
       └─ M_AttributeUse → [trim_action, trim_tolerance_mm, joint_type, ...]
            └─ M_Attribute → ValueType, DefaultValue

C_OrderLine
  └─ M_AttributeSetInstance_ID → ASI #47
       └─ M_AttributeInstance → trim_action='CUT_FILL', trim_tolerance_mm=25

AD_Rule
  └─ source_column='dx', rule_type='DIMENSIONAL'
       └─ CalloutEngine → reads ASI → dispatches verb with overrides

How It Replaces PP_Order Routing

iDempiere Manufacturing BIM Equivalent Why Lighter
PP_Product_Planning M_Product.M_AttributeSet_ID One FK, not a planning table
PP_Order_BOMLine (sequence) M_AttributeUse.SeqNo Attribute ordering within set
PP_Order_Node (operation) AD_Rule (reactive callout) Declarative rules, not operation graph
PP_Cost_Collector Not needed Costing is on M_PriceList, not per-operation

Resolution at Verb Dispatch Time

When a verb fires (via DiffVerb → CalloutEngine → VerbRegistry):

1. Product lookup:    M_Product.M_AttributeSet_ID → 'BIM_Wall'
2. Attribute schema:  M_AttributeUse WHERE M_AttributeSet_ID = 'BIM_Wall'
                      → [trim_action, trim_tolerance_mm, joint_type, ...]
3. Instance values:   M_AttributeInstance WHERE M_AttributeSetInstance_ID = <ASI>
                      → trim_action='CUT_FILL', trim_tolerance_mm=25
4. Default fallback:  M_Attribute.DefaultValue WHERE Name = 'trim_tolerance_mm'
                      → 50 (if no ASI override)
5. Effective params:  effective = ASI_override ?? M_Attribute.DefaultValue
6. Verb execution:    TRIM_WALLS_TO_ROOF(action=CUT_FILL, tolerance=25)

This is the same three-tier resolution as §3.5.1 (ASI_override ?? allocated_*_mm ?? catalog_default), extended from geometric dimensions to verb parameters.

Where the User Works

The user edits C_OrderLine — the order line. They never touch M_Product, M_AttributeSet, or AD_Rule. Those are catalog/engine concerns.

When a product has M_AttributeSet_ID = 'BIM_Wall', it does NOT mean the wall must be trimmed. It means the wall CAN carry verb-parameter attributes (trim_action, tolerance, joint_type). The ASI on the order line is the user's override channel:

  • No ASI → callout decides from AD_Rule defaults (most common case)
  • ASI trim_action=SKIP → user explicitly says "don't trim this wall"
  • ASI trim_action=CUT_FILL → user explicitly says "cut and fill"

Same iDempiere pattern: picking a product on a Sales Order doesn't force a specific tax rate. The product's tax category defines which rates ARE VALID. The order line's context (date, jurisdiction) selects the actual rate. Here: the product's attribute set defines which verb params are valid. The order line's ASI selects the actual values.

What This Means

  • New verb parameter = INSERT INTO M_Attribute + INSERT INTO M_AttributeUse. No Java change.
  • Per-instance override = INSERT INTO M_AttributeInstance on the order line's ASI. No Java change. User edits the order line.
  • New product type with different verb params = INSERT INTO M_AttributeSet + map attributes via M_AttributeUse. No Java change.
  • AD_Rule controls WHEN verbs fire. ASI controls HOW they execute. Both are data. The engine (CalloutEngine + VerbRegistry) is generic.

Schema: migration/ASI_002_attribute_detail.sql. Callout wiring: DocValidate.md §1.5. ASI field matrix: BIM_Designer_SRS.md §28.7 + §31.

3.6 Parasitic Discipline Compilation

ARC and STR are spatial disciplines — they build from the ground up. Every child has a dx/dy/dz tack offset: wall sits on slab, column at grid intersection. The BOM walk produces fully-placed elements.

MEP disciplines (FP, ACMV, ELEC, CW, SP, LPG) are parasitic — they attach to rooms and zones that ARC already placed. Their compilation model is fundamentally different.

3.6.1 Eight OrderLines, One Building

Each discipline is a separate C_OrderLine. No merging, no shared lines.

C_Order: TE (Terminal)
├── C_OrderLine seq=10: ARC_SYSTEM   (AD_Org_ID=1)  → shell: rooms, walls, finishes
├── C_OrderLine seq=20: STR_SYSTEM   (AD_Org_ID=2)  → structure: columns, beams, slabs
├── C_OrderLine seq=30: FP_SYSTEM    (AD_Org_ID=3)  → fire protection
├── C_OrderLine seq=40: ACMV_SYSTEM  (AD_Org_ID=4)  → air conditioning / ventilation
├── C_OrderLine seq=50: ELEC_SYSTEM  (AD_Org_ID=5)  → electrical
├── C_OrderLine seq=60: CW_SYSTEM    (AD_Org_ID=6)  → cold water
├── C_OrderLine seq=70: SP_SYSTEM    (AD_Org_ID=7)  → sanitary plumbing
└── C_OrderLine seq=80: LPG_SYSTEM   (AD_Org_ID=8)  → gas

Sequence controls explosion order. ARC explodes first (produces rooms with positions). STR explodes second (DocEvent can verify columns align to beams). MEP disciplines explode after — they reference ARC rooms that already exist in the compiled output.

3.6.2 Service Point Discovery via M_Product_Category

Each ARC room is a product with a category. Some rooms serve a specific discipline: pump room = category FP, AHU room = category ACMV, TNB room = category ELEC.

Each discipline root BOM has the same category as its service rooms. FP_SYSTEM has m_product_category = FP. To find where FP tacks to, the compiler matches:

SELECT dx, dy, dz FROM c_orderline
WHERE m_product_category_id = (SELECT M_Product_Category_ID
                               FROM M_Product_Category WHERE Value = 'FP')
  AND Discipline = 'ARC'

No new columns. No cross-references. The existing product taxonomy IS the wiring between ARC shell and MEP disciplines.

Root BOM tack origins — where each discipline's root product attaches (TE reference data from SJTII_Terminal extraction, world coordinates):

OrderLine Root BOM Tacks to (ARC room) M_Product_Category Origin (world m) Walk direction
seq=10 ARC ARC_SYSTEM Building LBD (world anchor) ARC (84.6, -51.2, -30.7) Ground up ↑
seq=20 STR STR_SYSTEM Foundation level STR (84.6, -51.2, -30.7) Ground up ↑
seq=30 FP FP_SYSTEM Pump room (GF) FP (~108, -17, 2.9) Vertical riser ↑ then horizontal per floor →
seq=40 ACMV ACMV_SYSTEM AHU / plant room (L1+) ACMV (~126, -12, 6.1) Horizontal per floor from AHU →
seq=50 ELEC ELEC_SYSTEM DB room (per floor) ELEC (~127, -19, 7.0) Vertical riser ↑ then radial per floor →
seq=60 CW CW_SYSTEM Water tank / meter room (below GF) CW (~103, -29, -1.2) Ground up ↑ then horizontal to wet rooms →
seq=70 SP SP_SYSTEM Manhole / building drain (below GF) SP (~115, -10, -1.1) Top down ↓ (gravity drainage)
seq=80 LPG LPG_SYSTEM Gas meter room (GF external) LPG (~92, -30, 1.8) Horizontal from meter to kitchens →

How it works:

  1. ARC explodes → produces rooms. Some rooms carry discipline-specific categories (pump room = FP, AHU room = ACMV, DB room = ELEC, etc.)
  2. When FP_SYSTEM explodes, the compiler queries ARC output for rooms with M_Product_Category = FP. Those room positions become FP_SYSTEM's tack anchors.
  3. FP's children (riser, branch pipes, sprinkler heads) distribute from those anchors using qty + DocEvent rules. No dx/dy/dz on the BOM line.

ARC rooms as discipline anchors: The ARC shell must produce room products with categories matching each MEP discipline present in the building. If the Order config has no pump room, FP_SYSTEM has nowhere to tack. The Order author (YAML or Designer user) ensures service rooms exist for each discipline OrderLine they add.

Compulsory service rooms in CO BOM: For a CO (Commercial) building, the ARC root BOM includes service rooms as compulsory children — pump room (category FP), AHU room (category ACMV), DB room (category ELEC), water tank room (category CW), wet core (category SP), gas meter room (category LPG). These are the frame mounting points. Like a toy car chassis: certain attachment points are pre-moulded into the frame; the discipline modules snap onto them.

BUILDING_TE_STD (CO)
└── ARC children (compulsory frame):
    ├── ROOM_PUMP         (M_Product_Category = FP)     ← FP tacks here
    ├── ROOM_AHU          (M_Product_Category = ACMV)   ← ACMV tacks here
    ├── ROOM_DB           (M_Product_Category = ELEC)   ← ELEC tacks here
    ├── ROOM_WATER_TANK   (M_Product_Category = CW)     ← CW tacks here
    ├── ROOM_WET_CORE     (M_Product_Category = SP)     ← SP tacks here
    ├── ROOM_GAS_METER    (M_Product_Category = LPG)    ← LPG tacks here
    └── ... (other rooms: lobby, retail, office — no discipline anchor)

3.6.2a OrderLine.Product Callout — Auto-Insert Discipline Lines

When the user selects a CO product on the first C_OrderLine, a Callout automatically creates the sibling discipline OrderLines. This is the standard iDempiere C_OrderLine.M_Product_ID callout pattern — when product changes, callout fires.

User action:  C_OrderLine.M_Product_ID = BUILDING_TE_STD
                ↓
Callout:      OrderLine.Product.onProductChanged()
                ↓
Logic:        1. Read M_Product_Category → CO
              2. Read CO BOM children → find discipline root products
                 (each child with AD_Org_ID > 0 is a discipline)
              3. For each discipline root: INSERT C_OrderLine
                 - Product_ID = discipline root BOM (FP_SYSTEM, ACMV_SYSTEM, ...)
                 - AD_Org_ID  = discipline org (3=FP, 4=ACMV, ...)
                 - Sequence   = 30, 40, 50, 60, 70, 80
                 - Qty        = from extraction peek, or 0 (fill-all)
                ↓
Result:       8 OrderLines created (ARC=10, STR=20, FP=30, ... LPG=80)

No invention. This is the same mechanism as iDempiere's Sales Order callout: select a product kit → callout auto-inserts component lines. The callout reads the BOM to discover what lines to create. The user can then edit quantities, remove disciplines they don't need, or add custom lines.

YAML as Order input: For Rosetta Stone testing, the YAML classify_te.yaml lists the 8 discipline sections explicitly — this is Order data, not extraction logic. The callout is the interactive (Designer GUI) equivalent of what YAML declares as C_OrderLine configuration.

Generalisation across building categories: The callout is not CO-specific. It reads the BOM to discover discipline children. Different building categories have different discipline sets — the callout discovers them, not hardcodes them.

Category Typical disciplines Service anchor concept
RE (Residential) ARC, STR, ELEC, CW+SP (maybe LPG) Rooms — kitchen, bathroom, DB cupboard
CO (Commercial) All 8 — ARC, STR, FP, ACMV, ELEC, CW, SP, LPG Rooms — pump room, AHU room, DB room, etc.
IN (Infrastructure) Varies by sub-type (see below) Zones — spans, chainage segments, stations

Infrastructure (IN) discipline mapping:

Infra sub-type Disciplines Service zones (≈ rooms)
Bridge (BR) STR (piers, deck, girders), GEO (foundations), ROAD (approach), DRAIN, ELEC (lighting) Span, pier, abutment — each a zone product
Road (RD) ROAD (pavement, markings), DRAIN (storm drains, culverts), ELEC (street lighting), TRAFFIC (signals) Chainage segments (100m sections)
Rail (RL) RAIL (track, sleepers), STR (stations), ELEC (OHE, signalling), DRAIN Track sections, station zones, signal locations
Industrial (IP) PROCESS (pipes, vessels), STR (steel frame), ELEC, INST (instrumentation) Process units, pipe racks, control rooms

The mechanism is identical: the parent product (bridge, road, rail) has compulsory children (zones) with discipline-specific M_Product_Category. The callout reads them and creates OrderLines. A bridge pier has M_Product_Category = STR; the STR OrderLine tacks there — same pattern as a pump room having M_Product_Category = FP.

Room vs Zone: For buildings (RE, CO), the service anchor is a room — an IfcSpace with walls, floor, ceiling, and a typed category. For infrastructure (IN), the service anchor is a zone — a spatial segment without enclosure. Both are M_Product entries with categories. The BOM walker treats them identically: a product with children that the walker recurses into.

RE is a subset, not an exception. A house with only ARC + ELEC + SP simply has a BOM with 3 discipline children. The callout creates 3 OrderLines. No code change — the BOM is the configuration.

Callout architecture: DocValidate.md §1.5 (Column.Callout spec). Product → verb routing via ASI: BBC.md §3.5.2. Infrastructure buildings: BR 7/7, IP 7/7 PASS (standard elements). RD/RL: 0 elements (infra walker gap — non-standard IFC hierarchy).

3.6.3 Per-Discipline Trace — Start, Walk, End

Each discipline has a service origin (where it starts), a walk pattern (how it distributes), and placement rules (standards that govern spacing and coverage). The walker uses qty from the BOM line as its stop condition.

ARC — Architecture (AD_Org_ID=1, seq=10)
Aspect Detail
Start Building origin (0,0,0). Builds the spatial shell from ground up.
Walk Spatial tack. Every child has dx/dy/dz from extraction. Walls, windows, doors, finishes, furniture — each placed at its extracted world position relative to floor LBD.
End All rooms defined. Room containers (FLOOR, ROOM) have world positions in c_orderline. These become tack anchors for all parasitic disciplines.
Elements TE: 34,724 (72%). IfcPlate(33K), Wall(330), Window(236), Furniture(176).
Rules UBBL Part III (building dimensions). Room area/width/height per occupancy (AD_DocEvent_Rule, check_method=MIN_DIMENSION).
Key output Typed rooms with M_Product_Category: pump rooms (FP), AHU rooms (ACMV), DB rooms (ELEC), etc. These categories wire parasitic disciplines to their service points.
STR — Structure (AD_Org_ID=2, seq=20)
Aspect Detail
Start Foundation level (FN). Columns, footings at grid intersections.
Walk Spatial tack. Columns at grid nodes, beams spanning between columns, slabs covering bays. Each has dx/dy/dz from extraction.
End Structural frame complete. Column grid defines the structural bays that ARC rooms sit within.
Elements TE: 1,429. Slab(614), Beam(432), Member(312), Column(68).
Rules MS 1195 / EC2 (concrete). Column vertical continuity: max XY drift ≤ 25mm across storeys (STR_COLUMN_CONTINUITY). Beam-column connectivity (DocEvent: REQUIRED_HOST).
DocEvent Verify columns align to beams. STR elements must sit within ARC structural grid — DocEvent checks that STR tacks fall on ARC grid nodes.
FP — Fire Protection (AD_Org_ID=3, seq=30)
Aspect Detail
Start Pump room (M_Product_Category=FP in ARC output). FP_SYSTEM origin = pump room c_orderline position.
Walk Parasitic. BOM gives qty per floor, no dx/dy/dz. DocEvent distributes: risers through riser shafts (vertical, one per shaft), then branch pipes per floor, then sprinkler heads per room.
End Every room with occupancy requiring sprinklers has coverage. Walker stops at qty if set, or covers all eligible rooms if qty=0.
Elements TE: 6,863. PipeFitting(3,146), PipeSegment(2,672), FireSuppression(909), Alarm(80).
Rules NFPA 13 (sprinkler spacing): min 3000mm, max 4600mm per Light Hazard §8.6.2.2.1. Coverage area per head: 9.3m² LH, 12.1m² OH1. UBBL Part VII (fire requirements by occupancy). ad_fp_coverage table (4 hazard classes).
Placement Grid distribution within each room. Spacing = room_area / coverage_per_head. TE mining confirms bimodal: 3000-4000mm (standard) and 4500mm (dominant grid).
ACMV — Air Conditioning & Mechanical Ventilation (AD_Org_ID=4, seq=40)
Aspect Detail
Start AHU/plant room (M_Product_Category=ACMV in ARC output). ACMV_SYSTEM origin = AHU room position.
Walk Parasitic. Main ducts from AHU through ceiling void (horizontal per floor). Branch ducts to each room. Air terminals (diffusers/grilles) distributed per room by air change requirement.
End Every habitable room has supply + return air terminals.
Elements TE: 1,621. DuctFitting(713), DuctSegment(568), AirTerminal(289), Proxy(51).
Rules MS 1525 (energy efficiency, air-conditioned buildings). ASHRAE 62.1 (ventilation rates). Air changes per hour by occupancy: office 6-8 ACH, retail 8-12 ACH, toilet 15 ACH. Duct sizing by airflow (velocity method).
Placement Air terminals: grid within room, spacing derived from throw distance. Ducts: ROUTE verb along ceiling void, branch at room entry points.
ELEC — Electrical (AD_Org_ID=5, seq=50)
Aspect Detail
Start TNB/main switchboard room (M_Product_Category=ELEC in ARC output). ELEC_SYSTEM origin = DB room position.
Walk Parasitic. Main distribution board → sub-DB per floor (vertical riser) → cable trays per floor (horizontal) → light fixtures + power outlets per room.
End Every room has lighting to required lux level + power outlets per occupancy.
Elements TE: 1,172. LightFixture(814), Proxy(339), Appliance(19).
Rules MS 1525 (lighting power density). IES lighting levels: office 300-500 lux, corridor 100 lux, toilet 150 lux. Light fixture grid: max spacing ~5000mm (IES_LIGHT_SPACING, advisory). TE mining: avg grid pitch 4000mm, tight clustering per storey.
Placement Light fixtures: ceiling grid aligned (600mm module). Spacing = room_area / (lux_target / lux_per_fixture). TE mining confirms per-storey consistency (Level 1-3: ±2mm Z variance).
Cross-discipline NEC 300.4: min 150mm clearance from SP pipes. TE mining: 11 true overlaps, 35 under 150mm on lower floors. NEC_ELEC_SP_CLEARANCE rule.
CW — Cold Water (AD_Org_ID=6, seq=60)
Aspect Detail
Start Water tank room / meter room (M_Product_Category=CW in ARC output). CW_SYSTEM origin = tank room position.
Walk Parasitic. Rising main (vertical riser) → floor distribution (horizontal header) → branch pipes to each wet room → fixtures (taps, valves, flow terminals).
End Every wet room (kitchen, bathroom, toilet) has water supply points.
Elements TE: 1,431. PipeFitting(638), PipeSegment(619), FlowTerminal(106), Valve(57).
Rules MS 1228 (water supply). Pipe sizing by fixture unit method. Min pressure at highest fixture. Riser: one per core/shaft. Branch: ROUTE verb following wall/ceiling.
Placement Fixtures at predefined wall positions (basin, sink, WC). Pipes ROUTE from riser to each fixture. Gravity-fed upper floors, pump-fed if pressure insufficient.
SP — Sanitary Plumbing (AD_Org_ID=7, seq=70)
Aspect Detail
Start Each bathroom/toilet on each floor (M_Product_Category=SP in ARC output, or rooms with sanitary fixtures). SP works top-down — waste flows by gravity.
Walk Parasitic, reversed. Fixtures (WC, basin, floor trap) per wet room → waste pipes collecting per floor → soil stack (vertical, one per wet core) → ground floor manhole → building drain.
End Every sanitary fixture connected to drainage. Soil stacks extend to roof (vent).
Elements TE: 979. PipeSegment(455), PipeFitting(372), FlowTerminal(150), Valve(2).
Rules MS 1228 (sanitary plumbing). Uniform Plumbing Code (UPC). Min pipe gradient: 1:40 (100mm pipe), 1:60 (150mm pipe). Trap seal: min 50mm water depth. Vent pipe sizing by drainage fixture units.
Placement Fixtures at wet room positions. Waste pipes ROUTE with minimum gradient from fixture to stack. Soil stack at wet core position (vertical, full building height + vent above roof).
LPG — Liquefied Petroleum Gas (AD_Org_ID=8, seq=80)
Aspect Detail
Start Gas meter room / LPG storage (M_Product_Category=LPG in ARC output). Must be external or ventilated per DOSH regulations. LPG_SYSTEM origin = meter room position.
Walk Parasitic. Gas riser (if multi-storey) → floor header → branch to each kitchen/cooking area → gas points (hob connections, valves).
End Every kitchen/cooking area has gas supply with isolation valve.
Elements TE: 209. PipeFitting(87), PipeSegment(75), Valve(47).
Rules MS 830 (gas installation). DOSH (Dept of Safety & Health) regulations. Emergency shut-off valve at entry to each floor. Isolation valve at each appliance. Min clearance from ignition sources. Gas detector in enclosed spaces. Pipe sizing by gas demand (MJ/h).
Placement Gas points at kitchen positions. Riser in dedicated shaft (never in electrical riser). Horizontal runs along external walls or ventilated ceiling voids.

3.6.4 Two Walk Modes

ARC / STR (spatial) MEP (parasitic)
BOM walk gives Positions (dx/dy/dz per child) Quantities (qty per room type)
Placement by Compiler (deterministic tack accumulation) DocEvent rules (distribute through ARC rooms)
User control Adjust tack offsets in Designer Adjust qty or drag elements in Designer

Spatial walk (ARC/STR): Parent owns the attachment point (§3.2). The child's dx/dy/dz is a fixed offset. The walker accumulates: world_pos = parent_LBD + offset. Every element has a deterministic position.

Parasitic walk (MEP): The child BOM line has qty but no meaningful dx/dy/dz. The walker produces: "this floor needs 47 sprinklers." DocEvent validation then distributes them across eligible ARC rooms by rule. The user can adjust in Designer after.

Note on SP: Sanitary plumbing is the only discipline that walks top-down (waste flows by gravity from upper floors to ground). The BOM still lists fixtures per floor — but the DocEvent must connect them downward to soil stacks, verifying minimum pipe gradient at each segment.

3.6.5 Qty as Control Knob

The M_BOM_Line qty on a parasitic child is the user's intent. Qty is always in the product's cost_uom (from M_Product):

Product Type cost_uom Qty Meaning
Sprinkler head, fitting, valve EA qty = 47 → distribute exactly 47 items
Pipe segment, duct segment M qty = 18 → 18 linear meters of pipe
Wall panel, slab M2 qty = 24 → 24 square meters
Qty Meaning
qty = 47 Distribute exactly 47 items (EA) or 47 units of measure. Walker stops when count reached.
qty = 0 (or blank) No limit — distribute until all eligible rooms are covered.

During Order setup (YAML authoring), the author peeks at the extracted DB to see what exists: SELECT count(*) FROM i_element_extraction WHERE discipline = 'FP' → 47. This becomes the qty on the FP OrderLine.

In the Designer GUI, the user edits qty directly on the C_OrderLine. Recompile → different density. Same BOM recipe, different quantity.

3.6.6 The Pipeline for Parasitic Disciplines

1. BOM walk      → "Floor GF needs 12 sprinklers (EA), 3 risers (EA)"  (WHAT + HOW MANY)
2. DocEvent      → distribute into ARC rooms by jurisdiction     (WHERE — rule-based)
                   rules: NFPA 13, UBBL, MS 1525, MS 1228, MS 830
3. Designer GUI  → user drags/adjusts positions                  (WHERE — user override)
4. Recompile     → final placed elements in output.db

Steps 1–2 are automatic. Step 3 is optional (user intervention). Step 4 is deterministic — same order produces same output.

iDempiere parallel: BOM = bill of materials (recipe). DocEvent = business rules (routing/distribution). C_OrderLine = user execution (configure-to-order). The BOM never changes — only the order and the rules change.

3.6.7 Cross-Discipline Clearance

After all 8 OrderLines explode, cross-discipline clash detection fires (DISC_VALIDATION_DB_SRS §10.4.3 stage 4). Uses ERP-maths centreline clearance (not mesh intersection):

clearance = centreline_2D_distance - radius_a - radius_b
where radius = MIN(width, depth) / 2    ← pipe cross-section

TE mining (M12): 11 true overlaps ELEC×SP, 35 pairs under 150mm on lower floors. Rule: NEC_ELEC_SP_CLEARANCE, min 150mm, WARN severity.

Standards by jurisdiction and discipline: DISC_VALIDATION_DB_SRS §10.4.3. TE mining data: TE_MINING_RESULTS. Shared discipline recipes: DV025 migration in ERP.db (FP_SYSTEM seed, S100-p73). DocEvent rule schema: AD_DocEvent_Rule in ERP.db (DISC_VALIDATION_DB_SRS §10.4.3).

3.7 Rosetta Stone

The Rosetta Stone exercise (SH/DX/TE) proves the pipeline is lossless: the Application Dictionary (BOMs + products + rules) compiles back to the original building with G1-G6 gates GREEN. See TestArchitecture.md for gate specification and coverage.

3.8 locator_ref — Exception-Order Addressing (Session D)

Every C_OrderLine carries a locator_ref: a dot-separated path from the root that uniquely identifies WHERE in the exploded BOM tree this node sits.

Syntax:  segment.segment.segment
Segment: M_Product_Category code (preferred) or bom_id/product_id (fallback)
Example: RE.GF.LI.SOFA_001
         │   │  │  └─ leaf product (no category → product_id used)
         │   │  └─── room category (LI = Living)
         │   └────── floor category (GF = Ground Floor)
         └────────── building category (RE = Residential)

Stability guarantee: locator_ref is derived from BOM structure (M_Product_Category at each level), not from runtime state or insertion order. The same BOM always produces the same locator_refs. This makes them safe for exception orders to reference across recompilations.

Exception-order mutations (ProjectOrderBlueprint.md §1.1):

Mutation C_OrderLine state Compiler behaviour
Remove Qty=0 on a locator_ref BomDropper skips entire subtree
Compress is_reference_class=1, Qty=N Walker instantiates N copies at computed dz offsets
Replace Different family_ref on a locator_ref BomDropper swaps the product (existing)
Add New C_OrderLine with locator_ref Direct insertion (addDiscipline, Session A)

Migration: W005_orderline_locator_ref.sql — adds locator_ref TEXT and is_reference_class INTEGER DEFAULT 0 to C_OrderLine.


4. Tack Convention — The Spatial Handshake

4.0 The One Rule

Every m_bom_line is a tack instruction: place the child's bounding box corner at this offset from the parent's bounding box corner.

  • LBD = Left-Back-Down = bounding box minimum corner = (minX, minY, minZ)
  • Left = X minimum, Back = Y minimum, Down = Z minimum
  • dx/dy/dz on an m_bom_line = the position within the parent where the child's LBD corner sits

No centroids. No special cases. One rule for every line at every depth.

Convention extends to design workspace: C_OrderLine.dx/dy/dz uses the same LBD convention. See G4_SRS.md §5.4 and BIM_Designer.md §17.10.3.

tack_from / tack_to (Lego principle): The parent's m_bom_line defines tack_from — where the child's corner goes. The child's tack_to is always its own LBD = (0,0,0). The child never looks up.

SH_LIVING_SET  (parent AABB = 8000 × 2000 × 1200 mm)
  │
  ├─ tack_from=(0.3, 0.1, 0.0) → Piano         tack_to=(0,0,0) = piano's LBD
  ├─ tack_from=(2.5, 0.8, 0.0) → SOFA_BOM      tack_to=(0,0,0) = sofa group's LBD
  ├─ tack_from=(5.8, 0.2, 0.0) → Dining Table   tack_to=(0,0,0) = table's LBD
  └─ BUFFER fills remaining space (§4.2)

The child can be reused in any parent that has a slot big enough.

Typed coordinate hierarchy: Code enforces typed coordinates: LocalCoord, StoreyCoord, WorldCoord (sealed classes). Accumulation chain: LocalCoord.toWorld(StoreyCoord) → WorldCoord. DriftGuardTest prevents direct WorldCoord construction — all world positions must flow through the chain.

4.1 World Coordinate Reconstruction

element_LBD = building_origin + tack_from[1] + tack_from[2] + ... + tack_from[N]
centroid    = element_LBD + (width/2, depth/2, height/2)

where each tack_from[i] is the full 3D position (dx, dy, dz) from that level's m_bom_line. The walker accumulates all three axes through the BOM chain.

Origin convention: Only the BUILDING BOM carries a non-zero origin (m_bom.origin_x/y/z). All child BOMs have origin = (0, 0, 0) — position is encoded solely in the parent's tack_from. Non-zero child origins cause double-count. Centroid is computed only at the output stage — never enters the BOM.

Example (SH Living Room):

BUILDING (origin = -9.235, -2.746, -0.470)     ← world LBD stored in m_bom.origin_x/y/z
  tack_from=(0.0, 0.0, 0.0) → FLOOR BOM        ← ground floor LBD = building LBD
    tack_from=(2.2, 5.2, 0.5) → LIVING BOM      ← living room LBD sits here in the floor
      tack_from=(0.3, 0.1, 0.0) → Piano (leaf)  ← piano sits here in the living room
      tack_from=(2.5, 0.8, 0.0) → SOFA BOM      ← sofa group sits here
        tack_from=(0.0, 0.0, 0.0) → Couch        ← couch at sofa group's LBD corner
        tack_from=(1.2, 0.3, 0.0) → Coffee Table  ← table offset from sofa LBD

Piano world LBD: X = -9.235 + 0.0 + 2.2 + 0.3 = -6.735 Y = -2.746 + 0.0 + 5.2 + 0.1 = 2.554 Z = -0.470 + 0.0 + 0.5 + 0.0 = 0.030

Piano centroid = piano_LBD + (piano_width/2, piano_depth/2, piano_height/2)

Scope box origin (origin_m) was historically a YAML-authored containment filter for extraction. Since S100-p125, extraction uses IFC spatial containment (rel_contained_in_space) instead. Scope boxes now live in Order processing only — the BIM Designer GUI uses them for sub-room zone splits at order time. Tack_from comes from the room's measured LBD relative to the floor's LBD.

4.1.1 validateBOM() — The Spatial Analogue

BomValidator.java (9 checks) enforces spatial BOM integrity: child within parent AABB, no overlap, SUM(children)+BUFFER = parent, non-negative tack_from.

4.2 BUFFER — The Completeness Invariant

Definition: A BUFFER is a phantom m_bom_line that fills the remaining AABB gap so that the parent's allocated dimensions equal the sum of its children's allocated dimensions along each axis.

Invariant: For every non-leaf BOM:

parent.width  = SUM(children.allocated_width)    along tack-X
parent.depth  = SUM(children.allocated_depth)    along tack-Y
parent.height = SUM(children.allocated_height)   along tack-Z

Where allocated_dim = the child's own AABB dim along that axis (or the BUFFER's phantom dim for the remainder). The BUFFER absorbs the gap between the sum of real children and the parent envelope.

Why BUFFER matters: Without BUFFER, the compiler cannot prove that a BOM's spatial claim is fully accounted for. BUFFER is the difference between a recipe that says "these items go here" and one that says "these items fill exactly this space." The second form is verifiable; the first is not.

4.2.1 AABB Qualifier (session 43)

The aabb_qualifier column on m_bom disambiguates which envelope the AABB dimensions represent. Without qualification, AABB is ambiguous — different builders compute from different reference surfaces.

Qualifier What it measures Use case
INNER Room clear volume, finish-to-finish Furniture placement, PHANTOM index, Click-to-Place
STRUCTURAL Centerline-to-centerline (structural grid) Grid layout, structural analysis, column spacing
OUTER Full object extent including projections Clash detection, site boundary, extraction AABB
OPENING Clear opening in host element Door/window placement, accessibility clearance

Maps to GD&T tolerance zones: INNER=LMC, OUTER=MMC, STRUCTURAL=Basic, OPENING=Virtual.

ScopeBomBuilder: SET BOMs tagged OUTER (computed from element extents). Empty SET BOMs (no assigned elements) tagged INNER (Order-defined room dims = available space). FloorRoomBomBuilder: FLOOR ROOM BOMs tagged INNER (architect intent from IFC or Order config). StructuralBomBuilder/DisciplineBomBuilder: default OUTER (computed from elements).

4.2.2 PHANTOM — Spatial Availability Index (session 43)

PHANTOMs are component_type='PHANTOM' lines in m_bom_line. SAP empty storage bin principle: the bin has a capacity (INNER dims from Order config), the PHANTOM represents remaining capacity after placed elements are subtracted.

PHANTOM.width  = max(0, parent.INNER.width  - children_bbox.width)
PHANTOM.depth  = max(0, parent.INNER.depth  - children_bbox.depth)
PHANTOM.height = max(0, parent.INNER.height - children_bbox.height)

Per-axis, independently. Not a 3D packing problem — 1D subtraction per axis. BOMWalker dispatches to onPhantom() → no output element, no placement. Click-to-Place (G-13) queries PHANTOMs for instant "where can I place this?" Zero-cost foam: sits in the BOM, walker skips it, enables spatial queries.

Witness claims: - W-AABB-QUAL-1: IMPLEMENTED — m_bom.aabb_qualifier tags INNER vs OUTER correctly. - W-PHANTOM-1: IMPLEMENTED — PHANTOM lines fill remaining INNER volume. 66 PHANTOMs across 82 SET BOMs. - W-CENTROID-DIFF-1: IMPLEMENTED — hosted elements use centroid distance for band classification. - W-TACK-1: IMPLEMENTED (advisory) — child AABB fits within parent. SH: pending promotion to FAIL. TE: 28.5% overshoot (CLUSTER approximate grouping). Promotion blocked until overshoot < 5%. - W-BUFFER-1: IMPLEMENTED — SUM(children) vs parent. SH: 2/3 balanced. TE: 12/50 balanced (CLUSTER exceeds centroid-based envelope). - W-WALKTHRU-DIFFERS-1: RESOLVED — single compilation path.

Product classification (shape archetype, scale band, semantic category) is a product catalog concern — see DATA_MODEL.md and component_library.db schema.


5. The 12-Stage Pipeline

# Stage What it does
1 Metadata Referential integrity checks against BOM + ERP databases
2 Compile Explodes BOM tree, accumulates tack offsets into world coordinates
3 Write Emits SQLite output DB (C_OrderLine + elements_meta)
4 Verb BIM COBOL post-processing → W_Verb_Node audit trail
5 Digest Per-element SHA256 spatial fingerprint
6 Geometry Mesh integrity validation
7 Prove Mathematical placement proofs

Single compilation path: element positions are read from m_bom_line (tack offsets per §4) and accumulated through the BOM chain into world coordinates. C_OrderLine → M_Product → BOM explosion (iDempiere prepareIt pattern).


6. BIM COBOL — Verb-Driven BOM Mutation

The GUI emits BIM COBOL verbs, never direct SQL. 77 verbs in 5 tiers:

Tier Verbs Purpose
P0 Primitive CREATE BOM, ADD LINE, SET TACK, SET ROTATION, SET DIMENSIONS, REMOVE LINE, DELETE BOM, SET LINE PROPERTY BOM CRUD atoms
Utility VALIDATE AABB, SNAP TO GRID, EXTRACT AABB Validation + transform
L1 Convenience CREATE ROOM, FURNISH ROOM, RESIZE ROOM, STRIP ROOM Room-level composed verbs
Data SELECT, LIST, DESCRIBE, COUNT, AGGREGATE, EXPORT, CLONE, SUMMARIZE BOM Query + export
Original PLACE BOM, WIRE LIGHTING, ROUTE SPRINKLERS, TILE SURFACE, CHECK BOM, ... Geometry + inspection

Layered composition: L1 verbs call P0 primitives. L2 (floor-level) will call L1. Never skip layers. Each verb = one file, one keyword, one payload record.

Full grammar spec: docs/BIM_COBOL.md


7. Verification: The Rosetta Stone Gate

Maths that proves visuals without cheating.

Verification gates: See TestArchitecture.md §Verification for the complete G1-G6 gate specification, tamper rules (T1-T16), and the 4-layer defense model.

All 6 gates GREEN for SH and DX. Current counts in PROGRESS.md.


8. What This Is Not

It is NOT Because
Revit / ArchiCAD Those are authoring tools. This is a reproducer from committed data.
Rule-based AI No heuristics, no ML. Selection cascade is deterministic.
Parametric design Parameters come from BOM data, not design exploration.
Approximate Digest is SHA256. Same Order → same output. No tolerance.
Interactive Batch compilation. Like COBOL/ERP — process the order, produce the output.

9. The Data Flywheel — Emergent Intelligence

Processing 34 real buildings creates a data flywheel — each onboarded IFC enriches a pool of mined dimensional observations that validates the next one. Unlike fixed BIM rules (building codes, clash thresholds), this system validates against empirical evidence from its own corpus.

Cycle: New IFC → DimensionRangeValidator checks W/D/H against mined ranges → pipeline compiles BOM → extract_validation_rules.sh mines new patterns → apply_mined_rules.sh feeds back into ERP.db → pool grows.

Three validation layers:

Layer Question Source When
Dimensional (DV010) "Is this wall a plausible size?" Mined from 20 buildings (415 rules) IFC onboarding
Compliance "Does this room meet building code?" Researched from UBBL/IRC/NFPA (63 rules) Design time
Relational "Does this bathroom have the right MEP?" Mined + researched (186 schedules, 4,801 placement rules) MEP placement

Current state: 415 rules, 25 IFC classes, 1,245 parameters mined from 20 buildings.

9.1 Layer 2 — Building Profile Validation

Every building has a signature — the percentage distribution of its IFC classes (e.g., residential = ~35% IfcWall, MEP = 90%+ flow elements). BuildingProfileValidator compares each new building's profile against archetype profiles mined from the 34-building corpus. Catches mis-labelled files, wrong discipline tagging, and anomalous element compositions. Advisory only — never blocks.

Storage: ad_building_profile table in ERP.db (one row per building/ifc_class).

9.2 Flywheel Layers

Layer Status Question Spec
0 — Extraction DONE "Here are 97,000 elements" Rosetta Stone pipeline
1 — Dimension Ranges DONE (s47) "Is this wall a plausible size?" DimensionRangeValidator
2 — Building Profiles DONE (s47) "Does this building's composition make sense?" §9.1 above
3 — Shape Comprehension EYES module "This IfcWall is shaped like a beam" EYES_SRS.md
4–6 Future Relational patterns, sequence patterns, archetype clusters

Each layer uses the same mechanism — query the corpus, aggregate, compare. No neural networks. No training. Just SQL aggregation on real data.


10. The Compilation End State

The compiler runs without human assistance. Given a BOM database and the product catalog (ERP.db + component_library.db), it produces a complete, verified output.

Adding a new building = adding BOM data. The compiler is the constant.


11. Building Registration

Adding a new building = adding BOM data to the Application Dictionary. BuildingRegistry.loadActive() reads C_DocType from the BOM database — zero building names in Java. See WorkOrderGuide.md for the onboarding pipeline.


Why the ERP Concept Is Most Powerful

This spec covers the core compilation model — how BOMs become buildings. But the ERP paradigm extends far beyond compilation: exception-based ordering, order inheritance, rule packs, C_Project site management, and the full 4D–8D dimension stack.

Read next: Project Order Blueprint — the extended spec that shows where the Construction Order goes when applied to real construction projects.


Assembly hierarchy: PREFAB_ARCHITECTURE.md | Terminal ERP model: TerminalAnalysis.md §ERP Model Architecture | Action roadmap: ACTION_ROADMAP.md