{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$id": "https://portrayal.dev/schemas/v1/device.schema.json",
 "title": "Portrayal device manifest (v1)",
 "type": "object",
 "required": [
  "format",
  "kind",
  "name",
  "version",
  "maturity",
  "profile",
  "manufacturer",
  "model",
  "chassis",
  "views"
 ],
 "additionalProperties": false,
 "properties": {
  "format": {
   "const": 1
  },
  "kind": {
   "const": "device"
  },
  "name": {
   "$ref": "#/$defs/segment"
  },
  "version": {
   "type": "string",
   "pattern": "^\\d+\\.\\d+\\.\\d+$",
   "description": "Semver, and the bump rules are tied to the device's own lock, `device.lock.json` beside its manifest, rather than left to judgement: the lock records a fingerprint per device split into `shape`, `names`, `surface` and `gaps`, and `devicelock` reads the change and says which bump it needs. Surface alone is a patch; ids added with nothing moved or removed is a minor; anything else about shape or names is a major, because a moved slot invalidates a cached coordinate exactly as a renamed id invalidates a held reference. spec/DESIGN.md section 9 carries the argument and CONTRIBUTING step 5 the order to run it in - the check must see the lock BEFORE you regenerate it, or the bump it would have asked for is lost. L53 is what fails when content moved and the version did not."
  },
  "maturity": {
   "enum": [
    "draft",
    "modelled",
    "verified"
   ],
   "description": "how far this device has been taken, and the standard lint holds it to. draft: no requirement. modelled: must carry a provenance block citing a datasheet or hardware guide. verified: additionally no dimension may be estimated. REQUIRED, because absent used to mean `draft` and twelve finished devices were reading as drafts on that default - the Edgecore AS7726-32X, all five Smartoptics DCPs, the FS enclosure. A default that silently mislabels the work is worse than a field somebody has to fill in."
  },
  "manufacturer": {
   "type": "string"
  },
  "model": {
   "type": "string"
  },
  "part-numbers": {
   "type": "object",
   "additionalProperties": {
    "type": "string"
   }
  },
  "aliases": {
   "type": "array",
   "minItems": 1,
   "uniqueItems": true,
   "description": "THE OTHER NAMES THIS BOX IS SOLD OR LISTED UNDER, so a hardware compatibility list that names it by an AS number, a marketing name or an OEM's name resolves to one drawing (#514). `model` stays the canonical name and is never repeated here. Orderable SKUs are not aliases - they live in `configurations.*.part-numbers`. Every entry has one shape, an object, so every consumer reads it one way and no alias is a bare string with its caveat lost. `kind` says what sort of name it is: `vendor` the manufacturer's own other number for the same box, `marketing` the manufacturer's catalogue name, `oem` a name another company sells it under, `superseded` a name the manufacturer used for it before, `variant` a NEAR-TWIN that differs in a stated way (the `note` says how), listed so a search finds the closest drawing without claiming it is the same box. L111 holds a name to one device unless every claimant marks it `shared`, and never lets it equal another device's `model`. configs.json and devices.json publish the names only.",
   "items": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "name",
     "kind"
    ],
    "properties": {
     "name": {
      "type": "string",
      "pattern": "^\\S(.*\\S)?$"
     },
     "kind": {
      "enum": [
       "vendor",
       "marketing",
       "oem",
       "superseded",
       "variant"
      ]
     },
     "note": {
      "type": "string",
      "description": "the caveat that keeps the alias honest - where the mapping comes from, or how a variant differs"
     },
     "shared": {
      "const": true,
      "description": "this name is knowingly claimed by more than one device - an OEM name mapped to either of a pair. L111 accepts a duplicate only when EVERY claimant says so"
     }
    }
   }
  },
  "description": {
   "type": "string"
  },
  "profile": {
   "$ref": "#/$defs/segment",
   "description": "WHAT KIND OF DEVICE THIS IS, from the classes in spec/schemas/profiles.yaml - the file that says what a device of each class has to STATE before the `specified` capability flag is true. REQUIRED, because a device without one is not judged leniently, it is not judged at all: `capability._specified` returns `cannot evaluate`, and 67 of 89 devices were in that position while the flag read as a fact about the other 22. The classes are deliberately few and each carries the argument for its own minimum set - `optical` owes no cpu or memory, `server` owes no switching performance."
  },
  "datasheet": {
   "type": "object",
   "additionalProperties": false,
   "description": "THE VENDOR DATASHEET this device was modelled from. A guide, manual or quick-start goes in `references:` instead - the distinction is what the document IS, not which came first. The `archive` field was removed in #274: it was in this schema from the start, no entry ever used it, no tool ever read it and no document ever said what it was for, which makes it a field nobody knows the rule for.",
   "properties": {
    "title": {
       "type": "string",
       "description": "the document as it names itself on its own first page, not as its file is named. A reader has to be able to search for it."
      },
    "url": {
       "type": "string",
       "description": "where the document was obtained, when that is known. Absent on 43 of 96 entries and that is not a defect: a document read from a vendor portal behind a login, or received directly, has no citable address, and inventing one would be worse than omitting it."
      },
    "sha256": {
       "type": "string",
       "description": "SHA-256 of the file AS HELD IN THE WORKING CORPUS. THE RULE IS: present exactly when the document is staged locally, absent when it was read but is not held - which is what makes a hash checkable rather than decorative. Every hash in the library satisfies it: all 37 resolve to a file under working/. A hash for a document nobody holds is a claim nobody can test, so do not add one from a vendor page. #274."
      }
   }
  },
  "references": {
   "type": "array",
   "description": "additional source documents - hardware guides, installation guides, quick-start guides, user manuals - registered exactly like the datasheet. A device may register these and no datasheet: seven ASR 9000s were modelled from two Cisco guides and no datasheet at all, and that is the honest record rather than a gap.",
   "items": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
     "title": {
      "type": "string",
      "description": "the document as it names itself on its own first page, not as its file is named. A reader has to be able to search for it."
     },
     "url": {
      "type": "string",
      "description": "where the document was obtained, when that is known. Absent on 43 of 96 entries and that is not a defect: a document read from a vendor portal behind a login, or received directly, has no citable address, and inventing one would be worse than omitting it."
     },
     "sha256": {
      "type": "string",
      "description": "SHA-256 of the file AS HELD IN THE WORKING CORPUS. THE RULE IS: present exactly when the document is staged locally, absent when it was read but is not held - which is what makes a hash checkable rather than decorative. Every hash in the library satisfies it: all 37 resolve to a file under working/. A hash for a document nobody holds is a claim nobody can test, so do not add one from a vendor page. #274."
     }
    }
   }
  },
  "lint": {
   "type": "object",
   "additionalProperties": false,
   "description": "WHAT THIS DEVICE HAS DECIDED ABOUT A RULE, as opposed to what it has not got round to. The difference matters and nothing could express it: `library/lint-baseline.json` records what is merely already true, and this records what a person has argued. The Casa C40G is the case it was written for - it keeps a rear vent field that L44 reports as 100% buried, and its provenance already says why at length, in prose no tool reads: the field is invisible in 2D because the PEMs or the AC panel cover it, and it is what punches the rear panel in 3D, which is exactly what you see when a PEM is pulled. That is a decision, and it belongs somewhere a count can see it.",
   "properties": {
    "waive": {
     "type": "object",
     "description": "rule code -> why this device will not satisfy it. The reason is REQUIRED and is checked for length, because a waiver whose reason is `n/a` is the thing that turns a rule off for everyone who copies the device. Waived warnings are still counted and still listed in the run's summary - they are separated, never hidden.",
     "propertyNames": {"pattern": "^L[0-9]+$"},
     "additionalProperties": {"type": "string", "minLength": 40}
    }
   }
  },
  "stack-exceptions": {
   "type": "array",
   "description": "A BELLY-TO-BELLY CAGE STACK THAT IS NOT DRAWN THE LIBRARY'S WAY, and why. Lint L108 holds every stacked SFP/QSFP/QSFP-DD cage pair to one convention - a row pair upper rotate 0 over lower rotate 180, a column pair (cages turned on their side) left 270 beside right 90, so both bails face outward - and this is the one place a device, its layout.yaml, or a component contract says a pair is otherwise. Each entry names the pair (or several pairs that share one reason) by id and carries the reading that overturns the convention. The pairing is spec/tools/portrayal/stacks.py; OSFP stacks are not checked. See docs/pluggables-3d-design.md, the stacked-cage decisions.",
   "items": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "reason"
    ],
    "oneOf": [
     {
      "required": [
       "pair"
      ]
     },
     {
      "required": [
       "pairs"
      ]
     }
    ],
    "properties": {
     "pair": {
      "type": "array",
      "items": {
       "type": "string"
      },
      "minItems": 2,
      "maxItems": 2,
      "description": "the two cage ids, upper then lower (or left then right)"
     },
     "pairs": {
      "type": "array",
      "minItems": 1,
      "items": {
       "type": "array",
       "items": {
        "type": "string"
       },
       "minItems": 2,
       "maxItems": 2
      },
      "description": "several pairs that share one reason"
     },
     "reason": {
      "type": "string",
      "minLength": 40,
      "description": "the recorded reading that overturns the convention - a photograph, a drawing, the lamps - cited, not a preference"
     },
     "view": {
      "type": "string",
      "description": "the view the pair is in, where the same ids pair in more than one"
     }
    }
   }
  },
  "provenance": {
   "type": "object",
   "description": "WHERE EACH FIGURE ON THIS DEVICE CAME FROM, keyed by the figure. Each entry is an object, not a sentence: `note` carries the prose and `confidence` carries the word, so a rule can ask what only a reader could answer before. The rule that needed it is L15's - `maturity: verified` means no dimension may be estimated, and it was implemented as `str(value).startswith(\"estimated\")` against a corpus where 42 entries opened with the word and 80 more said it somewhere in the middle of a 566-character paragraph. `confidence` is OPTIONAL and 744 of 1688 entries do not carry one: they were migrated with the prose intact and no word invented for them, because reading 744 paragraphs and deciding measured-or-estimated by eye is how an estimate becomes a measurement. L93 counts them, and the count is meant to fall. `source` is not here yet - it wants ids on the `datasheet`/`references` entries, which is roc-ops/Portrayal#274.",
   "additionalProperties": {
    "type": "object",
    "additionalProperties": false,
    "required": ["note"],
    "properties": {
     "confidence": {
      "$ref": "#/$defs/confidence",
      "description": "the library's word for how this figure is known, filled only where it is actually known. Absent means nobody has said - NOT that the figure is weak."
     },
     "note": {
      "type": "string",
      "description": "the prose: what was read, out of which document, at what scale, and what was rejected on the way. It is the reasoning, and it is the reason this block is long."
     }
    }
   }
  },
  "attrs": {
   "type": "object",
   "description": "facts about the device that belong to no view, filed by section. Keys are globally unique across sections and keep their own prefix (`power.power-max-w`, not `power.max-w`): each one is flattened onto the SVG root as `data-<key>`, where it is read without its container, and the section is a classification of a fact rather than a namespace for it - so re-filing a key changes no drawing and no export. Lint L25 is an error if two sections claim one key. `spec/schemas/profiles.yaml` states what each device class owes in terms of these sections.",
   "additionalProperties": false,
   "properties": {
    "physical": {
     "type": "object",
     "description": "the box as an object: weight, and anything dimensional the chassis block does not already hold. `rack: '13 RU'` beside `chassis.ru: 13` is NOT physical data, it is the same fact written twice - the structured one is the one a tool can use",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "performance": {
     "type": "object",
     "description": "what the box forwards and on what: capacity, forwarding rate, buffers, table sizes, the modes its cages run and the optics they take",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "power": {
     "type": "object",
     "description": "what it draws and what it is fed. `power-max-w`, `power-typical-w`, `power-min-w`, suffixed `-ac-w`/`-dc-w` only where the vendor states both and they differ. These are the WHOLE BOX's spellings and are refused on a component contract (lint L28); a module says `power-draw-max-w` or `power-output-w`, which is what stops a sum over a chassis's occupants resolving to a plausible wrong number",
     "properties": {
      "power-envelope": {
       "description": "what the vendor's own figure COVERS, where a document says - and it is what decides whether a module total may be reconciled against that figure at all. Casa states '3.6 kW per fully loaded chassis'; Cisco publishes no chassis draw for the ASR9000 at all, only a per-card table and an instruction to compute your own budget. The same key on two chassis can therefore mean opposite things, and without this word a consumer cannot tell which it is holding. An enum rather than a free string because a value that licenses a reconciliation drifts into `fully-loaded`, `full` and `yes`. ABSENT means nobody has looked; `unstated` means somebody read the datasheet and it is silent, which is a finding - the same distinction `lifecycle.eol: none-announced` draws",
       "enum": [
        "bare",
        "as-tested",
        "fully-configured",
        "unstated"
       ]
      }
     },
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "thermal": {
     "type": "object",
     "description": "operating temperature and the machinery that holds it - fans, airflow. `operating-temp-f2b` and `operating-temp-b2f` are two facts, not two spellings: the airflow SKUs have different ranges",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "environmental": {
     "type": "object",
     "description": "conditions the box is rated for but does not control - storage temperature, humidity, altitude",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "platform": {
     "type": "object",
     "description": "what it is built from: ASIC, CPU, memory, storage, flash, BMC, external TCAM, the software it ships with",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "features": {
     "type": "object",
     "description": "capabilities that are switched on rather than dimensioned - MACsec, PoE, FlexE, timing, TPM, GNSS. Where a datasheet says 'optional' the fact may belong in `configurations` as a SKU variant instead; state it here only while the SKUs are unmodelled",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "management": {
     "type": "object",
     "description": "how it is operated and observed: management ports, firmware bundles, telemetry the box exposes about itself",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "compliance": {
     "type": "object",
     "description": "what it is certified or tested to. Wording is load-bearing and must survive verbatim: 'NEBS Level 3 (pre-test; certificate by request)' is a materially different claim from 'NEBS Level 3', and flattening it would be a false statement about a product",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    },
    "lifecycle": {
     "type": "object",
     "description": "GA is assumed. Record only what the vendor has ANNOUNCED, with the link",
     "additionalProperties": false,
     "properties": {
      "eol": {
       "enum": [
        "none-announced",
        "announced"
       ],
       "description": "ABSENT means nobody has looked. `none-announced` means somebody searched and the vendor has announced nothing - a real finding, and it must not be indistinguishable from nobody looking. `announced` owes `eol-announcement`"
      },
      "eol-announcement": {
       "type": "string",
       "description": "URL of the notice. A date with no link is a claim about a product that no reader can check"
      },
      "end-of-sale": {
       "type": "string"
      },
      "end-of-support": {
       "type": "string"
      },
      "end-of-life": {
       "type": "string"
      }
     }
    },
    "other": {
     "type": "object",
     "description": "the long tail: a real device fact that fits no section yet. Not an escape hatch - lint L24 counts it and the gaps register carries it as `attrs-unclassified`, so it shrinks as patterns emerge or stays visibly unshrunk",
     "additionalProperties": {
      "type": [
       "string",
       "number",
       "boolean"
      ]
     }
    }
   }
  },
  "chassis": {
   "type": "object",
   "required": [
    "width",
    "height",
    "depth"
   ],
   "additionalProperties": false,
   "properties": {
    "width": {
     "type": "number",
     "description": "millimetres, across the rack face"
    },
    "height": {
     "type": "number",
     "description": "millimetres, the rack dimension - 1RU is 44.45"
    },
    "depth": {
     "type": "number",
     "description": "millimetres, front face to rear face"
    },
    "ru": {
     "type": "number"
    },
    "weight-kg": {
     "type": "number"
    },
    "airflow": {
     "description": "WHERE AIRFLOW LIVES. The chassis is the home; a configuration states `airflow` only when it DIFFERS from this, which render.py has always assumed - it reads the configuration first and falls back here. Airflow genuinely varies by build in 7 of the 48 devices that state it; in the rest it is one fact about the box, and repeating it per configuration gave one fact two homes. The enums differ on purpose: `side` and `passive` are chassis answers a configuration cannot give, because a fanless enclosure and a side-breathing modular chassis offer no airflow option to choose between. L91 checks the division.",
     "enum": [
      "front-to-back",
      "back-to-front",
      "side",
      "passive"
     ]
    },
    "power": {
     "description": "WHAT THE BOX IS FED WITH, stated the way `airflow` is: the chassis is the home, and a configuration states `power` only when its build DIFFERS - which, unlike airflow, is the usual case, because most switches are sold as an AC and a DC build of one chassis. So a box with one feed says it here once (`power: dc` on a fixed -48 V router), and a box with an AC and a DC build says it on each configuration and not here. `<device>.configs.json` publishes each build's resolved answer as `configs[].power`, and `options.power` there and in devices.json is the union across the builds a buyer can order - what a tool filtering by feed reads instead of parsing `ac`/`dc` out of a configuration name or `input-dc` out of attrs (#513). L118 checks the division; L119 asks for it where a device has supplies and says nothing.",
     "$ref": "#/$defs/power-feed"
    },
    "color": {
     "type": "string",
     "description": "housing color (faceplate fill); default dark"
    },
    "edge": {
     "type": "string",
     "description": "faceplate EDGE COLOUR - the stroke render.py draws around the housing rect, defaulting to #22262a. A colour and not a dimension, which is why devicelock hashes it under `surface`: recolouring an edge moves nothing, and this entry carrying no description at all is why it read as geometry for as long as it did (#271)."
    },
    "silk": {
     "type": "string",
     "description": "silkscreen/label text color; default light"
    }
   }
  },
  "configurations": {
   "type": "object",
   "description": "renderable variants: bay population, skin choices, airflow/region context. EACH ONE SAYS WHAT KIND OF THING IT IS, because the field was carrying four different meanings at once and a consumer could not tell an orderable SKU from somebody's illustration - the C40G exported a DCIM device type called 'C40G bdm-3plus1', named after whichever redundancy drawing happened to be listed second.",
   "propertyNames": {
    "$ref": "#/$defs/segment"
   },
   "additionalProperties": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
     "description": {
      "type": "string"
     },
     "default": {
      "type": "boolean",
      "description": "the configuration a viewer opens with. Where a `base` exists it is the default and nothing else may claim it: opening on an illustration is how `default` came to mean 'whatever the drawing happens to be populated with', which is what four of the library's devices did."
     },
     "airflow": {
      "description": "ONLY WHEN THIS BUILD DIFFERS FROM THE CHASSIS. A front-to-back and a back-to-front SKU of one switch are two configurations of one chassis, and that is what this is for; a chassis whose airflow does not depend on the build states it once under `chassis.airflow` instead. Restating the chassis value here is what L91 reports. Narrower than the chassis enum on purpose: `side` and `passive` are not options a buyer picks between.",
      "enum": [
       "front-to-back",
       "back-to-front"
      ]
     },
     "power": {
      "description": "ONLY WHEN THIS BUILD DIFFERS FROM THE CHASSIS - see `chassis.power`. An AC and a DC build of one switch are two configurations of one chassis, and each says which it is here; a box with one feed states it once on the chassis instead, and restating it here is what L118 reports. It must agree with the supplies the build seats: `dc` over a bay holding an `-ac` supply is L120.",
      "$ref": "#/$defs/power-feed"
     },
     "occupants": {
      "type": "object",
      "description": "what is PLUGGED IN, keyed by the id of the receptacle it plugs into. A populated port is not a different port - it is the same cage with an optic in it - so this is a configuration the way `bays` is, and a device can be drawn bare or fitted without either being a separate model. Sugar for a `mate-to` placement: the occupant is positioned by its `mate` connection point landing on the host's, and L12 holds the two to the same `interface`, so nothing here is a second positioning path. The value is a component ref, or a mapping carrying `ref` and an explicit `id` where the derived `<host>-occupant` is not the name you want on the row",
      "additionalProperties": {
       "oneOf": [
        {
         "type": "string"
        },
        {
         "type": "object",
         "additionalProperties": false,
         "required": [
          "ref"
         ],
         "properties": {
          "ref": {
           "type": "string"
          },
          "id": {
           "type": "string"
          },
          "attrs": {
           "type": "object"
          },
          "skin": {
           "type": "string"
          }
         }
        }
       ]
      }
     },
     "bays": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      },
      "description": "what is seated, keyed by bay id, overriding that bay's own `default`. AN EMPTY STRING MEANS THE BAY IS EMPTY - it is how a `kind: base` empties the traffic slots while leaving supplies, fans and engines seated, and the renderer already draws it as a real hole with the depth of what the bay accepts rather than a black rectangle painted on the panel. The convention worked before it was written down here, which is its own argument for writing it down."
     },
     "skins": {
      "type": "object",
      "description": "component name -> skin name",
      "additionalProperties": {
       "$ref": "#/$defs/segment"
      }
     },
     "region-context": {
      "type": "object",
      "additionalProperties": {
       "type": "string"
      }
     },
     "part-numbers": {
      "type": "object",
      "description": "orderable SKUs for this configuration: model -> {part, power-cord}",
      "additionalProperties": {
       "type": "object",
       "additionalProperties": false,
       "properties": {
        "part": {
         "type": "string"
        },
        "power-cord": {
         "type": "string"
        }
       }
      }
     },
     "bay-attrs": {
      "type": "object",
      "description": "bay path -> attrs for the occupant seated THERE: `psu-1: {watts: 750W}` prints on that supply alone, where `component-attrs` reaches every instance of a component. A nested path - `riser-1/slot-1` - reaches a module's own bay. The keys are the occupant's `fields`, and the values print on its `data-from` nodes.",
      "additionalProperties": {
       "type": "object",
       "additionalProperties": {
        "type": [
         "string",
         "number"
        ]
       }
      }
     },
     "component-attrs": {
      "type": "object",
      "description": "component name OR placement/bay id -> extra data-* attrs for this configuration. A component name reaches every instance of that part and is how a configuration says which FRU model or part number is fitted; a PLACEMENT OR BAY ID reaches exactly one, and is how it says something true of that instance alone. The two compose, the instance winning. Keying by the component alone could not express the ASR 9001-S, which is the same metal as the 9001 with two of its four SFP+ ports disabled until a licence is applied: both draw six `std/sfp-ganged`, so `{sfp-ganged: ...}` marks all six when two are meant. This is the OCCUPANCY/PROPERTY split in docs/what-a-configuration-cannot-say.md: `bays` and `only-in` say what is THERE, and this says what is TRUE of it. L94 checks the key resolves, because a key that matches nothing is silently ignored.",
      "additionalProperties": {
       "type": "object",
       "additionalProperties": {
        "type": "string"
       }
      }
     },
     "kind": {
      "enum": [
       "base",
       "orderable",
       "example",
       "model"
      ],
      "description": "`base`: the chassis with enough in it to power on and log in - supplies, fans and the routing or supervisory engines populated, TRAFFIC SLOTS EMPTY. On a fixed one-RU switch that is the fans plus one power flavour. At most one per device, and it is the default: what you spec first is a chassis you can reach, before deciding what goes in it. `orderable`: a SKU of this same device - power feed, airflow, region. Named EXPLICITLY even when the base already embodies one of them, so that asking for the DC variant does not mean knowing which flavour the base happens to be. Exports as a device type. `example`: an illustrative population - a redundancy scheme, a worked configuration. Renders as a picture, and NEVER names or creates a device type, because it is not a thing anyone can order. `model`: this configuration is really a different product - the MX80's mx5/mx10/mx40, the ASR-9001-S. Flagged rather than solved: these most likely want to become devices of their own, and until they do this at least stops them reading as variants of one chassis."
     },
     "source": {
      "type": "string",
      "description": "where this configuration comes from. A `base` is either DERIVED - built from the group roles, populating everything `service` and `management` and blanking everything `traffic` - or SOURCED, when the vendor publishes the configuration as an orderable bundle and the library should follow it rather than invent one. Juniper publishes MX10016-BASE and MX10016-PREMIUM with their exact component counts; Cisco and Casa do not, for the chassis here. The two are not equally good and a consumer should be able to tell them apart, which is what this says."
     },
     "views": {
      "type": "object",
      "propertyNames": {
       "enum": [
        "front",
        "rear",
        "top",
        "bottom",
        "left",
        "right"
       ]
      },
      "additionalProperties": {
       "$ref": "#/$defs/segment"
      },
      "description": "which panel each face wears in this configuration, as {face: view name}. Faces not named fall back to the view named after them, so a configuration that overrides only the front keeps the standard rear and sides. THIS IS WHAT COUPLES THE FACES: a front and a rear that do not go together on real hardware cannot both be named here, so the impossible machine has nowhere to live. Scoping a bay with `only-in` cannot do that - it varies occupants within one panel and says nothing about any other face."
     }
    }
   }
  },
  "groups": {
   "type": "object",
   "description": "every `group:` used in any view, declared once: the vendor's word for one of them (term), where numbering starts, and shared attrs. Lint L17 rejects an undeclared group",
   "propertyNames": {
    "$ref": "#/$defs/segment"
   },
   "additionalProperties": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
     "index-origin": {
      "enum": [
       0,
       1
      ]
     },
     "term": {
      "type": "string",
      "description": "vendor display word, e.g. 'Port', 'Slot', 'Bay'"
     },
     "role": {
      "enum": [
       "traffic",
       "fabric",
       "management",
       "service",
       "indicator",
       "furniture"
      ],
      "description": "what this block is FOR - the one thing the drawing cannot work out for itself. A PSU bay, a fan bay and a line-card bay are all data-class `bay`, the same hole with a module in it, so anything ranking the tree by class cannot tell which of them is why the box exists and which two keep it alive. Only the group knows. `traffic` is the work the device is bought to do - line-card slots and the ports that carry user traffic. `fabric` is the cell or chassis interconnect of a distributed chassis - the ports on a DDC or VDR line-card box that lead to its fabric boxes, and every port on the fabric box itself. PORTS ONLY: it is not a customer port, which is `traffic`, and it is not a fabric CARD, which is a control-plane board in `service` below; an exporter still lists these ports as interfaces, because they are cabled like any other. It is its own bucket so that a filter on `traffic` means the ports a customer plugs into, and so that one on `fabric` finds the interconnect on every such box, whatever the vendor called the bank. `management` is how you reach and discipline it - the OOB jack, the console, the craft port, the timing and sync interfaces; a separate bucket rather than part of traffic because on five devices the management block is written FIRST in the manifest, so leaving it in with the traffic ports still opened the tree with a console socket. `service` is what keeps it running - PSUs, fans, filters, power shelves, AND THE CONTROL-PLANE CARDS: routing engines, route processors, supervisors, fabric and switch-control boards. They are named here because leaving them unnamed let the convention drift both ways - nine devices had routing engines in `service` and two in `management`, and the ASR 9912 and 9922 had their ROUTE PROCESSORS in `traffic`, which made the first base configuration built for a 9922 empty both of them. An RE is not `management`: that bucket is jacks and ports, the things you plug into, not the cards behind them. `indicator` is what you read - LEDs, displays. `furniture` is what you neither connect to nor read - ears, rivets, labels, grounding points, doors. Ordering WITHIN a role stays document order, which already reads correctly; this only says which bucket the block falls in. Lint L37"
     },
     "physical-context": {
      "type": "string",
      "description": "Redfish PhysicalContext value shared by everything in this block, e.g. 'PowerSupply' for the PSU bays. A whole group is the natural place to say it once"
     },
     "mixed": {
      "type": "string",
      "description": "the job these ports do together, when they do NOT share a media or a speed and that is deliberate: a management cluster of SFP+, USB and RJ45, a timing block of RJ45 and coax. The default shape of a port group is one family - one media, one speed - because that is what lets the block declare its facts once in `attrs`; a group spanning families is normally a bucket nobody has sorted yet, and lint L23 says so. This field is the author's answer: the vendor's faceplate calls these one thing, and splitting them by media would be a worse drawing. State the function, e.g. 'management cluster' or 'timing inputs and outputs' - it is a reason, not a switch, and L23 also objects if it is set on a group that is in fact one family"
     },
     "attrs": {
      "type": "object"
     },
     "states": {
      "description": "what this indicator's states MEAN on this device. Overrides the component's own vocabulary, which can only be generic - the same led-dot is a speed lamp on one port and a link lamp on the next. A block of eighteen speed lamps shares one vocabulary and says it once; a placement inside it may still override",
      "oneOf": [
       {
        "type": "array",
        "minItems": 1,
        "items": {
         "$ref": "#/$defs/state"
        }
       },
       {
        "type": "object",
        "minProperties": 1,
        "description": "per contracted element, when one component carries two lamps that do NOT share a vocabulary - a management jack whose left lamp is green for 1G and whose right is amber for 10M/100M. Keys are element ids of the referenced component; lint L20 rejects one that names no element",
        "propertyNames": {
         "$ref": "#/$defs/segment"
        },
        "additionalProperties": {
         "type": "array",
         "minItems": 1,
         "items": {
          "$ref": "#/$defs/state"
         }
        }
       }
      ]
     },
     "description": {
      "type": "string",
      "description": "the vendor's own sentence about this block of indicators, kept beside the tokens"
     }
    }
   }
  },
  "views": {
   "type": "object",
   "minProperties": 1,
   "required": [
    "front"
   ],
   "propertyNames": {
    "$ref": "#/$defs/segment"
   },
   "additionalProperties": {
    "type": "object",
    "additionalProperties": false,
    "description": "one face of the device, written in the order the part is made: size the panel, say whether it is an open frame, punch it, print it, populate it. Key order is linted (L16).",
    "properties": {
     "empty": {
      "type": "string",
      "minLength": 40,
      "description": "this face was LOOKED FOR AND IS NOT DOCUMENTED, and this says where you looked. A face with nothing on it is otherwise indistinguishable from a face nobody got to, so the capability check counted both as unfinished and the only way to pass was to draw something - which on an undocumented face means inventing it. Prose rather than a boolean, and long enough that a bare `empty: yes` will not validate, because the claim being made is about a search rather than about the drawing. A view declaring this must actually be bare (L60); if a source turns up, the sentence goes and the feature is drawn."
     },
     "size": {
      "type": "object",
      "description": "view dimensions in mm when not the chassis front elevation (e.g. top = width x depth)",
      "required": [
       "w",
       "h"
      ],
      "additionalProperties": false,
      "properties": {
       "w": {
        "type": "number"
       },
       "h": {
        "type": "number"
       }
      }
     },
     "open-frame": {
      "type": "boolean",
      "description": "THIS FACE'S BAYS OPEN INTO ONE SHARED INTERIOR, not into a pocket each. An open-frame chassis - card-guide rails top and bottom, a mid-plane, and nothing between the slots, as on the CommScope CH3000 - is seen straight through wherever a slot is empty, front to rear. Without this every empty bay compiles to a pocket as deep as its deepest occupant, with four walls, a floor and a back, and the chassis reads in 3D as a row of closed tubes. With it EVERY bay on the face, occupied or not, compiles to a see-through mouth flagged `data-open-frame` - the kit punches it and builds no walls, floor or back round it - and the face's root carries `data-open-frame`, which tells the kit to line the inside of the box so the interior reads as the chassis's. A seated module is drawn over its mouth as usual, and pulling it leaves the frame open rather than a dark box. Declare it on every face whose slots open into the interior (a mid-plane chassis: front and rear)."
     },
     "panel": {
      "type": "object",
      "additionalProperties": false,
      "description": "the sheet metal - what it is (decor) and where it is punched (cutouts)",
      "properties": {
       "decor": {
        "type": "array",
        "description": "non-addressable chassis decoration (vent fields etc.)",
        "items": {
         "type": "object",
         "required": [
          "at"
         ],
         "additionalProperties": false,
         "properties": {
          "at": {
           "$ref": "#/$defs/xy"
          },
          "size": {
           "$ref": "#/$defs/xy"
          },
          "pattern": {
           "enum": [
            "vent",
            "holes",
            "grille",
            "slots",
            "slots-h",
            "ribs"
           ]
          },
          "vent": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "air vent: dark cells (or the whole rect when plain-fill) become holes into a cavity this deep in 3D"
          },
          "pattern-offset": {
           "$ref": "#/$defs/xy",
           "description": "shift the pattern phase under this rect (align lattice rows across corners)"
          },
          "pattern-pitch": {
           "$ref": "#/$defs/xy",
           "description": "this rect's pattern tile in mm, w x h. Without it every face gets the tile the pattern was first drawn for - `slots-h` is 14 x 8 because that is one vendor's louvre pitch - so a bezel with eight rows of louvres in 25mm could only be drawn with three. Pair it with `pattern-offset` to put the lattice's first cell on the rect's own corner"
          },
          "id": {
           "type": "string",
           "description": "name this rect in the drawing. Without one it is numbered by kind, and somebody opening the SVG in an editor finds an anonymous rectangle they can only identify by clicking it and watching what highlights"
          },
          "kind": {
           "type": "string",
           "description": "what this rect IS - port-shell, recess, label-plate, bezel. A vent field and a stroked outline name themselves from `pattern` and `stroke`; everything else is an unlabelled grey box unless the author says otherwise. It reaches the drawing as data-kind, so the file can be READ rather than clicked through"
          },
          "out": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "decor protrudes this many mm from the face in 3D (raised stampings)"
          },
          "lift": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "standoff: the raised decor STARTS this many mm off the face, so it spans lift..out instead of 0..out. Without it `out` can only build a solid block from the panel outward, which is wrong for anything with a section - a cable-management hook is a thin plate at the FRONT of a horizontal foot, and extruding its outline from the panel gave a cube where the hardware has an L. `relief.features` has carried `lift` all along; decor not having it was an asymmetry in the vocabulary rather than a decision."
          },
          "confidence": {
           "$ref": "#/$defs/confidence"
          },
          "source": {
           "type": "string",
           "description": "where precisely, in one phrase. Start with '<ns>/<name>@<major>' when the number came from another part, so the claim can be resolved rather than only read."
          },
          "sink": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "decor is recessed this many mm into the face in 3D (pressed grooves)"
          },
          "fill": {
           "type": "string",
           "description": "plain fill (e.g. groove color) when no pattern"
          },
          "rx": {
           "type": "number"
          },
          "stroke": {
           "type": "string",
           "description": "outline-only decor (fiber channels etc.); fill defaults to none"
          },
          "stroke-width": {
           "type": "number"
          },
          "rotate": {
           "type": "number",
           "description": "text decor rotation in degrees about its anchor"
          },
          "shape": {
           "enum": [
            "rect",
            "circle",
            "d-sub",
            "slot",
            "octagon"
           ],
           "default": "rect",
           "description": "outline of the painted patch. `rect` (with optional `rx`) is the default; `d-sub` is the D-subminiature outline of TE 114-40010 Figure 3 and `slot` a stadium. THE SAME VOCABULARY A CUTOUT'S `shape` TAKES, because paint printed around a connector takes the connector's shape and until now had no way to say so - a patch behind a D-sub could only be a rectangle or a rounded one. `circle` is accepted for symmetry and still draws as a rect; decor has `rx`."
          }
         }
        }
       },
       "cutouts": {
        "type": "array",
        "items": {
         "type": "object",
         "additionalProperties": false,
         "required": [
          "id",
          "at",
          "size"
         ],
         "properties": {
          "id": {
           "$ref": "#/$defs/segment"
          },
          "at": {
           "$ref": "#/$defs/xy"
          },
          "size": {
           "$ref": "#/$defs/xy",
           "description": "[w, h] in mm"
          },
          "shape": {
           "enum": [
            "rect",
            "circle",
            "d-sub",
            "slot",
            "octagon"
           ],
           "default": "rect"
          },
          "rx": {
           "type": "number",
           "description": "corner radius for rect"
          },
          "depth": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "mm the hole runs into the chassis. Without it a cutout is painted dark and nothing more; with it the 3D kit builds a recess this deep and lays whatever is drawn inside the hole on its floor. A cutout a bay's `rear:` names is a PASSAGE instead: walls only, no floor, so the module's own body stands in it and an empty slot is open through to the front."
          },
          "wall": {
           "type": "string",
           "description": "colour of the recess walls, with `depth`"
          },
          "empty": {
           "type": "boolean",
           "description": "a hole with deliberately nothing in it (a blanked-off option). Without this, a cutout no placement sits in is a lint finding"
          },
          "provenance": {
           "$ref": "#/$defs/provenance"
          },
          "rotate": {
           "type": "number",
           "description": "degrees to turn the aperture about its own centre. A `d-sub` or `slot` hole has a direction and the part that fills it already says which - the placement's own `rotate` - so without this a turned connector sat in an untuned hole and the corners its aperture does not have showed as ink."
          }
         }
        }
       }
      }
     },
     "silkscreen": {
      "type": "array",
      "description": "Printed matter on the CHASSIS panel. Painted after the panel and its decor and BEFORE components, because on the real device the silkscreen goes down on the punched panel and the modules are installed on top of it. A legend that a module would cover is therefore invisible here, which is the correct behaviour: on the hardware it would be invisible too. Silkscreen printed on a MODULE's own faceplate belongs in that component's skin, inside <g id=\"silkscreen\">, and travels with it.",
      "items": {
       "type": "object",
       "additionalProperties": false,
       "required": [
        "at"
       ],
       "properties": {
        "id": {
         "$ref": "#/$defs/segment",
         "description": "optional stable id; needed only when something else refers to this mark"
        },
        "at": {
         "$ref": "#/$defs/xy"
        },
        "text": {
         "type": "string",
         "description": "printed legend. Multi-line uses \\n. Exactly one of text or path"
        },
        "font-size": {
         "type": "number"
        },
        "anchor": {
         "enum": [
          "start",
          "middle",
          "end"
         ]
        },
        "rotate": {
         "type": "number",
         "description": "degrees about the anchor"
        },
        "fill": {
         "type": "string",
         "description": "ink colour; defaults to chassis silk"
        },
        "for": {
         "description": "what this mark annotates. One placement or bay, or a list when the mark joins things - a leader line from a breaker to its terminal names both. Emitted as data-for. Printed ink annotates something on the face it is printed on: a bare id in this view, never another view and never the chassis.",
         "oneOf": [
          {
           "$ref": "#/$defs/segment"
          },
          {
           "type": "array",
           "minItems": 1,
           "items": {
            "$ref": "#/$defs/segment"
           }
          }
         ]
        },
        "path": {
         "type": "string",
         "description": "printed line or symbol as an SVG path in view mm - a leader line, a polarity arrow, an earth mark. Exactly one of text or path"
        },
        "filled": {
         "type": "boolean",
         "description": "this path ENCLOSES ink rather than tracing a route. A leader line is stroked; a solid arrowhead is filled. Without it every symbol is an outline held shut by its own stroke width, which leaves a pinhole in anything small and cannot be scaled - at twice the size the hole is four times as obvious"
        },
        "fill-rule": {
         "enum": [
          "nonzero",
          "evenodd"
         ],
         "description": "evenodd where a filled mark has a hole in it - a bored nut, a ring"
        },
        "stroke-width": {
         "type": "number"
        }
       },
       "oneOf": [
        {
         "required": [
          "text"
         ]
        },
        {
         "required": [
          "path"
         ]
        }
       ]
      }
     },
     "components": {
      "type": "object",
      "additionalProperties": false,
      "description": "what is installed into the panel. bays are things that come out; placements are things that do not",
      "properties": {
       "bays": {
        "type": "array",
        "items": {
         "type": "object",
         "required": [
          "id",
          "at",
          "size"
         ],
         "additionalProperties": false,
         "properties": {
          "only-in": {
           "type": "array",
           "items": {
            "$ref": "#/$defs/segment"
           },
           "minItems": 1,
           "uniqueItems": true,
           "description": "THE CONFIGURATIONS IN WHICH THIS PART OF THE METAL EXISTS AT ALL, which is a different question from what goes in it. Absent means every configuration, which is almost always the answer. A C40G ordered for AC has ONE bolted panel across the bottom rear where a DC chassis has two power-entry openings - not two empty openings, different sheet metal - and without this a configuration could vary only its OCCUPANTS: the AC rear drew two empty PEM bays as black rectangles, and the bay picker offered a DC power entry module on a chassis that cannot take one. Scoping lives HERE, on the bay or placement, rather than in the configuration block, because it is a statement about GEOMETRY and geometry is what a view holds - a configuration says what is fitted, not where the holes are. Distinct from `optional`, which is a build-time flag that hides a part from every configuration at once. Lint L41"
          },
          "id": {
           "$ref": "#/$defs/segment"
          },
          "at": {
           "$ref": "#/$defs/xy"
          },
          "size": {
           "type": "object",
           "required": [
            "w",
            "h"
           ],
           "additionalProperties": false,
           "properties": {
            "w": {
             "type": "number"
            },
            "h": {
             "type": "number"
            }
           }
          },
          "opening": {
           "type": "object",
           "required": [
            "w",
            "h"
           ],
           "additionalProperties": false,
           "properties": {
            "w": {
             "type": "number"
            },
            "h": {
             "type": "number"
            }
           },
           "description": "the HOLE IN THE SHEET METAL, when it differs from `size`. `size` is the space the slot RESERVES - what the occupant is placed into and checked against - and on a card cage that is the card including its ejector brackets. The hole the card's plate covers is smaller, and it is the hole you see when the bay is empty. On an ASR 9006 they are 395.70 and 352.59: painting the reserved space dark put 21.5 mm of opening over metal at each end. Absent means the two are the same, which is the common case. Measured only - never inferred from the occupant, which is the mistake this exists to undo."
          },
          "accepts": {
           "type": "array",
           "items": {
            "type": "string"
           },
           "minItems": 1
          },
          "group": {
           "$ref": "#/$defs/segment"
          },
          "rel-pos": {
           "type": "integer"
          },
          "physical-context": {
           "type": "string",
           "description": "Redfish PhysicalContext value, e.g. 'PowerSupply', 'Fan', 'NetworkingDevice'. This is the join between the drawing and a running box: it is what lets an ENTITY-MIB entPhysicalTable row or a Redfish Thermal sensor be laid over the bay it actually describes. The `wired` capability flag is exactly 'every bay and every port group says this'"
          },
          "default": {
           "type": [
            "string",
            "null"
           ],
           "description": "ref populated by default config; null = open"
          },
          "rotate": {
           "type": "number",
           "description": "degrees to rotate the occupant, e.g. a line card mounted horizontally in one chassis and vertically in another"
          },
          "mirror": {
           "type": "boolean",
           "description": "flip the occupant horizontally about its own centre line; handedness, not rotation - a riser whose cards face the other way"
          },
          "plan": {
           "type": "object",
           "additionalProperties": false,
           "required": [
            "view",
            "at"
           ],
           "description": "WHERE THIS BAY'S OCCUPANT IS SEEN FROM ANOTHER FACE. A riser is seated in its rear bay and stands inside the chassis; with the lid off, the top view should show it there. The occupant's contract names the component that draws it from above (`plan.ref`), and this says which view that lands in and where - the plan's top-left in that view's mm. `in:` and `under:` are the target view's, so the plan sits in its well and paints under the lid. `mirror` flips it, for a riser whose cards face the other way. The occupants of the occupant's own bays come along at the offsets those bays declare. The result is a PROJECTION: it carries `data-of` naming the seated part and no data-path of its own, so the tree lists the part once, selecting either face marks both, and the 3D kit builds nothing from it - the body already stands there.",
           "properties": {
            "view": {
             "type": "string"
            },
            "at": {
             "$ref": "#/$defs/xy"
            },
            "in": {
             "$ref": "#/$defs/segment"
            },
            "under": {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/segment"
             }
            },
            "mirror": {
             "type": "boolean"
            }
           }
          },
          "rear": {
           "type": "object",
           "additionalProperties": false,
           "required": [
            "view",
            "cutout"
           ],
           "description": "WHERE THIS BAY'S OCCUPANT IS SEEN FROM BEHIND, through an open back. The occupant's contract names what draws its back (`faces.rear`); this says which view that lands in and which of that view's panel `cutouts` it is seen through. WHERE in the cutout is not the bay's to say: the back of a module is the back of its body, so it lands where the SEATED occupant's own `body.footprint` stands, mirrored across the bay since it is seen from behind - x = cutout x + (bay w - footprint x - footprint w), y = cutout y + footprint y (the whole face when there is no footprint). A cassette and an adapter panel in the same bay put their backs in two different places. HOW IT IS TURNED is not the bay's to say twice: a bay with `rotate` shows its occupant's back turned by the same angle the other way round (a clockwise turn seen from the front reads anticlockwise from behind), about the hole's centre, with the footprint found in the bay's unturned box - so the cutout of a turned bay is the bay's own turned box, mirrored. The projection is drawn INSIDE the cutout for the 2D rear; in 3D the cutout is a passage and the module's own body, its back painted with the same face, stands in it at its real depth. Like `plan:`, the result is a PROJECTION: `data-of`, no data-path, and nothing built from it. The cutout must declare a `depth` (L72).",
           "properties": {
            "view": {
             "type": "string"
            },
            "cutout": {
             "$ref": "#/$defs/segment"
            }
           }
          },
          "floor": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "mm below the face at which this bay's occupant STANDS, when that is not the well's own floor. With `in:` alone a module stands on the well's floor and rises its own height; a card in a riser stands on its slot, and the three slots of a riser are three shelves at three heights inside one well. So a bay may say where its shelf is: the R740xd's riser 1 slots are 26.6, 44.8 and 64.6 down, from the model's seated cards. Only meaningful with `in:`, and never deeper than the well (L13)."
          },
          "in": {
           "$ref": "#/$defs/segment",
           "description": "the WELL this bay opens in - a placement in this view drawn as a recess. The module that fills it STANDS ON THE WELL'S FLOOR and rises to its own `size.d`: the bay's plane sits that far above the floor and the body reaches down to it. A 26.1 mm drive in the mid tray, whose floor is 31.31 down, tops out 5.21 below the lid; a 31.3 mm DIMM on the board's PCB, 79.9 down, tops out at 48.6. A well's depth is its floor, never the top of what is in it. Implies the well is `under:` this bay. Checked: the target must be a well in this view."
          },
          "under": {
           "oneOf": [
            {
             "$ref": "#/$defs/segment"
            },
            {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/segment"
             }
            }
           ],
           "description": "Same as on a placement: the ids in this view that lie OVER this bay. A bay is an opening, so it always has somewhere to be under - the R740xd's DIMM sockets sit under the mid-drive tray and its drive bays on the nine configurations that carry one, and the DIMMs' 42.6 mm top clears the tray's 48.89 mm floor. Checked by L13 exactly as a placement's is: the upper part must exist in the view, and the pair is then not compared."
          },
          "for": {
           "description": "the placement or bay this one belongs to. A bay is a hole in this face; what it belongs to is on this face too, so a bare id and nothing else. Emitted as data-for.",
           "oneOf": [
            {
             "$ref": "#/$defs/segment"
            },
            {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/segment"
             }
            }
           ]
          },
          "interface": {
           "$ref": "#/$defs/segment",
           "description": "THE INTERFACE THIS SLOT PRESENTS, for an opening whose occupants are not a list anyone published. `accepts` is the vendor's compatibility matrix transcribed - it works because a switch's card catalogue is closed and printed, and the module reference's support tables ARE those lists. An open form factor has no table to transcribe: anyone may build a PCIe card, so a list is unbounded, and one written anyway asserts something the vendor never said. Such a bay names the interface instead and carries no list. Occupants declare the same key via `mates`, exactly as they do for a cage. Deliberately not an enum - a new interface is a new key, not a schema change. Electrical detail (a generation, a lane width) belongs in `attrs`, NOT here: a gen3 card seats in a gen4 slot and an x8 card in an x16, so matching on an exact key would refuse hardware that fits. See roc-ops/Portrayal#43."
          }
         },
         "anyOf": [
          {
           "required": [
            "accepts"
           ]
          },
          {
           "required": [
            "interface"
           ]
          }
         ],
         "description": "A bay says what goes in it in ONE OF TWO WAYS, and must say it somehow: `accepts` for a closed vendor matrix, `interface` for an open form factor. Both together is fine - a slot can be standard AND have a published list. Neither is a hole that promises nothing."
        }
       },
       "placements": {
        "type": "array",
        "items": {
         "type": "object",
         "required": [
          "ref",
          "id"
         ],
         "additionalProperties": false,
         "properties": {
          "only-in": {
           "type": "array",
           "items": {
            "$ref": "#/$defs/segment"
           },
           "minItems": 1,
           "uniqueItems": true,
           "description": "THE CONFIGURATIONS IN WHICH THIS PART OF THE METAL EXISTS AT ALL, which is a different question from what goes in it. Absent means every configuration, which is almost always the answer. A C40G ordered for AC has ONE bolted panel across the bottom rear where a DC chassis has two power-entry openings - not two empty openings, different sheet metal - and without this a configuration could vary only its OCCUPANTS: the AC rear drew two empty PEM bays as black rectangles, and the bay picker offered a DC power entry module on a chassis that cannot take one. Scoping lives HERE, on the bay or placement, rather than in the configuration block, because it is a statement about GEOMETRY and geometry is what a view holds - a configuration says what is fitted, not where the holes are. Distinct from `optional`, which is a build-time flag that hides a part from every configuration at once. Lint L41"
          },
          "ref": {
           "type": "string",
           "pattern": "^[a-z0-9-]+/[a-z0-9-]+@\\d+$",
           "description": "namespace/component@major"
          },
          "id": {
           "$ref": "#/$defs/segment"
          },
          "at": {
           "$ref": "#/$defs/xy"
          },
          "rotate": {
           "type": "number"
          },
          "skin": {
           "$ref": "#/$defs/segment"
          },
          "group": {
           "$ref": "#/$defs/segment"
          },
          "interfaces": {
           "type": "array",
           "items": {
            "$ref": "#/$defs/segment"
           },
           "minItems": 2,
           "uniqueItems": true,
           "description": "THE INTERFACES THIS PLACEMENT PRESENTS, when it is more than one and is not itself an interface. A Compact SFP (CSFP) module fits a standard SFP cage and carries two independent BiDi fibre connections, each its own switch interface; the switch's silicon has both whether or not a module is seated, and devices here ship unpopulated, so the two belong to the HOST CAGE and are declared on it: `interfaces: [port-1, port-3]`. The DCIM export then emits one interface per id, typed from the cage, and none for the placement. Not breakout: breakout is a MODE a port is configured into and stays a description on one interface, where these are always present and a DCIM must list each. Ids must be unique in the view and not name a placement or bay, and only a port presents interfaces. Lint L105. roc-ops/Portrayal#443"
          },
          "rel-pos": {
           "type": "integer"
          },
          "physical-context": {
           "type": "string",
           "description": "Redfish PhysicalContext value. See the same key on a bay; declaring it once on the group is usually right, and a placement overrides"
          },
          "optional": {
           "$ref": "#/$defs/segment",
           "description": "render only when --with <tag> is given (e.g. ears)"
          },
          "attrs": {
           "type": "object"
          },
          "states": {
           "description": "what this indicator's states MEAN on this device. Overrides the component's own vocabulary, which can only be generic - the same led-dot is a speed lamp on one port and a link lamp on the next",
           "oneOf": [
            {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/state"
             }
            },
            {
             "type": "object",
             "minProperties": 1,
             "description": "per contracted element, when one component carries two lamps that do NOT share a vocabulary - a management jack whose left lamp is green for 1G and whose right is amber for 10M/100M. Keys are element ids of the referenced component; lint L20 rejects one that names no element",
             "propertyNames": {
              "$ref": "#/$defs/segment"
             },
             "additionalProperties": {
              "type": "array",
              "minItems": 1,
              "items": {
               "$ref": "#/$defs/state"
              }
             }
            }
           ]
          },
          "description": {
           "type": "string",
           "description": "the vendor's own sentence about this indicator, kept beside the tokens - 'Blue = 100G, Green = 40G'. Prose, never a state list"
          },
          "provenance": {
           "$ref": "#/$defs/provenance"
          },
          "mate-to": {
           "$ref": "#/$defs/segment",
           "description": "id of a placement in this view to mate into. The renderer positions this component so its `mate` connection-point coincides with the host's, so an occupant never carries hand-computed centring offsets. Mutually exclusive with `at`."
          },
          "for": {
           "description": "what this part is bound to - an LED to its port, a button to its module. One target or a list, and a list means collectively: a FAN lamp over five trays names all five. A target in another view of the same device is written 'view/id', because a front-panel PSU lamp indicates a PSU that lives in the rear. The literal 'chassis' names the whole unit, for an indicator with no modelled subject. Emitted as data-for; never inferred from names.",
           "oneOf": [
            {
             "$ref": "#/$defs/for-target"
            },
            {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/for-target"
             }
            }
           ]
          },
          "frames": {
           "oneOf": [
            {
             "$ref": "#/$defs/segment"
            },
            {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/segment"
             }
            }
           ],
           "description": "ids this part SURROUNDS rather than covers - a frame whose bounding box takes in openings its ink does not. L13 compares boxes, which is right for almost every part and wrong for a frame; this is the declaration that makes the difference checkable instead of assumed. Name only what the openings actually clear. NOT a way to silence a real overlap: a part that genuinely paints over another is an error. Distinct from `behaviour: mounts`, which says a part lies OVER what is behind it and draws after the bays - a drive cage surrounds its drives and must draw BEFORE them, so the two are not interchangeable."
          },
          "in": {
           "$ref": "#/$defs/segment",
           "description": "the WELL this part stands in - a placement in this view that render.py draws as a recess (an unmounted, non-module part with `size.d`). A top view with its lid off is a section, and a part on the floor of a well is not at the face: the R740xd's heatsinks stand on the system board's PCB 79.9 mm down. The renderer sinks the part by the well's depth (a negative z-lift that relief.js sums like any other) and its `out` features rise from the floor. Implies the well is `under:` this part for L13 and for paint order, so it need not be repeated. Checked: the target must be a well in this view."
          },
          "mirror": {
           "type": "boolean",
           "description": "flip this instance horizontally about its own centre line; handedness, not rotation. The same key a bay offers its occupant: the R740xd's riser 3 is riser 1's cage at the other wall, so its card stands with the PCB outboard and the connectors inboard the other way about."
          },
          "under": {
           "oneOf": [
            {
             "$ref": "#/$defs/segment"
            },
            {
             "type": "array",
             "minItems": 1,
             "items": {
              "$ref": "#/$defs/segment"
             }
            }
           ],
           "description": "ids in this view that lie OVER this part - the system board names the shroud and the fan wall that stand on it, the shroud names the lid that closes over it. A faceplate has one height and two parts in one place is a sizing error; a view with its cover off is a section through several, and everything in it overlaps in plan because it is stacked. This is how a placement says which. L13 does not compare a pair one of which is declared under the other, and it CHECKS the claim rather than trusting it: the lower part must be a well (an aperture with `size.d`) or the upper must be `behaviour: mounts`, because otherwise there is no height for the two to be at. NOT a way to silence a real overlap between two flat parts."
          },
          "inset": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "mm this INSTANCE is mounted behind the panel face. A part's relief says how far it stands proud when its flange bolts to the sheet metal; a connector on a board set back from the faceplate is the same part mounted differently, and this is where that difference belongs - on the placement, not in a second copy of the part. Every protrusion the instance declares is reduced by this much and anything left wholly behind the panel is not drawn."
          },
          "lift": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "mm this INSTANCE sits proud of the panel face - the mirror of `inset`, and used when the part is bolted not to the sheet metal but to something already standing off it. Every protrusion the instance declares is raised by this much. `relief.features` has carried `lift` all along, composed `parts:` gained it, and `panel.decor` gained it; a placement not having it was the same asymmetry in the vocabulary rather than a decision, and it forced the alternative of a second copy of the part with fattened numbers - which is exactly what `inset` exists to avoid in the other direction."
          }
         },
         "oneOf": [
          {
           "required": [
            "at"
           ]
          },
          {
           "required": [
            "mate-to"
           ]
          }
         ]
        }
       }
      }
     },
     "regions": {
      "type": "array",
      "items": {
       "type": "object",
       "required": [
        "id"
       ],
       "additionalProperties": false,
       "properties": {
        "id": {
         "$ref": "#/$defs/segment"
        },
        "at": {
         "$ref": "#/$defs/xy"
        },
        "size": {
         "type": "object",
         "required": [
          "w",
          "h"
         ],
         "additionalProperties": false,
         "properties": {
          "w": {
           "type": "number"
          },
          "h": {
           "type": "number"
          }
         }
        },
        "physical-context": {
         "type": "string",
         "description": "Redfish PhysicalContext value where applicable"
        },
        "members": {
         "type": "array",
         "items": {
          "$ref": "#/$defs/segment"
         }
        },
        "label": {
         "type": "string"
        }
       }
      }
     },
     "face": {
      "enum": [
       "front",
       "rear",
       "top",
       "bottom",
       "left",
       "right"
      ],
      "description": "which face this view draws, when the view is not NAMED after one. A view called `front` needs no `face`; a second front - a different panel for the same face, say a 12 x 3.5 inch front beside a 24 x 2.5 inch one - declares `face: front` and is a VARIANT. A variant draws only when a configuration binds it, and draws under the face's name, so a consumer still asks for `front` and gets whichever front that configuration wears."
     }
    }
   }
  },
  "portfolio": {
   "type": "object",
   "description": "where this device sits in the vendor's own catalogue, in the vendor's own words. Free text on purpose: the controlled vocabulary is to be harvested from what accumulates, not designed up front from a dozen devices. All fields optional; a device without a portfolio block sorts under its manufacturer.",
   "additionalProperties": false,
   "properties": {
    "line": {
     "type": "string",
     "description": "the vendor's business line, e.g. 'Service Provider'"
    },
    "family": {
     "type": "string",
     "description": "the vendor's word for the category, e.g. 'Aggregation Router'"
    },
    "series": {
     "type": "string",
     "description": "marketing series, e.g. 'AGR400'; the model is the SKU root"
    },
    "also-listed-in": {
     "type": "array",
     "items": {
      "type": "string"
     },
     "minItems": 1,
     "uniqueItems": true,
     "description": "other catalogue categories the vendor ALSO files this device under, in the vendor's own words. `line` is the category of the vendor's own canonical product URL; this is everywhere else it appears. A consumer filtering by category should union the two, so a box the vendor lists twice shows up in both lists."
    }
   }
  },
  "gaps": {
   "type": "array",
   "description": "unknowns no rule can ever find, because the manifest looks complete. 'The HIG has no port-lamp table' is a fact about a DOCUMENT, not about this file, and only someone who read the source knows it. Detectable gaps - a port with no media, an indicator with no `for:` - are derived from the lint rules at build time and must NOT be listed here: a hand-kept list of machine-findable problems goes stale the first time someone fixes one. Declare the unknowns, derive the rest.",
   "items": {
    "type": "object",
    "additionalProperties": false,
    "required": [
     "what",
     "reason",
     "wanted"
    ],
    "properties": {
     "what": {
      "$ref": "#/$defs/segment",
      "description": "the missing knowledge, as a token: port-led-semantics, fan-direction"
     },
     "scope": {
      "type": "array",
      "description": "what it applies to: group names, instance ids, or view names",
      "items": {
       "type": "string"
      }
     },
     "reason": {
      "description": "why it is missing, because that decides who can close it. `sources-disagree` is the one that is NOT an absence: two documents we hold state different values for one fact, and picking the bigger number and saying nothing loses a real finding. It is the normal case rather than the exceptional one - the AGR400's datasheet says 527 W max where its own QSG says 638 W at 25 C, and the C100G guide gives the fan assemblies 100 W each in prose and 330 W for the three of them in Table A-2. The `note` carries both figures with their sources",
      "enum": [
       "vendor-silent",
       "sources-disagree",
       "nos-dependent",
       "needs-hardware",
       "needs-photo",
       "needs-drawing"
      ]
     },
     "note": {
      "type": "string",
      "description": "what was looked at and what it did not say. Cite the section, so the next reader does not repeat the search"
     },
     "wanted": {
      "type": "string",
      "description": "required, and the most valuable field: the difference between 'we don't know' and a request someone can act on"
     },
     "blocks": {
      "type": "array",
      "description": "capability flags or levels this gap stands in the way of, so a consumer can join a flag to its explanation structurally rather than by reading the prose in `wanted`. Optional, and an empty list is a fine answer - do not invent a blocker to fill the field. Absent or empty means 'blocks nothing we can name', not 'unknown'.",
      "items": {
       "$ref": "#/$defs/segment"
      }
     }
    }
   }
  }
 },
 "$defs": {
  "power-feed": {
   "description": "a supply feed, in three words: `ac` is mains through an IEC or similar inlet; `dc` is telecom DC, -48 V nominal (-36 to -75 V across the whitebox datasheets); `hvdc` is 240/380 V DC. A build fed by two kinds at once - a fixed box with an AC inlet AND a DC terminal both fitted - lists both, and a list is never used for one.",
   "oneOf": [
    {"enum": ["ac", "dc", "hvdc"]},
    {"type": "array", "items": {"enum": ["ac", "dc", "hvdc"]}, "minItems": 2, "uniqueItems": true}
   ]
  },
  "provenance": {
   "description": "field name -> where the figure came from. The same shape as component.schema.json's `$defs.provenance`, and deliberately so: a provenance note means one thing whether it is written on a device, a cutout or a placement.",
   "type": "object",
   "additionalProperties": {
    "type": "string",
    "description": "field name -> source, e.g. 'datasheet \u00a7Physical', 'photo-measured', 'measured', 'estimated'"
   }
  },
  "state": {
   "description": "one state a lamp can be in. The name is a token because `state-<name>` is a CSS class; the sentence explaining it belongs in `description` alongside THE BEHAVIOR BLOCK IS THE SAME SHAPE AS component.schema.json's and is kept that way deliberately: a state means one thing whether it is declared on a part or overridden on a placement. When `rate` and `sequence` were added they went into the component schema only, and a device that stated a blink frequency on a placement was rejected by a rule nobody had thought about - two definitions of one concept, drifting.",
   "oneOf": [
    {
     "$ref": "#/$defs/segment"
    },
    {
     "type": "object",
     "required": [
      "name"
     ],
     "additionalProperties": false,
     "properties": {
      "name": {
       "$ref": "#/$defs/segment"
      },
      "behavior": {
       "description": "how the lamp is lit, as opposed to what colour it is lit. Omitted means solid. A state is colour AND behaviour: solid green and blinking green are different facts on real hardware. `rate` is the blink frequency in hertz, because a lamp blinking twice a second and one blinking four times a second are two different documented conditions on the same drive and flattening both to `blinking` throws the distinction away. `sequence` is for a pattern no single colour and rate can express - Dell's drive status lamp blinks green three seconds, amber three seconds, then off for six, which is three phases with durations rather than a blink of anything.",
       "oneOf": [
        {
         "enum": [
          "solid",
          "blinking",
          "alternating",
          "sequence"
         ]
        },
        {
         "type": "object",
         "required": [
          "mode"
         ],
         "additionalProperties": false,
         "properties": {
          "mode": {
           "enum": [
            "solid",
            "blinking",
            "alternating",
            "sequence"
           ]
          },
          "color": {
           "type": "string",
           "description": "the second colour, for `alternating` - the lamp flashes between `color` and this one"
          },
          "rate": {
           "type": "number",
           "exclusiveMinimum": 0,
           "description": "blink frequency in hertz, for `blinking` and `alternating`. Omitted means once a second, which is what every state written before this property existed meant by `blinking`."
          },
          "phases": {
           "type": "array",
           "minItems": 2,
           "description": "for `sequence`: the cycle in order. Each phase names a colour and how long it holds; a phase with NO colour is the lamp off, which is a phase like any other and is how 'blinks green, amber, and turns off' gets said. Durations are seconds and may be omitted, in which case the phases divide the cycle evenly - some vendors give the order without the timing, and recording that as an even division is more honest than inventing durations.",
           "items": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
             "color": {
              "type": "string",
              "description": "the colour this phase holds; absent means the lamp is off for it"
             },
             "seconds": {
              "type": "number",
              "exclusiveMinimum": 0,
              "description": "how long this phase holds"
             }
            }
           }
          }
         }
        }
       ]
      },
      "color": {
       "type": "string",
       "description": "LED color for this state, emitted as scoped CSS"
      }
     }
    }
   ]
  },
  "segment": {
   "type": "string",
   "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
  },
  "for-target": {
   "description": "what a placement's `for:` names: a bare id, meaning a placement or bay in the same view; 'view/id', meaning one in another view of the same device, because a front-panel PSU lamp indicates a PSU that lives in the rear and only the qualified form can spell that; or the literal 'chassis', the whole unit, for an indicator whose subject is not a modelled part. A list means collectively - a FAN lamp over five trays names all five and belongs to none of them.",
   "type": "string",
   "pattern": "^[a-z0-9]+(-[a-z0-9]+)*(/[a-z0-9]+(-[a-z0-9]+)*)?$"
  },
  "xy": {
   "type": "array",
   "items": {
    "type": "number"
   },
   "minItems": 2,
   "maxItems": 2
  },
  "confidence": {
   "enum": [
    "measured",
    "photo-measured",
    "drawing",
    "datasheet",
    "registry",
    "borrowed",
    "estimated",
    "known-wrong"
   ],
   "description": "where a NUMBER came from. THE SAME VOCABULARY THE COMPONENT SCHEMA USES, deliberately - a device's 3D projections were the one place in the library that could never say where they came from, and giving them a second set of words would have been the defect this vocabulary exists to prevent. See spec/schemas/component.schema.json $defs/confidence for what each token means; `borrowed` in particular asserts that the named origin MEASURED the magnitude, and lint L36 checks it."
  }
 }
}
