{
  "name": "Bill of materials",
  "nodes": [
    {
      "id": "n_product",
      "name": "Product",
      "labels": ["Product", "Item"],
      "shape": "circle",
      "description": "A saleable configuration with its own catalogue number. Kept separate from the top assembly it is built from, because several products can ship the same assembly in different packaging, regions or badging.",
      "properties": [
        { "name": "sku", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Stock keeping unit as sold. The identifier a customer orders against, which is why it is the natural key." },
        { "name": "name", "type": "string", "required": true, "description": "Marketing name of the product." },
        { "name": "launchedOn", "type": "date", "description": "Date the product became orderable." },
        { "name": "isActive", "type": "boolean", "default": "true", "description": "False once the product is withdrawn from sale. Its assembly usually outlives it, still serving spares." },
        { "name": "listPrice", "type": "float", "description": "Published unit price before discount, in the catalogue currency." }
      ]
    },
    {
      "id": "n_part",
      "name": "Part",
      "labels": ["Part", "Item"],
      "shape": "circle",
      "description": "Any item with a part number: a raw component, a sub-assembly, or a complete assembly. Deliberately one entity rather than separate Part and Assembly types — an assembly is simply a part that contains others, and splitting the two makes multi-level explosion impossible to express.",
      "properties": [
        { "name": "partNumber", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Internal part number. The identifier the organisation controls, as opposed to any manufacturer's own — which is why sourcing hangs off a separate node." },
        { "name": "name", "type": "string", "required": true, "description": "Short engineering name for the part." },
        { "name": "partType", "type": "enum", "required": true, "default": "purchased", "description": "How the part is obtained: purchased, manufactured, assembly or phantom. A phantom exists to group children in the structure and is never stocked in its own right." },
        { "name": "massGrams", "type": "float", "description": "Total mass of one part, including everything below it in the structure. Held here as the released figure rather than derived, because the sum of the children drifts during design." },
        { "name": "standardCost", "type": "float", "description": "Costed value of one part used for valuation and roll-up. Distinct from any price a supplier quotes." },
        { "name": "leadTimeDays", "type": "integer", "description": "Working days from order to availability, for planning. Zero for a part made in house from stock." },
        { "name": "isActive", "type": "boolean", "default": "true", "description": "False once the part is superseded and may not be used in new designs, though it stays in the model to explain existing assemblies." }
      ]
    },
    {
      "id": "n_material",
      "name": "Material",
      "labels": ["Material"],
      "shape": "square",
      "description": "A substance a part is made from. Reference data shared across the whole catalogue: it changes when a specification is revised, not when a design does.",
      "properties": [
        { "name": "materialCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Controlled code for the material, such as ABS-FR or AL6082. Stable across suppliers." },
        { "name": "name", "type": "string", "required": true, "description": "Common name of the material." },
        { "name": "densityGramsPerCm3", "type": "float", "description": "Density, used to estimate mass from geometry before a part is weighed." },
        { "name": "isRohsCompliant", "type": "boolean", "default": "true", "description": "Whether the material meets restriction-of-hazardous-substances limits. Non-compliance propagates upward through every assembly containing it." },
        { "name": "recycledContentPercent", "type": "float", "default": "0", "description": "Proportion of the material from recycled feedstock, reported for environmental compliance." }
      ]
    },
    {
      "id": "n_unit_of_measure",
      "name": "UnitOfMeasure",
      "labels": ["UnitOfMeasure"],
      "shape": "square",
      "description": "How a quantity of something is counted. Reference data, and load-bearing here: a bill of materials that does not say whether 2.5 means metres or each is not a bill of materials.",
      "properties": [
        { "name": "uomCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Short code such as EA, MM or KG. The token that appears next to every quantity." },
        { "name": "name", "type": "string", "required": true, "description": "Full name of the unit." },
        { "name": "dimension", "type": "enum", "required": true, "default": "count", "description": "What the unit measures: count, length, mass, volume, area or time. Units may only be converted within one dimension." },
        { "name": "isBaseUnit", "type": "boolean", "default": "false", "description": "True for the reference unit of its dimension, which every other unit in that dimension converts through." }
      ]
    },
    {
      "id": "n_manufacturer",
      "name": "Manufacturer",
      "labels": ["Manufacturer", "Organisation"],
      "shape": "circle",
      "description": "The company that actually makes a component. Routinely confused with the supplier that sells it: one manufacturer's part reaches you through several distributors, and one distributor sells many manufacturers' parts.",
      "properties": [
        { "name": "manufacturerCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Internal code for the manufacturer." },
        { "name": "name", "type": "string", "required": true, "description": "Registered name of the manufacturing company." },
        { "name": "countryCode", "type": "string", "description": "ISO 3166-1 alpha-2 code of the country of incorporation. Not necessarily where the part is made." },
        { "name": "websiteUrl", "type": "string", "description": "Manufacturer's site, the usual route to current datasheets." }
      ]
    },
    {
      "id": "n_manufacturer_part",
      "name": "ManufacturerPart",
      "labels": ["ManufacturerPart"],
      "shape": "circle",
      "description": "A specific manufacturer's own catalogue entry, approved as an acceptable fill for an internal part. The layer that makes second-sourcing possible: one internal part number, several approved manufacturer parts, any of which may be bought.",
      "properties": [
        { "name": "mpn", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Manufacturer part number exactly as the maker publishes it, including package and packing suffixes that change what arrives." },
        { "name": "partDescription", "type": "string", "required": true, "description": "The manufacturer's own description of the component." },
        { "name": "lifecycleStatus", "type": "enum", "required": true, "default": "active", "description": "Where the component sits in its commercial life: preliminary, active, nrnd or obsolete. NRND — not recommended for new designs — is the warning that arrives before obsolescence." },
        { "name": "packageCode", "type": "string", "description": "Physical package, such as 0805 or SOT-23. Two otherwise identical components in different packages are not interchangeable." },
        { "name": "datasheetUrl", "type": "string", "description": "Link to the datasheet the approval was made against." },
        { "name": "isRohsCompliant", "type": "boolean", "default": "true", "description": "Whether this specific manufacturer part is compliant, which can differ between package variants of the same component." }
      ]
    },
    {
      "id": "n_supplier",
      "name": "Supplier",
      "labels": ["Supplier", "Organisation"],
      "shape": "circle",
      "description": "A company that sells parts, whether or not it makes them. Purchase orders are raised against a supplier; approval to buy is held here rather than on the part.",
      "properties": [
        { "name": "supplierCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Internal code for the supplier, used on purchase orders." },
        { "name": "name", "type": "string", "required": true, "description": "Trading name of the supplier." },
        { "name": "countryCode", "type": "string", "description": "ISO 3166-1 alpha-2 code of the shipping origin, which drives duty and lead time." },
        { "name": "isApproved", "type": "boolean", "required": true, "default": "false", "description": "Whether the supplier has passed vendor qualification. Unapproved suppliers can be quoted but not ordered from." },
        { "name": "approvedOn", "type": "date", "description": "Date qualification was granted. Blank while the supplier is unapproved." }
      ]
    },
    {
      "id": "n_supplier_offer",
      "name": "SupplierOffer",
      "labels": ["SupplierOffer"],
      "shape": "diamond",
      "description": "One supplier's standing terms for one manufacturer part. A reified relationship: the pairing carries a price, a minimum order, a lead time and an expiry, none of which belong to the supplier or to the part alone, and the same pairing is quoted again at a different quantity break.",
      "properties": [
        { "name": "offerId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier. A supplier quotes the same part at several quantity breaks, so supplier plus part does not identify an offer." },
        { "name": "unitPrice", "type": "float", "required": true, "description": "Price for one unit at this quantity break, in the offer's currency." },
        { "name": "currencyCode", "type": "string", "required": true, "description": "ISO 4217 code the offer is priced in, which is the supplier's currency and not necessarily the buyer's." },
        { "name": "priceBreakQuantity", "type": "integer", "required": true, "default": "1", "description": "Order quantity at which this unit price starts to apply. The reason one supplier and one part have several offers." },
        { "name": "minimumOrderQuantity", "type": "integer", "required": true, "default": "1", "description": "Smallest quantity the supplier will accept on an order, regardless of what the build needs." },
        { "name": "leadTimeDays", "type": "integer", "description": "Working days from purchase order to delivery under this offer." },
        { "name": "stockOnHand", "type": "integer", "description": "Units the supplier reports available now. A snapshot, not a commitment." },
        { "name": "validUntil", "type": "date", "description": "Date the quoted price expires and must be re-sought." }
      ]
    },
    {
      "id": "n_change_notice",
      "name": "ChangeNotice",
      "labels": ["ChangeNotice"],
      "shape": "circle",
      "description": "A controlled instruction to alter one or more parts. The record of why the structure is what it is, which is the question a bill of materials is most often asked six months later.",
      "properties": [
        { "name": "changeNoticeNumber", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Controlled reference such as ECN-2026-0142. Quoted on drawings and purchase orders." },
        { "name": "title", "type": "string", "required": true, "description": "One-line summary of what the change does." },
        { "name": "raisedOn", "type": "date", "required": true, "description": "Date the change was proposed." },
        { "name": "status", "type": "enum", "required": true, "default": "draft", "description": "Progress through change control: draft, under_review, approved, implemented or rejected. Only an implemented notice has altered the structure." },
        { "name": "approvedOn", "type": "date", "description": "Date approval was granted. Blank until it is." },
        { "name": "reason", "type": "enum", "default": "correction", "description": "Why the change was raised: cost_reduction, obsolescence, correction, compliance or customer_request. The field that makes change history worth keeping." }
      ]
    },
    {
      "id": "n_document",
      "name": "Document",
      "labels": ["Document"],
      "shape": "circle",
      "description": "A controlled drawing, specification, datasheet or certificate. Carries its own revision, which is why it is a node: a part can be correct while the drawing describing it is out of date.",
      "properties": [
        { "name": "documentNumber", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Controlled document reference." },
        { "name": "title", "type": "string", "required": true, "description": "Title as it appears in the title block." },
        { "name": "documentType", "type": "enum", "required": true, "default": "drawing", "description": "Kind of document: drawing, datasheet, specification, certificate or test_report. Determines who is allowed to approve it." },
        { "name": "revision", "type": "string", "required": true, "description": "Revision letter or number of this issue. A reference to a document without a revision is ambiguous." },
        { "name": "issuedOn", "type": "date", "description": "Date this revision was released." },
        { "name": "fileUrl", "type": "string", "description": "Where the released file is held." }
      ]
    }
  ],
  "relationships": [
    {
      "type": "BUILT_FROM", "from": "n_product", "to": "n_part", "cardinality": "n-1",
      "description": "The top-level assembly a saleable product ships as. Many-to-one because regional and badged variants of one product commonly share a single assembly."
    },
    {
      "type": "CONTAINS", "from": "n_part", "to": "n_part", "cardinality": "n-n",
      "description": "The structure itself: one part goes into another. Self-referencing so that assemblies nest to any depth, which is what makes a multi-level explosion possible at all. Everything below sits on this relationship rather than on either part, because none of it is true of a part on its own — it is true of this part in this parent.",
      "properties": [
        { "name": "quantity", "type": "float", "required": true, "description": "How many of the child go into one of the parent. The property this whole model exists to place correctly: put it on the child instead and it is wrong the moment that child appears in a second assembly needing a different number. Held as a float because a length of wire or a mass of adhesive is a legitimate quantity." },
        { "name": "referenceDesignators", "type": "string", "description": "Positions the child occupies on the parent, such as R1,R2,R5. Belongs to the pairing by definition — the same resistor is R1 here and R14 somewhere else." },
        { "name": "scrapFactorPercent", "type": "float", "default": "0", "description": "Expected waste when fitting this child into this parent, added when purchasing rounds a build quantity up. Varies by where the part is used, not by what the part is." },
        { "name": "isOptional", "type": "boolean", "default": "false", "description": "True where the child is fitted only in some configurations of the parent." },
        { "name": "effectiveFrom", "type": "date", "description": "Date this line becomes part of the structure. Effectivity is a property of the pairing, so a change swaps one line for another rather than editing a part." },
        { "name": "effectiveTo", "type": "date", "description": "Date this line leaves the structure. Blank while current." }
      ]
    },
    {
      "type": "MAY_SUBSTITUTE_FOR", "from": "n_part", "to": "n_part", "cardinality": "n-n",
      "description": "Approval to fit one part where another is specified. Deliberately directional, and the reason is worth reading: substitution is almost never mutual. A tighter-tolerance or higher-rated part may stand in for a looser one, while the reverse would fail inspection. Modelling it as a symmetric relationship would quietly authorise the substitution nobody approved.",
      "properties": [
        { "name": "approvedOn", "type": "date", "required": true, "description": "Date engineering approved the substitution." },
        { "name": "approvedBy", "type": "string", "description": "Who granted the approval, for audit." },
        { "name": "conditions", "type": "string", "description": "Limits on the approval, such as a region, a customer or a date range. A substitution approved without conditions is rare." },
        { "name": "preferenceRank", "type": "integer", "default": "1", "description": "Order to try substitutes in when the specified part is unavailable, lowest first." }
      ]
    },
    {
      "type": "MADE_OF", "from": "n_part", "to": "n_material", "cardinality": "n-n",
      "description": "What a part is physically made from. Many-to-many with facts on the pairing, for the same reason as the structure itself: how much of a material a part contains is true of the combination, not of the material.",
      "properties": [
        { "name": "massContributionGrams", "type": "float", "required": true, "description": "Mass of this material in one part. Summing these across a part's materials is how compliance and recycling figures are reported." },
        { "name": "percentageByMass", "type": "float", "description": "Share of the part's total mass, where it is declared as a proportion rather than an absolute." },
        { "name": "isPrimary", "type": "boolean", "default": "false", "description": "True for the material the part is principally made of, used when a single material must be reported." }
      ]
    },
    {
      "type": "MEASURED_IN", "from": "n_part", "to": "n_unit_of_measure", "cardinality": "n-1",
      "description": "The unit a part's quantities are counted in. Without it, a quantity on the structure is a bare number and the bill of materials cannot be costed or picked."
    },
    {
      "type": "SOURCED_AS", "from": "n_part", "to": "n_manufacturer_part", "cardinality": "1-n",
      "description": "The manufacturer parts approved as acceptable fills for an internal part. One-to-many is the whole point: a single approved source is a single point of failure, and this is where second sourcing lives."
    },
    {
      "type": "MADE_BY", "from": "n_manufacturer_part", "to": "n_manufacturer", "cardinality": "n-1",
      "description": "The company that makes this catalogue part. Separate from whoever sells it."
    },
    {
      "type": "OFFERS", "from": "n_supplier_offer", "to": "n_manufacturer_part", "cardinality": "n-1",
      "description": "The manufacturer part an offer is quoting. Several offers point at one part, one per supplier and quantity break."
    },
    {
      "type": "FROM_SUPPLIER", "from": "n_supplier_offer", "to": "n_supplier", "cardinality": "n-1",
      "description": "The supplier standing behind an offer, and therefore the party an order would be placed with."
    },
    {
      "type": "PRICED_PER", "from": "n_supplier_offer", "to": "n_unit_of_measure", "cardinality": "n-1",
      "description": "The unit an offer's price is quoted against. Frequently differs from the unit the part is consumed in — cable priced per metre and consumed per millimetre — and ignoring that is a hundredfold costing error."
    },
    {
      "type": "AFFECTS", "from": "n_change_notice", "to": "n_part", "cardinality": "n-n",
      "description": "The parts a change notice alters. Many-to-many because one notice commonly touches a family of parts, and a part accumulates changes over its life.",
      "properties": [
        { "name": "changeType", "type": "enum", "required": true, "default": "modified", "description": "What the notice does to this part: added, removed, modified or superseded. One notice can do different things to different parts, which is why this sits on the pairing." },
        { "name": "fromRevision", "type": "string", "description": "Revision of the part before the change." },
        { "name": "toRevision", "type": "string", "description": "Revision of the part after the change." }
      ]
    },
    {
      "type": "DESCRIBES", "from": "n_document", "to": "n_part", "cardinality": "n-n",
      "description": "The parts a controlled document covers. Many-to-many: an assembly drawing describes several parts, and a part is covered by a drawing, a specification and a test report at once."
    },
    {
      "type": "SUPPORTED_BY", "from": "n_change_notice", "to": "n_document", "cardinality": "n-n",
      "description": "Documents issued as evidence for a change — the marked-up drawing, the test report, the supplier's obsolescence notice. What makes an approved change auditable rather than merely recorded."
    }
  ]
}
