ERP.db SRS — Discipline Validation Database¶
Foundation: BBC · DATA_MODEL · BIM_COBOL · MANIFESTO · TestArchitecture
Version: 1.3 (2026-03-31) Depends on: DISC_VALIDATE_SRS.md §9-10, DocAction_SRS.md §1.3, CALIBRATION_SRS.md
1. Schema — ERP.db (22 tables)¶
Authoritative DDL: migration/DV001_ERP_schema.sql, migration/DV003_element_mep_alias.sql
| Table | PK | Rows | Purpose |
|---|---|---|---|
ad_space_type |
space_type_id | 41 | Space type taxonomy (BEDROOM, OFFICE, etc.) |
ad_element_mep |
element_type | 12 | Canonical MEP types (OUTLET, SPRINKLER, etc.) |
ad_space_type_mep_bom |
(space_type_id, mep_product_id) | 186 | Discipline schedule: what MEP goes in each room |
ad_fp_coverage |
hazard_class | 4 | NFPA 13 sprinkler coverage thresholds |
ad_assembly_connector |
connector_id | 10 | Assembly connection topology |
ad_assembly_manifest |
manifest_id | 37 | Assembly interface definitions |
ad_wall_face |
id | 204 | Room boundary faces per building |
placement_rules |
id | 4801 | Host/offset placement rules |
ad_space_adjacency |
(space_type_a, space_type_b) | 22 | Room adjacency relationships |
ad_fp_trigger |
trigger_id | 12 | FP system trigger conditions |
ad_code_requirement |
(code_id, clause, element_type, space_type) | 23 | Building code requirements |
ad_room_slot |
slot_id | 38 | Room assembly slot definitions |
ad_space_dim |
space_type | 37 | Space dimension constraints |
ad_space_exterior_rule |
space_type_id | 24 | Exterior exposure rules |
ad_space_type_opening |
(space_type_id, opening_role, family_id) | 103 | Opening requirements per space |
ad_space_type_furniture |
space_type_id | 37 | Furniture schedule per space |
ad_space_type_mep |
space_type_id | 22 | MEP service requirements per space |
ad_element_mep_alias |
alias_id | 84 | IFC version-agnostic product resolution (§2.1) |
ad_ifc_class_map |
ifc_class | 46 | IFC class extraction authority (§2.2) |
ad_val_rule |
ad_val_rule_id | 415 | Mined dimension rules: typical W/D/H per (ifc_class, storey) |
ad_val_rule_param |
ad_val_rule_param_id | 1245 | Rule parameters (typical_width_mm, typical_depth_mm, typical_height_mm) |
W_Calibration_Result |
id | 0 | CalibrationTest output (runtime writes) |
AD_SysConfig |
Name | 3 | Schema/seed/alias version tracking |
Compliance tables (AD_Val_Rule, AD_Clash_Rule, AD_Occupancy_Class, AD_Validation_Result) and shared discipline recipes (M_BOM, M_BOM_Line) also live in ERP.db. Full compliance schema: see DocValidate.md.
2. Cross-Database References¶
SQLite has no cross-database FK. References use name convention — same
pattern as iDempiere AD_Reference lookups:
| ERP.db column | Resolves to | Method |
|---|---|---|
ad_element_mep.element_type |
M_Product by alias cascade |
Java: try ifc_class → predefined_type → type_class → element_name LIKE |
ad_space_type_mep_bom.mep_product_id |
ad_element_mep.element_type |
SQL within ERP.db (same DB) |
ad_assembly_connector.assembly_id |
M_Product.name |
Java: SELECT * FROM M_Product WHERE name = ? |
placement_rules.element_name |
M_Product.name or m_bom.bom_id |
Java: lookup by name |
ERP.db component_library.db
┌─────────────────────────┐ ┌──────────────────────┐
│ ad_element_mep │ │ M_Product │
│ element_type: SPRINKLER──── name ────▶│ name: SPRINKLER │
│ ifc_class: IfcFire... │ │ width, depth, height│
│ discipline: FP │ │ ifc_class │
│ ports: [{"IN":0.015}] │ └──────────────────────┘
└─────────────────────────┘
No geometry in ERP.db. No discipline metadata in component_library.db.
2.1 IFC Version-Agnostic Resolution — ad_element_mep_alias¶
IFC2x3 lumps all MEP into generic classes (IfcFlowTerminal). IFC4 splits them into specific subtypes (IfcOutlet). Real-world IFC files use vendor-specific naming. 4-tier resolution cascade:
Priority 1: ifc_class — IfcOutlet → OUTLET (IFC4 direct match)
Priority 2: predefined_type — POWEROUTLET → OUTLET (IFC4 enum)
Priority 3: type_class — IfcOutletType → OUTLET (IFC2x3 via IfcRelDefinesByType)
Priority 4: element_name — %Receptacle% → OUTLET (name pattern, last resort)
84 aliases covering all 12 canonical MEP types. DX resolution: 101/119 distinct MEP names (85%).
2.2 IFC Class Extraction Authority — ad_ifc_class_map¶
Authority table for extract.py. Read at startup — adding a new IFC type
= one INSERT, zero code changes.
ad_ifc_class_map (46 rows)
┌──────────────────────┬────────────┬─────────────────┬─────────────────┬──────────┬───────────┐
│ ifc_class (PK) │ discipline │ category │ attachment_face │ ifc_schema│ domain │
├──────────────────────┼────────────┼─────────────────┼─────────────────┼──────────┼───────────┤
│ IfcTrackElement │ RAIL │ TRACK_ELEMENT │ BOTTOM │ IFC4X3 │ RAIL │
│ IfcCourse │ ROAD │ PAVEMENT_LAYER │ BOTTOM │ IFC4X3 │ ROAD │
│ IfcBeam │ STR │ BEAM │ ENDS │ IFC4 │ BUILDING │
│ IfcLightFixture │ ELEC │ LIGHT │ TOP │ IFC4 │ BUILDING │
│ ... │ ... │ ... │ ... │ ... │ ... │
└──────────────────────┴────────────┴─────────────────┴─────────────────┴──────────┴───────────┘
See InfrastructureAnalysis.md §3.3.
3. Connection Map¶
CompilationPipeline → component_library.db (LOD)
PlacementValidator → ERP.db (compliance rules)
CalibrationDAO → ERP.db + TE_BOM.db
MEPAD/MEPBOMResolver → ERP.db (discipline metadata)
ManifestResolver → ERP.db (discipline metadata)
DocEvent → ERP.db (schedules) + component_library.db (LOD fetch)
Handler cascade H1-H6 → ERP.db (discipline metadata + compliance rules)
Connection compConn; // component_library.db — LOD catalog
Connection discConn; // ERP.db — discipline metadata + compliance rules
Connection bomConn; // {prefix}_BOM.db — building BOM
4. File Location¶
library/
├── component_library.db ← LOD catalog (M_Product, geometries)
├── ERP.db ← discipline metadata + compliance rules
└── {PREFIX}_BOM.db ← per-building BOM
migration/
├── DV001_ERP_schema.sql ← schema DDL (19 tables)
├── DV002_seed_from_component.sql ← seed via ATTACH (17 tables)
├── DV003_element_mep_alias.sql ← IFC alias cascade (84 rows)
├── DV005_ifc_class_map.sql ← IFC class extraction authority (46 rows)
└── V001..V006 ← compliance rule migrations
5. Traceability¶
| Witness | What it Proves | Test |
|---|---|---|
| W-DV-DB-SCHEMA | DDL creates all 20 required tables | DiscValidationDBTest |
| W-DV-DB-SEED | Seed data matches component_library.db source counts | DiscValidationDBTest |
| W-DV-DB-REF | Reference pointers resolve across databases | DiscValidationDBTest |
| W-DV-DB-ALIAS | Alias cascade resolves IFC2x3↔IFC4 (84 rows, 4 tiers) | DiscValidationDBTest |
| W-DV-DB-ND | Schema changes do not disturb component_library.db | DiscValidationDBTest |
| W-TACK-STABLE | Tack chain FP error ≤ 0.005mm per pair across all fleet buildings (evidence: ≤ 0.002mm, 1,653 pairs) | GEO gate G3-DIGEST |
6. AD_Org — Disciplines as Organizational Units¶
iDempiere uses AD_Org to partition data by organizational unit. In construction, disciplines ARE organizational units — each is a trade with its own contractor, products, rules, and scope of work.
AD_Client = 'BIM_PROJECT' (tenant — the whole project)
└── AD_Org = '*' (shared data: building grid, space types, structural frame)
└── AD_Org = 'ARC' (architectural: doors, windows, furniture, finishes)
└── AD_Org = 'STR' (structural: beams, columns, slabs, foundations)
└── AD_Org = 'FP' (fire protection: sprinklers, alarms, risers)
└── AD_Org = 'ELEC' (electrical: lights, outlets, switches, cable trays)
└── AD_Org = 'ACMV' (HVAC: ducts, diffusers, AHUs)
└── AD_Org = 'CW' (cold water: pipes, fittings, valves)
└── AD_Org = 'SP' (sanitary/plumbing: fixtures, waste pipes)
└── AD_Org = 'LPG' (gas: gas pipes, meters)
Two orthogonal axes:
- M_Product_Category = taxonomy (WHAT type: sprinkler head, pipe segment, alarm)
- AD_Org = ownership (WHO manages: FP trade, ELEC trade)
What AD_Org replaces:
- m_bom.bom_category string → AD_Org_ID FK
- C_OrderLine.Discipline string → AD_Org_ID FK
- component_types.discipline string → AD_Org_ID FK
- Scattered resolveDiscipline(ifcClass) logic → single FK lookup
iDempiere data partitioning: Every row with AD_Org_ID = 'FP' is visible
only to the FP trade. Shared infrastructure (AD_Org = '*') is visible to all.
This enables per-discipline BOM views, validation scoping, and trade-specific
product catalogs — all from a single FK.
6.1 Spatial Model — Space + Occupant + Verb + Rule¶
A discipline is a contractor with a checklist, not a room with walls.
Disciplines are not spatial containers — a fire protection pipe network spans the entire floor. Discipline is a line attribute (AD_Org_ID), not a tree level. See TerminalAnalysis.md §Compilation Status.
The BOM hierarchy is recursive and abstract:
SPACE (M_Product, IsBOM=Y)
└── OCCUPANT line (M_BOM_Line, with AD_Org_ID + verb_ref)
A SPACE has an AABB (extent) and an M_Product_Category (what kind of space). An OCCUPANT has an AD_Org_ID (who), a verb_ref (how), and AD_Val_Rule (checklist).
The compiler resolves placement through three stages, matching how iDempiere processes documents:
┌─────────────────────────────────────────────────────────────┐
│ 1st: DocEvent per Org (discipline blanket + govt standards) │
│ AD_DocEvent_Rule fires top-down as the walker traverses │
│ root → leaf. AD_Org blanket-applies ALL rules for the │
│ discipline — spacing, connectivity, host, AND government │
│ standards (NFPA 13, UBBL). Jurisdiction-swappable here. │
│ Same as iDempiere ModelValidator per organization. │
│ │
│ 2nd: AttributeSet (per-product / per-instance) │
│ M_AttributeSet defines what CAN vary per product type. │
│ M_AttributeSetInstance carries actual values per instance. │
│ Resolved per line item — K-factor, dimensions, material. │
│ │
│ 3rd: AD_Val_Rule (user per-line override — last) │
│ User sees exploded sub-lines, adds/changes/waives rules. │
│ Same as iDempiere AD_Val_Rule — a lookup filter the user │
│ attaches to specific lines. Not automatic. Not blanket. │
└─────────────────────────────────────────────────────────────┘
for each BOM line in parent:
verb = line.verb_ref → Strategy (GoF)
org = child.product.AD_Org_ID → 1st: DocEvent blanket + standards
asi = orderline.ASI → 2nd: per-instance attributes
verb.place(child, parent.space, asi)
// 3rd: AD_Val_Rule — only if user attached override to this line
This is standard iDempiere processing order: ModelValidator (Org-scoped) → line item resolution (ASI) → user validation rules (AD_Val_Rule).
Anti-pattern: shouldSkip(). There is ONE compile path, not two paths
with a skip. The walker always walks the BOM tree. The verb determines what
happens at each line — PLACE emits at tack offset, ROUTE generates from
rules, FRAME generates structural grid. A shouldSkip() that produces an
empty BuildingSpec and falls through to a separate emit path is the same
structural cheat as if ("CO".equals(...)) — just checking verbs instead
of category. The fix is one walker, verb-dispatched, no skip.
No if ("CO".equals(...)). No if (discipline == "FP"). Behaviour from
metadata, not from code — same as iDempiere's DocAction pattern.
Covering vs Inside — two spatial relationships, both just BOM lines:
| Relationship | Verb family | Example |
|---|---|---|
| INSIDE | PLACE | Sofa at (dx,dy,dz) in living room |
| COVERING | ROUTE, TILE, FRAME, WIRE | Sprinklers covering a floor per NFPA 13 |
INSIDE: child sits AT a point within the parent space (tack offset = position). COVERING: child SPANS the parent space (verb determines pattern, rule determines density).
6.2 Discipline Profiles — Abstract Recipe, Space-Dependent Placement¶
Each discipline has a recipe (BOM cascade from its top-level Category) and Org defaults (discipline-wide practice). The parent space determines quantity and placement. Government standards validate the result post-hoc.
| AD_Org | Top Category | Verb | Spatial | Recipe cascade |
|---|---|---|---|---|
| ARC (1) | ARC_DESIGN | PLACE, TILE | INSIDE | Walls, doors, windows, plates, furniture |
| STR (2) | STR_FRAME | FRAME | COVERING | Column + Beam + Slab |
| FP (3) | FP_MAIN_ROOM | ROUTE | COVERING | Riser → branches → fittings → heads |
| ELEC (4) | ELEC_DISTRIBUTION | WIRE | COVERING | Panel → circuits → fixtures |
| ACMV (5) | ACMV_PLANT | ROUTE | COVERING | AHU → ducts → fittings → terminals |
| CW (6) | CW_SUPPLY | ROUTE | COVERING | Riser → pipe runs → fittings → valves |
| SP (7) | SP_DRAINAGE | ROUTE | COVERING | Stack → drainage pipes → fixtures |
| LPG (8) | LPG_SUPPLY | ROUTE | COVERING | Meter → gas piping → fittings |
OrderLine entry point: C_OrderLine.Product has a Category (the top
Category of that discipline). BomDrop explodes the product's BOM, cascading
through the discipline's own BOM tree. Category at each tier = the product
group (substitution shelf). The designer can swap any product for another
in the same Category without changing the BOM structure.
The recipe is abstract — "cover this zone with sprinklers." Processing follows iDempiere order: DocEvent per Org (1st, discipline blanket + government standards) → ASI resolution per instance (2nd) → AD_Val_Rule user override on specific lines (3rd, on demand).
Cardinal rules (also in class Javadoc): - MEP_RECIPE is abstract and reusable. It encodes a geometric pattern (standoff, chain geometry). Never explode recipe runs into per-instance LEAF rows in compilation. - Validation Rules drive final expression. DV rules (AD_Rule / M_BOM Validation Layer) resolve a recipe to a specific project. The callout only registers scope and Qty. - One DISCIPLINE row per discipline. Qty = IFC element count from extraction (ad_sysconfig MEP__COUNT), not recipe archetype count. - LEAF children from _SYSTEM BOM only** (FP_RISER, FP_SPRINKLER_LAYOUT, etc.).
See:
OrderLineProductCallout.javaclass Javadoc,IFCtoERP.javaclass Javadoc.
6.3 Three-Stage Validation — iDempiere Processing Order¶
| Stage | iDempiere parallel | What it does | Fires | Example |
|---|---|---|---|---|
| 1st: DocEvent per Org | ModelValidator.docValidate() | Blanket discipline rules INCLUDING government standards, top-down | Automatically during BOM walk, per AD_Org | FP: NFPA 13 spacing, UBBL fire rating, general pipe sizing rules |
| 2nd: AttributeSet | M_AttributeSetInstance | Per-product/per-instance resolution | Per line item during placement | K-factor=5.6, pipe_dia=50mm, material=copper |
| 3rd: AD_Val_Rule | AD_Val_Rule (lookup filter) | User-initiated per-line rule addition or override | On demand, after explosion, on specific sub-lines | User adds stricter spacing rule to a particular FP branch |
| Cross-discipline | AD_Clash_Rule | Clearance between disciplines | After all disciplines placed | 150mm clearance FP vs ELEC |
Key distinction: In iDempiere, AD_Val_Rule is a lookup filter — it narrows available choices per field, not a document validator. ModelValidator (DocEvent) is where real validation lives. Government standards (NFPA 13, UBBL, MS1183) are general enough to be 1st-stage blanket rules — they apply to EVERY element in the discipline. 3rd-stage AD_Val_Rule is for when the user sees specific exploded sub-lines and wants to add, change, or waive a rule on THOSE lines.
1st: DocEvent(org) → blanket discipline rules + government standards
2nd: ASI resolution → per-instance attributes on each line
3rd: AD_Val_Rule → user adds/changes/waives rule on specific lines
Per-space compliance (S100-p82, f3c4d793): Room dimension evaluation (area, width, height) against AD_DocEvent_Rule is implemented. SKIP propagation cascades through the BOM walk.
Jurisdiction-swappable: Government standards live in 1st-stage DocEvent
rules, scoped by jurisdiction. Same BOM, same Org, different jurisdiction
→ different DocEvent rules fire. Malaysian building uses UBBL rules,
US building uses NFPA/IBC rules. The BOM and ASI don't change.
AD_DocEvent_Rule — 1st Stage Schema (ERP.db)¶
DocEvent rules are shared across all buildings (like AD_Org, M_Product). They live in ERP.db. Each rule is scoped to an AD_Org (discipline) and optionally to a jurisdiction.
-- Ready-made discipline event rules in ERP.db
-- Fires automatically during BOM walk when AD_Org matches
CREATE TABLE IF NOT EXISTS AD_DocEvent_Rule (
ad_docevent_rule_id INTEGER PRIMARY KEY AUTOINCREMENT,
ad_org_id INTEGER NOT NULL DEFAULT 0, -- 0=all, 3=FP, 4=ELEC, etc.
name TEXT NOT NULL, -- 'NFPA13_LH_SPACING'
description TEXT,
rule_type TEXT NOT NULL, -- SPACING, CONNECTIVITY, HOST,
-- COMPLETENESS, DIMENSION, STANDARD
standard_ref TEXT, -- 'NFPA 13 §8.6.2.2.1', 'UBBL s.43(1)'
jurisdiction TEXT, -- MY, US, UK, SG, INTL, NULL=universal
check_method TEXT NOT NULL, -- MIN_DISTANCE, MAX_DISTANCE,
-- REQUIRED_HOST, COUNT_PER_AREA,
-- MIN_DIMENSION, MAX_COVERAGE, DIMENSION_RANGE
ifc_class TEXT, -- target element (NULL=all in discipline)
m_product_category_id TEXT, -- target category (NULL=all)
severity TEXT NOT NULL DEFAULT 'WARN', -- BLOCK, WARN
firing_event TEXT NOT NULL DEFAULT 'BEFORE_PLACE',
-- BEFORE_PLACE, AFTER_PLACE, AFTER_COMPLETE
is_active INTEGER NOT NULL DEFAULT 1,
provenance TEXT, -- EXTRACTED:UBBL_1984, MINED:TE, RESEARCHED
FOREIGN KEY (ad_org_id) REFERENCES AD_Org(ad_org_id)
);
CREATE TABLE IF NOT EXISTS AD_DocEvent_Rule_Param (
ad_docevent_rule_param_id INTEGER PRIMARY KEY AUTOINCREMENT,
ad_docevent_rule_id INTEGER NOT NULL,
name TEXT NOT NULL, -- max_spacing_mm, min_area_m2, min_count
value TEXT NOT NULL, -- '4600', '9.3', '1'
value_type TEXT DEFAULT 'NUM', -- NUM, TEXT, BOOL
condition_expr TEXT, -- 'productCategory IN (BD,LR,DR)'
FOREIGN KEY (ad_docevent_rule_id) REFERENCES AD_DocEvent_Rule(ad_docevent_rule_id)
);
CREATE INDEX idx_docevent_rule_org ON AD_DocEvent_Rule(ad_org_id);
CREATE INDEX idx_docevent_rule_jurisdiction ON AD_DocEvent_Rule(jurisdiction);
Existing ERP.db ad_val_rule (415 rows): These are mined DIMENSION_RANGE
observations from 34 buildings — they are effectively 1st-stage DocEvent rules.
Migration path: copy qualifying rows into AD_DocEvent_Rule with
rule_type='DIMENSION', check_method='DIMENSION_RANGE',
provenance='MINED:{building}'.
6.3.1 Current DV Rule Coverage — MEP Gap¶
Audit findings (S104-closeout, 2026-04-01). Queries run against library/ERP.db.
Stage 1 — AD_DocEvent_Rule (8 rows total):
- 7 rows at AD_Org_ID=0 (blanket/ARC): UBBL dimension rules (room area, kitchen dim,
corridor width, staircase width, ceiling height, bathroom area, egress distance).
All FiringEvent=BEFORE_COMPILE. CheckMethods: MIN_AREA, MIN_DIMENSION, MIN_WIDTH,
MIN_HEIGHT, MAX_DISTANCE.
- 1 row at AD_Org_ID=3 (FP): UBBL_PVII_SPRINKLER_SPACING — SPACING, MAX_SPACING.
- Gap: ACMV, CW, ELEC, SP, LPG have zero Stage 1 DocEvent rules. §6.3 declares
"FP: NFPA 13 spacing, UBBL fire rating, general pipe sizing rules" — only UBBL
sprinkler spacing is present. NFPA 13 coverage area, pipe sizing, flow rate rules
are absent from AD_DocEvent_Rule.
Stage 3 — ad_val_rule (415 rows):
- All rows are check_method='DIMENSION_RANGE' (physical dimension observations
mined from 34 buildings). No AD_Org_ID column — mapped by ifc_class.
- MEP IFC classes covered: IfcAirTerminal (3), IfcDuctFitting (4), IfcDuctSegment (4),
IfcFireSuppressionTerminal (1), IfcFlowController (5), IfcFlowFitting (10),
IfcFlowSegment (11), IfcFlowTerminal (29). Total MEP dimension rules: ~67 rows.
- These are mined dimension ranges (W/D/H observations), not code requirement checks.
No spacing limits, flow rates, clearance requirements, or coverage area rules.
- Gap: No Stage 3 rules that enforce NFPA, IMC, IPC, NEC, UBBL MEP clauses.
Hardcoded Java thresholds (geometric heuristics, not code requirements):
- RoutingConstraints.java (all values extracted from PATTERN G3, IFC-mined):
- Wall clearance by discipline: ACMV 175mm, FP 173mm, CW 216mm, SP 223mm, LPG 106mm
- Ceiling clearance averages and minimums per discipline
- 50mm wall clearance tolerance (isValidWallClearance line 101)
- 10mm ceiling clearance tolerance (isValidCeilingClearance line 116)
- FederationConstants.java:94: MEP-to-structure minimum clearance constant
- Assessment: these are geometric routing heuristics from observed IFC data, not
codified requirements. They belong in RoutingConstraints.java or could migrate
to ad_code_requirement but are not missing from any DV stage.
ad_code_requirement table (23 rows):
- Populated with real code requirements: NFPA_13 (sprinkler spacing/coverage by room),
NEC_2020 (outlet spacing, GFCI, lighting, switch clearance), IMC_2021 (exhaust fan),
IPC_2021 (sink/toilet), MS_1184 (door/toilet), MS_1228 (floor trap), NFPA_101
(emergency lighting distance).
- Schema has: max_spacing_m, min_clearance_m, coverage_radius, qty_per_area etc.
- Not yet connected to DV rule generation. No query path from ad_code_requirement
→ AD_DocEvent_Rule or ad_val_rule. This table is the intended home for NFPA/UBBL
MEP code requirements (confirmed by schema), but migration to Stage 1 is not done.
Conclusion: MEP DV rule coverage is a real gap. Stage 1 has 1 FP rule (UBBL sprinkler
spacing). Stages 2–3 have no MEP code requirement checks. ad_code_requirement has the
right data but is not wired into the validation pipeline. A future migration prompt should
bridge ad_code_requirement → AD_DocEvent_Rule for each discipline's code references.
AD_Val_Rule — 3rd Stage (ERP.db, compliance rules)¶
The V001 schema stays as-is within ERP.db. AD_Val_Rule is a user-initiated per-line rule addition/change/waiver (government standards are 1st-stage DocEvent, not 3rd-stage AD_Val_Rule).
The user opens an exploded order, sees specific sub-lines, and attaches: - ADD: "Apply stricter 3000mm spacing to THIS branch" (new rule on line) - CHANGE: "Override threshold from 4600mm to 3800mm for THIS zone" - WAIVE: "Acknowledge and accept this deviation" (AD_Val_Rule_Exception)
This is exactly how iDempiere's AD_Val_Rule works — a lookup filter that the user configures on a specific field/line to narrow or adjust what's valid.
6.4 BOM Tree Structure¶
BUILDING → FLOOR → LEAF — same depth for all building categories.
Tack is element.minX - floor.minX. Always positive. Discipline resolves
from the child product: m_bom_line.child_product_id →
M_Product → M_Product_Category → AD_Org_ID. Standard iDempiere —
every record carries AD_Org, the line is just a relationship.
| Component | Responsibility |
|---|---|
DisciplineBomBuilder |
LEAF lines directly under FLOOR (no DISCIPLINE SET level) |
BomValidator |
W-TACK-1/W-BUFFER-1 check FLOOR→LEAF |
CompilationPipeline |
Single walk path, no category skip hack |
PlacementCollectorVisitor |
Resolve AD_Org_ID from child product, not discipline stack |
6.5 The BOM Is Already Perfect¶
A BOM is a BOM — same as manufacturing a car. You don't label an engine assembly with a tier tag. It's a product with children. The tree structure IS the hierarchy:
- Root:
getParentBOM()returns null - Any level:
getChildren()returns its children - Leaf:
getChildren()returns empty - Category:
getProductCategory()returns the substitution shelf
No level labels. No vocabulary. A building, a car, a bridge, a ship — same three methods, different products. M_Product_Category groups interchangeable products at each level (same shelf = same swap pool).
VIEW_CONTRACTS.md v_qualified_bom currently uses a legacy bom_type
bind parameter. Migration pending to use M_Product_Category instead.
6.6 Shared Discipline Recipes in ERP.db¶
Discipline BOMs are shared across all buildings. FP is FP — one recipe, all buildings, same rules. ACMV is ACMV. The recipe does not change; the space and rules determine the result.
ERP.db holds the shared recipes alongside AD_Org:
ERP.db
├── AD_Org (WHO: FP, ACMV, ELEC, CW, SP, LPG)
├── AD_SysConfig (discipline-wide defaults per Org)
├── M_Product (WHAT: sprinklers, pipes, ducts, fittings)
├── M_Product_Category (TIER: product taxonomy = substitution shelf)
└── M_BOM — shared discipline recipes (each a BOM cascade):
├── FP_SYSTEM (Category=FP_MAIN_ROOM, Org=FP)
│ ├── FP_RISER (Category=FP_RISER, verb=ROUTE)
│ ├── FP_SPRINKLER_LAYOUT (Category=FP_DISTRIBUTION, verb=ROUTE)
│ └── FP_PUMP_LINK (Category=FP_SUPPLY, verb=ROUTE)
├── ACMV_SYSTEM (Category=ACMV_PLANT, Org=ACMV)
├── ELEC_SYSTEM (Category=ELEC_DISTRIBUTION, Org=ELEC)
└── CW_SYSTEM (Category=CW_SUPPLY, Org=CW)
ERP.db also contains (government standards):
├── AD_Val_Rule (NFPA 13, UBBL, MS1183 — compliance checks)
├── AD_Val_Rule_Param (thresholds per rule)
└── AD_Clash_Rule (cross-discipline clearance)
component_library.db is strictly leaf geometry (meshes, LODs). It never holds BOMs or recipes — only what things look like, not how they assemble.
OrderLine → top Category → BOM cascade:
C_Order: "Build TE"
├── C_OrderLine #1: TE_ARC_STR (Category=ARC, Org=ARC) ← extracted BOM
├── C_OrderLine #2: FP_SYSTEM (Category=FP_MAIN_ROOM, Org=FP)
├── C_OrderLine #3: ACMV_SYSTEM (Category=ACMV_PLANT, Org=ACMV)
├── C_OrderLine #4: ELEC_SYSTEM (Category=ELEC_DISTRIBUTION, Org=ELEC)
├── C_OrderLine #5: SP_SYSTEM (Category=SP_DRAINAGE, Org=SP)
├── C_OrderLine #6: CW_SYSTEM (Category=CW_SUPPLY, Org=CW)
├── C_OrderLine #7: LPG_SYSTEM (Category=LPG_SUPPLY, Org=LPG)
└── C_OrderLine #8: STR_SYSTEM (Category=STR_FRAME, Org=STR)
Eight lines. Each line's Product has a top Category — the entry point into that discipline's BOM cascade. BomDrop explodes recursively. At each tier, Category = the substitution shelf (designer can swap products within it).
Processing order (same as iDempiere document processing): 1. DocEvent per Org — discipline blanket rules + government standards (NFPA 13, UBBL, MS1183) apply top-down as walker traverses root→leaf. Jurisdiction-swappable at this stage. 2. ASI resolution — per-product/per-instance attributes (K-factor, dimensions, capacity) 3. AD_Val_Rule — user-initiated per-line override/addition on specific exploded sub-lines
Jurisdiction-swappable: same BOM, same Org, different AD_DocEvent_Rule set for Malaysian (UBBL) vs US (NFPA) code. BOM and ASI don't change.
6.6.1 Discipline Separation — Two-Class Architecture¶
Discipline separation spans two pipeline classes:
Class A — IFCtoBOM (extraction): Produces the ARC+STR envelope only.
MEP elements (FP, ACMV, ELEC, CW, SP, LPG) are not written into the
per-building *_BOM.db. IFCtoBOM counts MEP elements per discipline from
elements_meta.discipline and writes them back into the YAML
(disciplines: [{disc: FP, qty: 99}]) AND to ad_sysconfig in BOM DB.
The YAML becomes the reusable preset template for that building type.
YAML as chooser: Designer opens the YAML, sees pre-populated discipline
qtys from extraction, can reduce scope (e.g., qty: 50 for a partial wing).
Qty=0 or qty>capacity both mean "fill the whole building per rules" —
the compiler fills until no more space and stops gracefully, never forces.
For RE buildings, mep_disciplines: controls which DISCs the Callout
creates (default: ELEC + SP). Deleting a discipline from YAML removes it.
Without YAML (direct OrderLine), user gets default DISCs from Callout.
Class B — DAGCompiler (compilation): Product Callout reads YAML
disciplines: qty, then creates DISCIPLINE OrderLines pointing to shared
recipes in ERP.db (FP_SYSTEM, ACMV_SYSTEM, etc.). DocEvent per Org
fires on each discipline OrderLine — applies jurisdiction rules (NFPA 13,
UBBL, MS 1228). RouteBuilder/CrawlRouter generates MEP routing until qty
terminals served. AD_Val_Rule validates the output.
Pending: DisciplineBomBuilder currently writes all elements flat
under FLOOR. Corrective in prompt 00b_discipline_separation.txt —
IFCtoBOM produces ARC+STR only, MEP comes from ERP.db shared recipes
via DAGCompiler Callout.
For generative buildings, the compiler applies the shared recipes using verb Strategy + DocEvent per Org + ASI. AD_Val_Rule validates the output.
6.7 GoF Design Patterns¶
| Pattern | Application |
|---|---|
| Composite | BOM tree: SPACE contains OCCUPANT lines, recursively |
| Visitor | BOMWalker visits each line |
| Strategy | Verb determines placement method (DocEvent + ASI) |
| Specification | AD_DocEvent_Rule validates during walk (1st, blanket + standards). AD_Val_Rule = user override (3rd, per-line) |
6.8 Cross-References¶
| Building | Disciplines | Concern |
|---|---|---|
| TE | 8 (ARC,STR,FP,ACMV,ELEC,CW,SP,LPG) | SET as tree level → tack overflow. TerminalAnalysis.md |
| DM | 3 (ARC,STR,FP) | First FP trial — addDiscipline(). DemoHouseAnalysis.md |
| FK | 2 (ARC,STR) + ROOF debate | FZKHausAnalysis.md |
| Infrastructure | ROAD,RAIL,GEO,LAND,SIGN | Extended codes. InfrastructureAnalysis.md |
6.9 Stair Validation Rules — Candidate AD Table Extensions (S100-p84)¶
Existing infrastructure: ad_stair_requirement (7 rows in BOM.db, seeded by
scripts/create_ad_vertical_circulation.py), VerticalCirculationAD.java,
VerticalCirculationValidator.java, StairwellCheck.java.
Rules NOT yet in ad_stair_requirement — candidates for addition:
| Rule ID | Parameter | Value | Standard |
|---|---|---|---|
| STAIR_COMFORT_2RG | 2R+G check | 550-700mm (ideal 630) | Blondel formula |
| STAIR_HEADROOM | min_headroom_mm | 2000 | UBBL practice / BS 5395 |
| STAIR_MAX_FLIGHT | max_flight_rise_mm | 3000 | UBBL By-Law 168 |
| STAIR_RISER_UNIFORM | max_variance_mm | 9.5 | IBC s1011.5.4 |
| STAIR_GUARD_HEIGHT | min_guard_mm | 1070 | IBC s1015.3 |
| STAIR_GUARD_OPENING | max_sphere_mm | 100 | IBC s1015.4 (child safety) |
| STAIR_NOSING_MAX | max_nosing_mm | 32 | IBC s1011.5.5 |
| STAIR_PRESSURIZE | pressure_pa | 50-100 | UBBL By-Law 178 (>18m) |
TE relevance: 178 unfactored stair components across GF-L4. Building height 59.8m → >18m threshold → 2.0hr fire rating, min 1200mm width, pressurization required, min 2 stairs. These rules + ASI (per-instance run length, landing width) resolve stair geometry without manual pattern recognition. See TerminalAnalysis.md §Stair Validation Rules.
6.10 Movement Verbs — Routing Linear Elements Through Buildings¶
Linear MEP elements (pipes, ducts, cables) don't just get placed at points. They move through the building — following surfaces, bending at corners, branching at junctions, penetrating floors. Each direction change or connection produces a joint fitting product alongside the segment.
TE extraction proves fittings outnumber segments in most disciplines:
| Discipline | Segments | Fittings | Ratio | Implication |
|---|---|---|---|---|
| FP | 2,672 | 3,146 | 1.18× | More joints than pipes |
| ACMV | 568 | 713 | 1.26× | Every duct turn = fitting |
| CW | 619 | 638 | 1.03× | Nearly 1:1 |
| SP | 455 | 372 | 0.82× | Longer runs, fewer turns |
| LPG | 75 | 87 | 1.16× | Small system, many valves |
Movement Verb Catalogue¶
Each movement verb produces two BOM lines: a segment (pipe/duct/cable) and a joint fitting (elbow/tee/reducer/sleeve). The fitting is an M_Product from the component library with its own LOD mesh.
| Verb | Action | Joint product | Geometry |
|---|---|---|---|
| FOLLOW | Trace along surface (wall, ceiling, beam) | None — straight run | PipeSegment / DuctSegment, qty = length ÷ stock_size |
| BEND | Change direction at angle | Elbow fitting (90°, 45°, custom) | ForgeEngine: PIPE_BEND (arc geometry, S99) |
| RISE / DROP | Change elevation (through floor or along wall) | Elbow or offset fitting | Vertical segment + 2 elbows |
| BRANCH | Split into sub-paths | Tee or Wye fitting | T-junction, diameters from parent + children |
| REDUCE | Change diameter | Reducer fitting | Concentric or eccentric reducer |
| PENETRATE | Pass through floor or wall | Sleeve + fire collar (if fire-rated) | Hole + sleeve product + sealant |
Composition in BIM COBOL¶
Movement verbs compose into a routing script. Each line in the script produces BOM lines (segments + fittings):
ROUTE FP FROM PUMP_ROOM
FOLLOW CEILING SPACING 4500 → PipeSegment × N
BEND 90 AT GRID_A → PipeFitting (elbow)
BRANCH TEE TO ROOM_101 ROOM_102 → PipeFitting (tee) + 2 sub-routes
REDUCE 50mm TO 25mm → PipeFitting (reducer)
PENETRATE SLAB WITH FIRE_COLLAR → Sleeve + FireCollar products
The BOM IS the routing. No graph data structure needed — parent-child with sequence controlling path order. Each fitting is a BOM child with qty=1 (EA) at the transition point.
UOM conversion: CrawlOps produce lengths in mm internally. RouteStage converts to the product's cost_uom at persistence: mm ÷ 1000 → M for pipe/duct segments; fitting qty stays 1 (EA). This is the single conversion point — all internal geometry is mm, all persisted qty matches M_Product.cost_uom.
Joint Product Resolution¶
When a movement verb needs a fitting, it resolves from the component library:
Inputs: verb (BEND), angle (90°), parent_diameter (50mm), material (Poly Steel)
Lookup: M_Product WHERE ifc_class='IfcPipeFitting'
AND diameter=50 AND material='Poly Steel' AND angle=90
Result: Product_ID → LOD mesh from component_library.db
If no exact match: ForgeEngine computes the geometry (PIPE_BEND, S99). ASI carries per-instance overrides (actual angle, actual diameter).
Movement Verbs per Discipline¶
| Discipline | Primary verbs | Typical route | Standards governing routing |
|---|---|---|---|
| FP | FOLLOW ceiling, BRANCH tee, PENETRATE slab | Riser → floor header → branches → heads | NFPA 13 §8 (spacing), UBBL Part VII |
| ACMV | FOLLOW ceiling void, BEND, BRANCH, REDUCE | AHU → main duct → branches → diffusers | MS 1525 (duct sizing), ASHRAE 62.1 |
| ELEC | FOLLOW cable tray, BRANCH, PENETRATE | DB → riser → tray → outlets/lights | MS IEC 60364 (cable sizing), NEC 300.4 |
| CW | FOLLOW wall/ceiling, RISE, BRANCH, REDUCE | Tank → riser → floor header → fixtures | MS 1228 (pipe sizing by fixture unit) |
| SP | DROP (gravity), FOLLOW gradient, BRANCH wye | Fixtures → waste → stack → drain | MS 1228 (min gradient 1:40 / 1:60) |
| LPG | FOLLOW ext wall, BRANCH, REDUCE | Meter → riser → kitchen → gas points | MS 830 (gas installation) |
SP is special: all other disciplines flow outward/upward from a source. SP flows downward by gravity. FOLLOW must maintain minimum gradient (1:40 for 100mm pipe). The verb checks slope at each segment.
6.11 Parasitic Discipline Implementation Tasks¶
Implementation in 4 phases: POC first to prove assumptions, then build out.
Phase 0 — POC: Prove the Wiring (2-3 prompts)¶
Early proof-of-concept tasks that validate the architecture before committing to the full build. Each is a standalone Rosetta Stone test.
| Task | What it proves | Deliverable | Gate |
|---|---|---|---|
| T0.1 Service room categories | ARC rooms with discipline-typed M_Product_Category tack correctly; FP_SYSTEM origin resolves from ARC pump room | Seed 6 service room products (FP, ACMV, ELEC, CW, SP, LPG) in ERP.db M_Product. Add to SH YAML as dummy rooms. Verify category match query returns correct dx/dy/dz. | SH 7/7, query returns pump room coords |
| T0.2 OrderLine callout POC | Callout reads CO BOM children and auto-creates discipline OrderLines | Implement OrderLineProductCallout.java. Wire to C_OrderLine.M_Product_ID. Test: set product=BUILDING_TE_STD → verify 8 OrderLines created with correct AD_Org_ID and sequence. |
Unit test: 8 lines, correct orgs |
| T0.3 Parasitic qty walk | Walker handles qty-only BOM lines (no dx/dy/dz) without crashing; produces container c_orderlines with correct qty | Add FP_SYSTEM as 2nd OrderLine on SH (qty=2 sprinklers, dummy). Verify walker produces c_orderline with qty=2, host_type=LEAF, AD_Org_ID=3. No placement — just qty passthrough. | SH 7/7 (no regression), FP orderline exists |
| T0.4 FOLLOW verb POC | ROUTE verb extended: FOLLOW a ceiling surface, lay N segments of stock length | Add FOLLOW as ROUTE sub-mode. Test: FOLLOW ceiling in SH living room → produces PipeSegment × ceil(room_length / stock_pipe_length). Fitting count = 0 (straight run). | Witness: W-FOLLOW-1 |
Phase 1 — Movement Verbs (3-4 prompts)¶
Core routing verbs, each tested independently on SH before fleet.
| Task | Verb | Joint product | Test |
|---|---|---|---|
| T1.1 BEND | Change direction, insert elbow | Elbow fitting from component library or ForgeEngine PIPE_BEND | W-BEND-1: angle + diameter → correct fitting product |
| T1.2 BRANCH | Split path, insert tee/wye | Tee fitting, parent + child diameters | W-BRANCH-1: main → 2 sub-routes, tee inserted |
| T1.3 REDUCE | Change diameter, insert reducer | Reducer fitting | W-REDUCE-1: 50mm→25mm, reducer product |
| T1.4 PENETRATE | Pass through slab/wall | Sleeve + fire collar (if fire-rated) | W-PENETRATE-1: sleeve inserted at floor crossing |
Phase 2 — Discipline Routing (3-4 prompts)¶
Wire movement verbs into discipline-specific DocEvent rules.
| Task | Discipline | Route pattern | Standard |
|---|---|---|---|
| T2.1 FP routing | FP | Riser → floor header → branches → sprinkler grid | NFPA 13 §8 spacing, ad_fp_coverage hazard class |
| T2.2 ELEC routing | ELEC | DB → cable tray → light fixture grid per room | MS 1525 lighting power density, IES lux |
| T2.3 CW + SP routing | CW, SP | CW: tank → riser → fixtures. SP: fixtures → stack → drain (gravity) | MS 1228, UPC gradient rules |
| T2.4 ACMV routing | ACMV | AHU → main duct → branches → air terminals | MS 1525, ASHRAE 62.1 air changes |
Phase 3 — Integration (2-3 prompts)¶
| Task | What | Deliverable |
|---|---|---|
| T3.1 Multi-discipline TE | All 8 OrderLines explode on TE with parasitic walk | TE discipline distribution matches extraction (§3.6.3 expected counts) |
| T3.2 Cross-discipline clearance | NEC_ELEC_SP_CLEARANCE fires after all disciplines placed | Detect 11 known overlaps from TE mining (M12) |
| T3.3 Infrastructure POC | BR (bridge) with zone-based anchors instead of rooms | BR 7/7 with STR + DRAIN discipline OrderLines |
| T3.4 RE subset | SH with ARC + ELEC + SP (3 disciplines, subset callout) | SH 7/7, 3 OrderLines, light fixtures + plumbing placed |
T3.1 Implementation — Pipeline Wiring¶
P104 verification found 4 blockers. Resolution:
B1 — RouteDocEvent not in pipeline. RouteDocEvent.fireAll() must be called during compilation, after CompileStage (which produces ARC c_orderline positions) and before WriteStage (which writes output.db). The callout (OrderLineProductCallout.onProductChanged + expandDisciplineLines) must also move from BuildingRegistryTest into the pipeline at the same point.
Pipeline sequence with routing:
CompileStage → [callout + RouteDocEvent.fireAll()] → WriteStage
The callout reads ERP.db shared BOMs, creates DISCIPLINE OrderLines, expands LEAF children with qty. RouteDocEvent reads BuildingGeometry (ARC c_orderline positions) and produces RouteResult per discipline. Both must fire before WriteStage persists to output.db.
B2 — No edge persistence. CrawlRouter produces CONNECTS_TO edges in-memory. BIMEyes P16 (WasteGradientProof) and P17 (SystemConnectedProof) need queryable data in output.db.
Output tables:
| Table | Columns | Source | Consumer |
|---|---|---|---|
| system_edges | discipline, from_index, to_index, from_xyz, to_xyz, edge_type | RouteResult.edges() | P17 BFS connectivity |
| system_nodes | discipline, node_index, node_type, xyz, diameter_mm, product, length_mm | RouteResult.segments() + fittings() | P16 SP gradient check |
B4 — BIMEyes gate. ProveStage gates P15/P16/P17 behind hasRelationalData() which checks ad_room_boundary. CO buildings have DISCIPLINE OrderLines but no ad_room_boundary. Gate must also check system_edges > 0.
T3.4 Implementation — RE Subset¶
B3 — Callout pre-populates by category, YAML removes exceptions.
The Callout inserts a sensible default per M_Product_Category. The user (or YAML) always sees something — never starts from blank:
| Category | Callout default | Rationale |
|---|---|---|
| CO (Commercial) | all 6 MEP (FP, ELEC, ACMV, CW, SP, LPG) | Commercial buildings need full MEP |
| RE (Residential) | ELEC, SP | Every house needs electrical + plumbing at minimum |
| IN (Infrastructure) | none | Roads/bridges have no building MEP |
Two-phase flow:
- Callout fires → inserts default discipline OrderLines for the category
- YAML handling → reads
remove_disciplines, deactivates (IsActive='N') those entries. The OrderLine row stays visible so the user can re-enable.
# RE house — default [ELEC, SP] already inserted by Callout
# No YAML key needed — user sees ELEC + SP and can add/remove in GUI
# RE house that doesn't want plumbing
remove_disciplines: [SP] # deactivates SP, keeps ELEC
# CO warehouse that doesn't need gas
remove_disciplines: [LPG] # deactivates LPG, keeps 5 others
# RE house that wants fire protection added beyond default
add_disciplines: [FP] # adds FP to the ELEC + SP default
This is the iDempiere Configure-to-Order pattern: category provides the template, user modifies by exception. Deactivation (not deletion) preserves the discipline row for audit trail and re-enablement.
BIM Designer GUI equivalent: user selects building category → discipline
OrderLines appear pre-populated. Toggle switch per discipline to
enable/disable. Same IsActive column that YAML's remove_disciplines
controls. Adding a discipline not in the default = addDiscipline() mutation
(Session A).
Code changes needed:
-
OrderLineProductCallout.onProductChanged()— add RE default set[ELEC, SP]. Currently RE returns 0 unless YAML whitelist present. New logic: if no YAML override, insert category default. IN stays at 0. -
New:
applyYamlOverrides()— after callout, readremove_disciplinesfrom ad_sysconfig, SETIsActive='N'on matching DISCIPLINE OrderLines. Readadd_disciplines, insert any not already present. -
Remove current
mep_disciplineswhitelist logic (lines 56-60, 87-91) — replaced by the two-phase default+override pattern.
SH: mep_disciplines: [ELEC, SP] → 2 DISCIPLINE OrderLines + ARC + STR = 4.
T3.5 Finding — MEP UOM Correction¶
Finding (S100 watchdog): M_Product.cost_uom in ERP.db is M3 (cubic meters)
for nearly all MEP products (pipe segments, fittings, ducts, terminals, valves).
This came from extraction — bounding box volume was computed for all elements.
ARC/STR products have correct UOM (walls=M2, beams=M, doors=EA). MEP products do not. Correct trade UOM by ifc_class:
| ifc_class | Current | Correct | Reason |
|---|---|---|---|
| IfcPipeSegment | M3 | M | Pipe bought by linear meter |
| IfcPipeFitting | M3 | EA | Fittings bought per piece |
| IfcDuctSegment | M3 | M | Duct bought by linear meter |
| IfcDuctFitting | M3 | EA | Fittings bought per piece |
| IfcFlowTerminal | M3 | EA | Sprinkler heads, taps = each |
| IfcFlowFitting | M3 | EA | Couplings, adapters = each |
| IfcFlowController | M3 | EA | Valves, dampers = each |
| IfcAirTerminal | M3 | EA | Diffusers, grilles = each |
| IfcLightFixture | M3 | EA | Light fixtures = each |
| IfcFireSuppressionTerminal | M3 | EA | Sprinkler heads = each |
| IfcValve | M3 | EA | Valves = each |
| IfcAlarm | M3 | EA | Alarms = each |
| IfcReinforcingBar | M3 | KG | Rebar bought by weight (PWD 203A, NRM2) |
| IfcCourse | M3 | M2 | Masonry = wall face area (PWD 203A §G) |
| IfcCovering | M3 | M2 | Cladding, insulation, ceiling tiles = area (PWD 203A §H) |
| IfcFurnishingElement | M3 | EA | Furniture bought per piece |
Impact: RouteStage persists qty from CrawlRouter. Pipe segment qty should be in meters (length from FollowOp), fitting qty should be 1 (each from BendOp/ BranchOp/ReduceOp). If cost_uom is M3, the 5D cost engine will multiply qty × unit_cost incorrectly (volume vs length vs count).
Fix: DV migration updating cost_uom by ifc_class for MEP products. iDempiere
convention: C_UOM_ID FK to C_UOM table. Current schema uses TEXT cost_uom
— acceptable for now, align to C_UOM_ID INTEGER FK in a future PK conformance
pass.
Migration SQL pattern (for coder to implement as DV029):
-- Linear segments: M3 → M (bought by linear meter)
UPDATE M_Product SET cost_uom = 'M'
WHERE ifc_class IN ('IfcPipeSegment', 'IfcDuctSegment', 'IfcFlowSegment')
AND cost_uom = 'M3';
-- Discrete fittings/terminals: M3 → EA (bought per piece)
UPDATE M_Product SET cost_uom = 'EA'
WHERE ifc_class IN ('IfcPipeFitting', 'IfcDuctFitting', 'IfcFlowTerminal',
'IfcFlowFitting', 'IfcFlowController', 'IfcAirTerminal',
'IfcLightFixture', 'IfcFireSuppressionTerminal', 'IfcValve', 'IfcAlarm')
AND cost_uom = 'M3';
-- Furnishings: M3 → EA (bought per piece)
UPDATE M_Product SET cost_uom = 'EA'
WHERE ifc_class IN ('IfcFurnishingElement', 'IfcFurniture')
AND cost_uom = 'M3';
-- Rebar: M3 → KG (bought by weight — PWD 203A, NRM2, AIQS)
UPDATE M_Product SET cost_uom = 'KG'
WHERE ifc_class = 'IfcReinforcingBar'
AND cost_uom = 'M3';
-- Masonry courses: M3 → M2 (wall face area — PWD 203A §G)
UPDATE M_Product SET cost_uom = 'M2'
WHERE ifc_class = 'IfcCourse'
AND cost_uom = 'M3';
-- Coverings: M3 → M2 (area coverage — PWD 203A §H)
UPDATE M_Product SET cost_uom = 'M2'
WHERE ifc_class = 'IfcCovering'
AND cost_uom = 'M3';
Scope: ERP.db M_Product table (~320 MEP + 346 rebar + 7 course + 35 covering rows). component_library.db M_Product must also be updated (same SQL). BOM.db copies are regenerated on next extraction — no manual fix needed. New UOM value: KG (not previously used). No schema change — cost_uom is TEXT.
Dependencies¶
T0.1 (service rooms) ──→ T0.3 (qty walk) ──→ T2.x (discipline routing)
T0.2 (callout) ──→ T3.1 (multi-disc TE)
T0.4 (FOLLOW) ──→ T1.x (movement verbs) ──→ T2.x
P94 (BomWriter) ──→ T1.x (new BOM lines need single write path)
T0.1 is the critical first task. If the category-match query doesn't return the right room coordinates, the entire parasitic model breaks. Prove it on SH first (small, fast, 7/7 GREEN).
6.12 Routing Architecture Assessment — Industry Position & Known Gaps¶
The CrawlRouter (§10.4.10) is a prescriptive recipe engine: each discipline builder encodes a standard installation pattern as a sequence of CrawlOps. This section assesses the approach against industry practice and documents known gaps for future work.
Industry Comparison¶
| Approach | Used by | How it works |
|---|---|---|
| Search-based (A*/Dijkstra on voxel grid) | Revit auto-route, GenMEP, academia | Voxelize building → pathfind → connect endpoints |
| Pathway/spine fill | EVOLVE MEP | User defines routing spine, tool fills segments along it |
| Port-to-port connector | Bentley OpenPlant | Shortest path between two component connection ports |
| Prescriptive recipe | This compiler (CrawlOp) | Discipline builder encodes installation pattern as ops |
| AI/generative | Auto BIM Route AI | ML generates candidate routes within constraints |
No other tool produces BOM-integrated, discipline-aware, deterministic routing. Search-based tools produce geometry first; BOM is an afterthought. Our CrawlOp model produces the BOM as primary output — segments and fittings are product references with diameter, length, and position. Geometry is derived from the BOM.
Strengths of Prescriptive Recipe¶
-
BOM is primary output. Every segment and fitting is an M_Product with qty, UOM, and cost. No post-processing step to extract material take-off.
-
Deterministic. Same building + same recipe = same output, always. Auditable: every segment traces to a CrawlOp in a spec-cited builder.
-
Discipline-aware. Each builder encodes domain knowledge — FP knows riser→header→branch (NFPA 13), SP knows gravity drainage (MS 1228 gradient 1:40), ACMV knows duct sizing (ASHRAE 62.1). A* does not know any of this.
-
O(n) not O(V log V). No voxel grid needed. Routing scales linearly with op count, not building volume.
-
IFC-compatible.
system_edges/system_nodesmap toIfcRelConnectsPorts+IfcFlowSegment/IfcFlowFitting. CrawlOp is the generation engine; IFC is the serialization target.
Known Gaps¶
Gap 1: Ceiling void routing — CLOSED (P118)¶
Problem: All 6 builders route at floor.zMm() (floor slab top). In
practice, pipes and ducts run in the ceiling void — underside of the slab
above, minus clearance.
Fix (P118, S100-p118): ceilingHeightMm(floorRef) added to
BuildingGeometry + SqlBuildingGeometry. All 6 RouteBuilders updated:
horizontal MEP runs at ceiling void Z (nextFloor.z - slabThickness - 50mm
clearance). P118b: SqlBuildingGeometry.floors() now uses absolute Z from
walked placements instead of BOM-relative c_orderline dz.
Gap 2: No obstacle avoidance during routing¶
Problem: CrawlOps execute sequentially with no spatial awareness of other routes or ARC elements. If FP riser and ACMV duct occupy the same shaft, the router does not detect or avoid the conflict.
Current mitigation: CheckClashVerb (BIM COBOL verb) runs in VerbStage
(Step 7) as a post-route R-tree overlap check with 50mm clearance
(BIMConstants.MEP_STRUCTURE_CLEARANCE). Clash is detected after the fact,
not prevented.
Industry practice: GenMEP and academia use obstacle-aware A* (voxels blocked by existing elements). This is the main advantage of search-based routing.
Future hybrid: Prescriptive recipes for the discipline skeleton (riser→header→branch pattern) + search-based pathfinding for last-mile segment paths within each branch. BlenderBIM Issue #6521 proposes a voxel-A* orthogonal pathfinder that could serve as the last-mile solver.
Gap 3: Verb bypass — CLOSED (P119)¶
Problem: RouteBuilders composed CrawlOps directly, bypassing VerbRegistry. Routing was not auditable as verb lines in the pipeline log.
Fix (P119, f9fc4bc9):
CrawlOp.toVerbLine() on all 5 ops. DisciplineRouteBuilder.plan() replaces
buildRoute() — builders return RoutePlan, default buildRoute() logs verb
lines at INFO then executes via CrawlRouter. All 6 builders refactored.
SH: ELEC 15 + SP 13 verb lines. DX: ELEC 25 + SP 23.
Gap 4: Missing real-world concerns¶
| Concern | Status | Path to fix |
|---|---|---|
| Hanger/support spacing | EXISTS (HangVerb, SMACNA 1200mm) |
Already a verb — wire into RouteBuilder output |
| Insulation | CLOSED (P121) | Insulation as BOM child per discipline: FP 25mm (fire-rated), ACMV 50mm (thermal), CW 25mm (condensation), SP/ELEC 0mm |
| Soffit clearance | MISSING | Derive from ceiling void height. Minimum 50mm below soffit per MS 1525 |
| Access/maintenance points | MISSING | Valves and cleanouts at branch points. SP needs cleanout access per MS 1228 §5 |
| LPG wall thickness | CLOSED (P121) | wallThickness(floorRef) added to BuildingGeometry. Queries c_orderline for LEAF WALL elements. Fallback: 200mm |
Gap 5: Standard citation depth — CLOSED (P120)¶
Problem: RouteBuilders cited standards but didn't trace to specific clauses.
Fix (P120, c78c743b):
standardRefs() on all 6 builders — NFPA 13, MS 1228, MS 1525, MS 830,
ASHRAE 62.1. Logged at INFO with parameter values for compliance audit.
Proof Consumption (P15/P16/P17)¶
The three BIMEyes proofs that consume routing edges are fully implemented:
| Proof | What it checks | Data consumed | Status |
|---|---|---|---|
| P15 PIPE_IN_HOST | Pipe bbox within host room bbox | PlacementData + RoomData | Gated by relational data |
| P16 WASTE_GRADIENT | SP pipes slope downward | CONNECTS_TO edges, fromZ >= toZ |
Gated by system_edges > 0 |
| P17 SYSTEM_CONNECTED | BFS — every terminal reaches a source | CONNECTS_TO adjacency graph | Gated by system_edges > 0 |
Gate at ProveStage (Step 11): hasRelationalData() OR isGenerative() OR hasSystemEdges().
Currently blocked for most buildings because ad_element_dependency (the
CONNECTS_TO edge source for P16/P17) is populated only for legacy buildings.
The RouteStage edges land in system_edges/system_nodes — wiring these
into P16/P17 is the next integration step.
Room-Aware Branching (Level 2)¶
Gaps 1–5 fix the routing skeleton — correct Z, auditable verbs, traceable standards, missing products. This section addresses the next layer: making branches spatially intelligent within rooms.
Current behaviour¶
When a route branches to a room, every builder does the same thing:
RoomDimensions roomDims = geo.roomDimensions(room.ref());
double roomRun = roomDims != null ? roomDims.longestAxis() : 3000;
branchOps.add(new FollowOp(roomRun, STOCK_LENGTH_MM));
The branch enters the room and follows the longest axis for its full length. It does not know where it enters, where fixtures are, or how the room is shaped. Every room gets the same treatment regardless of discipline.
What each discipline actually needs¶
| Discipline | In-room behaviour | Standard | What's missing |
|---|---|---|---|
| FP | Grid of sprinkler heads at ceiling, max spacing per hazard class | NFPA 13 §8.6.2 (LH 4.6m), §8.6.3 (OH 4.0m) | Grid layout from room AABB, head count = ceil(width/spacing) × ceil(depth/spacing) |
| ELEC | Perimeter run at dado height (sockets), ceiling run (lights) | MS IEC 60364, IES lux tables | Two sub-routes per room: ceiling grid + wall perimeter |
| CW | Drop from ceiling to fixture positions (basin, sink, WC) | MS 1228 fixture unit table | Fixture type → position offset from room origin. ad_fixture_type table |
| SP | Drop from fixture to floor waste, connect to horizontal waste | MS 1228 §5 gradient tables | Gravity: fixture height → floor level. Gradient maintained on horizontal |
| ACMV | Ceiling diffuser grid, spacing per air changes/hour | ASHRAE 62.1, MS 1525 §6 | Diffuser count = room_volume × ACH / diffuser_capacity. Room height matters |
| LPG | Single drop from ceiling to gas point (kitchen range, heater) | MS 830 §4 | One connection point per room. Gas cock fitting at drop |
Data already available¶
RoomTarget has ref, position, discipline. RoomDimensions has
widthMm, depthMm, heightMm. This is enough for grid layouts
(FP sprinklers, ACMV diffusers) and perimeter runs (ELEC sockets).
What's not available: fixture positions within a room. CW needs to know
where the basin is. SP needs to know where the WC is. These come from the
BOM — m_bom_line LEAF elements with product category matching
IfcSanitaryTerminal, IfcFlowTerminal, etc. The BOM walker (CompileStage,
Step 3) already placed these elements with world coordinates.
Proposed: BuildingGeometry.fixturesInRoom(roomRef, discipline)¶
New query method on the interface:
record FixtureTarget(String ref, Point3D position, String ifcClass, String product)
List<FixtureTarget> fixturesInRoom(String roomRef, String discipline)
Implementation in SqlBuildingGeometry: query elements_meta or
c_orderline LEAF rows whose parent chain includes the room and whose
discipline matches. These are the ARC-placed elements from CompileStage —
real positions, not invented.
Branch sub-route composition¶
With fixture positions available, each discipline's branch becomes a mini-route within the room:
FP (sprinkler grid):
enter room at ceiling Z → compute grid from AABB + NFPA spacing
→ FOLLOW to first row → BRANCH to each head position
→ FOLLOW to next row → BRANCH to each head position
CW (fixture drops):
enter room at ceiling Z → for each fixture:
→ FOLLOW along ceiling to fixture X,Y
→ BEND 90° down → FOLLOW to fixture Z (basin height ~850mm)
→ fitting: stop valve + connector
SP (waste collection):
for each fixture → DROP to floor (fixture Z → floor Z)
→ FOLLOW along floor to waste pipe (gradient 1:40)
→ connect to floor waste header
Each sub-route uses the same CrawlOps (FollowOp, BendOp, BranchOp) but now with real target positions instead of "follow longest axis."
Phasing¶
Level 2 builds on Gaps 1–5:
| Phase | Prereq | Scope |
|---|---|---|
| L2.1 | P118 (ceiling Z) | fixturesInRoom() query on BuildingGeometry |
| L2.2 | L2.1 | FP grid layout — sprinkler head spacing from NFPA 13 per room AABB |
| L2.3 | L2.2 | CW/SP fixture drops — real fixture positions from BOM |
| L2.4 | L2.3 | ELEC dual-route — ceiling lights + perimeter sockets |
| L2.5 | L2.4 | ACMV diffuser grid — air changes per room volume |
Each phase is one bounded task for a coder. FP grid (L2.2) is the natural first because sprinkler spacing is the most formula-driven (NFPA 13 table lookup from hazard class + room AABB → grid dimensions → head count).
6.12.1 Compilation Isolation Invariant (S103)¶
Rule: The DAGCompiler SHALL NOT open any extraction DB, IFC file, or input/
source during compilation. The only permitted connections are:
| Connection | Purpose | Direction |
|---|---|---|
bom.db (System.getProperty) |
BOM recipes, C_OrderLine, ad_sysconfig | Read + Write |
library/ERP.db |
Shared discipline recipes, products, validation rules | Read-only |
output.db |
Compilation output (elements_rtree, c_orderline, system_edges) | Write |
Verification: GEO (PlacementCollectorVisitor.emitGeoSummary) opens the extraction DB in a separate proof stage (read-only comparison). It cannot feed back into the walk. GEO is an auditor, not a participant.
Why this matters for LMP: The tack chain in m_bom_line.dx/dy/dz was computed
at extraction time from IFC positions. At compilation time, the BOM walker reads
only m_bom_line — it never sees the IFC source. The BUILDING origin is the single
anchor point; everything below cascades parent-relative. No absolute borrowing.
Enforcement: Code review. No *_extracted.db or *_input* import exists in
DAGCompiler/src/main/java/. Any future addition must pass this gate.
6.12.2 MEP Placement — Shim + Joint Piece Architecture (S103/S104)¶
There is no difference between ARC placement and MEP placement. The walker reads M_BOM → M_BOM_Line → dx/dy/dz and accumulates. Same code path for placing a wall, a desk, a pipe tee, or a sprinkler head. Same PlacementCollectorVisitor. Same maths. Same GEO proof. No routing engine, no canvas, no pathfinding, no runtime computation.
1. Tack Point — Same as ARC¶
A tack point is just a point. Parent tack + dx/dy/dz = child tack. The walker accumulates. That's the whole algorithm — same for ARC, same for MEP.
The tack point is NOT tied to AABB or LBD convention. What the point physically represents depends on the product — LBD corner of a wall, centre of a pipe end, centre of a screw hole. The BOM doesn't care. It stores a number. The walker adds numbers. The product's geometry knows where to render relative to its tack point.
Tack stability invariant: PlacementCollectorVisitor computes each
element's world absolute tack as:
child_abs = parent_abs + rotated(dx, dy, dz) + bomOrigin
where parent_abs is the immediately preceding ancestor's absolute tack,
read from the anchor stack (anchorStack.peek()). Computation is O(1) per
node — no recomputation from root, no mutable running counter. The stack is
the cache of all ancestor absolute tacks; each node reads only its direct
parent. Accumulated FP error across a chain is bounded by the GEO proof
(evidence: ≤ 0.002mm over 1,653 element pairs, SH/TE/RM fleet). The
tolerance for GEO gate passage is ≤ 0.005mm per pair (LMP threshold).
2. The Shim — Zero Offset Host Alignment¶
The shim is a phantom product — no geometry, no mass, not rendered. It represents the host surface where MEP attaches. The shim melts into the host: its position IS the host surface position. Zero offset.
A shim placed against a wall has the wall's XYZ. A shim placed against a ceiling has the ceiling's XYZ. No gap, no clearance, no 5mm standoff. The shim IS the surface in coordinate terms.
The device that attaches to the host (pipe bracket, sprinkler mount) carries its own standoff intrinsically. A bracket that sits 5mm proud of the wall? That 5mm is in the device's M_BOM_Line dx/dy/dz within the recipe — the product knows its own offset. The shim contributes nothing except the coordinate frame origin at the host surface.
ERP.db M_Product — Shims (phantom host surface anchors, ~10-15 types)
FP_CEILING_SHIM host_ifc_class=IfcCovering mount=BOTTOM
ELEC_CEILING_SHIM host_ifc_class=IfcCovering mount=BOTTOM
ELEC_WALL_SHIM host_ifc_class=IfcWall mount=SIDE
CW_CEILING_SHIM host_ifc_class=IfcCovering mount=BOTTOM
SP_FLOOR_SHIM host_ifc_class=IfcSlab mount=TOP
ACMV_CEILING_SHIM host_ifc_class=IfcCovering mount=BOTTOM
LPG_WALL_SHIM host_ifc_class=IfcWall mount=SIDE
The shim's position comes from the MAKE line dx/dy/dz in the BOM tree — same as any other BOM line. No runtime host matching. No query. No ShimMatcher computation. The position was extracted from the IFC once and stored as a number on the M_BOM_Line.
3. Entry Point — OrderLine → MEP_System BOM (abstract, VR-resolved)¶
The MEP walk enters through an OrderLine whose Product has AD_Org = discipline (FP, ELEC, ACMV, CW, SP, LPG). That product IS the root MEP_System BOM in ERP.db. BomDropper explodes it — finds the shim (phantom parent) — recurses into children. Same explosion as ARC OrderLine → BUILDING BOM → FLOOR → ROOM.
OrderLine (AD_Org=FP, Product=FP_CEILING_SHIM)
→ BomDropper explodes FP_CEILING_SHIM M_BOM
→ children: PIPE_STRAIGHT_50MM, PIPE_TEE_50_25MM, SPRINKLER_HEAD_K56...
→ walker accumulates dx/dy/dz for each child
Each child is identified by exact product ID — no fuzzy matching, no
proximity search. The recipe says PIPE_STRAIGHT_50MM_POLY_STEEL, that's
what gets placed.
4. Joint Pieces — The Toolbox¶
Joint pieces are M_Products in ERP.db. Extracted from RosettaStone IFCs
by ifc_class and PredefinedType — a pipe segment IS a straight piece,
an IfcPipeFitting IS a tee or elbow. No IfcRelConnectsPorts needed
(our IFCs don't carry port data). The fleet teaches vocabulary by example.
Each piece in component_library.db carries LOD metadata for orientation:
- attachment_face — CENTER / TOP / BOTTOM / SIDE
- up_axis — which local axis points up (Z)
- forward_axis — which local axis is the run direction (Y or X)
- orientation — PENDANT / UPRIGHT / HORIZONTAL / VERTICAL / MIXED
This is facing data only — no tack-in/tack-out ports. Rotation from tack
point is on M_BOM_Line.rotation_rule. The walker reads it, applies it.
The piece itself is the simplest BOM — one product with orientation metadata.
5. The MEP BOM — Shim Root, Joint Piece Children¶
FLOOR (in *_BOM.db)
└── FP_CEILING_SHIM (KITCHEN) ← phantom, melts into ceiling XYZ
├── PIPE_STRAIGHT_50MM dx=0 dz=-5 ← 5mm below ceiling (device offset)
├── PIPE_TEE_50_25MM dx=L dz=-5 ← tee at branch point
├── PIPE_STRAIGHT_25MM dy=S dz=-5 ← branch from tee
└── SPRINKLER_HEAD_K56 dy=0 dz=-305 ← terminal drop (300mm pendant)
FLOOR
└── FP_CEILING_SHIM (KITCHEN) ← one shim per room per discipline
└── FP_CEILING_SHIM (CORRIDOR)
└── ELEC_WALL_SHIM (KITCHEN)
└── SP_FLOOR_SHIM (BATHROOM)
The walker recurses: FLOOR → shim → pieces. Same as FLOOR → ROOM → FURNITURE. All dx/dy/dz are small, local, verifiable — the shim absorbed the building-scale positioning. Device standoffs are intrinsic to each child's BOM line offset — the shim contributes zero.
6. InterimWorkshop — Variable-Length Pieces¶
A pipe run between two fittings may need a non-standard length. Like a real contractor who cuts pipe from stock to fit the gap.
BOM Line Model Extension (from Compiere/e-Evolution Manufacturing)¶
The M_BOM_Line gains c_uom_id — the Unit of Measure from iDempiere.
qty becomes REAL (was INTEGER). The UOM tells the walker how to
interpret qty:
| c_uom_id | qty meaning | Walker action |
|---|---|---|
| EA | Instance count (default) | expandVerb() — TILE/ROUTE/CLUSTER/etc |
| MM | Length in millimetres | InterimWorkshop — recompute primitive |
| M | Length in metres | InterimWorkshop — recompute primitive |
When c_uom_id is a length unit (MM/M), the walker calls
InterimWorkshop instead of expandVerb(). No CUT verb needed — the
UOM is the signal. This follows Compiere convention where BOMQty +
C_UOM_ID drive the BOM Drop interpretation.
qty_type (VARIABLE/FIXED) guards the line: VARIABLE allows runtime
adjustment, FIXED rejects it. A tee fitting is FIXED (tack i/o
predetermined). A straight pipe is VARIABLE (outlet shifts with length).
Mathematical Primitives, Not Parametric¶
MEP pieces are mathematical primitives — cylinder, box, torus arc. The workshop does not stretch or deform an existing mesh. It recomputes the primitive from first principles:
Cylinder(diameter=50mm, length=2345mm, LOD=16 faces)
→ vertices: r*cos(θ), r*sin(θ), z ∈ [0, 2345mm]
→ 16 side faces + 2 end caps
This is NOT parametric mesh generation (which is forbidden). Parametric means exploring design space with arbitrary parameters. This is geometry from data — same principle as the entire compiler. The LOD (face count, tessellation) comes from the library template. The dimensions come from the BOM line. The maths produces the mesh.
Materials are flat RGBA per face (no UV mapping, no textures). A shorter cylinder has shorter rectangular side faces with the same uniform colour. No stretching, no shrinking artifact.
InterimWorkshop Interface¶
/**
* Runtime mesh computation for variable-length pieces.
* Like a contractor's workshop: recompute the primitive at the required
* length. LOD from library template. Dimensions from BOM line.
* Result is ephemeral — not stored back.
*/
public class InterimWorkshop {
/** Recompute primitive mesh at target length along forward axis. */
static MeshResult recompute(MProduct product, String forwardAxis,
double targetLengthMm, int lodFaceCount);
}
Flat Sibling Pattern — Small BOMs¶
MEP pieces under the shim are flat siblings, not nested chains. Each is a 1-2 piece mini-BOM — like a contractor's work packet:
FP_CEILING_SHIM (KITCHEN)
├── PIPE_STRAIGHT_50MM dx=0 qty=2345 c_uom_id=MM qty_type=VARIABLE
├── PIPE_TEE_50_25MM dx=2.345 qty=1 c_uom_id=EA qty_type=FIXED
├── PIPE_STRAIGHT_50MM dx=2.345 qty=1800 c_uom_id=MM qty_type=VARIABLE
├── PIPE_TEE_50_25MM dx=4.145 qty=1 c_uom_id=EA qty_type=FIXED
└── SPRINKLER_HEAD_K56 dx=2.345 dy=0.5 dz=-0.3 qty=1 c_uom_id=EA
Fittings are at fixed positions (from spacing rules / extraction). Straight pipes fill gaps between fittings — their length (qty in MM) is exactly the gap. Each sibling is independently tacked from the shim. Shortening one pipe does NOT cascade to neighbours — the next fitting's dx is independently defined.
The tack i/o for a VARIABLE piece: inlet = dx (BOM line offset from parent). Outlet = dx + qty(mm) along forward_axis. The next sibling's dx equals that outlet point. Deterministic, no drift.
7. IFCtoERP — Tack Extraction¶
Tack offsets between joined MEP pieces are extracted by IFCtoERP while reading RosettaStone IFC geometry. Only at extraction time are both pieces' positions available. The maths: child_pos - parent_pos = dx/dy/dz. Same subtraction as ARC walls and slabs.
IFCtoERP writes directly to ERP.db (no intermediate database): - Joint piece M_Products (piece type + diameter from AABB) - Shim M_Products (phantom, per discipline x host surface type) - M_BOM recipes (shim-rooted, children with dx/dy/dz tack offsets)
The dx/dy/dz on each M_BOM_Line IS the vector from parent tack to child tack. Extracted once. Stored as data. Walked as numbers.
8. Slope, Branching, and Risers — Three Triaged Edge Cases¶
These three patterns are not new walker code. They are data on the BOM line — the walker already handles them through existing mechanisms (dz accumulation, sub-assemblies, rotation). The triage closes each gap by defining how the metadata tables express the pattern.
8a. Slope / Gradient (Sanitary Drainage)¶
MS 1228 §5 requires waste pipes at minimum 1:40 gradient (25mm drop per metre). This is NOT a runtime computation — the gradient is baked into each successive pipe segment's dz on the M_BOM_Line.
SP_FLOOR_SHIM (BATHROOM)
├── PIPE_STRAIGHT_50MM dx=0 dz=0 qty=1000 c_uom_id=MM
├── PIPE_STRAIGHT_50MM dx=1.0 dz=-0.025 qty=1000 c_uom_id=MM ← 25mm drop
├── PIPE_STRAIGHT_50MM dx=2.0 dz=-0.050 qty=1000 c_uom_id=MM ← 50mm drop
└── FLOOR_TRAP dx=0.5 dz=-0.010 qty=1 c_uom_id=EA
The gradient is expressed as metadata in ad_mep_laying_rule:
| rule_id | discipline | rule_type | parameter | value | standard | section |
|---|---|---|---|---|---|---|
| LAY-SP-001 | SP | GRADIENT | min_slope | 0.025 | MS 1228 | §5.3 |
| LAY-SP-002 | SP | GRADIENT | max_slope | 0.040 | MS 1228 | §5.3 |
| LAY-LPG-001 | LPG | GRADIENT | min_slope | 0.010 | MS 830 | §4.2 |
The IFCtoERP extraction computes per-segment dz from IFC positions.
The validation rule (AD_Val_Rule) checks placed pipe Z-deltas against
the table. The code is abstract — it reads min_slope from the table,
compares against placed dz/dx ratio. No hardcoded gradients.
8b. Branching at Tees¶
A tee fitting has 3 connections: main-in, main-out, branch-out. The main run continues as flat siblings. The branch is a nested sub-BOM under the tee — the tee IS a sub-assembly with its own children.
FP_CEILING_SHIM (KITCHEN)
├── PIPE_STRAIGHT_50MM dx=0 qty=2345 c_uom_id=MM ← main run
├── PIPE_TEE_50_25MM dx=2.345 qty=1 c_uom_id=EA ← tee (sub-assembly)
│ └── PIPE_STRAIGHT_25MM dy=0 qty=1500 c_uom_id=MM ← branch child
│ └── SPRINKLER_HEAD_K56 dy=1.5 dz=-0.3 qty=1 c_uom_id=EA
├── PIPE_STRAIGHT_50MM dx=2.345 qty=1800 c_uom_id=MM ← main run continues
└── PIPE_TEE_50_25MM dx=4.145 qty=1 c_uom_id=EA ← next tee
The tee's branch children have dx/dy/dz relative to the tee's tack point (centre of the fitting). The walker recurses into the tee sub-BOM (onSubAssembly), walks its children, exits (onSubAssemblyComplete), then continues with the next main-run sibling. Standard BOM recursion — no branching-specific code.
Branch metadata in ad_mep_fitting_rule:
| rule_id | piece_type | connection | axis | offset_rule | standard |
|---|---|---|---|---|---|
| FIT-TEE-001 | PIPE_TEE | MAIN_OUT | X | diameter/2 from centre | — |
| FIT-TEE-002 | PIPE_TEE | BRANCH_OUT | Y | diameter/2 from centre | — |
| FIT-ELB-001 | PIPE_ELBOW | OUT | varies | angle × bend_radius | — |
| FIT-RED-001 | PIPE_REDUCER | OUT | X | length along axis | — |
The table defines each fitting's connection offsets. IFCtoERP reads these when building M_BOM recipes. The walker doesn't know about fittings — it just sees sub-assemblies with dx/dy/dz.
8c. Vertical Risers¶
A riser is a pipe running vertically through floors. The BOM line
carries rotation_rule = 90° around the horizontal axis, rotating the
pipe's forward_axis from Y (horizontal default) to Z (vertical).
BUILDING
└── FP_RISER_ASSEMBLY (bom_type=MEP)
├── PIPE_STRAIGHT_50MM dz=0 rotation_rule=PI/2 qty=3000 c_uom_id=MM
├── PIPE_STRAIGHT_50MM dz=3.0 rotation_rule=PI/2 qty=3000 c_uom_id=MM
└── PIPE_STRAIGHT_50MM dz=6.0 rotation_rule=PI/2 qty=3000 c_uom_id=MM
The walker's existing rotation stack handles this — cumulative rotation is pushed on onSubAssembly, applied to dx/dy/dz in onLeaf, popped on exit. InterimWorkshop recomputes along the library's forward_axis (Y); the walker's rotation transforms the half-extents to world frame (Z).
Riser metadata in ad_mep_riser_rule:
| rule_id | discipline | parameter | value | standard | section |
|---|---|---|---|---|---|
| RSR-FP-001 | FP | max_floor_height_mm | 4000 | NFPA 13 | §8.15 |
| RSR-FP-002 | FP | riser_diameter_mm | 65 | NFPA 13 | §8.15.1 |
| RSR-CW-001 | CW | min_pressure_kpa | 150 | MS 1228 | §3.4 |
| RSR-SP-001 | SP | stack_min_diameter_mm | 100 | MS 1228 | §5.7 |
9. Metadata Tables — Standards as Data, Code as Abstract Walker¶
All laying rules, fitting rules, and riser rules live in ERP.db as metadata tables. The code reads them. The code never embeds standard numbers, gradient values, spacing limits, or diameter rules.
ERP.db metadata tables (read by walker + validation, never by user):
ad_mep_laying_rule — slope gradients, clearances, material rules
ad_mep_fitting_rule — connection offsets per fitting type
ad_mep_riser_rule — vertical run constraints per discipline
ad_val_rule — post-walk validation (existing, P15/P16/P17)
ad_space_type_mep_bom — room type → discipline mapping (existing, 186 rows)
Why this matters for usability: A user (engineer, project manager) can open ERP.db, read the laying rule table, see "MS 1228 §5.3: min slope = 0.025", and understand exactly what the system enforces. No source code reading required. Changing a regulation = updating a row in the table, not editing Java. The compiler is truly user-friendly when the rules are visible metadata and the code is an abstract walker.
Validation flow:
1. Walker places pieces (accumulates dx/dy/dz, calls InterimWorkshop)
2. AD_Val_Rule reads placed positions from output.db
3. Compares against ad_mep_laying_rule / ad_mep_riser_rule thresholds
4. Emits PASS/FAIL per rule per element — no hardcoded thresholds in code
10. Validation — Standards Confirm the Walk¶
Standards validate placement AFTER the walk. They do NOT drive it.
Validation layering (three tiers):
| Tier | Mechanism | When | Scope |
|---|---|---|---|
| 1 — GEO proof | G3-DIGEST gate (W-TACK-STABLE) | Per RosettaStone run | Tack FP stability ≤ 0.005mm across all fleet elements |
| 2 — CheckClashVerb (primary) | AD_Val_Rule clash rules | Post-walk audit pass | Cross-discipline penetration; host-surface clearance |
| 3 — Per-element soft check (supplementary) | PlacementValidator.isValidPlacement() | During BOM walk, per leaf | Room-bounds overreach; early-warning log only, no throw |
Tier 3 is supplementary — it logs early warnings but does not replace Tier 2. CheckClashVerb remains the primary clearance enforcement mechanism. A Tier 3 violation is a warning; a Tier 2 violation is a FAIL. Tier 3 MUST NOT be promoted to a hard fail without first confirming CheckClashVerb cannot detect the same violation.
| Standard | AD_Val_Rule check | Source table | Input |
|---|---|---|---|
| NFPA 13 §8.6 | Sprinkler spacing ≤ 4600mm (LH) | ad_mep_laying_rule | Placed sprinkler positions |
| NFPA 13 §8.15 | Riser diameter ≥ 65mm | ad_mep_riser_rule | Riser product dimensions |
| MS 1228 §5.3 | Waste gradient ≥ 1:40 | ad_mep_laying_rule | SP pipe dz/dx ratio |
| MS 1228 §3.4 | Min pressure at highest fixture | ad_mep_riser_rule | Riser height vs pressure |
| ASHRAE 62.1 | Air changes per room | ad_mep_laying_rule | Diffuser count vs room volume |
| MS 830 §4.2 | Gas clearance ≥ 150mm | ad_mep_laying_rule | LPG pipe vs ignition sources |
| MS 830 §4.2 | Gas pipe gradient ≥ 1:100 | ad_mep_laying_rule | LPG pipe dz/dx ratio |
11. Phasing¶
| Phase | Scope | Prereq |
|---|---|---|
| J1 | IFCtoERP: extract joint piece M_Products + tack offsets from RosettaStone IFCs → ERP.db | S103 |
| J2 | Shim products: phantom host surface anchors in ERP.db (zero offset) | J1 |
| J3 | MEP recipes: shim-rooted M_BOM assemblies with dx/dy/dz in ERP.db | J2 |
| J4 | M_BOM_Line model: c_uom_id (EA/MM/M), qty→REAL, qty_type guard (FIXED/VARIABLE) | J3 |
| J5 | InterimWorkshop: mathematical primitive recomputation for length-unit lines | J4 |
| J6 | Metadata tables: ad_mep_laying_rule, ad_mep_fitting_rule, ad_mep_riser_rule | J5 |
| J7 | Validation: AD_Val_Rule reads metadata tables, post-walk PASS/FAIL per rule | J6 |
| J8 | Fleet: TE + RM, measure coverage per discipline per room | J7 |
J1-J3 are data preparation (ERP.db). J4 is the BOM line model extension (iDempiere convention: BOMQty + C_UOM_ID). J5 is the only new walker code (mathematical primitive recomputation — not parametric). J6 defines the metadata tables that make standards visible to users without reading code. J7-J8 are verification. The walker itself (PlacementCollectorVisitor) gains a UOM check in onLeaf() but no MEP-specific logic — UOM drives the dispatch. The code stays abstract; the rules live in tables.
Existing Infrastructure¶
| Component | Status | Role |
|---|---|---|
| PlacementCollectorVisitor | DONE | Walks M_BOM, accumulates dx/dy/dz — identical for ARC and MEP |
| BomDropper | DONE | Explodes recipes recursively — identical for ARC and MEP |
| IFCtoERP | DONE (J1+J2) | 785 joint piece + 11 shim M_Products in ERP.db |
| DisciplineBomBuilder | DONE (J3) | MEP elements in BOM with shim as parent |
| X_M_BOMLine | EXTEND (J4) | Add c_uom_id, qty→REAL, qty_type FIXED/VARIABLE guard |
| InterimWorkshop | PROMPT (00f) | Primitive recomputation: cylinder/box/torus from LOD + dimensions |
| ad_mep_laying_rule | NEW (J6) | Slope gradients, clearances, spacing — standards as visible data |
| ad_mep_fitting_rule | NEW (J6) | Connection offsets per fitting type (tee, elbow, reducer) |
| ad_mep_riser_rule | NEW (J6) | Vertical run constraints per discipline |
| CrawlRouter + RouteBuilders | DONE (S100) | Generative fallback (buildings without IFC MEP data) |
| system_edges | DONE (S100) | P15/P16/P17 proof input |
| ad_space_type_mep_bom | EXISTS (ERP.db, 186 rows) | Room type → discipline mapping |
6.12.3 Hybrid Pattern Architecture — RouteWalker for Unclassified Buildings (S104)¶
Problem statement: Some IFC models (RM and equivalent IFC2x3 buildings) carry MEP geometry
as generic IfcFlowSegment/IfcFlowFitting with no sub-discipline attribute. Per §11.1, CW and
SP cannot be disambiguated at the element level for these buildings (G2). The §6.12.2 shim+recipe
architecture requires element-level discipline assignment, which G2 blocks.
Solution: Pattern-over-anchors. The RouteWalker mines topology patterns from discipline-complete
buildings (TE: CW 619 segments, SP 455 segments, discipline confirmed via elements_meta.discipline)
and applies them to anchor points extracted from the unclassified building (RM). Individual pipe
classification is not required — the pattern defines the system.
This does not violate §6.12.1. IFCtoERP extracts anchor coordinates from the RM extraction DB at extraction time and writes them to ERP.db. DAGCompiler reads ERP.db only. No extraction DB access at compile time.
1. Anchors — Source and Terminal Points¶
An anchor is an extracted coordinate pair (source_xyz, terminal_xyz) representing a connection between MEP endpoints. The 491 RM generic pipe elements yield anchor pairs — pipe start and end points — without discipline assignment.
Anchor types: - METER — connection from water source/riser to fixture (identified by flanking fixture presence) - FIXTURE — terminal endpoint: sink, basin, toilet, WC, floor trap (from element_type keywords) - VALVE — flow controller (from IfcFlowController) - GENERIC — pipe endpoint with no identifiable context
IFCtoERP writes anchors to ad_mep_anchor:
CREATE TABLE ad_mep_anchor (
anchor_id TEXT PRIMARY KEY,
source_building TEXT NOT NULL,
anchor_type TEXT NOT NULL CHECK(anchor_type IN ('METER','FIXTURE','VALVE','GENERIC')),
x_m REAL NOT NULL,
y_m REAL NOT NULL,
z_m REAL NOT NULL,
storey TEXT,
ifc_guid TEXT
);
Anchor extraction rule: for each MEP element AABB in the extraction DB, compute the geometric centre (cx, cy, cz). If the element has two spatially distinct ends (length > 3× min AABB dim), emit two anchors (start and end). Otherwise emit one anchor at centre. Fixture-type elements (toilet/sink/drain keywords) are typed as FIXTURE.
2. Pattern — Topology Rows in ad_mep_pattern¶
A pattern is a sequence of routing steps mined from a discipline-complete building. Each row is
one step: a node type transition (from → to), the direction axis, and the offset rule. Steps are
ordered by sequence. RouteWalker iterates steps in order against the anchor set.
CREATE TABLE ad_mep_pattern (
pattern_id TEXT NOT NULL,
discipline TEXT NOT NULL, -- CW, SP, FP, ACMV, ELEC
building_type TEXT NOT NULL, -- COMMERCIAL, RESIDENTIAL, TERMINAL, CLINIC
sequence INTEGER NOT NULL,
from_node_type TEXT NOT NULL, -- METER, FIXTURE, VALVE, RISER, JUNCTION, STACK
to_node_type TEXT NOT NULL,
direction_axis TEXT NOT NULL, -- X, Y, Z, GRADIENT
piece_type TEXT NOT NULL, -- PIPE_STRAIGHT, PIPE_ELBOW, FLOOR_TRAP, etc.
offset_rule TEXT, -- DIRECT, MIN_GRADIENT, STACK_OFFSET
gradient REAL, -- for GRADIENT axis: dz per metre (e.g. 0.025 for SP)
notes TEXT,
source_building TEXT, -- which building this pattern was mined from
PRIMARY KEY (pattern_id, sequence)
);
CW pattern (mined from TE, discipline=CW): supply run from meter/riser → horizontal main → branch to fixture. Direction: horizontal (X or Y), then vertical drop (Z) to fixture connection.
SP pattern (mined from TE, discipline=SP): fixture drain → horizontal run with gradient → stack/waste riser. Direction: horizontal with GRADIENT (dz/dx = 0.025), then vertical stack (Z).
Example SP pattern rows (mined from TE 455 SP segments):
pattern_id=SP_TERMINAL_01 discipline=SP building_type=TERMINAL
seq from_node_type to_node_type direction_axis piece_type offset_rule gradient
10 FIXTURE JUNCTION GRADIENT PIPE_STRAIGHT MIN_GRADIENT 0.025
20 JUNCTION JUNCTION GRADIENT PIPE_STRAIGHT MIN_GRADIENT 0.025
30 JUNCTION STACK Z PIPE_STRAIGHT STACK_OFFSET —
Example CW pattern rows:
pattern_id=CW_TERMINAL_01 discipline=CW building_type=TERMINAL
seq from_node_type to_node_type direction_axis piece_type offset_rule
10 METER JUNCTION X PIPE_STRAIGHT DIRECT
20 JUNCTION FIXTURE Y PIPE_STRAIGHT DIRECT
30 JUNCTION FIXTURE Z PIPE_STRAIGHT DIRECT
Building type matching: RouteWalker selects patterns by building_type. RM is RESIDENTIAL.
TE is TERMINAL. If no exact match, fall back to the nearest pattern by element count similarity.
Pattern mining from TE is a one-time extraction step (00q-mine prompt).
3. RouteWalker — Pattern Application Within Envelope¶
RouteWalker takes:
- Anchor set from ad_mep_anchor (for the target building)
- Pattern from ad_mep_pattern (by discipline + building_type)
- Envelope from compile DB c_orderline WHERE Discipline='ARC' (walls, slabs, ceilings)
Algorithm:
1. Select anchors by storey
2. For each pattern step (seq order): connect nearest unconnected anchor pair matching (from_node_type → to_node_type)
3. Generate M_BOM_Line entries: piece_type from step row, dz from gradient rule, length from anchor distance
4. ARC envelope used for constraint: generated pipe must not penetrate ARC AABB (clash check)
5. Write to compile DB c_orderline with Discipline=discipline
RouteWalker operates entirely within DAGCompiler. It reads ERP.db (anchors + patterns) and the compile DB (ARC envelope from c_orderline). It writes to the compile DB. No extraction DB access.
RouteWalker is NOT a routing engine (no A* pathfinding, no graph search). It is a pattern applier: for each pattern step, find the nearest matching anchor pair, emit BOM lines. The pattern encodes all routing intelligence. The walker only matches and emits.
4. Witness Claims¶
W-PATTERN-CW — RouteWalker generates CW pipe network for RM:
For Revit_MEP, RouteWalker with CW_TERMINAL_01 pattern applied to METER+FIXTURE anchors produces a connected CW network: all FIXTURE anchors reachable from at least one METER anchor, zero CW pipes intersecting ARC AABB (clash=0), all generated segments horizontal or vertical (no diagonal), pipe count within 20% of TE CW segment count scaled by floor area ratio.
W-PATTERN-SP — RouteWalker generates SP pipe network for RM:
For Revit_MEP, RouteWalker with SP_TERMINAL_01 pattern applied to FIXTURE+STACK anchors produces a connected SP network: all FIXTURE anchors drain to at least one STACK anchor, all generated GRADIENT segments have dz/dx ≥ 0.025 (MS 1228 §5.3), zero SP pipes intersecting ARC AABB (clash=0), STACK anchor count ≥ 1 per storey.
GEO DRIFT scope: W-PATTERN-CW and W-PATTERN-SP use clash+gradient assertions, not centroid matching. Centroid DRIFT (±50mm) applies only to extracted elements (FP, ACMV in RM; all disciplines in TE). Generated CW/SP geometry for RM is not compared against IFC positions (none exist at discipline level) — it is validated structurally (clash, connectivity, gradient).
5. Phasing¶
| Phase | Scope | Prereq |
|---|---|---|
| 00q-schema | DDL: ad_mep_anchor + ad_mep_pattern in ERP.db migration |
G1 fix: add discipline to _import_joint_piece_types |
| 00q-mine | Mine CW+SP patterns from TE (Terminal_Extracted.db → ad_mep_pattern rows) | 00q-schema |
| 00q-anchor | IFCtoERP: extract anchor points from RM into ad_mep_anchor |
00q-schema |
| 00r-walker | DAGCompiler: RouteWalker class — pattern select + anchor match + BOM line emit | 00q-anchor |
| 00r-envelope | RouteWalker: ARC envelope clash check using compile DB c_orderline | 00r-walker |
| 00s-witness | W-PATTERN-CW + W-PATTERN-SP gate tests | 00r-envelope |
| 00t-g3fix | IFCtoERP.discFromClass(): read elements_meta.discipline first (G3 fix) | 00q-anchor |
6. What This Does Not Change¶
- §6.12.1 Compilation Isolation Invariant: unchanged. IFCtoERP extracts; DAGCompiler compiles.
- §6.12.2 shim+recipe architecture: unchanged for TE (all disciplines extracted, classified, recipe-walked).
- GEO DRIFT proof for TE: unchanged (extracted elements, centroid matching).
- RM FP, ACMV: continue on shim+recipe path (those IFC classes are typed).
- RouteWalker is additive — it supplements the shim+recipe walk for CW/SP in buildings with G2.
6.13 IFC-Driven Extraction¶
Status: DONE (S100-p125, commit 3e056227). SH IFC-driven, FK scope box fallback.
The finding¶
The extraction pipeline (ScopeBomBuilder) assigns elements to SET BOMs
using YAML-authored scope boxes (origin_m, aabb_mm). This is manual —
the human defines rectangular containment volumes for each room zone.
But the IFC file already carries this information:
spatial_structure:
IfcBuilding
IfcBuildingStorey "Ground Floor"
IfcSpace "1 - Living room" ← 12 elements contained
IfcSpace "2 - Bedroom" ← 2 elements contained
IfcSpace "3 - Entrance hall" ← 0 elements
IfcBuildingStorey "Roof"
IfcSpace "4 - Roof" ← 0 elements
rel_contained_in_space: element_guid → space_guid (14 assignments)
rel_fills_host: element_guid → host_guid (7 door/window → wall)
Dry run on SH (58 elements): 14 elements assigned to spaces by IFC, 44 orphans (structural: walls, slabs, ceilings, curtain wall). The orphans are correctly structural — not in any room.
Extraction flow¶
IFC spatial containment (S100-p125):
Read rel_contained_in_space from extracted.db
→ "1 - Living room" contains 12 elements
→ "2 - Bedroom" contains 2 elements
YAML maps: ifc_space "1 - Living room" → template SH_LIVING_SET
ifc_space "2 - Bedroom" → template SH_BED_SET
VerbDetector groups within each IFC space
YAML format¶
floor_rooms:
Ground Floor:
bom_id: FLOOR_SH_GF_STD
product_category: GF
spaces:
- { ifc_space: "1 - Living room", template_bom: SH_LIVING_SET, role: LIVING, seq: 10 }
- { ifc_space: "2 - Bedroom", template_bom: SH_BED_SET, role: MASTER, seq: 30 }
No origin_m, no aabb_mm. IFC spatial containment is the sole source
during extraction. YAML maps space names to BOM templates. Scope boxes are
an Order processing concern — the BIM Designer GUI and BOM Drop use
scope boxes when the user defines sub-room zones at order time (e.g.,
splitting a Living room into dining + seating zones).
For buildings without IfcSpace data, extraction groups by storey only
(existing StructuralBomBuilder behaviour). Sub-room grouping is deferred
to order time.
Impact on CLUSTER¶
IFC-driven extraction doesn't eliminate CLUSTER directly — the 6 dining chairs are still 6 identical products in one space. But it changes the extraction architecture from "sort by manual box" to "sort by IFC containment" which:
- Removes human coordinate authoring errors (wrong scope box origin)
- Uses the architect's spatial intent (they modelled the IfcSpaces)
- Enables IFC
rel_fills_hostfor door/window→wall BOM nesting - Reduces YAML from ~15 lines per room to ~2 lines per room
Structural orphans¶
44 elements not in any IfcSpace are structural: walls (5), slabs (2), ceilings (3), curtain wall (26), doors (3), windows (4), roof (1).
Doors and windows have rel_fills_host → they belong to their host wall.
Walls and slabs are floor-level structural → StructuralBomBuilder handles
them (unchanged).
The current extraction already handles orphans correctly —
StructuralBomBuilder picks up everything not assigned to a SET.
IfcRelAggregates extraction (S100-p126)¶
Status: DONE (48d14537)
New extraction table rel_aggregates captures IFC parent-child assembly
decomposition:
CREATE TABLE rel_aggregates (
parent_guid TEXT NOT NULL,
child_guid TEXT NOT NULL,
PRIMARY KEY (parent_guid, child_guid)
);
Populated from IfcRelAggregates relationships in the IFC file. Results:
- SH: 34 rows. 2 assemblies with 13 children each (curtain wall halves), 1 with 3, 2 with 2, 2 singletons.
- DX: 38 rows. 2 assemblies with 10 children each (stair assemblies), 2 with 5, 1 with 4, 3 singletons.
Phantom parents: Parent GUIDs have no entry in elements_meta because
IfcCurtainWall and IfcStair are not in the extraction class list. Assembly
structure is visible only via rel_aggregates join to elements_meta on
child_guid.
Java pipeline unchanged — rel_aggregates is read-only context for P129
(assembly BOMs).
Spatial container auto-discovery (S100-p127)¶
Status: DONE (7745affd)
StoreyConfig renamed to SpatialContainerConfig — abstract naming that
works for buildings (storeys) and infrastructure (segments).
Auto-discovery: When YAML storeys: section is empty, containers are
auto-discovered from storeyElements keys:
- Sort by min Z of their elements (seq: 1010, 1020, ...)
code= generic abbreviation (first letter of each word, uppercase)- No hardcoded name→code mapping — algorithm works for any building
Results:
- SH: 3 containers — Ground Floor→GF, Roof→RO, Unknown→UN
- DX: 5 containers — T/FDN→TF, Level 1→L1, Unknown→UN, Level 2→L2, Roof→RO
YAML storeys: kept as Order override (backward compat). Empty YAML =
auto-discover. BOM IDs change with auto-derived codes (e.g. SH_ROOF_STR →
SH_RO_STR) — opaque keys, no functional impact.
Scope excludes for CompositionBomBuilder (S100-p128)¶
Status: DONE (f807bb3c)
Elements assigned to SET BOMs by ScopeBomBuilder are excluded from CompositionBomBuilder mirror partition. Fixes DX reconciliation delta +50→0 (furniture was double-counted in both SET BOMs and half-unit).
DX YAML floor_rooms removed — dead code since P125.
IFC assembly BOMs (S100-p129)¶
Status: DONE (1153671c)
StructuralBomBuilder reads rel_aggregates from extraction DB. Groups children
by parent GUID, creates ASSEMBLY BOMs for groups with 2+ children. MAKE lines
link FLOOR → ASSEMBLY. Elements not in any assembly stay as flat leaves.
- SH: 2 curtain wall assemblies (13 children each, factorized to 4 BOM lines)
- DX: 2 stair assemblies (3 children each)
- FK/IN: No rel_aggregates matches — zero regression
Phantom parents (IfcCurtainWall, IfcStair) have no elements_meta entry —
assembly structure visible only via rel_aggregates child_guid join.
References: DISC_VALIDATE_SRS.md §9 (5-table LOD chain) | DocAction_SRS.md §1.3 (processIt DocEvent) | CALIBRATION_SRS.md (DocEvent vs Terminal) | G4_SRS.md §2 (output.db pattern)
§11 — DISC BOM Single Source of Truth — Audit Findings (S104)¶
Audit scope: Read-only forensics. Two test stones: Revit_MEP (RM) + SJTII_Terminal (TE).
DBs queried: library/ERP.db, DAGCompiler/lib/input/Revit_MEP_extracted.db, DAGCompiler/lib/input/Terminal_Extracted.db.
§11.1 — CW/SP Disambiguation Rule¶
TE (SJTII_Terminal) — RESOLVED¶
elements_meta.discipline carries the sub-discipline per element row in the extraction DB.
This column is populated at IFC extraction time and is the authoritative source.
Evidence (Terminal_Extracted.db):
ifc_class discipline count
─────────────────────────────────────────────
IfcPipeSegment FP 2672
IfcPipeSegment CW 619
IfcPipeSegment SP 455
IfcPipeSegment LPG 75
IfcPipeFitting FP 3146
IfcPipeFitting CW 638
IfcPipeFitting SP 372
IfcPipeFitting LPG 87
IfcFlowController FP 14
IfcFlowController CW 7
IfcFlowTerminal SP 150
IfcFlowTerminal CW 106
IfcFireSuppressionTerminal FP 909
CW/SP rule for TE: read elements_meta.discipline → map to AD_Org_ID (CW=6, SP=7, FP=3, LPG=8).
No keyword heuristic needed. No geometry needed.
RM (Revit_MEP) — G2: CW/SP_UNRESOLVABLE¶
RM uses IFC2x3 generic classes. All pipe/fitting elements have discipline='MEP' (flat, no sub-type).
mep_systemstable: 0 rows (system membership not extracted)system_nodestable: 0 rowselement_properties: onlyReferencewith values (FITTING, FRAME, CURTAIN_PANEL) — no system names- No
PredefinedType,ObjectType,FlowDirection,SystemType, orServiceTypeproperties on any pipe element - Element names:
Pipe Types:Standardcovers 491 of 1060 IfcFlowSegment/IfcFlowFitting instances — no system info
Partial name-based discrimination exists: - "DWV" / "Sanitary" in element_name → SP - "Conduit" → ELEC - Light fixture names (Recessed/Sconce/Pendant Light) → ELEC
But the dominant generic Pipe Types:Standard class (~46% of flow elements) has no IFC attribute that
distinguishes CW from SP.
G2: CW/SP_UNRESOLVABLE for RM.
Resolution path: the RM IFC model requires re-export from Revit with IfcSystem assignments
(IfcDistributionSystem.SystemType) populated per piping network. This is a model quality gap,
not a compiler gap.
§11.2 — Piece-Type to Discipline Map (RM + TE)¶
Column headers: → discipline = AD_Org_ID, source attribute = what drives the classification.
piece_type → discipline source attribute
──────────────────────────────────────────────────────────────────────
DUCT_STRAIGHT → ACMV (5) IfcDuctSegment (class, unambiguous)
DUCT_TEE → ACMV (5) IfcDuctFitting (class, unambiguous)
DUCT_ELBOW → ACMV (5) IfcDuctFitting (class, unambiguous)
DUCT_TRANSITION → ACMV (5) IfcDuctFitting (class, unambiguous)
DUCT_CROSS → ACMV (5) IfcDuctFitting (class, unambiguous)
DUCT_TAKEOFF → ACMV (5) IfcDuctFitting (class, unambiguous)
DUCT_WYE → ACMV (5) IfcDuctFitting (class, unambiguous)
DUCT_FITTING → ACMV (5) IfcDuctFitting (class, unambiguous)
AIR_TERMINAL → ACMV (5) IfcAirTerminal (class, unambiguous)
AIR_DIFFUSER → ACMV (5) IfcAirTerminal (class, unambiguous)
AIR_GRILLE → ACMV (5) IfcAirTerminal (class, unambiguous)
SPRINKLER_HEAD → FP (3) IfcFireSuppressionTerminal (class, unambiguous)
HOSE_REEL → FP (3) IfcFireSuppressionTerminal (class, unambiguous)
SMOKE_DETECTOR → FP (3) IfcFireSuppressionTerminal (class, unambiguous)
LIGHT_FIXTURE → ELEC (4) IfcLightFixture (class, unambiguous)
TOILET → SP (7) element_type keyword (toilet/wc)
URINAL → SP (7) element_type keyword (urinal)
FLOOR_TRAP → SP (7) element_type keyword (trap)
BIDET_SPRAY → SP (7) element_type keyword (bidet)
INSPECTION_CHAMBER → SP (7) element_type keyword (inspection)
GREASE_TRAP → SP (7) element_type keyword (interceptor/grease)
── AMBIGUOUS — needs elements_meta.discipline ──────────────────────
PIPE_STRAIGHT → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
RM: UNRESOLVABLE (G2)
PIPE_ELBOW → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
RM: UNRESOLVABLE (G2)
PIPE_TEE → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
RM: UNRESOLVABLE (G2)
PIPE_REDUCER → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
RM: UNRESOLVABLE (G2)
PIPE_CROSS → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
RM: UNRESOLVABLE (G2)
PIPE_COUPLING → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
RM: UNRESOLVABLE (G2)
PIPE_FLANGE → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
PIPE_FITTING → FP/CW/SP/LPG elements_meta.discipline (TE: resolved)
VALVE → FP/CW elements_meta.discipline (TE: FP=14, CW=7)
SINK → CW or SP elements_meta.discipline (TE: CW=106 via FlowTerminal)
SHOWER → CW or SP elements_meta.discipline
TAP → CW elements_meta.discipline (TE: CW FlowTerminal)
PLUMBING_FIXTURE → CW or SP elements_meta.discipline (ambiguous by keyword alone)
── RM IFC2x3 generics — AMBIGUOUS (G2 applies) ────────────────────
FLOW_SEGMENT → MEP (undivided) IfcFlowSegment, RM only, discipline='MEP'
FLOW_FITTING → MEP (undivided) IfcFlowFitting, RM only, discipline='MEP'
AMBIGUOUS count: 14 pipe/fitting/valve/fixture piece types require elements_meta.discipline
to resolve. For TE this is available. For RM the 14 types remain unresolvable (G2).
§11.3 — FP Archetype Pattern + ELEC/ACMV/CW/SP equivalents¶
DISC BOM (abstract layer) — FP_SYSTEM in ERP.db¶
bom_id seq child_product_id verb_ref
──────────────────────────────────────────────
FP_SYSTEM 10 FP_RISER ROUTE
FP_SYSTEM 20 FP_SPRINKLER_LAYOUT ROUTE
FP_SYSTEM 30 FP_PUMP_LINK ROUTE
ELEC_SYSTEM, ACMV_SYSTEM, CW_SYSTEM, SP_SYSTEM, LPG_SYSTEM: 0 ROUTE lines (empty shells).
MEP_RECIPE BOMs (spatial layer) — current state¶
FP recipes confirmed in library/ERP.db:
- RM (routing topology): RM_FP_L2_RUN_1 = FP_CEILING_SHIM(seq=10) + SPRINKLER_HEAD_15MM(seq=20–70)
→ maps to FP_SPRINKLER_LAYOUT only. No riser or pump recipe.
- ST (coverage topology): 41 archetypes ST_FP_ARCH_* = FP_CEILING_SHIM(seq=10) + SPRINKLER_HEAD_30MM(seq=20, dz=standoff)
→ all map to FP_SPRINKLER_LAYOUT archetype (coverage, no routing chain).
FP_RISER and FP_PUMP_LINK are not yet represented in any MEP_RECIPE BOM. TE has 2672 FP IfcPipeSegment elements (the riser network) but they are not extracted into recipes.
FP → ELEC/ACMV/CW/SP archetype equivalents¶
| DISC BOM verb | FP piece types | ACMV equivalent | CW/SP equivalent | ELEC equivalent |
|---|---|---|---|---|
| FP_RISER | PIPE_STRAIGHT (FP) | DUCT_STRAIGHT (ACMV) | PIPE_STRAIGHT (CW/SP) | FLOW_SEGMENT (ELEC) |
| FP_SPRINKLER_LAYOUT | SPRINKLER_HEAD | AIR_DIFFUSER/GRILLE | TOILET/SINK/TAP | LIGHT_FIXTURE |
| FP_PUMP_LINK | VALVE (FP) | — | VALVE (CW) | — |
Recipe count by discipline (library/ERP.db, bom_type=MEP_RECIPE):
building FP ACMV CW SP ELEC
─────────────────────────────────────
RM 1 441 341 0 0
ST (TE) 41 169 582 1 45
CE (clinic) 0 0 434 0 0
CH 0 0 588 0 0
CP 0 0 1005 0 0
Note: CE/CH/CP CW recipe explosion (434/588/1005) from misclassified IfcFlowTerminal light fixtures → see §11.4.
§11.4 — IFCtoERP Discipline Assignment (current state)¶
Method: IFCtoERP.discFromClass(String ifcClass, String elementType) (line 698, IFCtoERP.java).
case "IfcFireSuppressionTerminal" -> "FP" // CORRECT — class is unique
case "IfcDuctSegment", "IfcDuctFitting",
"IfcAirTerminal" -> "ACMV" // CORRECT — classes are unique
case "IfcLightFixture" -> "ELEC" // CORRECT — class is unique
case "IfcFlowTerminal" -> keyword check: // PARTIAL
drain/waste/soil/inspection/interceptor → SP
else → CW // BUG: light fixtures in RM are IfcFlowTerminal
case "IfcPipeSegment", "IfcPipeFitting",
"IfcFlowController" -> keyword check: // PARTIAL
soil/drain/sp_ → SP
else → CW // OK for TE, misses LPG
default -> CW // BUG: IfcFlowSegment/IfcFlowFitting → forced CW
G3: IFCtoERP_DISCIPLINE_BLIND
discFromClass() does not read elements_meta.discipline. It re-derives discipline from IFC class
and element_type keyword heuristics. Two concrete failures:
-
RM IfcFlowTerminal → CW (wrong): RM contains light fixtures as
IfcFlowTerminal(e.g.M_Plain Recessed Lighting Fixture,M_Pendant Light). These have no "drain/waste" keyword so they fall through to CW. Effect: CE/CH/CP produce hundreds of CW MEP_RECIPE BOMs for light fixtures. -
RM IfcFlowSegment/IfcFlowFitting → CW (default, wrong): These generic IFC2x3 classes cover all pipe types (CW+SP+FP conduit). Default=CW silently classifies SP drains and electrical conduit as CW.
Fix scope: IFCtoERP only. Strategy:
- In readMepElementsWithPositions(), read elements_meta.discipline alongside ifc_class.
- If discipline is a known sub-discipline code (FP/CW/SP/ACMV/ELEC/LPG), use it directly.
- Fall back to discFromClass() keyword heuristic only when discipline is null or 'MEP'.
- Add keyword: IfcFlowTerminal with "light"/"fixture"/"lamp" → ELEC (catches RM light fixture misclassification).
§11.5 — Gaps Blocking 00q–00t¶
| Gap | Description | Scope | Resolution |
|---|---|---|---|
| G1 | _import_joint_piece_types has no discipline/AD_Org_ID column |
Schema | Add column in 00q SQL migration; populate from elements_meta.discipline at extract time |
| G2 | RM CW/SP UNRESOLVABLE — IFC2x3 model lacks system membership, no sub-discipline attribute for generic pipe elements | IFC model (Revit) | Re-export RM from Revit with IfcDistributionSystem assignments; OR accept RM as CW-only (no SP) for current sprint |
| G3 | IFCtoERP discFromClass() ignores elements_meta.discipline; defaults IfcFlowTerminal light fixtures to CW; defaults all IfcFlowSegment/FlowFitting to CW |
IFCtoERP.java | Fix: read discipline column first; keyword fallback only if null/'MEP'; add light-fixture keyword for IfcFlowTerminal |
00q safe to proceed?
- For TE: YES.
elements_meta.disciplineresolves CW/SP/FP/LPG. G3 fix required to populateAD_Org_IDcorrectly in_import_joint_piece_types. G1 schema extension required. - For RM: PARTIAL. FP, ACMV, ELEC, SPRINKLER_HEAD resolved. CW/SP pipes blocked by G2. RM can proceed for typed-class disciplines (FP=6 elements, ACMV=2081 elements) but CW/SP pipe recipes will be inaccurate until G2 is resolved at the source model level.
00q prerequisite: G1 (schema) + G3 (discipline read) must be fixed before writing ROUTE lines for ELEC/ACMV/CW/SP/GAS. Otherwise the staging table cannot carry AD_Org_ID per row and the single-source-of-truth architecture is broken.