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_BOMX flattens 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 authoritative link is a pair of fields (maintained by E0052 / VB_E0052A), not bill-chain position:

DirectionFieldMeaning
Product → PhysPRODUCT.1287 product record, attr 7 (PHYS)the product’s physical part
Phys → ProductPRODUCT.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):

  1. First component = ITEMMANF parent record, attr 3 (bill-chain head).
  2. Walk BMF CO!PARENT!comp attr 1 (NCOMP, next component) until empty.
  3. Component qty = BMF attr 7; a component’s MRP = PRODUCT attr 10; GT code = ITEMMANF attr 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 the NCOMP chain breadth-first, one batch per depth (~15 calls for the whole fleet). A first-class POST /api/bom/dev/children endpoint 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)

CategoryProducts%Meaning
CLEAN14,17864.6%only bill child = the linked phys (single-part product)
WRONG_FIRST5,02322.9%first bill child isn’t the phys (phys usually present, later)
NO_BILL1,8268.3%no bill children at all (no phys in BOM)
EXTRA_AFTER_PHYS6623.0%phys is first child, but more components follow
NO_PHYS_LINK2651.2%no attr-7 phys link to compare
CheckResult
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 empty18
Reverse link → a different product29

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)%multiSingle-part classes (~phys only)%multi
0151 Gun Mover Positioning93%0361 Sweco Screens0%
0411 Work Door87%0029 Alignment Tooling0%
0307 Robot PBS85%0130 Floor Stand0%
0364 Sweco Unit85%0231 Plasma Gun0%
0299 Robot ABB84%0295 Reclaim Tube Liner0%
0344 Spindle Rotating84%0144 Gun Holder Blast0%

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)

DefectCountNotes
Phys absent from single-level bill85many 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)3real physical products: Dust House Ductwork (779250), Blast Manifold (763659), Modified Ring Clamp (256315)
Reverse link → different product29possible mis-link / shared phys
Forward link missing (in NO_PHYS_LINK)265product has no attr-7 phys

Reproducing the audit

  • Products: page GET /api/data/dev/PRODUCT.LDS.1287/export (also yields PHYS.NO, PROD.CLASS).
  • Forward link / phys: batch PRODUCT.1287 → attr 7.
  • First bill child: batch ITEMMANF → attr 3; bill walk: batch BMF 1!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.