PRODUCT BOM Structure — Product ⇄ Phys ⇄ Components
How PSI products are structured in AFTEC: the product→phys→components pattern, how to read a product’s single-level bill of material, and a fleet-wide audit (all 21,954 products, 2026-07-06) of what products actually contain. Companion to the PRODUCT.1287 Field & Program Map.
The model
A product (a PRODUCT.LDS.1287 row; GT.CODE='PRODUCT') is a drawing-level wrapper. Its structure is:
PRODUCT (drawing) → PHYS (physical assembly) → component parts (purchased / manufactured / documents)
887280 887281 027465, 008730, 775137, …
- The product’s only normal single-level bill child is its linked phys.
- The real components hang off the phys, one level down.
VB_BOMXflattens product→phys→components via a “phys pass-through,” so a full explosion shows the components directly under the product — but at the product’s own bill level there is just the phys.
The explicit phys ⇄ product link
The authoritative link is a pair of fields (maintained by E0052 / VB_E0052A), not bill-chain position:
| Direction | Field | Meaning |
|---|---|---|
| Product → Phys | PRODUCT.1287 product record, attr 7 (PHYS) | the product’s physical part |
| Phys → Product | PRODUCT.1287 phys record, attr 2 (PROD.DWG) | back-pointer to the product |
PRODUCT.LDS.1287 PHYS.NO is a daily-rebuilt copy of the product’s attr 7.
Reading a single-level bill
The bill is a linked list in the plain BMF file (keyed CO!PARENT!COMPONENT; not BMF.1287):
- First component =
ITEMMANFparent record, attr 3 (bill-chain head). - Walk
BMFCO!PARENT!compattr 1 (NCOMP, next component) until empty. - Component qty =
BMFattr 7; a component’s MRP =PRODUCTattr 10; GT code =ITEMMANFattr 9.
Do not enumerate with per-record GETs
A single-level bill walk done as thousands of individual record reads will exhaust Kestrel and wedge the API (this happened during the 2026-07-06 audit and required a service reboot). Use the batch endpoint
POST /api/data/dev/{table}/batch?stream=false(≤5000 IDs, one UniData session) and walk theNCOMPchain breadth-first, one batch per depth (~15 calls for the whole fleet). A first-classPOST /api/bom/dev/childrenendpoint is proposed in GHE issue #54 to make this a single call.
Fleet audit — all 21,954 products (2026-07-06)
Category distribution (single-level bill child vs linked phys)
| Category | Products | % | Meaning |
|---|---|---|---|
| CLEAN | 14,178 | 64.6% | only bill child = the linked phys (single-part product) |
| WRONG_FIRST | 5,023 | 22.9% | first bill child isn’t the phys (phys usually present, later) |
| NO_BILL | 1,826 | 8.3% | no bill children at all (no phys in BOM) |
| EXTRA_AFTER_PHYS | 662 | 3.0% | phys is first child, but more components follow |
| NO_PHYS_LINK | 265 | 1.2% | no attr-7 phys link to compare |
Phys ⇄ product link integrity — essentially universal
| Check | Result |
|---|---|
| Forward link present (product attr 7 → phys) | 21,689 of 21,954 |
| Reverse link correct (phys attr 2 → product) | 21,642 (99.5%) |
| Reverse link empty | 18 |
| Reverse link → a different product | 29 |
WRONG_FIRST is not broken links: 97.9% of those products have the phys as a direct bill child,
just not first — a bill-ordering artifact (documents/manufactured parts precede it), not a missing link.
Single-part vs kit — both valid, and class-driven
Whether a product carries other purchased/manufactured (P/M) parts besides its phys is not a defect —
it depends on what kind of thing the product is. MRP and GT code don’t discriminate (99% of products
are MRP=M / GT=PRODUCT); Product Class does.
- 14,568 products (66%) have the phys as their only P/M content (14,178 CLEAN + 390 whose extras are all Documents).
- 5,295 products bill other P/M parts alongside the phys.
| Kit-like classes (mostly multi-part) | %multi | Single-part classes (~phys only) | %multi | |
|---|---|---|---|---|
| 0151 Gun Mover Positioning | 93% | 0361 Sweco Screens | 0% | |
| 0411 Work Door | 87% | 0029 Alignment Tooling | 0% | |
| 0307 Robot PBS | 85% | 0130 Floor Stand | 0% | |
| 0364 Sweco Unit | 85% | 0231 Plasma Gun | 0% | |
| 0299 Robot ABB | 84% | 0295 Reclaim Tube Liner | 0% | |
| 0344 Spindle Rotating | 84% | 0144 Gun Holder Blast | 0% |
Mechanisms, robots, doors and units are genuine assemblies; screens, tooling, stands, liners and holders
are single fabricated/purchased items. Product-class codes map to names via PRODUCT.CLASS.1287 (record
key = class code, CLASS.NAME = attr 1).
Genuine defects (small, targeted)
| Defect | Count | Notes |
|---|---|---|
| Phys absent from single-level bill | 85 | many are inherently non-physical (Part Drawing / Machine Homing / Alignment “PHYS.” placeholders) — needs a physical-vs-process split |
| Worst case (phys absent and reverse link broken) | 3 | real physical products: Dust House Ductwork (779250), Blast Manifold (763659), Modified Ring Clamp (256315) |
| Reverse link → different product | 29 | possible mis-link / shared phys |
| Forward link missing (in NO_PHYS_LINK) | 265 | product has no attr-7 phys |
Reproducing the audit
- Products: page
GET /api/data/dev/PRODUCT.LDS.1287/export(also yieldsPHYS.NO,PROD.CLASS). - Forward link / phys: batch
PRODUCT.1287→ attr 7. - First bill child: batch
ITEMMANF→ attr 3; bill walk: batchBMF1!P!comp→ attr 1 (NCOMP), BFS by depth. - Reverse link: batch
PRODUCT.1287(phys) → attr 2. - Enrichment: batch
PRODUCT→ attr 1 (desc), attr 10 (MRP);ITEMMANF→ attr 9 (GT);PRODUCT.CLASS.1287→ attr 1 (class name).
All attribute positions are centralized in the API at Schema/AftecFields.cs and validated against the
live DICT at GET /api/admin/schema/validate. The full workbook (per-product rows, deviation children,
defects, by-class analytics) is generated from these batch reads.
Related
- PRODUCT.1287 Field & Program Map — the field layout + programs behind these records
- PSI.UniData.API — the endpoints used
- Inventory & Work Orders — BMF / ITEM* file context