Multi-Discipline BOM Design¶
Foundation: BBC · DATA_MODEL · BIM_COBOL · MANIFESTO · TestArchitecture
CTFL Review Status (session 33, 2026-03-19)¶
Last reviewed: 2026-03-19 session 34 — CTFL review: §10.4 H3 blocker added, §10.5 handler witness claims + acceptance criteria added (12 witnesses, 6 auto-fix limits). Open: H1-H6 handlers DESIGNED, NOT IMPLEMENTED. Remaining blockers: AD_Clash_Rule (0 rows → H2), AD_Val_Rule SPACING (0 rows → H3), AD_Val_Rule CONTINUITY (0 rows → H5). See §10.4 for full status matrix.
How discipline-separated BOMs organize extracted buildings and prepare for generative placement
Governing principle: Each construction discipline (ARC, STR, PLB, ELC, FPR...) is a separate BOM sub-tree under its storey. This mirrors real-world drawing sheets (A-101, S-101, P-101, E-101) and construction contracts. The BOM structure is designed once for both extracted and generative modes — extraction populates positions, generative mode populates rules. Same schema, two modes.
1. Discipline Vocabulary¶
Derived from Terminal extraction (51,088 elements across 9 disciplines) and validated against standard construction drawing sheet conventions.
| Code | Discipline | Drawing Sheet | Terminal Count | DX Count | IFC Classes |
|---|---|---|---|---|---|
| ARC | Architectural | A-series | 34,724 | 183 | IfcWall, IfcDoor, IfcWindow, IfcSlab, IfcFurnishingElement, IfcCovering, IfcRailing, IfcStairFlight, IfcRoof, IfcRampFlight |
| STR | Structural | S-series | 1,429 | 12 | IfcBeam, IfcMember, IfcColumn, IfcSlab, IfcWall |
| FP | Fire Protection | FP-series | 6,863 | — | IfcFireSuppressionTerminal, IfcAlarm, IfcPipeSegment, IfcPipeFitting, IfcValve, IfcFlowController |
| ACMV | Air Conditioning & Mechanical Ventilation | M-series | 1,621 | — | IfcDuctSegment, IfcDuctFitting, IfcAirTerminal |
| CW | Chilled Water | — | 1,431 | — | IfcPipeSegment, IfcPipeFitting, IfcFlowTerminal, IfcValve |
| ELEC | Electrical | E-series | 1,172 | — | IfcLightFixture, IfcElectricAppliance |
| SP | Sanitary Plumbing | P-series | 979 | — | IfcPipeSegment, IfcPipeFitting, IfcFlowTerminal, IfcValve |
| LPG | Liquefied Petroleum Gas | — | 209 | — | IfcPipeSegment, IfcPipeFitting, IfcValve* |
| REB | Reinforcing Bar | S-series (sub) | 2,660 | — | IfcReinforcingBar |
* Shared IFC classes: IfcPipeSegment, IfcPipeFitting, IfcValve, IfcFlowTerminal
appear in multiple disciplines (FP, CW, SP, LPG). Disambiguation requires
system_type from IFC property sets — see §5.
DX coarse mapping: DX extraction currently classifies all piping/electrical as
MEP (904 elements). Reclassification into PLB/ELC/FPR needed for fine-grained
discipline BOMs. SH has 3 CW elements (58 total: ARC+STR+CW).
SH stays flat: No discipline wrapper for proven single-discipline stone. RosettaStone integrity preserved as-is for sanity.
2. BOM Tree Structure — 5 Levels¶
Mirrors real construction drawing sheet hierarchy: discipline letter + storey number.
L0: BUILDING_DX_STD (the project)
│
├── DUPLEX_MEP_TRUNK_STD (storey-spanning verticals: risers)
│ ├── PLB_RISER_DX (plumbing riser)
│ ├── ELC_RISER_DX (electrical riser, if any)
│ └── FPR_RISER_DX (fire protection riser, if any)
│
├── FLOOR_DX_L1_STD (L1: storey)
│ ├── ARC_DX_L1 (A-101: discipline BOM)
│ │ ├── room SETs (LIVING, KITCHEN…) (assembly)
│ │ │ └── wall, door, window, furniture (leaves)
│ │ └── structural-arch (slabs, stairs, railings)
│ ├── PLB_DX_L1 (P-101: plumbing)
│ │ └── pipe runs, fittings, fixtures
│ ├── ELC_DX_L1 (E-101: electrical)
│ │ └── conduit runs, receptacles, switches, lights
│ ├── FPR_DX_L1 (FP-101: fire protection)
│ │ └── sprinkler mains, heads, alarms
│ └── STR_DX_L1 (S-101: structural)
│ └── beams, members
│
└── FLOOR_DX_L2_STD (L2: storey)
├── ARC_DX_L2 (A-201)
├── PLB_DX_L2 (P-201)
├── ELC_DX_L2 (E-201)
├── FPR_DX_L2 (FP-201)
└── STR_DX_L2 (S-201)
Terminal at scale (same structure):
L0: BUILDING_TE_STD
├── [per storey]
│ ├── ARC_TE_LXX (~34,724 elements / N storeys)
│ ├── STR_TE_LXX (~1,429)
│ ├── FP_TE_LXX (~6,863)
│ ├── ACMV_TE_LXX (~1,621)
│ ├── CW_TE_LXX (~1,431)
│ ├── ELEC_TE_LXX (~1,172)
│ ├── SP_TE_LXX (~979)
│ ├── LPG_TE_LXX (~209)
│ └── REB_TE_LXX (~2,660)
└── [risers / storey-spanning trunks]
Why 5 levels is correct: A general contractor doesn't hand the whole building to the electrician. He hands him E-101 (Floor 1 Electrical), E-201 (Floor 2 Electrical), and the riser diagram. Three BOM nodes = three scopes of work. Each discipline BOM per storey = one drawing sheet = one subcontract scope. The hierarchy encodes contractual reality, not just element counts. Even FPR with 1 element on a floor gets its own node — fire protection is a separate contract, separate permit, separate inspection.
Storey first, then discipline: The subcontractor works one floor at a time within his trade. He completes Floor 1 electrical, then moves up. He doesn't install all conduits in the building top-to-bottom. Exception: risers (vertical trunks spanning storeys) sit at BUILDING level, not under any floor.
3. M_Product_Category as Discipline Classifier¶
M_Product_Category (the former M_BomCategory, now aligned with iDempiere naming) serves as the shared catalog
classifier. Discipline routing uses AD_Org_ID (integer FK to AD_Org), not string codes:
| AD_Org_ID | Discipline | Drawing Sheet | Legacy bom_category |
|---|---|---|---|
| (per tenant) | Architectural | A-series | ARC |
| (per tenant) | Structural | S-series | STR |
| (per tenant) | Plumbing | P-series | PLB |
| (per tenant) | Electrical | E-series | ELC |
| (per tenant) | Fire Protection | FP-series | FPR |
| (per tenant) | HVAC/Mechanical | M-series | ACMV |
| (per tenant) | Sanitary Plumbing | P-series | SP |
| (per tenant) | LPG | — | LPG |
| (per tenant) | Reinforcing Bar | S-series (sub) | REB |
Storey codes (GF, L1, L2, RF, FN, MS, CW) and room types (LI, BD, KT, FR, RE)
remain on bom_category as catalog classifiers — they are not disciplines.
The discipline lives on AD_Org_ID, NOT on M_Product. A product is
just a product (pipe, conduit, sprinkler head). Which discipline it belongs to
is determined by which discipline org its BOM tree sits in. Same product can appear
in multiple discipline BOMs — same screw in 50 BOMs (pure iDempiere pattern).
4. Two Classifiers on M_Product — Independent Dimensions¶
iDempiere teaches us two orthogonal classifiers:
| Classifier | Lives On | What It Means | Example |
|---|---|---|---|
| M_Product_Category | m_bom (the BOM node) |
Organizational grouping — which trade | PLB, ELC, FPR |
| M_AttributeSet | M_Product (the leaf) |
Engineering behavior — how to parameterize | BIM_Pipe, BIM_Wall |
They correlate but don't collapse:
- A BIM_Pipe product lives inside a PLB discipline BOM — but could also appear
in FPR (fire protection pipes use same fittings).
- A BIM_Component product (smoke detector) is discipline-agnostic at the product
level — it becomes FPR by living under a FPR discipline BOM.
M_AttributeSet is for generative mode — when a user selects a wall variant (material, thickness) like iDempiere GardenWorld's T-shirt (Size S/M/L × Color Red/Blue). For extracted RosettaStones, there's no selection moment — the IFC file already decided. AttributeSet columns stay NULL during extraction.
Five attribute sets (§11.38, designed for generative future):
| M_AttributeSet | IsInstance | Generative Role |
|---|---|---|
BIM_Pipe |
1 (length varies) | Cross-section stamp; length = instance attribute |
BIM_Conduit |
1 (length varies) | Same pattern as pipe |
BIM_Wall |
1 (height varies) | Thickness stamp; height/length = instance |
BIM_Slab |
1 (area varies) | Thickness stamp; area = instance |
BIM_Component |
0 (identical) | Every instance is the same (qty on BOM line) |
5. YAML Schema v2 — Discipline Classification¶
5.1 Structure¶
schema_version: 2
building:
# ... existing fields (building_type, prefix, storeys, etc.) ...
disciplines:
ARC:
ifc_classes: [IfcWall, IfcDoor, IfcWindow, IfcSlab,
IfcFurnishingElement, IfcCovering,
IfcRailing, IfcStairFlight, IfcRoof, IfcRampFlight]
ad_org_id: ARC # resolved to integer FK at load time
STR:
ifc_classes: [IfcBeam, IfcMember, IfcColumn, IfcPlate]
ad_org_id: STR
PLB:
ifc_classes: [IfcPipeSegment, IfcPipeFitting, IfcFlowTerminal, IfcValve]
filter: { system_type: [Domestic Cold Water, Domestic Hot Water,
Sanitary, Waste, Hydronic Supply, Hydronic Return] }
ad_org_id: PLB
ELC:
ifc_classes: [IfcLightFixture, IfcElectricAppliance,
IfcFlowSegment, IfcFlowTerminal, IfcFlowController]
filter: { system_type: [Electrical, Telecom, Lightning] }
ad_org_id: ELC
FPR:
ifc_classes: [IfcFireSuppressionTerminal, IfcAlarm, IfcSensor,
IfcPipeSegment, IfcPipeFitting, IfcValve, IfcFlowController]
filter: { system_type: [Fire Alarm, Sprinkler, Fire Suppression] }
ad_org_id: FPR
ACMV:
ifc_classes: [IfcDuctSegment, IfcDuctFitting, IfcAirTerminal]
ad_org_id: ACMV
SP:
ifc_classes: [IfcPipeSegment, IfcPipeFitting, IfcFlowTerminal, IfcValve]
filter: { system_type: [Sanitary Waste, Storm Drain, Vent] }
ad_org_id: SP
LPG:
ifc_classes: [IfcPipeSegment, IfcPipeFitting, IfcValve]
filter: { system_type: [LPG, Gas] }
ad_org_id: LPG
REB:
ifc_classes: [IfcReinforcingBar]
ad_org_id: REB
5.2 Disambiguation via system_type Filter¶
Some IFC classes (IfcPipeSegment, IfcPipeFitting, IfcValve) appear across
multiple disciplines. The filter.system_type disambiguates using IFC property
set data (Pset_DistributionSystemCommon.PredefinedType or similar).
Prerequisite: The extraction pipeline must capture system_type from IFC
property sets into I_Element_Extraction. This column does not exist yet.
Until it does, DX MEP elements can be classified by element_ref string
parsing (e.g., "Domestic Cold Water" in the Revit type name) as an interim
heuristic.
5.3 Schema v1 Backward Compatibility¶
Buildings without a disciplines: key (schema_version 1, e.g. classify_sh.yaml)
default to single-discipline ARC — no discipline BOM level inserted. SH
RosettaStone is untouched.
6. Extracted vs Generative — One Schema, Two Modes¶
The BOM model is designed once. Extraction populates positions. Generative mode (future) populates rules. Same columns, different fill patterns:
| Column | Extracted (current) | Generative (future) |
|---|---|---|
m_bom_line.dx/dy/dz |
Copied from IFC (parent-relative) | Computed from rules + AABB |
m_bom_line.layout_strategy |
NULL | GRID, CEILING_RUN, AXIS_ALIGNED |
m_bom_line.z_rule |
NULL | CEILING_OFFSET, WALL_HEIGHT, FLOOR_EMBED |
m_bom_line.anchor_face |
NULL | TOP, WALL, FLOOR |
m_bom_line.qty |
1 (one line per element) | N (rule expands to N instances) |
m_attribute regulation params |
NULL | max_spacing, coverage_area, regulation_ref |
6.1 Generative Pattern Grammars (future reference)¶
Three discipline families produce three fundamentally different placement patterns:
ARC = Containment (nested boxes) Room enclosure: walls forming polygon + openings + furniture placed within AABB. Already proven in SH. Pattern = spatial arrangement within a bounding box.
STR = Grid repetition (parametric spacing) Column-beam grid at regular intervals. BOM captures one bay; qty + parametric offset reproduces the grid.
MEP = Topology (directed connection graph) Pipe/conduit runs: source → segment → fitting → segment → terminal device. Pattern = directed connection chain. Tack I/O captures which port connects where.
Examples (illustrative, not current scope): - Sprinkler grid: NFPA 13 Light Hazard → max 4.6m spacing, 2.3m from wall, 200mm below ceiling. Given room AABB, compiler produces head positions. - Conduit ceiling run: NEC-compliant axis-aligned routing, 25mm below slab soffit, 90° turns only, hanger every 1500mm.
These rules slot into the existing m_bom_line columns (layout_strategy,
z_rule, anchor_face) and m_attribute overflow table without schema changes.
6.2 Tack I/O for MEP Topology (future)¶
Current m_bom_line has placement (dx/dy/dz) but not explicit connection ports.
For ARC containment, position is sufficient. For MEP topology, the walker needs
to know which port of a tee connects to which downstream pipe.
Future columns or m_attribute entries:
- tack_in — upstream connection port identifier
- tack_out — downstream connection port identifier(s)
- connection_type — rigid, flexible, sealed, open
Not needed for extraction (positions are explicit). Required for generative MEP routing where the compiler must chain segments through fittings.
7. Implementation Sequence¶
Phase 1: DX Discipline Reclassification¶
- Reclassify DX's 904
MEPelements into PLB/ELC/FPR usingelement_refheuristic - Update
I_Element_Extraction.disciplinefor DX elements - Verify counts: PLB + ELC + FPR = 904
Phase 2: DX Discipline BOM Generation¶
- Update
classify_dx.yamlto schema_version 2 withdisciplines:map - Extend IFCtoBOM pipeline to read discipline map and create per-storey discipline BOM nodes (ARC_DX_L1, PLB_DX_L1, ELC_DX_L1, etc.)
- Generate
DX_BOM.dbwith discipline-organized BOM tree - Verify: BOM walk reproduces all 1099 elements
Phase 3: Compilation Pipeline¶
- BOM walker traverses discipline BOM level transparently (it's just another m_bom node — no walker changes needed if the tree is correct)
- Verify: SH compilation unchanged (no discipline level in SH BOM)
- Verify: DX compilation produces same 1099 elements via discipline BOMs
- G1-G6 gates GREEN for both SH and DX
Phase 4: Terminal Discipline Structure (Phase B scope)¶
- Terminal already has 9 disciplines in
I_Element_Extraction.discipline - Assign storey values (currently "Unknown" for all Terminal elements)
- Create
classify_te.yamlschema_version 2 - Generate
TE_BOM.dbwith 9 discipline sub-trees per storey - Scale proof: 51,088 elements organized into ~N×9 discipline BOMs
Future: Generative Extension¶
- Add
system_typecolumn toI_Element_Extraction(from IFC property sets) - Populate
layout_strategy,z_rule,anchor_faceon m_bom_line - Populate
m_attributewith regulation parameters (spacing, clearance, code ref) - Compiler reads rules + AABB → produces positions (instead of copying from IFC)
- Tack I/O for MEP connection topology
8. Design Decisions¶
D1: Storey first, discipline second. Matches real construction sequencing — subcontractors work one floor at a time within their trade. Exception: risers (storey-spanning verticals) sit at BUILDING level.
D2: Fine discipline split even for small counts. FPR with 1 element on a floor still gets its own BOM node. Fire protection is a separate contract, separate permit, separate inspection. The BOM encodes contractual reality, not element count.
D3: SH stays flat.
No discipline wrapper for proven single-discipline RosettaStone. Schema v1
backward compatibility — buildings without disciplines: key default to
single-discipline, no extra BOM level.
D4: Discipline on BOM, not on Product.
AD_Org_ID carries the discipline (integer FK to AD_Org — see S78). M_Product is discipline-agnostic.
Same product can appear in multiple discipline BOMs (iDempiere pattern: same
screw in 50 BOMs).
D5: AttributeSet is for generative mode. M_AttributeSet on M_Product defines product variant behavior (pipe length varies, component is identical). Irrelevant for extraction — relevant when the compiler generates placements from rules instead of copying from IFC.
D6: One schema, two modes. m_bom_line columns (layout_strategy, z_rule, anchor_face) and m_attribute regulation params stay NULL for extraction, get populated for generative mode. No schema changes when transitioning from extraction to generation.
9. LOD Assembly — Library Geometry Only¶
No parametric mesh in pipeline (feedback_no_parametric.md). Every element gets its geometry from component_library.db:
C_OrderLine.family_ref (product_id)
→ M_Product (width, depth, height, ifc_class)
→ component_definitions (attachment_face, orientation, geometry_hash)
→ component_geometries (vertices, faces, normals via geometry_hash)
ASI overrides instance sizing:
M_AttributeSetInstance → M_AttributeInstance (width_mm, depth_mm, height_mm)
ASI controls scaling — library LOD provides base shape.
9.1 DocEvent LOD Resolution Chain¶
When YAML.discipline = DocEvent, the engine resolves default products per
space type and area via a 5-table chain spanning ERP.db (steps 1-3)
and component_library.db (steps 4-5):
DocEvent processIt() for discipline FP in a BEDROOM (12m²):
Step 1: ad_space_type_mep_bom
WHERE space_type_id = 'BEDROOM' AND mep_product_id = 'SPRINKLER'
→ qty_normal = 0 (use per_area instead)
→ per_area_normal = 0.07/m² → qty = ceil(12 × 0.07) = 1
→ placement_rule = 'CEILING_GRID'
→ host_surface = 'CEILING'
→ building_code = 'NFPA 13', code_clause = '8.5.5'
Step 2: ad_element_mep
WHERE element_type = 'SPRINKLER'
→ ifc_class = 'IfcFireSuppressionTerminal'
→ discipline = 'FP'
→ host_type = 'CEILING'
→ ports = [{"id":"IN","size":0.015}]
→ code_ref = 'NFPA 13'
Step 3: ad_fp_coverage (FP-specific spacing)
WHERE hazard_class = 'LIGHT' (from YAML occupancy_class or project default)
→ max_spacing_m = 4.6, min_spacing_m = 1.8
→ wall_distance_m = 2.3
→ max_coverage_m2 = 18.6
→ k_factor = 5.6
Step 4: M_Product (actual product with dimensions)
Resolved via ad_element_mep_alias cascade (DISC_VALIDATION_DB_SRS.md §5.1):
P1: ifc_class match → P2: predefined_type → P3: type_class → P4: element_name LIKE
→ product_id, width, depth, height
→ Multiple products may match — select by building_type affinity
or closest dimensions to room context
Step 5: component_definitions → component_geometries (LOD mesh)
WHERE name LIKE '%sprinkler%' (matched via product_id → geometry_hash)
→ attachment_face = 'TOP' (pendant) or 'BOTTOM' (upright)
→ orientation = 'PENDANT' or 'UPRIGHT'
→ geometry_hash → vertices, faces, normals (actual mesh)
9.2 Metadata Tables — What Each Contributes¶
| Table | Role | Key Columns | Drives |
|---|---|---|---|
ad_space_type_mep_bom |
Default product schedule per room type | space_type_id, mep_product_id, qty_normal, per_area_normal, placement_rule, host_surface, building_code | HOW MANY + WHERE |
ad_element_mep |
Logical MEP element definition | element_type, ifc_class, discipline, host_type, ports, mount_height, clearance | WHAT (ifc_class + ports) |
ad_fp_coverage |
FP-specific coverage by hazard class | hazard_class, max_spacing_m, max_coverage_m2, wall_distance_m, k_factor | SPACING (FP only) |
M_Product |
Physical product with dimensions | product_id, width, depth, height, ifc_class | DIMENSIONS |
component_definitions |
LOD geometry link + attachment convention | name, geometry_hash, attachment_face, orientation | LOD MESH + ATTACHMENT |
component_geometries |
Actual mesh data | geometry_hash, vertices, faces, normals | GEOMETRY |
9.3 Quantity Resolution — Fixed vs Per-Area¶
| Mode | When | Formula | Example |
|---|---|---|---|
| Fixed qty | qty_normal > 0 AND per_area_normal = 0 |
qty = qty_normal |
BATHROOM/TOILET = 1, KITCHEN/SINK = 1 |
| Per-area | per_area_normal > 0 |
qty = ceil(room_area_m2 × per_area_normal) |
OFFICE/SPRINKLER = ceil(50 × 0.07) = 4 |
| Both zero | qty_normal = 0 AND per_area_normal = 0 |
Not placed in this room type | CORRIDOR/OUTLET = 0 |
9.4 Placement Rule → Verb Mapping¶
| Placement Rule | Verb | Position Computation |
|---|---|---|
CEILING_CENTER |
PLACE | Room centroid, at ceiling Z |
CEILING_GRID |
TILE / ALONG | Grid at spacing from ad_fp_coverage or AD_Val_Rule |
WALL_ENTRY |
PLACE | Near door, at mount_height from ad_element_mep |
WALL_SPACED |
ALONG | Evenly spaced along longest wall |
WALL_BACK |
PLACE | Against back wall, at mount_height |
WALL_SINK |
PLACE | Adjacent to sink, at mount_height |
WALL_COOKER |
PLACE | Above cooker position |
WALL_HIGH |
MOUNT | High on wall (AC point: ~2.3m) |
COUNTER_BACK |
PLACE | At counter height, against wall |
FLOOR_LOW |
PLACE | Floor level, low point for drainage |
AUTO |
(engine decides) | Fallback — centroid or grid based on qty |
9.5 Coverage by Space Type (current seed data)¶
| Space Type | SPRINKLER | LIGHT | OUTLET | EXHAUST | DIFFUSER | Code Base |
|---|---|---|---|---|---|---|
| BEDROOM | 0.07/m² | 1 | 3 | — | 1 | NFPA 13 + NEC + MS1525 |
| BATHROOM | 1 | 1 | 1 GFCI | 1 | — | NFPA 13 + NEC + IMC |
| KITCHEN | 1 | 2 | 3×20A + 2 GFCI | 1 | 1 | NFPA 13 + NEC + IMC |
| LIVING | 0.07/m² | 2 | 4 | — | 1 | NFPA 13 + NEC + MS1525 |
| OFFICE | 0.07/m² | 0.1/m² | 0.2/m² | — | 1 | NFPA 13 + NEC |
| CORRIDOR | 0.05/m² | 1 | — | — | 1 | NFPA 13 + NEC |
| ASSEMBLY_HALL | 0.07/m² | 0.1/m² | 6 | — | 1 | NFPA 13 + NEC |
Metadata-driven. Adding a new space type or changing a product count = SQL UPDATE, no Java change.
10. Post-Placement Handlers — Connectivity, Clash, Completeness¶
After elements are placed (by either {prefix}_BOM pipeline or DocEvent),
handler routines run to ensure the placed elements form a valid construction
system. These are iDempiere ModelValidator.afterSave() equivalents — they
fire after each placement batch, not per-element.
10.1 Handler Cascade¶
Elements placed (from BOM pipeline or DocEvent)
│
├── H1: CONNECTIVITY handler
│ │ Ensures every terminal connects to a source via fittings/pipes.
│ │
│ │ IFC GROUPING SEMANTICS: IFC's IfcRelConnectsPortToElement and
│ │ IfcDistributionPort define system topology. The BOM tree mirrors
│ │ this: DISCIPLINE BOM → RISER (parent) → BRANCH (child) → HEAD
│ │ (leaf). Parent-child in the BOM IS the connection graph. A
│ │ disconnected element = orphan node with no parent in its
│ │ discipline sub-tree. ad_assembly_connector.connects_to names
│ │ the system network (PLUMBING_STACK, FP_MAIN, etc.) — the BOM
│ │ tree path from leaf to root must traverse these system names.
│ │
│ │ For FP: BFS from riser → every sprinkler head reachable?
│ │ For CW: BFS from supply → every fixture has supply_in?
│ │ For SP: BFS from stack → every fixture has waste_out?
│ │ For ELEC: BFS from panel → every outlet/light has circuit?
│ │
│ │ Uses: ad_assembly_connector.connects_to (system graph)
│ │ ad_element_mep.ports (port definitions)
│ │ BOM tree Parent_OrderLine_ID (containment = connectivity)
│ │
│ │ If disconnected:
│ │ → Auto-insert connecting pipe/conduit segments (ROUTE verb)
│ │ → Or flag WARN: "sprinkler_23 unreachable from riser"
│ │
│ └── Writes: W_Verb_Node (verb_ref='CONNECT FITTINGS')
│ W_Validation_Result (tier=1, CONNECTIVITY check)
│
├── H2: NON-CLASH handler
│ │ Ensures no hard/soft clashes between placed elements and
│ │ elements from OTHER disciplines already in the BOM.
│ │
│ │ For each newly placed element:
│ │ → ERP-maths clearance against all elements on same storey
│ │ clearance = centroid_dist - radius_a - radius_b
│ │ → Check AD_Clash_Rule for discipline pair
│ │
│ │ Uses: AD_Clash_Rule (discipline pairs, clash_type, min_distance_mm)
│ │ M_Product dimensions (cross-section radii)
│ │
│ │ If clash detected:
│ │ → HARD clash: nudge element to nearest clear position
│ │ → SOFT clash: flag WARN with clearance value
│ │ → MATERIAL clash: flag WARN with resolution_note
│ │ ("Add fire collar at penetration point")
│ │
│ └── Writes: W_Validation_Result (tier=2, CLASH check)
│ C_OrderLine.dx/dy/dz updated if nudged
│
├── H3: SPACING COMPLIANCE handler
│ │ Ensures placed elements meet code spacing requirements.
│ │
│ │ For FP: NN distance between heads ∈ [min_spacing, max_spacing]
│ │ Wall distance ≥ wall_distance_m (ad_fp_coverage)
│ │ For ELEC: NN distance between fixtures ≤ max_spacing_mm
│ │ For outlets: NEC 210.52 — max 1.8m from any point on wall
│ │
│ │ Uses: AD_Val_Rule (NFPA13_LH_SPACING, IES_LIGHT_SPACING, etc.)
│ │ ad_fp_coverage (hazard-class-specific thresholds)
│ │
│ │ If spacing violated:
│ │ → Adjust grid pitch to comply (recalc from processIt formula)
│ │ → Or insert additional element to fill gap
│ │ → Flag WARN if adjustment exceeds ±10% of typical_spacing
│ │
│ └── Writes: W_Validation_Result (tier=1, SPACING check)
│
├── H4: HOST ATTACHMENT handler
│ │ Ensures every placed element has a valid host surface.
│ │
│ │ IFC GROUPING SEMANTICS: IFC's IfcRelVoidsElement (openings in
│ │ walls) and IfcRelFillsElement (doors/windows filling voids)
│ │ encode host relationships explicitly. For MEP, IFC uses
│ │ IfcRelContainedInSpatialStructure to place elements within
│ │ their spatial container (storey/room). The BOM tree mirrors
│ │ this: a CEILING-hosted element sits under a FLOOR node whose
│ │ AABB defines the ceiling surface. A WALL-hosted element sits
│ │ under a ROOM node with ad_wall_face defining available faces.
│ │ Host existence = parent node + face existence in the BOM tree.
│ │
│ │ CEILING elements: parent FLOOR node has slab (AABB height > 0)
│ │ WALL elements: parent ROOM node has ad_wall_face for that face
│ │ FLOOR elements: parent FLOOR node has ground slab
│ │
│ │ Uses: ad_element_mep.host_type
│ │ ad_space_type_mep_bom.host_surface
│ │ ad_wall_face (room boundary faces)
│ │ placement_rules (offset_from_host)
│ │ BOM tree AABB (parent node dimensions)
│ │
│ │ If host missing:
│ │ → Flag WARN: "light_01 has no ceiling host at Z=2.8m"
│ │ → Snap to nearest valid host surface
│ │
│ └── Writes: W_Validation_Result (tier=1, HOST check)
│
├── H5: VERTICAL CONTINUITY handler
│ │ For risers/stacks that span storeys: verify X,Y alignment.
│ │
│ │ IFC GROUPING SEMANTICS: IFC's spatial containment hierarchy
│ │ (IfcRelContainedInSpatialStructure, IfcRelAggregates) encodes
│ │ the storey relationship. When elements sit under their correct
│ │ IfcBuildingStorey, the absolute Z is inherited from the storey
│ │ elevation. The BOM tree mirrors this exactly:
│ │ BUILDING → FLOOR (dz = storey elevation) → DISCIPLINE → LEAF
│ │ Tack dz on each node is RELATIVE to parent (BBC.md §4).
│ │ World Z = sum of dz up the tree. This means vertical alignment
│ │ is a TREE STRUCTURE concern, not a raw coordinate concern.
│ │
│ │ What H5 actually checks:
│ │ → Riser elements exist under correct storey nodes
│ │ → Same riser's dx/dy is IDENTICAL across storey nodes
│ │ (only dz differs — inherited from parent FLOOR node)
│ │ → If dx/dy differs across storeys → tree is malformed
│ │
│ │ Uses: AD_Val_Rule WHERE rule_type='CONTINUITY'
│ │ max_xy_drift_mm per discipline
│ │ BOM tree parent hierarchy (Parent_OrderLine_ID)
│ │
│ │ If drift detected:
│ │ → Snap riser segment dx/dy to match storey below
│ │ → Flag WARN if drift > tolerance
│ │ → Root cause is almost always REPARENT to wrong node
│ │
│ └── Writes: W_Validation_Result (tier=3, CONTINUITY check)
│
└── H6: COMPLETENESS handler
│ Ensures the room has all required MEP elements per
│ ad_space_type_mep_bom schedule.
│
│ IFC GROUPING SEMANTICS: IFC's IfcRelAggregates decomposes a
│ spatial element into its parts. A room (IfcSpace) aggregates
│ all elements within it. The BOM tree mirrors this: ROOM node
│ contains all LEAF children. Completeness = counting children
│ of a ROOM node by mep_product_id and comparing to the schedule.
│ The spatial container IS the count boundary — no spatial query
│ needed, just a tree walk of C_OrderLine children.
│
│ For each space_type_id × mep_product_id row:
│ → Count C_OrderLine children under ROOM node
│ WHERE family_ref resolves to matching mep_product_id
│ → Compare to qty_normal (or ceil(area × per_area_normal))
│ → If count < required: flag WARN with missing items
│
│ Uses: ad_space_type_mep_bom (the schedule)
│ BOM tree Parent_OrderLine_ID (room containment)
│ AD_Org_ID on ROOM node (space_type + discipline lookup)
│
│ Example: BATHROOM missing EXHAUST_FAN
│ → WARN: "BATHROOM requires 1 EXHAUST_FAN per IMC 2021 403.3"
│
└── Writes: W_Validation_Result (tier=1, COMPLETENESS check)
10.2 Handler Summary¶
Common handlers fire for ALL disciplines — both {prefix}_BOM and DocEvent.
They are shared cross-cutting checks, like iDempiere's tax calculation that
fires on every invoice line regardless of product category.
| Handler | What | Common? | Fires After | Auto-Fix | Tier |
|---|---|---|---|---|---|
| H1 CONNECTIVITY | Every terminal reachable from source | COMMON | Each discipline batch | Insert connecting segments | 1 |
| H2 NON-CLASH | No hard/soft clashes across disciplines | COMMON | Each discipline batch | Nudge to clear position | 2 |
| H3 SPACING | Code-compliant spacing (NFPA, NEC, IES) | DocEvent only | FP, ELEC batches | Adjust grid pitch | 1 |
| H4 HOST ATTACHMENT | Every element has valid host surface | COMMON | Each discipline batch | Snap to nearest host | 1 |
| H5 VERTICAL CONTINUITY | Risers/stacks aligned across storeys | COMMON | After all storeys | Snap to match below | 3 |
| H6 COMPLETENESS | Room has all required MEP per schedule | COMMON | After all disciplines | Flag missing items | 1 |
H1, H2, H4, H5, H6 are common — they validate structural integrity
regardless of how elements arrived (BOM pipeline or DocEvent). A pipe from
{prefix}_BOM needs connectivity just as much as one placed by DocEvent.
H3 (SPACING) is DocEvent-only because {prefix}_BOM elements already have
their spacing baked into the BOM tack positions from extraction. DocEvent
computes spacing from rules, so it needs the compliance check.
10.3 Handler → Verb → Metadata Linkage¶
| Handler | Verb Used | Metadata Read |
|---|---|---|
| H1 | CONNECT FITTINGS, JOIN |
ad_assembly_connector, ad_element_mep.ports |
| H2 | CHECK CLASH |
AD_Clash_Rule, M_Product (cross-section) |
| H3 | CHECK PLACEMENT |
AD_Val_Rule, ad_fp_coverage |
| H4 | ATTACH, MOUNT, HANG |
placement_rules, component_definitions.attachment_face |
| H5 | (vertical check) | AD_Val_Rule WHERE rule_type='CONTINUITY' |
| H6 | (schedule audit) | ad_space_type_mep_bom |
All handlers write to W_Validation_Result in output.db. The ambient
compliance strip in BIM Designer reads these results to show live status.
Handlers that auto-fix also update C_OrderLine.dx/dy/dz (nudge/snap).
10.4 Implementation Status & Preconditions¶
Status (session 34): H1-H6 handlers are DESIGNED, NOT IMPLEMENTED. Zero handler code exists. The cascade above is the target specification.
Metadata table readiness (ERP.db created session 33):
| Table | DB | Status | Seeded? | Blocks |
|---|---|---|---|---|
ad_space_type_mep_bom |
ERP.db | CREATED (DV001+DV002) | YES — 186 rows (41 space types × 12 MEP products) | H6 |
ad_element_mep |
ERP.db | CREATED (DV001+DV002) | YES — 12 element types | H1, H4 |
ad_element_mep_alias |
ERP.db | CREATED (DV003) | YES — 84 alias rows (4-tier IFC cascade) | H1, H4 |
ad_fp_coverage |
ERP.db | CREATED (DV001+DV002) | YES — 4 hazard classes | H1, H3 |
ad_assembly_connector |
ERP.db | CREATED (DV001+DV002) | YES — 10 connector rows | H1 |
ad_wall_face |
ERP.db | CREATED (DV001+DV002) | YES — 204 rows | H4 |
placement_rules |
ERP.db | CREATED (DV001+DV002) | YES — 4801 rows | H4 |
AD_Clash_Rule |
ERP.db | SCHEMA ONLY | NO — 0 rows | H2 blocked |
AD_Val_Rule (SPACING) |
ERP.db | SCHEMA ONLY | NO — 0 rows of type SPACING | H3 blocked |
AD_Val_Rule (CONTINUITY) |
ERP.db | SCHEMA ONLY | NO — 0 rows of type CONTINUITY | H5 blocked |
H3 SPACING blocked: H3 requires both ad_fp_coverage (seeded, 4 rows) AND
AD_Val_Rule WHERE rule_type='SPACING' (0 rows). The coverage thresholds exist
but the rule engine entry to trigger H3 does not. H3 fires DocEvent-only because
{prefix}_BOM elements have spacing baked into tack positions from extraction.
Precondition for TE validation: CLUSTER verb fidelity must improve before handlers can produce meaningful results. Current 29mm max positional error (improved from 29m after F3 sort fix, session 34) may still generate false-positive clash/connectivity/spacing violations at tight tolerances. Handler implementation should follow CLUSTER→exact verb promotion.
Precondition for generative (DocEvent) validation: All metadata tables
above must be seeded. H3 SPACING requires ad_fp_coverage + AD_Val_Rule
rows. H2 NON-CLASH requires AD_Clash_Rule discipline-pair rows.
10.5 Handler Witness Claims¶
Each handler requires witness claims BEFORE implementation (CTFL best practice).
Implementation is BLOCKED until these claims are written as @Test methods.
| Witness | Handler | What it Proves | Acceptance Criteria |
|---|---|---|---|
| W-H1-CONNECT-1 | H1 | Every FP head reachable from riser via BOM tree path | BFS from riser node reaches all IfcFireSuppressionTerminal leaves. Path length ≤ 50 nodes. |
| W-H1-CONNECT-2 | H1 | Disconnected element flagged WARN | Orphan leaf with no parent in discipline sub-tree → W_Validation_Result(tier=1, result='WARN'). |
| W-H2-CLASH-1 | H2 | No hard clash between FP and ELEC on same storey | ERP-maths clearance ≥ AD_Clash_Rule.min_distance_mm for all FP×ELEC pairs. |
| W-H2-CLASH-2 | H2 | Clash detected and nudged | Element moved to nearest clear position. C_OrderLine.dx/dy/dz updated. W_Validation_Result(tier=2, result='WARN'). |
| W-H3-SPACING-1 | H3 | FP NN spacing within [min, max] | All head pairs ≥ min_spacing_m AND ≤ max_spacing_m from ad_fp_coverage. |
| W-H3-SPACING-2 | H3 | Wall distance ≥ wall_distance_m | Every head ≥ wall_distance_m from nearest wall face (ad_fp_coverage). |
| W-H4-HOST-1 | H4 | Ceiling element has valid host | Parent FLOOR node has slab (AABB height > 0). ad_element_mep.host_type = 'CEILING' matches. |
| W-H4-HOST-2 | H4 | Missing host flagged WARN | Element with no valid host surface → W_Validation_Result(tier=1, result='WARN'). |
| W-H5-CONT-1 | H5 | Riser X,Y identical across storeys | Same riser's dx,dy values differ ≤ max_xy_drift_mm across all FLOOR nodes. |
| W-H5-CONT-2 | H5 | XY drift flagged WARN | Riser with dx drift > max_xy_drift_mm → W_Validation_Result(tier=3, result='WARN'). |
| W-H6-COMPLETE-1 | H6 | Room has all required MEP per schedule | BATHROOM has ≥ 1 EXHAUST_FAN, 1 SPRINKLER, 1 LIGHT per ad_space_type_mep_bom. |
| W-H6-COMPLETE-2 | H6 | Missing element flagged WARN | BATHROOM missing EXHAUST_FAN → W_Validation_Result(tier=1, result='WARN', description contains 'IMC 2021 403.3'). |
Auto-fix acceptance criteria:
| Handler | Auto-fix Action | Limit | If Exceeded |
|---|---|---|---|
| H1 | Insert connecting pipe/conduit segment | ≤ 3 segments | WARN (manual routing needed) |
| H2 | Nudge element to clear position | ≤ 100mm | WARN (clash too severe for auto-fix) |
| H3 | Adjust grid pitch | ≤ ±10% of typical_spacing | WARN (room too small/large for standard grid) |
| H4 | Snap to nearest valid host surface | ≤ 200mm | WARN (no host nearby) |
| H5 | Snap riser dx/dy to match storey below | ≤ max_xy_drift_mm | BLOCK (tree structure error) |
| H6 | Flag missing items (no auto-insert) | — | WARN always (user decides) |
References: BBC.md §1 (P0.1-DEDUP), BOMBasedCompilation.md §4 (tack convention), TheRosettaStoneStrategy.md (discipline vocabulary), CONCEPTUAL BLUEPRINT.txt (MEP AttributeSet taxonomy), DocAction_SRS.md §1 (processIt lifecycle), DocValidate.md §13 (three-tier cascade), BIM_COBOL.md §4.6 (joining verbs)