{"version":3,"sources":["../src/utils/cable-type.ts","../src/utils/handle-ids.ts","../src/utils/port-position.ts","../src/utils/power-connectors.ts","../src/utils/render-ports.ts","../src/utils/document-adapter.ts","../src/utils/port-contract.ts","../src/utils/netbox.ts"],"names":["readChoiceValue","value","trimmed","record","readChoiceLabel","cableTypeToEdgeType","candidates","tokens","candidate","token","CENTIMETRES_PER_UNIT","lengthToCentimetres","length","unit","key","factor","DIRECTIONAL_HANDLE_SUFFIX","FACE_ALIAS_HANDLE_SUFFIX","normalizeDirectionalHandleId","handleId","normalizeHandleId","isPowerPortType","portType","HEX_PAIR","RESERVED_SEGMENT_CHAR","encodeHandleSegment","segment","decodeHandleSegment","decoded","i","percentEscapeChar","ch","hex","baseCollidesWithGrammar","base","classifyHandleBase","escapeCollidingLabel","label","buildDevicePortBase","group","encodedGroup","buildPortHandleId","portBlockName","port","decodePortHandleId","delimiter","OUTLET_BASE","FIBER_TUBE_BASE","FIBER_BASE","TUBE_BASE","SIDE_PORT_BASE","SPLIT_IO_BASE","BREAKER_WAY_BASE","fiberTube","fiber","tube","sidePort","splitIo","breakerWay","alternateShapes","shape","parseHandleId","raw","directionMatch","afterDirection","faceMatch","parsed","alternates","buildBreakerWayHandleId","wayId","applyFaceAliasHandleId","face","isRecord","toNumber","normalizeSemanticPositionTokens","canonicalizeSideToken","hasCenterToken","resolveAxisOffsetPercent","primarySide","hasLeft","hasRight","hasTop","hasBottom","hasCenter","resolveSemanticEdgePortPosition","side","offsetPercent","resolveSemanticCompositePortPosition","hasFront","hasRear","xPercent","yPercent","parseSemanticPortPosition","normalizePortPositionInput","sideRaw","semanticPosition","offsetPx","widthPercent","heightPercent","getPDUPortType","portIndex","DEFAULT_FRONT_GROUP_NAME","DEFAULT_REAR_GROUP_NAME","DEFAULT_POWER_GROUP_NAME","DISTRIBUTED_OFFSET_MIN_PERCENT","DISTRIBUTED_OFFSET_MAX_PERCENT","clampPercent","normalizePortAnchor","getNormalizedPortPosition","position","hasExplicitAbsoluteCoordinates","hasExplicitOffset","resolveRenderPortFace","resolveRenderPortGroupName","getEntrySortValue","entry","toDistributedOffsetPercent","index","count","buildDistributedOffsetMap","entries","distributedEntries","offsetPercentByIndex","entriesByAnchor","anchor","anchorEntries","sortedEntries","left","right","orderDifference","toPortPosition","normalizedPosition","distributedOffsetPercent","resolvedOffsetPercent","toPort","normalizePortBlockCollection","portBlocks","normalizedBlocks","block","portsByUnpositionedSide","ports","isGroupedPortBlockArray","isFlatDevicePortArray","buildPortBlocksFromPorts","devicePorts","bucketsByKey","indexedPorts","distributedOffsetsByFace","groupName","existingBucket","rearPorts","bucket","distributedOffsetByIndex","getResolvedPortBlocks","data","MAX_POWER_PORTS","IMAGE_RENDER_KEYS","DIMENSION_RENDER_KEYS","GENERAL_RENDER_KEYS","UI_NODE_STATE_KEYS","EXCLUDED_CONFIG_KEYS","setOwn","target","clonePosition","clonePortPosition","normalized","toDiagramNodeKind","node","toDiagramPlacement","placement","parentId","toDiagramRender","render","images","acc","dimensions","toDiagramExternalRefs","refs","toDiagramConfig","config","extractUiState","nodes","previousUi","nodeState","resolvedFace","inferEndpointMedium","normalizedType","buildPduPortBlocks","powerPorts","_","connectorType","toPortBasedEndpoints","endpoints","blocks","frontIds","endpoint","rearAliasBase","collectFrontEndpointIds","createSyntheticEndpoints","way","fibersPerTube","tubeIndex","fiberCount","tubeCount","tubeStartIndex","portCount","inputCount","outputCount","toDiagramEndpoints","ENDPOINT_SOURCE_KEYS","endpointSourceFingerprint","fingerprint","sameFingerprint","a","b","endpointDerivationCache","copyEndpoints","toDiagramEndpointsCached","cached","mergeEndpoints","previousEndpoints","nextEndpoints","previousById","previous","mergeDocumentNode","previousNode","nextNode","networkGraphToDiagramDocument","snapshot","previousDocument","previousNodesById","aliasedIdsByNodeId","aliased","toEndpointId","nodeId","handle","previousEdgesById","edge","edges","nextEdge","toText","normalizeFaceInput","resolveDefaultFace","resolveDefaultGroup","type","resolveDevicePortFace","resolveDevicePortGroup","resolveDeviceNodeId","device","DEFAULT_INTERFACE_GROUP","DEFAULT_MANAGEMENT_GROUP","DEFAULT_POWER_GROUP","toNonEmptyString","normalizeConnectorType","normalizePortFace","getInterfaceCustomFields","iface","getNestedLayoutHints","resolvePortFaceHint","mappedFace","customFields","nestedLayout","resolvePortGroupHint","mappedGroup","explicitGroup","resolveConnectorTypeHint","rawType","mappedConnectorType","normalizePortType","normalizeCableType","parsePortPosition","extractMappedPosition","mappedPort","includePortPositions","defaultMapInterfaceToPort","sortInterfaces","interfaces","aPosition","bPosition","buildInterfacePorts","options","mapInterfaceToPort","targetFace","readStatus","readName","ref","netboxRackToNetworkNode","rack","netboxDeviceToNetworkNode","uHeight","isDeviceRef","resolveCableEnd","cable","terminations","termination","object","legacy","buildCableEdgeData","lengthUnit","lengthCm","netboxCableToNetworkEdge","indexDevicePortHandles","createNetBoxDeviceEnricher","heightByTypeId","interfacesByDeviceId","deviceId","list","typeId","lookedUpHeight","needsHeight","lookedUpInterfaces","needsInterfaces","netboxToNetworkDiagram","deviceNodeById","deviceNodeIdByName","portHandlesByNodeId","enrich","rawDevice","deviceNode","resolveNodeId","end","byId","resolveHandle","handles","source","sourceHandle","targetHandle","netboxToDiagramDocument","calculateUPositions","devices","_rackHeight","positions","usedPositions","d"],"mappings":"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","file":"netbox.mjs","sourcesContent":["import type { NetworkEdgeType } from '../types';\n\n/**\n * Shared helpers for the NetBox / inventory cable converters: classify a\n * source-of-truth cable type into the edge type the diagram draws, and convert\n * a length + unit into centimetres (the unit PWR-A12 reads).\n */\n\n/** Read a choice-ish value: a bare string, or a `{ value, label }` object. */\nexport function readChoiceValue(value: unknown): string | undefined {\n  if (typeof value === 'string') {\n    const trimmed = value.trim();\n    return trimmed.length > 0 ? trimmed : undefined;\n  }\n  if (value && typeof value === 'object') {\n    const record = value as { value?: unknown; label?: unknown };\n    return readChoiceValue(record.value) ?? readChoiceValue(record.label);\n  }\n  return undefined;\n}\n\n/** Read the human label of a choice-ish value (falls back to its value). */\nexport function readChoiceLabel(value: unknown): string | undefined {\n  if (value && typeof value === 'object') {\n    const record = value as { value?: unknown; label?: unknown };\n    return readChoiceValue(record.label) ?? readChoiceValue(record.value);\n  }\n  return readChoiceValue(value);\n}\n\n/**\n * Classify a cable type (NetBox `CableTypeChoices` value or label, or a free\n * inventory label) into a diagram edge type:\n *\n * - `power` — anything naming power (`power`, `Power`).\n * - `fiber` — single/multimode fibre (`smf*`, `mmf*`, `OS1/OS2`, `OM1…OM5`),\n *   active optical cable (`aoc`), and anything naming fibre/fiber/optical.\n * - `ethernet` — every other named type (twisted pair `cat*`, DAC, MRJ21,\n *   coax, USB, …): a copper/network run.\n *\n * Returns `undefined` for a blank/missing type so the caller can pick its\n * fallback.\n */\nexport function cableTypeToEdgeType(...candidates: unknown[]): NetworkEdgeType | undefined {\n  const tokens = candidates\n    .map((candidate) => readChoiceValue(candidate))\n    .filter((token): token is string => token !== undefined)\n    .map((token) => token.toLowerCase());\n  if (tokens.length === 0) return undefined;\n  if (tokens.some((token) => token.includes('power'))) return 'power';\n  if (\n    tokens.some(\n      (token) =>\n        /^(smf|mmf|aoc)\\b/.test(token) ||\n        /\\b(om[1-5]|os[12])\\b/.test(token) ||\n        /fib(er|re)|optic/.test(token),\n    )\n  ) {\n    return 'fiber';\n  }\n  return 'ethernet';\n}\n\nconst CENTIMETRES_PER_UNIT: Readonly<Record<string, number>> = {\n  km: 100000,\n  kilometer: 100000,\n  kilometers: 100000,\n  kilometre: 100000,\n  kilometres: 100000,\n  m: 100,\n  meter: 100,\n  meters: 100,\n  metre: 100,\n  metres: 100,\n  cm: 1,\n  centimeter: 1,\n  centimeters: 1,\n  centimetre: 1,\n  centimetres: 1,\n  mi: 160934.4,\n  mile: 160934.4,\n  miles: 160934.4,\n  ft: 30.48,\n  foot: 30.48,\n  feet: 30.48,\n  in: 2.54,\n  inch: 2.54,\n  inches: 2.54,\n};\n\n/**\n * Convert a length in `unit` to centimetres. Returns `undefined` when the\n * length is not a positive finite number or the unit is blank/unknown — an\n * unknown unit is never guessed at.\n */\nexport function lengthToCentimetres(length: unknown, unit: unknown): number | undefined {\n  if (typeof length !== 'number' || !Number.isFinite(length) || length <= 0) return undefined;\n  const key = readChoiceValue(unit)?.toLowerCase();\n  if (!key || !Object.prototype.hasOwnProperty.call(CENTIMETRES_PER_UNIT, key)) return undefined;\n  const factor = CENTIMETRES_PER_UNIT[key];\n  return Math.round(length * factor * 100) / 100;\n}\n","import type { NetworkEdge, Port } from \"../types\";\n\nconst DIRECTIONAL_HANDLE_SUFFIX = /-(source|target)$/i;\nconst FACE_ALIAS_HANDLE_SUFFIX = /--(rear|front)$/i;\n\n/**\n * Normalize source/target directional suffixes used for bidirectional handles.\n * Example: `Rear Ports-1-target` -\\> `Rear Ports-1`.\n */\nexport function normalizeDirectionalHandleId(\n  handleId: string | null | undefined\n): string {\n  if (!handleId) return \"\";\n  return handleId.replace(DIRECTIONAL_HANDLE_SUFFIX, \"\");\n}\n\n/**\n * Normalize rendered handle ids back to their canonical endpoint id.\n * This removes the directional suffix and the split-face alias, in the order\n * the renderer composes them: `<base>[--rear|--front][-source|-target]`.\n * (Previously only `--rear` was stripped; `--front` ghost aliases now\n * normalize too.)\n *\n * Losslessness: the strip is unconditional, so it is only safe when the base\n * itself cannot end in a reserved suffix. {@link buildPortHandleId} guarantees\n * this for device ports — a label like `target` or `foo-target` is escaped at\n * build time (`<group>-%74arget`, `<group>-foo%2Dtarget`) so it survives\n * normalization intact. Bare power labels and explicit `port.id`s are emitted\n * verbatim, so a caller that chooses an id ending in `-source`/`-target`/\n * `--rear`/`--front` owns that collision.\n */\nexport function normalizeHandleId(handleId: string | null | undefined): string {\n  return normalizeDirectionalHandleId(handleId).replace(\n    FACE_ALIAS_HANDLE_SUFFIX,\n    \"\"\n  );\n}\n\n/**\n * Returns true when a port type is considered power-related.\n */\nexport function isPowerPortType(portType: string | undefined): boolean {\n  return (\n    typeof portType === \"string\" && portType.toLowerCase().includes(\"power\")\n  );\n}\n\n/**\n * Handle id of a vertical PDU's supply inlet — the single point where a branch\n * circuit from a distribution-board breaker way lands.\n *\n * Deliberately non-numeric so it can never collide with an outlet id, which is\n * always a bare 1-based number (`\"1\"…\"N\"`).\n * @public\n */\nexport const PDU_INLET_HANDLE_ID = \"inlet\";\n\n/**\n * Handle id of a distribution board's incomer — the board's own supply inlet,\n * mounted by `DistributionBoardNode`. The board analogue of\n * {@link PDU_INLET_HANDLE_ID}.\n * @public\n */\nexport const BOARD_INCOMER_HANDLE_ID = \"incomer\";\n\nconst POWER_INLET_HANDLE_IDS = new Set<string>([\n  PDU_INLET_HANDLE_ID,\n  BOARD_INCOMER_HANDLE_ID,\n]);\n\n/**\n * Whether a handle id names a power-distribution node's supply inlet\n * (`inlet` on a PDU strip, `incomer` on a distribution board). Directional and\n * face-alias suffixes are normalized away first.\n * @public\n */\nexport function isPowerInletHandleId(handleId: string | null | undefined): boolean {\n  if (!handleId) return false;\n  return POWER_INLET_HANDLE_IDS.has(normalizeHandleId(handleId).trim().toLowerCase());\n}\n\n// ---------------------------------------------------------------------------\n// Device-port group/label codec\n//\n// buildPortHandleId joins a non-power port's group and label with `-`. A raw\n// join is non-injective — group `A` + label `B-C` and group `A-B` + label `C`\n// both produce `A-B-C` — and a clean join can also reproduce a fixed grammar\n// production (group `fiber` + label `3` -> `fiber-3`) or a directional/face\n// suffix (label `target` -> `<group>-target`, which normalizeHandleId would\n// truncate). Each segment is therefore percent-escaped over the two structural\n// characters only — the delimiter `-` (`%2D`) and the escape introducer `%`\n// (`%25`) — so the joined base has exactly one literal `-` (the join) and is\n// unambiguously splittable. A segment containing neither character is emitted\n// verbatim, so the overwhelmingly common case is byte-for-byte unchanged.\n// ---------------------------------------------------------------------------\n\n/** Two hex digits, for the tolerant percent-decoder. */\nconst HEX_PAIR = /^[0-9a-fA-F]{2}$/;\n\n/** A character the join grammar reserves and must escape (`-` or `%`). */\nconst RESERVED_SEGMENT_CHAR = /[-%]/;\n\n/**\n * Percent-escape the two structural characters (`%` then `-`) in a group/label\n * segment. Segments with neither are returned unchanged.\n */\nfunction encodeHandleSegment(segment: string): string {\n  if (!RESERVED_SEGMENT_CHAR.test(segment)) {\n    return segment;\n  }\n  return segment.replace(/%/g, \"%25\").replace(/-/g, \"%2D\");\n}\n\n/**\n * Inverse of {@link encodeHandleSegment}: a tolerant single-pass percent-decode\n * that reverses `%XX` escapes and leaves any malformed `%` literal, so it never\n * throws.\n */\nfunction decodeHandleSegment(segment: string): string {\n  if (!segment.includes(\"%\")) {\n    return segment;\n  }\n  let decoded = \"\";\n  for (let i = 0; i < segment.length; i += 1) {\n    if (\n      segment[i] === \"%\" &&\n      i + 2 < segment.length &&\n      HEX_PAIR.test(segment.slice(i + 1, i + 3))\n    ) {\n      decoded += String.fromCharCode(Number.parseInt(segment.slice(i + 1, i + 3), 16));\n      i += 2;\n    } else {\n      decoded += segment[i];\n    }\n  }\n  return decoded;\n}\n\n/** Percent-escape a single (ASCII) character, e.g. `3` -> `%33`. */\nfunction percentEscapeChar(ch: string): string {\n  const hex = ch.charCodeAt(0).toString(16).toUpperCase();\n  return `%${hex.length === 1 ? `0${hex}` : hex}`;\n}\n\n/**\n * True when a freshly joined device-port base would be misread as a fixed\n * grammar shape or stripped by a normalizer (it ends in a directional or face\n * suffix). Such a base must be disambiguated at build time.\n */\nfunction baseCollidesWithGrammar(base: string): boolean {\n  return (\n    classifyHandleBase(base).kind !== \"port\" ||\n    DIRECTIONAL_HANDLE_SUFFIX.test(base) ||\n    FACE_ALIAS_HANDLE_SUFFIX.test(base)\n  );\n}\n\n/**\n * Escape a label whose plain join would collide with the fixed grammar or a\n * reserved suffix. The first character — always ASCII in the colliding cases\n * (a digit, or the leading letter of `source`/`target`/`cable-button`) — is\n * percent-escaped, which no fixed shape or reserved suffix can begin with; the\n * remainder is escaped normally. The standard decoder reverses it unchanged.\n */\nfunction escapeCollidingLabel(label: string): string {\n  return percentEscapeChar(label[0]) + encodeHandleSegment(label.slice(1));\n}\n\n/**\n * Build the `<group>-<label>` base for a non-power device port with both\n * segments escaped (see the codec note above). Injective and — via\n * `decodePortHandleId` — reversible.\n */\nfunction buildDevicePortBase(group: string, label: string): string {\n  const encodedGroup = encodeHandleSegment(group);\n  const base = `${encodedGroup}-${encodeHandleSegment(label)}`;\n  if (label.length > 0 && baseCollidesWithGrammar(base)) {\n    return `${encodedGroup}-${escapeCollidingLabel(label)}`;\n  }\n  return base;\n}\n\n/**\n * Build a stable handle id for a port.\n *\n * Three shapes, in priority order:\n * 1. An explicit `port.id` is returned verbatim — it is the caller's contract,\n *    including any collision with a fixed grammar shape.\n * 2. A power port returns its `label` verbatim, preserving legacy ids (bare\n *    outlet numbers, `PSU-A`, ...).\n * 3. Otherwise the group and label are percent-escaped and joined\n *    (`<group>-<label>`) so the result is injective and never aliases a fixed\n *    grammar production or a directional/face suffix. Invert with\n *    `decodePortHandleId`.\n *\n * NOTE (pre-1.0 wire-format revision): device-port segments containing `-` or\n * `%` now encode those characters (`%2D` / `%25`), and a clean label that would\n * otherwise collide (e.g. group `fiber` + label `3` becomes `fiber-%33`) gets a\n * first-character escape. Segments with no reserved character are unchanged, so\n * common ids are stable.\n */\nexport function buildPortHandleId(\n  portBlockName: string,\n  port: Pick<Port, \"label\" | \"id\"> & { type?: string }\n): string {\n  if (typeof port.id === \"string\" && port.id.length > 0) {\n    return port.id;\n  }\n  if (isPowerPortType(port.type)) {\n    return port.label;\n  }\n  return buildDevicePortBase(portBlockName, port.label);\n}\n\n/**\n * Invert {@link buildPortHandleId}'s device-port group/label encoding, returning\n * the original `{ group, label }`. Only meaningful for the `<group>-<label>`\n * form: power labels and explicit `port.id`s are emitted verbatim by\n * {@link buildPortHandleId} and must not be decoded (there is no group to\n * recover). The split is unambiguous because the encoding leaves exactly one\n * literal `-` (the join delimiter) in the id.\n *\n * Intentionally NOT part of the public entry surface: it is a partial inverse\n * (meaningless on power labels and explicit ids), so it stays internal to\n * avoid a foot-gun. Exported from the module for the grammar's own tests only.\n * @internal\n */\nexport function decodePortHandleId(handleId: string): { group: string; label: string } {\n  const delimiter = handleId.indexOf(\"-\");\n  if (delimiter === -1) {\n    return { group: decodeHandleSegment(handleId), label: \"\" };\n  }\n  return {\n    group: decodeHandleSegment(handleId.slice(0, delimiter)),\n    label: decodeHandleSegment(handleId.slice(delimiter + 1)),\n  };\n}\n\n/**\n * Heuristic for whether a handle id represents a power endpoint.\n *\n * Prefer {@link applyDirectionalPortHandleId}'s `portType` option when the port\n * is available — this string heuristic is a fallback only.\n *\n * Matches library power-id shapes (`Power Ports-1`, `pwr-1`, `power_ac-1`,\n * `PSU-A`, `outlet-3`) without treating every label that merely contains the\n * word \"power\" (e.g. `Power Room Uplinks-1`) as a power endpoint.\n */\nexport function looksLikePowerHandleId(handleId: string): boolean {\n  const normalized = normalizeDirectionalHandleId(handleId).toLowerCase().trim();\n  const compact = normalized.replace(/\\s+/g, \" \");\n\n  if (/^(pwr|power_ac|power_dc)([-_]|$)/.test(compact)) return true;\n  if (/^power([-_ ]?ports)?[-_ ]?\\d+$/.test(compact)) return true;\n  if (/^pwr[-_]?\\d+$/.test(compact)) return true;\n  if (/^(psu|outlet)([-_]|\\d|$)/.test(compact)) return true;\n  if (/^way-.+$/.test(compact)) return true;\n  // The supply inlet of a PDU strip / distribution board is a power endpoint,\n  // so (like a way) it never takes a directional suffix.\n  if (POWER_INLET_HANDLE_IDS.has(compact)) return true;\n  if (/(^|[-_/ ])(psu|outlet)([-_/ ]|\\d|$)/.test(compact)) return true;\n  return false;\n}\n\n/**\n * Options for {@link applyDirectionalPortHandleId}.\n */\nexport interface ApplyDirectionalPortHandleIdOptions {\n  /**\n   * The port's `type` (e.g. `rj45`, `power_ac`). When provided, power-ness is\n   * decided from it via {@link isPowerPortType} — the same rule the renderer\n   * uses to decide whether a port mounts directional handles at all. Always\n   * pass this when the port is available.\n   */\n  portType?: string;\n}\n\n/**\n * Apply a directional (`source`/`target`) suffix to a rendered handle id — the\n * inverse of {@link normalizeDirectionalHandleId}. Any existing directional\n * suffix is replaced first, and passing no direction is a no-op.\n * Example: `Rear Ports-1` + `target` -\\> `Rear Ports-1-target`.\n *\n * Power endpoints are matched without a direction, so power handles are\n * returned unchanged. Whether a handle is a power handle is decided from\n * `options.portType` when given (exact — the renderer's rule). Without it the\n * decision falls back to {@link looksLikePowerHandleId}, which recognises\n * library power-id shapes (`Power Ports-1`, `pwr-*`, `PSU-*`, `outlet-*`)\n * without treating labels that merely contain the word \"power\" as power.\n * Pass `portType` whenever you have the port.\n */\nexport function applyDirectionalPortHandleId(\n  handleId: string,\n  direction?: \"source\" | \"target\" | null,\n  options?: ApplyDirectionalPortHandleIdOptions\n): string {\n  if (!direction) return handleId;\n  const hasPortType =\n    typeof options?.portType === \"string\" && options.portType.length > 0;\n  const isPower = hasPortType\n    ? isPowerPortType(options.portType)\n    : looksLikePowerHandleId(handleId);\n  if (isPower) return handleId;\n  return `${normalizeDirectionalHandleId(handleId)}-${direction}`;\n}\n\n// ---------------------------------------------------------------------------\n// Typed handle-id grammar\n//\n// Handle ids are the library's de-facto wire protocol: they are what React\n// Flow stores on edges, what documents persist, and what consumers match\n// against. This section is the single authority for that grammar. The full\n// composed form is:\n//\n//   <base>[--rear|--front][-source|-target]\n//\n// where <base> is one of the shapes below. Everything here is *structural* —\n// it reads only the string. Shapes whose tokens can collide with port group\n// names (a group literally named `fiber`, a port label `left-3`) cannot be\n// told apart from the string alone; callers that have the node should combine\n// this with the node type (as `classifyFibreEndpoint` does).\n// ---------------------------------------------------------------------------\n\n/**\n * The structural shape of a handle id's base (suffixes already stripped).\n *\n * - `outlet` — vertical-PDU outlets: a bare number (`\"4\"`).\n * - `fiber` — `fiber-<n>` (tube/cable nodes) or `fiber-<tube>-<n>`\n *   (FibreFlowMap rows).\n * - `tube` — `tube-<n>` (tube and multi-tube-cable nodes).\n * - `splice-side` — bare `left` / `right` (splice nodes).\n * - `side-port` — `left-<n>` / `right-<n>` (coupler ports).\n * - `split-io` — `input-<n>` / `output-<n>` (fibre-split nodes).\n * - `cable-button` — the literal `cable-button` connector on cable nodes.\n * - `breaker-way` — `way-<wayId>` (distribution-board breaker ways).\n * - `port` — everything else: device-family ports built by\n *   {@link buildPortHandleId} (`<group>-<label>`, a bare power label, or an\n *   explicit `port.id`). A dashed base is decoded into `group`/`label` (the\n *   segments are percent-escaped so the join delimiter is unambiguous); a\n *   dash-free base, which can never be a group-label join, is flagged\n *   `verbatim` (an opaque `port.id` or bare power label).\n * @public\n */\nexport type HandleShape =\n  | { kind: \"outlet\"; outlet: number }\n  | { kind: \"fiber\"; fiber: number; tube?: number }\n  | { kind: \"tube\"; tube: number }\n  | { kind: \"splice-side\"; side: \"left\" | \"right\" }\n  | { kind: \"side-port\"; side: \"left\" | \"right\"; index: number }\n  | { kind: \"split-io\"; io: \"input\" | \"output\"; index: number }\n  | { kind: \"cable-button\" }\n  | { kind: \"breaker-way\"; wayId: string }\n  | {\n      kind: \"port\";\n      /**\n       * Decoded port group, when the base is a `<group>-<label>` join (has a\n       * dash). Best-effort structural decode: for a verbatim `port.id` or power\n       * label that happens to contain a dash (`PSU-A`) this is the split before\n       * the first dash, not necessarily a real group.\n       */\n      group?: string;\n      /** Decoded port label — same best-effort decode as `group` above. */\n      label?: string;\n      /**\n       * `true` when the base has no dash, so it cannot be a group-label join —\n       * it is an opaque `port.id` or a bare power label with no `group`/`label`\n       * reading.\n       */\n      verbatim?: boolean;\n    };\n\n/**\n * A structurally parsed handle id.\n * @public\n */\nexport interface ParsedHandleId {\n  /** The id exactly as given. */\n  raw: string;\n  /** `raw` with the directional suffix and face alias stripped. */\n  base: string;\n  /** Directional suffix, when present (`X-source` / `X-target`). */\n  direction?: \"source\" | \"target\";\n  /**\n   * Split-face alias, when present (`X--rear` / `X--front`) — added by the\n   * renderer when both faces of a device are shown and a handle id would\n   * otherwise collide with its opposite face.\n   */\n  faceAlias?: \"front\" | \"rear\";\n  /** Structural classification of {@link ParsedHandleId.base}. */\n  shape: HandleShape;\n  /**\n   * Other shapes the base could structurally be, when it is genuinely\n   * ambiguous. Present (and non-empty) only when there is a second reading;\n   * absent otherwise. The primary {@link ParsedHandleId.shape} is never changed\n   * by this — `alternates` is a pure signal.\n   *\n   * The classic case is a verbatim `port.id` that happens to match a fixed\n   * shape: `tube-5` classifies as `{ kind: \"tube\", tube: 5 }` but could be an\n   * explicit port id, so `alternates` is `[{ kind: \"port\", verbatim: true }]`.\n   * A dashed group-label port carries the same verbatim alternate (it could be\n   * one opaque id). Only node-context-aware classification (which handle-ids\n   * cannot do from the string alone) can pick the right reading.\n   */\n  alternates?: HandleShape[];\n}\n\nconst OUTLET_BASE = /^\\d+$/;\nconst FIBER_TUBE_BASE = /^fiber-(\\d+)-(\\d+)$/;\nconst FIBER_BASE = /^fiber-(\\d+)$/;\nconst TUBE_BASE = /^tube-(\\d+)$/;\nconst SIDE_PORT_BASE = /^(left|right)-(\\d+)$/;\nconst SPLIT_IO_BASE = /^(input|output)-(\\d+)$/;\nconst BREAKER_WAY_BASE = /^way-(.+)$/;\n\nfunction classifyHandleBase(base: string): HandleShape {\n  if (OUTLET_BASE.test(base)) {\n    return { kind: \"outlet\", outlet: Number(base) };\n  }\n  const fiberTube = FIBER_TUBE_BASE.exec(base);\n  if (fiberTube) {\n    return { kind: \"fiber\", tube: Number(fiberTube[1]), fiber: Number(fiberTube[2]) };\n  }\n  const fiber = FIBER_BASE.exec(base);\n  if (fiber) {\n    return { kind: \"fiber\", fiber: Number(fiber[1]) };\n  }\n  const tube = TUBE_BASE.exec(base);\n  if (tube) {\n    return { kind: \"tube\", tube: Number(tube[1]) };\n  }\n  if (base === \"left\" || base === \"right\") {\n    return { kind: \"splice-side\", side: base };\n  }\n  const sidePort = SIDE_PORT_BASE.exec(base);\n  if (sidePort) {\n    return {\n      kind: \"side-port\",\n      side: sidePort[1] as \"left\" | \"right\",\n      index: Number(sidePort[2]),\n    };\n  }\n  const splitIo = SPLIT_IO_BASE.exec(base);\n  if (splitIo) {\n    return {\n      kind: \"split-io\",\n      io: splitIo[1] as \"input\" | \"output\",\n      index: Number(splitIo[2]),\n    };\n  }\n  if (base === \"cable-button\") {\n    return { kind: \"cable-button\" };\n  }\n  const breakerWay = BREAKER_WAY_BASE.exec(base);\n  if (breakerWay) {\n    return { kind: \"breaker-way\", wayId: decodeHandleSegment(breakerWay[1]) };\n  }\n  // A dashed base is always a possible `<group>-<label>` join — decode it (the\n  // percent-escapes are reversed). A dash-free base cannot be a join, so it is\n  // an opaque verbatim id / bare power label.\n  if (base.includes(\"-\")) {\n    const { group, label } = decodePortHandleId(base);\n    return { kind: \"port\", group, label };\n  }\n  return { kind: \"port\", verbatim: true };\n}\n\n/**\n * The alternate structural readings of a classified base, when it is genuinely\n * ambiguous. Every fixed shape, and every dashed group-label port, could also\n * be an explicit verbatim `port.id`; a bare verbatim port has no second reading.\n */\nfunction alternateShapes(shape: HandleShape): HandleShape[] {\n  if (shape.kind === \"port\") {\n    return shape.verbatim ? [] : [{ kind: \"port\", verbatim: true }];\n  }\n  return [{ kind: \"port\", verbatim: true }];\n}\n\n/**\n * Parse a rendered/stored handle id into its structural parts: base, optional\n * directional suffix, optional split-face alias, and the {@link HandleShape}\n * of the base. Total — never throws; unrecognized bases classify as `port`.\n * @public\n */\nexport function parseHandleId(handleId: string | null | undefined): ParsedHandleId {\n  const raw = handleId ?? \"\";\n  const directionMatch = DIRECTIONAL_HANDLE_SUFFIX.exec(raw);\n  const afterDirection = directionMatch\n    ? raw.slice(0, raw.length - directionMatch[0].length)\n    : raw;\n  const faceMatch = FACE_ALIAS_HANDLE_SUFFIX.exec(afterDirection);\n  const base = faceMatch\n    ? afterDirection.slice(0, afterDirection.length - faceMatch[0].length)\n    : afterDirection;\n  const shape = classifyHandleBase(base);\n  const parsed: ParsedHandleId = { raw, base, shape };\n  if (directionMatch) {\n    parsed.direction = directionMatch[1].toLowerCase() as \"source\" | \"target\";\n  }\n  if (faceMatch) {\n    parsed.faceAlias = faceMatch[1].toLowerCase() as \"front\" | \"rear\";\n  }\n  const alternates = alternateShapes(shape);\n  if (alternates.length > 0) {\n    parsed.alternates = alternates;\n  }\n  return parsed;\n}\n\n/**\n * Build a fiber handle id: `fiber-<n>`, or `fiber-<tube>-<n>` when a tube\n * index is given (the FibreFlowMap row form).\n * @public\n */\nexport function buildFibreHandleId(fiber: number, tube?: number): string {\n  return tube === undefined ? `fiber-${fiber}` : `fiber-${tube}-${fiber}`;\n}\n\n/**\n * Build a tube handle id: `tube-<n>`.\n * @public\n */\nexport function buildTubeHandleId(tube: number): string {\n  return `tube-${tube}`;\n}\n\n/**\n * Build a splice handle id: the bare `left` / `right` sides of a splice node.\n * @public\n */\nexport function buildSpliceHandleId(side: \"left\" | \"right\"): string {\n  return side;\n}\n\n/**\n * Build a coupler port handle id: `left-<n>` / `right-<n>` (1-based).\n * @public\n */\nexport function buildCouplerHandleId(side: \"left\" | \"right\", index: number): string {\n  return `${side}-${index}`;\n}\n\n/**\n * Build a fibre-split io handle id: `input-<n>` / `output-<n>`.\n * @public\n */\nexport function buildSplitIoHandleId(io: \"input\" | \"output\", index: number): string {\n  return `${io}-${index}`;\n}\n\n/**\n * Build a distribution-board breaker way handle id: `way-<wayId>`.\n *\n * The way id is percent-escaped exactly as {@link buildPortHandleId} escapes\n * its segments, so a way id containing `-` or `%` (or the `--rear` face alias\n * suffix) cannot be misread by {@link parseHandleId}; the parse side decodes\n * it symmetrically. Ids with no reserved character are unchanged.\n *\n * Ways are power endpoints, so the id carries no directional suffix (see\n * {@link applyDirectionalPortHandleId}) and {@link looksLikePowerHandleId}\n * recognises the prefix.\n * @public\n */\nexport function buildBreakerWayHandleId(wayId: string): string {\n  return `way-${encodeHandleSegment(wayId)}`;\n}\n\n/**\n * Append a split-face alias to a handle id: `X` + `rear` -\\> `X--rear`.\n * Any existing alias is replaced. The renderer applies this when a hidden or\n * ghost face would mount a handle whose id collides with the visible face.\n * @public\n */\nexport function applyFaceAliasHandleId(\n  handleId: string,\n  face: \"front\" | \"rear\"\n): string {\n  return `${handleId.replace(FACE_ALIAS_HANDLE_SUFFIX, \"\")}--${face}`;\n}\n\n/**\n * Normalize a handle id for tolerant matching by stripping `+` characters.\n * Port labels like `SFP+-1` historically produced handles that some producers\n * emitted `+`-stripped (`SFP-1`), so comparisons between independently built\n * handles must be `+`-insensitive. Apply to BOTH sides of a comparison — this\n * is a matching normalization, not part of the build grammar:\n * {@link buildPortHandleId} keeps `+` so built ids always equal rendered ids.\n *\n * This mapping is intentionally MANY-TO-ONE and lossy: it cannot be inverted\n * and must never be persisted as a canonical id. Distinct handles collapse\n * together — e.g. `SFP+-1` and `SFP-1` both sanitize to `SFP-1`, so a port\n * genuinely labelled `SFP-1` is indistinguishable from one labelled `SFP+-1`\n * under this comparison. Use it only as a last-resort tolerant match, applied\n * to both sides.\n *\n * Interaction with the injective build grammar (pre-1.0 wire-format revision):\n * {@link buildPortHandleId} percent-escapes `-` (`%2D`) and `%` (`%25`) inside\n * group/label segments. This sanitizer strips ONLY `+` and deliberately does\n * NOT decode those escapes, so tolerant matching requires both sides to share\n * the same escaping discipline. A lookup built through {@link buildPortHandleId}\n * (or a bare, unescaped label) still matches `+`-insensitively — a `+`-dropped\n * but still-escaped composite `Group-SFP%2D1` matches the registered\n * `Group-SFP+%2D1`. But a fully hand-built composite that both drops `+` AND\n * writes a raw `-` where the builder now writes `%2D` — `Group-SFP-1` against\n * `Group-SFP+%2D1` — no longer matches. That narrowed case is unreachable\n * through the builder (the single source of composite ids) and is pinned as a\n * boundary in the tests. Decoding is intentionally kept OUT of this function:\n * mounts.\n *\n * @internal Internal building block of {@link handlesMatch}; not part of the\n * public API. Consumers must compare handles with {@link handlesMatch} (or find\n * edges with {@link findEdgesForPortHandle}), which apply this rule to both\n * sides along with directional/face normalization.\n */\nexport function sanitizeHandleIdForMatching(handleId: string): string {\n  return handleId.replace(/\\+/g, \"\");\n}\n\n/**\n * True iff two handle ids refer to the same handle under the library's own\n * equivalence relation. This is the one function to use for \"is this edge on\n * this port?\" — it replaces bespoke candidate-set matchers.\n *\n * @remarks\n * The relation, applied to BOTH sides: strip the directional suffix and the\n * split-face alias ({@link normalizeHandleId} semantics), then apply the\n * narrowed `+`-insensitive rule (drop `+`, escaping-aware), then\n * compare. So `Rear Ports-1-target`, `Rear Ports-1--rear` and `Rear Ports-1`\n * all match, and `SFP+-1` matches `SFP-1`.\n *\n * It is intentionally string-structural and does NOT percent-decode: it matches\n * builder-produced ids (the single source of composite ids), where both sides\n * share the same escaping discipline. A hand-built composite that drops `+` AND\n * writes a raw `-` where the builder writes `%2D` (`Group-SFP-1` vs\n * `Group-SFP+%2D1`) is a deliberate non-match — that form is unreachable through\n * {@link buildPortHandleId}. Prefer this over the raw sanitizer, which leaks\n * matching concerns onto callers.\n *\n * @example\n * ```ts\n * handlesMatch(\"Rear Ports-1-target\", \"Rear Ports-1\"); // true\n * handlesMatch(\"Ports-1--rear\", \"Ports-1\");            // true\n * handlesMatch(\"SFP+-1\", \"SFP-1\");                      // true\n * handlesMatch(\"Ports-1\", \"Ports-2\");                   // false\n * ```\n * @public\n */\nexport function handlesMatch(a: string, b: string): boolean {\n  return (\n    sanitizeHandleIdForMatching(normalizeHandleId(a)) ===\n    sanitizeHandleIdForMatching(normalizeHandleId(b))\n  );\n}\n\n/**\n * Return the ids of every edge attached to the given `handleId` on `nodeId`,\n * matching handles with {@link handlesMatch}. An edge qualifies when\n * `source === nodeId` and its `sourceHandle` matches, or `target === nodeId` and\n * its `targetHandle` matches. Edges with an undefined `sourceHandle`/\n * `targetHandle` on the relevant side are skipped.\n *\n * @remarks\n * This is the multi-edge companion to {@link handlesMatch} and the replacement\n * for consumers' hand-rolled per-port edge scans. A self-loop edge (both ends on\n * `nodeId`, both handles matching) is returned once. The scan is a single linear\n * pass over `edges`; it does not build an index, so for repeated lookups over a\n * large edge set a caller may still want its own Map.\n *\n * @example\n * ```ts\n * const handle = buildPortHandleId(\"Rear Ports\", { label: \"1\" });\n * findEdgesForPortHandle(edges, \"device-1\", handle); // [\"e1\", \"e2\"]\n * ```\n * @public\n */\nexport function findEdgesForPortHandle(\n  edges: NetworkEdge[],\n  nodeId: string,\n  handleId: string\n): string[] {\n  const matched: string[] = [];\n  for (const edge of edges) {\n    if (\n      edge.source === nodeId &&\n      edge.sourceHandle !== undefined &&\n      handlesMatch(edge.sourceHandle, handleId)\n    ) {\n      matched.push(edge.id);\n      continue;\n    }\n    if (\n      edge.target === nodeId &&\n      edge.targetHandle !== undefined &&\n      handlesMatch(edge.targetHandle, handleId)\n    ) {\n      matched.push(edge.id);\n    }\n  }\n  return matched;\n}\n\n/**\n * For pass-through nodes with two sides, return the handle id of the opposite\n * side: splice `left` \\<-\\> `right`, coupler `left-<n>` \\<-\\> `right-<n>`.\n * Returns `undefined` for every other shape. Directional suffixes and face\n * aliases are stripped (neither occurs on splice/coupler handles in practice).\n * @public\n */\nexport function getOppositeSideHandleId(handleId: string): string | undefined {\n  const { shape } = parseHandleId(handleId);\n  if (shape.kind === \"splice-side\") {\n    return shape.side === \"left\" ? \"right\" : \"left\";\n  }\n  if (shape.kind === \"side-port\") {\n    return buildCouplerHandleId(shape.side === \"left\" ? \"right\" : \"left\", shape.index);\n  }\n  return undefined;\n}\n\n/**\n * Options for {@link encodeHandle}: the optional suffixes composed onto the\n * base, in grammar order `<base>[--rear|--front][-source|-target]`.\n * @public\n */\nexport interface EncodeHandleOptions {\n  /** Split-face alias to append (`--rear` / `--front`). */\n  face?: \"front\" | \"rear\";\n  /**\n   * Directional suffix to append (`-source` / `-target`). Applied structurally\n   * regardless of power-ness — use {@link applyDirectionalPortHandleId} instead\n   * when you need power ports left un-suffixed.\n   */\n  direction?: \"source\" | \"target\";\n}\n\n/**\n * The single structured encoder: turn a {@link HandleShape} into its handle-id\n * string, dispatching to the individual typed builders. The inverse of\n * {@link decodeHandle}/{@link parseHandleId} for the base classification.\n *\n * @remarks\n * Use this when you hold structured handle data (e.g. from a `DiagramEndpoint`)\n * and need the React Flow boundary string, instead of remembering which builder\n * a kind maps to. A `port` shape REQUIRES string `group` and `label` — it is\n * encoded through {@link buildPortHandleId} (so the same escaping/injectivity\n * applies). A `verbatim` port has no id to emit and cannot be re-encoded: pass\n * an explicit id to {@link buildPortHandleId} yourself for that case.\n *\n * Round-trip: `decodeHandle(encodeHandle(shape)).shape` deep-equals `shape` for\n * every non-verbatim kind.\n *\n * @throws TypeError when a `port` shape is missing `group` or `label`.\n * @example\n * ```ts\n * encodeHandle({ kind: \"fiber\", tube: 2, fiber: 7 });          // \"fiber-2-7\"\n * encodeHandle({ kind: \"port\", group: \"Rear Ports\", label: \"1\" }); // \"Rear Ports-1\"\n * encodeHandle({ kind: \"port\", group: \"Ports\", label: \"1\" }, { face: \"rear\", direction: \"source\" });\n * // \"Ports-1--rear-source\"\n * ```\n * @public\n */\nexport function encodeHandle(\n  shape: HandleShape,\n  opts?: EncodeHandleOptions\n): string {\n  let base: string;\n  switch (shape.kind) {\n    case \"outlet\":\n      base = String(shape.outlet);\n      break;\n    case \"fiber\":\n      base = buildFibreHandleId(shape.fiber, shape.tube);\n      break;\n    case \"tube\":\n      base = buildTubeHandleId(shape.tube);\n      break;\n    case \"splice-side\":\n      base = buildSpliceHandleId(shape.side);\n      break;\n    case \"side-port\":\n      base = buildCouplerHandleId(shape.side, shape.index);\n      break;\n    case \"split-io\":\n      base = buildSplitIoHandleId(shape.io, shape.index);\n      break;\n    case \"cable-button\":\n      base = \"cable-button\";\n      break;\n    case \"breaker-way\":\n      base = buildBreakerWayHandleId(shape.wayId);\n      break;\n    case \"port\": {\n      if (typeof shape.group !== \"string\" || typeof shape.label !== \"string\") {\n        throw new TypeError(\n          \"encodeHandle: a 'port' shape requires string 'group' and 'label'. \" +\n            \"Verbatim/opaque port ids cannot be re-encoded (there is no id in the \" +\n            \"shape) — pass the explicit id to buildPortHandleId instead.\"\n        );\n      }\n      base = buildPortHandleId(shape.group, { label: shape.label });\n      break;\n    }\n  }\n  let out = opts?.face ? applyFaceAliasHandleId(base, opts.face) : base;\n  if (opts?.direction) {\n    out = `${out}-${opts.direction}`;\n  }\n  return out;\n}\n\n/**\n * Decode a handle-id string into its structural parts. A documented alias of\n * {@link parseHandleId} — identical behavior, named for symmetry with\n * {@link encodeHandle}.\n *\n * @remarks\n * Total (never throws). Use the {@link encodeHandle}/`decodeHandle` pair when\n * you think in structured {@link HandleShape}s; use `parseHandleId` directly\n * when the \"parse a rendered id\" framing reads better. See\n * {@link ParsedHandleId.alternates} for the ambiguity signal on verbatim ids.\n *\n * @example\n * ```ts\n * decodeHandle(\"fiber-2-7\").shape; // { kind: \"fiber\", tube: 2, fiber: 7 }\n * decodeHandle(encodeHandle(x)).shape; // deep-equals x for non-verbatim x\n * ```\n * @public\n */\nexport function decodeHandle(\n  handleId: string | null | undefined\n): ParsedHandleId {\n  return parseHandleId(handleId);\n}\n","import type { PortPosition } from '../types';\n\nconst EDGE_OFFSET_LOW_PERCENT = 12;\nconst EDGE_OFFSET_CENTER_LOW_PERCENT = 34;\nconst EDGE_OFFSET_CENTER_PERCENT = 50;\nconst EDGE_OFFSET_CENTER_HIGH_PERCENT = 66;\nconst EDGE_OFFSET_HIGH_PERCENT = 88;\nconst FACE_POSITION_FRONT_PERCENT = 22;\nconst FACE_POSITION_REAR_PERCENT = 78;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst toNumber = (value: unknown): number | undefined => {\n  if (typeof value === 'number' && Number.isFinite(value)) {\n    return value;\n  }\n  if (typeof value === 'string' && value.trim().length > 0) {\n    const parsed = Number(value);\n    if (Number.isFinite(parsed)) {\n      return parsed;\n    }\n  }\n  return undefined;\n};\n\nconst normalizeSemanticPositionTokens = (value: string): string[] =>\n  value\n    .trim()\n    .toLowerCase()\n    .split(/[^a-z0-9]+/)\n    .filter((token) => token.length > 0);\n\nconst canonicalizeSideToken = (token: string): NonNullable<PortPosition['side']> | undefined => {\n  if (token === 'top' || token === 'up') {\n    return 'top';\n  }\n  if (token === 'right') {\n    return 'right';\n  }\n  if (token === 'bottom' || token === 'down') {\n    return 'bottom';\n  }\n  if (token === 'left') {\n    return 'left';\n  }\n  return undefined;\n};\n\nconst hasCenterToken = (tokens: string[]): boolean =>\n  tokens.includes('center') || tokens.includes('centre') || tokens.includes('middle') || tokens.includes('mid');\n\nconst resolveAxisOffsetPercent = (\n  primarySide: NonNullable<PortPosition['side']>,\n  tokens: string[],\n): number | undefined => {\n  const hasLeft = tokens.includes('left');\n  const hasRight = tokens.includes('right');\n  const hasTop = tokens.includes('top') || tokens.includes('up');\n  const hasBottom = tokens.includes('bottom') || tokens.includes('down');\n  const hasCenter = hasCenterToken(tokens);\n\n  if (primarySide === 'top' || primarySide === 'bottom') {\n    if (hasLeft && hasCenter && !hasRight) return EDGE_OFFSET_CENTER_LOW_PERCENT;\n    if (hasRight && hasCenter && !hasLeft) return EDGE_OFFSET_CENTER_HIGH_PERCENT;\n    if (hasLeft && !hasRight) return EDGE_OFFSET_LOW_PERCENT;\n    if (hasRight && !hasLeft) return EDGE_OFFSET_HIGH_PERCENT;\n    if (hasCenter) return EDGE_OFFSET_CENTER_PERCENT;\n    return undefined;\n  }\n\n  if (hasTop && hasCenter && !hasBottom) return EDGE_OFFSET_CENTER_LOW_PERCENT;\n  if (hasBottom && hasCenter && !hasTop) return EDGE_OFFSET_CENTER_HIGH_PERCENT;\n  if (hasTop && !hasBottom) return EDGE_OFFSET_LOW_PERCENT;\n  if (hasBottom && !hasTop) return EDGE_OFFSET_HIGH_PERCENT;\n  if (hasCenter) return EDGE_OFFSET_CENTER_PERCENT;\n  return undefined;\n};\n\nconst resolveSemanticEdgePortPosition = (tokens: string[]): PortPosition | undefined => {\n  const hasFaceTokens = tokens.includes('front') || tokens.includes('rear') || tokens.includes('back');\n  if (hasFaceTokens) {\n    return undefined;\n  }\n\n  const primarySide = tokens\n    .map(canonicalizeSideToken)\n    .find((side): side is NonNullable<PortPosition['side']> => side !== undefined);\n\n  if (!primarySide) {\n    return undefined;\n  }\n\n  const offsetPercent = resolveAxisOffsetPercent(primarySide, tokens);\n  if (offsetPercent === undefined) {\n    return undefined;\n  }\n\n  return {\n    side: primarySide,\n    offsetPercent,\n  };\n};\n\nconst resolveSemanticCompositePortPosition = (tokens: string[]): PortPosition | undefined => {\n  if (tokens.length === 0) {\n    return undefined;\n  }\n\n  const hasLeft = tokens.includes('left');\n  const hasRight = tokens.includes('right');\n  const hasCenter = hasCenterToken(tokens);\n  const hasFront = tokens.includes('front') || tokens.includes('top') || tokens.includes('up');\n  const hasRear = tokens.includes('rear') || tokens.includes('back') || tokens.includes('bottom') || tokens.includes('down');\n\n  let xPercent: number | undefined;\n  if (hasLeft && hasCenter && !hasRight) {\n    xPercent = EDGE_OFFSET_CENTER_LOW_PERCENT;\n  } else if (hasRight && hasCenter && !hasLeft) {\n    xPercent = EDGE_OFFSET_CENTER_HIGH_PERCENT;\n  } else if (hasLeft && !hasRight) {\n    xPercent = EDGE_OFFSET_LOW_PERCENT;\n  } else if (hasRight && !hasLeft) {\n    xPercent = EDGE_OFFSET_HIGH_PERCENT;\n  } else if (hasCenter) {\n    xPercent = EDGE_OFFSET_CENTER_PERCENT;\n  }\n\n  let yPercent: number | undefined;\n  if (hasFront && !hasRear) {\n    yPercent = FACE_POSITION_FRONT_PERCENT;\n  } else if (hasRear && !hasFront) {\n    yPercent = FACE_POSITION_REAR_PERCENT;\n  } else if (hasCenter) {\n    yPercent = EDGE_OFFSET_CENTER_PERCENT;\n  }\n\n  if (xPercent === undefined || yPercent === undefined) {\n    return undefined;\n  }\n\n  return { xPercent, yPercent };\n};\n\nexport const parseSemanticPortPosition = (value: string): PortPosition | undefined => {\n  const tokens = normalizeSemanticPositionTokens(value);\n  if (tokens.length === 0) {\n    return undefined;\n  }\n\n  if (tokens.length === 1) {\n    const side = canonicalizeSideToken(tokens[0]);\n    return side ? { side } : undefined;\n  }\n\n  return resolveSemanticEdgePortPosition(tokens) ?? resolveSemanticCompositePortPosition(tokens);\n};\n\nexport const normalizePortPositionInput = (value: unknown): PortPosition | undefined => {\n  if (typeof value === 'string') {\n    return parseSemanticPortPosition(value);\n  }\n\n  if (!isRecord(value)) {\n    return undefined;\n  }\n\n  const sideRaw = value.side ?? value.port_side ?? value.portSide;\n  const semanticPosition = typeof sideRaw === 'string' ? parseSemanticPortPosition(sideRaw) : undefined;\n  const simpleSide = typeof sideRaw === 'string' ? canonicalizeSideToken(sideRaw.trim().toLowerCase()) : undefined;\n\n  const side = simpleSide ?? semanticPosition?.side;\n  const offsetPercent = toNumber(\n    value.offsetPercent ??\n    value.offset_percent ??\n    value.offsetPct ??\n    value.offset_pct\n  ) ?? semanticPosition?.offsetPercent;\n  const offsetPx = toNumber(\n    value.offsetPx ??\n    value.offset_px ??\n    value.offsetPixels ??\n    value.offset_pixels\n  ) ?? semanticPosition?.offsetPx;\n  const xPercent = toNumber(\n    value.xPercent ??\n    value.x_percent ??\n    value.xPct ??\n    value.x_pct ??\n    value.horizontalPercent ??\n    value.horizontal_percent ??\n    value.x\n  ) ?? semanticPosition?.xPercent;\n  const yPercent = toNumber(\n    value.yPercent ??\n    value.y_percent ??\n    value.yPct ??\n    value.y_pct ??\n    value.verticalPercent ??\n    value.vertical_percent ??\n    value.y\n  ) ?? semanticPosition?.yPercent;\n  const widthPercent = toNumber(\n    value.widthPercent ??\n    value.width_percent ??\n    value.widthPct ??\n    value.width_pct ??\n    value.wPercent ??\n    value.w_percent\n  );\n  const heightPercent = toNumber(\n    value.heightPercent ??\n    value.height_percent ??\n    value.heightPct ??\n    value.height_pct ??\n    value.hPercent ??\n    value.h_percent\n  );\n\n  if (\n    side === undefined &&\n    offsetPercent === undefined &&\n    offsetPx === undefined &&\n    xPercent === undefined &&\n    yPercent === undefined &&\n    widthPercent === undefined &&\n    heightPercent === undefined\n  ) {\n    return undefined;\n  }\n\n  return {\n    side,\n    offsetPercent,\n    offsetPx,\n    xPercent,\n    yPercent,\n    widthPercent,\n    heightPercent,\n  };\n};\n","/**\n * Power connector utilities for consistent styling across devices and PDUs\n */\n\nexport interface PowerConnectorConfig {\n  type: 'C13' | 'C14' | 'C19' | 'C20' | 'AC';\n  color: string;\n  borderColor: string;\n  amperage: number;\n  voltage: number;\n  phases: number;\n}\n\nexport const POWER_CONNECTORS: Record<string, PowerConnectorConfig> = {\n  C13: {\n    type: 'C13',\n    color: '#f59e0b', // Orange\n    borderColor: '#d97706',\n    amperage: 10,\n    voltage: 230,\n    phases: 1\n  },\n  C14: {\n    type: 'C14',\n    color: '#f59e0b', // Orange\n    borderColor: '#d97706',\n    amperage: 10,\n    voltage: 230,\n    phases: 1\n  },\n  C19: {\n    type: 'C19',\n    color: '#fbbf24', // Amber\n    borderColor: '#d97706',\n    amperage: 16,\n    voltage: 230,\n    phases: 1\n  },\n  C20: {\n    type: 'C20',\n    color: '#fbbf24', // Amber\n    borderColor: '#d97706',\n    amperage: 16,\n    voltage: 230,\n    phases: 1\n  },\n  AC: {\n    type: 'AC',\n    color: '#60a5fa', // Blue (generic AC/NEMA)\n    borderColor: '#2563eb',\n    amperage: 15,\n    voltage: 120,\n    phases: 1\n  }\n};\n\nexport interface PowerPortStyle {\n  width: number;\n  height: number;\n  borderRadius?: string;\n  clipPath?: string;\n  color: string;\n  borderColor: string;\n  fontSize: string;\n}\n\n/**\n * Get consistent power port styling for any connector type\n */\nexport function getPowerPortStyle(connectorType: string): PowerPortStyle {\n  const connector = POWER_CONNECTORS[connectorType];\n  if (!connector) {\n    throw new Error(`Unknown connector type: ${connectorType}`);\n  }\n\n  const isHighPower = connector.amperage === 16; // C19/C20 are 16A\n  const isGenericAc = connector.type === 'AC';\n\n  // Size: C13/C14 are intentionally 30% larger while keeping C19/C20 unchanged.\n  const width = isHighPower ? 12 : isGenericAc ? 10 : 8 * 1.3;\n  const height = isHighPower ? 10 : isGenericAc ? 7 : 5 * 1.3;\n\n  // Shape: C13/C14 have chamfered corners, C19/C20 and generic AC use rounded corners.\n  const cornerStyle = isHighPower || isGenericAc\n    ? { borderRadius: isGenericAc ? '2px' : '3px' }\n    : {\n        clipPath: `polygon(2px 0, calc(100% - 2px) 0, 100% 2px, 100% 100%, 0 100%, 0 2px)`,\n        borderRadius: '0px'\n      };\n\n  return {\n    width,\n    height,\n    color: connector.color,\n    borderColor: connector.borderColor,\n    fontSize: isHighPower ? '3px' : isGenericAc ? '3.5px' : '4px',\n    ...cornerStyle\n  };\n}\n\n/**\n * Get connector configuration by type\n */\nexport function getConnectorConfig(connectorType: string): PowerConnectorConfig {\n  const connector = POWER_CONNECTORS[connectorType];\n  if (!connector) {\n    throw new Error(`Unknown connector type: ${connectorType}`);\n  }\n  return connector;\n}\n\n/**\n * Determine if a connector is high power (16A)\n */\nexport function isHighPowerConnector(connectorType: string): boolean {\n  const connector = getConnectorConfig(connectorType);\n  return connector.amperage === 16;\n}\n\n/** IEC 60320 designation embedded in a port `type` such as `power_c13`. */\nconst PORT_TYPE_CONNECTOR = /c(13|14|15|16|19|20|21|22)\\b/i;\n\n/**\n * Recover a connector designation from a port's `type` when the port carries no\n * explicit `connectorType` — `power_c13` becomes `C13`.\n *\n * The single implementation of that inference: the endpoint classifier\n * (`endpoint-metadata`), the validation graph and the renderer all read a\n * typed-only port through here, so a rule can never resolve a different\n * connector than the canvas drew.\n * @public\n */\nexport function inferConnectorFromPortType(portType: unknown): string | undefined {\n  if (typeof portType !== 'string') return undefined;\n  const match = PORT_TYPE_CONNECTOR.exec(portType);\n  return match ? `C${match[1]}` : undefined;\n}\n\n/**\n * Whether a PDU strip's data declares a supply inlet.\n *\n * The three fields the renderer reads before it mounts the inlet handle\n * (`inletConnector` / `inletAmps` / `inletGender`) — `feedType`, `voltage` and\n * `phases` only enrich the tooltip and are not an inlet on their own. Stated\n * once so the renderer, the canvas validator and PWR-A13 agree on when a strip\n * has somewhere for a branch circuit to land.\n * @public\n */\nexport function hasPduInlet(data: unknown): boolean {\n  if (!data || typeof data !== 'object') return false;\n  const { inletConnector, inletAmps, inletGender } = data as {\n    inletConnector?: unknown;\n    inletAmps?: unknown;\n    inletGender?: unknown;\n  };\n  if (typeof inletConnector === 'string' && inletConnector.trim().length > 0) return true;\n  if (typeof inletAmps === 'number' && Number.isFinite(inletAmps)) return true;\n  const gender =\n    typeof inletGender === 'string' ? inletGender.trim().toLowerCase() : undefined;\n  return gender === 'male' || gender === 'female';\n}\n\n/**\n * Get PDU port type based on position in cycle\n * PDU cycle: [8xC13, 2xC19] = 10 ports per cycle\n */\nexport function getPDUPortType(portIndex: number): string {\n  const cyclePosition = portIndex % 10; // 10 ports per cycle\n  return cyclePosition < 8 ? 'C13' : 'C19';\n}\n\n/**\n * Connector designation of a vertical-PDU outlet at a 1-based `position`.\n *\n * Prefers the strip's explicit `data.portTypes` sequence and falls back to the\n * default 8×C13 + 2×C19 cycle — the same precedence the renderer uses, so the\n * connector a rule reasons about is the connector that was drawn.\n * @public\n */\nexport function getPduOutletConnectorType(\n  portTypes: ReadonlyArray<string | undefined> | undefined,\n  position: number\n): string {\n  const index = Math.max(0, Math.floor(position) - 1);\n  const explicit = portTypes?.[index];\n  if (typeof explicit === 'string' && explicit.trim().length > 0) {\n    return explicit.trim().toUpperCase();\n  }\n  return getPDUPortType(index);\n}\n\n/**\n * Zero-based phase index of a PDU outlet at a 1-based `position` on an\n * `phases`-phase strip.\n *\n * Convention: outlets are attributed to phases **round-robin from the top** —\n * `(position - 1) % phases`. Real strips vary (some group outlets in per-phase\n * banks), but round-robin is the convention the consumer application uses, and\n * a phase-balance number is only meaningful if both sides attribute outlets the\n * same way. A non-positive or non-finite `phases` is read as single-phase, for\n * which every outlet is phase 0.\n * @public\n */\nexport function toOutletPhaseIndex(position: number, phases: number): number {\n  const count =\n    Number.isFinite(phases) && phases >= 1 ? Math.floor(phases) : 1;\n  const slot = Number.isFinite(position) ? Math.floor(position) : 1;\n  return (((slot - 1) % count) + count) % count;\n}\n\n/** Phase labels in round-robin order, indexed by {@link toOutletPhaseIndex}. */\nexport const PHASE_LABELS = ['L1', 'L2', 'L3'] as const;\n\n/**\n * Phase label (`L1`/`L2`/`L3`) of a PDU outlet at a 1-based `position`, or\n * `undefined` on a single-phase strip (where there is nothing to attribute) or\n * beyond the three labelled phases.\n * @public\n */\nexport function getPduOutletPhaseLabel(\n  position: number,\n  phases: number\n): (typeof PHASE_LABELS)[number] | undefined {\n  if (!Number.isFinite(phases) || phases < 2) return undefined;\n  return PHASE_LABELS[toOutletPhaseIndex(position, phases)];\n}\n\n/**\n * Get device connector type based on device name\n */\nexport function getDeviceConnectorType(deviceName: string): string {\n  const name = deviceName.toLowerCase();\n  \n  // High power devices use C20\n  if (name.includes('mx304') || name.includes('mx480') || name.includes('mx960')) {\n    return 'C20';\n  }\n  \n  // Standard devices use C14\n  return 'C14';\n}\n","import type {\n  DevicePort,\n  DeviceFace,\n  NetworkNode,\n  Port,\n  PortBlock,\n  PortPosition,\n} from '../types';\nimport { isPowerPortType } from './handle-ids';\nimport { normalizePortPositionInput } from './port-position';\n\ntype IndexedPort = {\n  index: number;\n  port: DevicePort;\n  position: PortPosition | undefined;\n};\n\ntype GroupedPortBucket = {\n  face: DeviceFace;\n  groupName: string;\n  entries: IndexedPort[];\n};\n\nconst DEFAULT_FRONT_GROUP_NAME = 'Network Ports';\nconst DEFAULT_REAR_GROUP_NAME = 'Rear Network Ports';\nconst DEFAULT_POWER_GROUP_NAME = 'Power Ports';\nconst DISTRIBUTED_OFFSET_MIN_PERCENT = 8;\nconst DISTRIBUTED_OFFSET_MAX_PERCENT = 92;\n\nfunction clampPercent(value: number): number {\n  return Math.max(0, Math.min(100, value));\n}\n\nfunction normalizePortAnchor(value: PortPosition['side'] | undefined): PortPosition['side'] | undefined {\n  if (value === 'top' || value === 'right' || value === 'bottom' || value === 'left') {\n    return value;\n  }\n  return undefined;\n}\n\nfunction getNormalizedPortPosition(position: unknown): PortPosition | undefined {\n  return normalizePortPositionInput(position);\n}\n\nfunction hasExplicitAbsoluteCoordinates(position: PortPosition | undefined): boolean {\n  return (\n    typeof position?.xPercent === 'number' &&\n    typeof position?.yPercent === 'number'\n  );\n}\n\nfunction hasExplicitOffset(position: PortPosition | undefined): boolean {\n  return (\n    typeof position?.offsetPercent === 'number' ||\n    typeof position?.offsetPx === 'number'\n  );\n}\n\nfunction resolveRenderPortFace(port: DevicePort): DeviceFace {\n  if (port.face === 'rear' || port.face === 'back') {\n    return 'rear';\n  }\n  if (port.face === 'front') {\n    return 'front';\n  }\n  return isPowerPortType(port.type) ? 'rear' : 'front';\n}\n\nfunction resolveRenderPortGroupName(port: DevicePort, face: DeviceFace): string {\n  if (typeof port.group === 'string' && port.group.trim().length > 0) {\n    return port.group.trim();\n  }\n  if (isPowerPortType(port.type)) {\n    return DEFAULT_POWER_GROUP_NAME;\n  }\n  return face === 'rear' ? DEFAULT_REAR_GROUP_NAME : DEFAULT_FRONT_GROUP_NAME;\n}\n\nfunction getEntrySortValue(entry: IndexedPort): number {\n  return typeof entry.port.order === 'number' && Number.isFinite(entry.port.order)\n    ? entry.port.order\n    : entry.index;\n}\n\nfunction toDistributedOffsetPercent(index: number, count: number): number {\n  if (count <= 1) {\n    return 50;\n  }\n  return (\n    DISTRIBUTED_OFFSET_MIN_PERCENT +\n    (index * (DISTRIBUTED_OFFSET_MAX_PERCENT - DISTRIBUTED_OFFSET_MIN_PERCENT)) / (count - 1)\n  );\n}\n\nfunction buildDistributedOffsetMap(entries: IndexedPort[]): Map<number, number> {\n  const distributedEntries = entries.filter((entry) => {\n    const anchor = normalizePortAnchor(entry.position?.side);\n    return !!anchor && !hasExplicitAbsoluteCoordinates(entry.position) && !hasExplicitOffset(entry.position);\n  });\n  const offsetPercentByIndex = new Map<number, number>();\n  const entriesByAnchor = new Map<NonNullable<PortPosition['side']>, IndexedPort[]>();\n\n  distributedEntries.forEach((entry) => {\n    const anchor = normalizePortAnchor(entry.position?.side);\n    if (!anchor) {\n      return;\n    }\n    const group = entriesByAnchor.get(anchor);\n    if (group) {\n      group.push(entry);\n      return;\n    }\n    entriesByAnchor.set(anchor, [entry]);\n  });\n\n  entriesByAnchor.forEach((anchorEntries) => {\n    const sortedEntries = [...anchorEntries].sort((left, right) => {\n      const orderDifference = getEntrySortValue(left) - getEntrySortValue(right);\n      if (orderDifference !== 0) {\n        return orderDifference;\n      }\n      return left.index - right.index;\n    });\n    sortedEntries.forEach((entry, index) => {\n      offsetPercentByIndex.set(entry.index, toDistributedOffsetPercent(index, sortedEntries.length));\n    });\n  });\n\n  return offsetPercentByIndex;\n}\n\nfunction toPortPosition(\n  normalizedPosition: PortPosition | undefined,\n  distributedOffsetPercent?: number,\n): PortPosition | undefined {\n  const anchor = normalizePortAnchor(normalizedPosition?.side);\n  if (hasExplicitAbsoluteCoordinates(normalizedPosition)) {\n    const position: PortPosition = {\n      xPercent: clampPercent(normalizedPosition?.xPercent as number),\n      yPercent: clampPercent(normalizedPosition?.yPercent as number),\n    };\n    if (typeof normalizedPosition?.widthPercent === 'number' && Number.isFinite(normalizedPosition.widthPercent)) {\n      position.widthPercent = clampPercent(normalizedPosition.widthPercent);\n    }\n    if (typeof normalizedPosition?.heightPercent === 'number' && Number.isFinite(normalizedPosition.heightPercent)) {\n      position.heightPercent = clampPercent(normalizedPosition.heightPercent);\n    }\n    if (anchor) {\n      position.side = anchor;\n    }\n    return position;\n  }\n\n  if (!anchor) {\n    return undefined;\n  }\n\n  const position: PortPosition = { side: anchor };\n  if (typeof normalizedPosition?.offsetPx === 'number' && Number.isFinite(normalizedPosition.offsetPx)) {\n    position.offsetPx = normalizedPosition.offsetPx;\n    return position;\n  }\n\n  const explicitOffsetPercent =\n    typeof normalizedPosition?.offsetPercent === 'number' && Number.isFinite(normalizedPosition.offsetPercent)\n      ? clampPercent(normalizedPosition.offsetPercent)\n      : undefined;\n  const resolvedOffsetPercent =\n    explicitOffsetPercent ?? (typeof distributedOffsetPercent === 'number'\n      ? clampPercent(distributedOffsetPercent)\n      : undefined);\n\n  if (typeof resolvedOffsetPercent === 'number') {\n    position.offsetPercent = resolvedOffsetPercent;\n  }\n\n  return position;\n}\n\nfunction toPort(entry: IndexedPort, distributedOffsetPercent?: number): Port {\n  const position = toPortPosition(entry.position, distributedOffsetPercent);\n  return {\n    id: entry.port.id,\n    label: entry.port.label,\n    sourceTypeLabel: entry.port.sourceTypeLabel,\n    connected: entry.port.connected ?? false,\n    type: entry.port.type,\n    cableType: entry.port.cableType,\n    connectorType: entry.port.connectorType,\n    compatibility: Array.isArray(entry.port.compatibility) ? [...entry.port.compatibility] : undefined,\n    ...(position ? { position } : {}),\n  };\n}\n\nfunction normalizePortBlockCollection(portBlocks: PortBlock[]): PortBlock[] {\n  const normalizedBlocks = portBlocks.map((block) => ({\n    ...block,\n    ports: block.ports.map((port) => {\n      const position = getNormalizedPortPosition(port.position);\n      return {\n        ...port,\n        ...(position ? { position } : { position: undefined }),\n      };\n    }),\n  }));\n\n  // A side without an offset is an automatic placement request. Distribute it\n  // across the whole face collection rather than restarting inside each block.\n  const portsByUnpositionedSide = new Map<\n    NonNullable<PortPosition['side']>,\n    Port[]\n  >();\n  normalizedBlocks.forEach((block) => {\n    block.ports.forEach((port) => {\n      const position = port.position as PortPosition | undefined;\n      const side = normalizePortAnchor(position?.side);\n      if (\n        !side ||\n        hasExplicitAbsoluteCoordinates(position) ||\n        hasExplicitOffset(position)\n      ) {\n        return;\n      }\n      const ports = portsByUnpositionedSide.get(side);\n      if (ports) {\n        ports.push(port);\n      } else {\n        portsByUnpositionedSide.set(side, [port]);\n      }\n    });\n  });\n\n  // These are owned output ports from the normalization pass above. Fill in\n  // automatic offsets in place instead of cloning every block a second time.\n  portsByUnpositionedSide.forEach((ports) => {\n    ports.forEach((port, index) => {\n      port.position = {\n        ...(port.position as PortPosition),\n        offsetPercent: toDistributedOffsetPercent(index, ports.length),\n      };\n    });\n  });\n\n  return normalizedBlocks;\n}\n\nfunction isGroupedPortBlockArray(value: unknown): value is PortBlock[] {\n  return (\n    Array.isArray(value) &&\n    value.length > 0 &&\n    value.every(\n      (entry) =>\n        entry !== null &&\n        typeof entry === 'object' &&\n        typeof (entry as PortBlock).name === 'string' &&\n        Array.isArray((entry as PortBlock).ports),\n    )\n  );\n}\n\nfunction isFlatDevicePortArray(value: unknown): value is DevicePort[] {\n  return (\n    Array.isArray(value) &&\n    value.length > 0 &&\n    value.every(\n      (entry) =>\n        entry !== null &&\n        typeof entry === 'object' &&\n        typeof (entry as DevicePort).label === 'string' &&\n        !(\n          typeof (entry as { name?: unknown }).name === 'string' &&\n          Array.isArray((entry as { ports?: unknown }).ports)\n        ),\n    )\n  );\n}\n\n/**\n * Normalize flat `DevicePort[]` input into explicit front/rear grouped blocks.\n * This is the bridge between the recommended simple port schema and the\n * lower-level grouped renderer input.\n * @public\n */\nexport function buildPortBlocksFromPorts(\n  devicePorts: DevicePort[],\n): { ports: PortBlock[]; rearPorts: PortBlock[] } {\n  const bucketsByKey = new Map<string, GroupedPortBucket>();\n  // Normalize each accepted port once. The placement, grouping and output\n  // passes share this representation rather than repeatedly parsing aliases.\n  const indexedPorts: IndexedPort[] = [];\n  devicePorts.forEach((port, index) => {\n    if (typeof port.label !== 'string' || port.label.trim().length === 0) return;\n    indexedPorts.push({\n      index,\n      port: { ...port, label: port.label.trim() },\n      position: getNormalizedPortPosition(port.position),\n    });\n  });\n  const distributedOffsetsByFace = {\n    front: buildDistributedOffsetMap(\n      indexedPorts.filter(({ port }) => resolveRenderPortFace(port) === 'front'),\n    ),\n    rear: buildDistributedOffsetMap(\n      indexedPorts.filter(({ port }) => resolveRenderPortFace(port) === 'rear'),\n    ),\n  };\n\n  indexedPorts.forEach((entry) => {\n    const face = resolveRenderPortFace(entry.port);\n    const groupName = resolveRenderPortGroupName(entry.port, face);\n    const key = `${face}:${groupName}`;\n    const existingBucket = bucketsByKey.get(key);\n    if (existingBucket) {\n      existingBucket.entries.push(entry);\n    } else {\n      bucketsByKey.set(key, { face, groupName, entries: [entry] });\n    }\n  });\n\n  const ports: PortBlock[] = [];\n  const rearPorts: PortBlock[] = [];\n\n  bucketsByKey.forEach((bucket) => {\n    const sortedEntries = [...bucket.entries].sort((left, right) => {\n      const orderDifference = getEntrySortValue(left) - getEntrySortValue(right);\n      if (orderDifference !== 0) {\n        return orderDifference;\n      }\n      return left.index - right.index;\n    });\n    const distributedOffsetByIndex = distributedOffsetsByFace[bucket.face];\n    const block: PortBlock = {\n      name: bucket.groupName,\n      ports: sortedEntries.map((entry) =>\n        toPort(entry, distributedOffsetByIndex.get(entry.index)),\n      ),\n    };\n    if (bucket.face === 'rear') {\n      rearPorts.push(block);\n      return;\n    }\n    ports.push(block);\n  });\n\n  return { ports, rearPorts };\n}\n\n/**\n * Node-data subset accepted by {@link getResolvedPortBlocks}: a flat\n * `DevicePort[]` on `ports`, grouped `PortBlock[]` on\n * `ports`/`portGroups`/`rearPortGroups`, or a `rearPorts` block fallback.\n * @public\n */\nexport type ResolvedPortBlockInput = Pick<\n  NetworkNode['data'],\n  'ports' | 'portGroups' | 'rearPortGroups'\n> & {\n  rearPorts?: PortBlock[];\n};\n\n/**\n * Resolve the grouped front/rear port blocks from any supported input form:\n * flat `ports: DevicePort[]`, grouped `PortBlock[]`, or explicit\n * `portGroups` / `rearPortGroups`.\n * @public\n */\nexport function getResolvedPortBlocks(\n  data: ResolvedPortBlockInput | undefined,\n): { ports: PortBlock[]; rearPorts: PortBlock[] } {\n  if (isGroupedPortBlockArray(data?.ports)) {\n    return {\n      ports: normalizePortBlockCollection(data.ports),\n      rearPorts: normalizePortBlockCollection(\n        Array.isArray(data?.rearPortGroups)\n          ? data.rearPortGroups\n          : isGroupedPortBlockArray(data?.rearPorts)\n            ? data.rearPorts\n            : [],\n      ),\n    };\n  }\n\n  if (isFlatDevicePortArray(data?.ports)) {\n    return buildPortBlocksFromPorts(data.ports);\n  }\n\n  return {\n    ports: normalizePortBlockCollection(Array.isArray(data?.portGroups) ? data.portGroups : []),\n    rearPorts: normalizePortBlockCollection(\n      Array.isArray(data?.rearPortGroups)\n        ? data.rearPortGroups\n        : isGroupedPortBlockArray(data?.rearPorts)\n          ? data.rearPorts\n          : [],\n    ),\n  };\n}\n\n/**\n * Resolve the front-facing grouped port blocks for a node/device data object.\n * @public\n */\nexport function getFrontPortBlocks(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): PortBlock[] {\n  return getResolvedPortBlocks(data).ports;\n}\n\n/**\n * Resolve the rear-facing grouped port blocks for a node/device data object.\n * @public\n */\nexport function getRearPortBlocks(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): PortBlock[] {\n  return getResolvedPortBlocks(data).rearPorts;\n}\n\nexport function countPortBlocks(portBlocks: PortBlock[] | undefined): number {\n  if (!Array.isArray(portBlocks)) {\n    return 0;\n  }\n\n  return portBlocks.reduce((total, block) => {\n    return total + (Array.isArray(block.ports) ? block.ports.length : 0);\n  }, 0);\n}\n\n/**\n * Determine whether a node/device data object resolves to any front ports.\n * @public\n */\nexport function hasFrontPorts(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): boolean {\n  return countPortBlocks(getResolvedPortBlocks(data).ports) > 0;\n}\n\n/**\n * Determine whether a node/device data object resolves to any rear ports.\n * @public\n */\nexport function hasRearPorts(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): boolean {\n  return countPortBlocks(getResolvedPortBlocks(data).rearPorts) > 0;\n}\n\n/**\n * Choose the default device face based on the resolved front/rear port data.\n * @public\n */\nexport function getDefaultDeviceFace(\n  data: Pick<NetworkNode['data'], 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n): DeviceFace {\n  const hasFront = hasFrontPorts(data);\n  const hasRear = hasRearPorts(data);\n  return hasRear && !hasFront ? 'rear' : 'front';\n}\n","import type {\n  DevicePort,\n  DiagramDocument,\n  DiagramEdge,\n  DiagramEdgeEndpoint,\n  DiagramEndpoint,\n  DiagramExternalRef,\n  DiagramNode,\n  DiagramNodeKind,\n  DiagramNodePlacement,\n  DiagramNodeRender,\n  DiagramUiState,\n  NetworkEdge,\n  NetworkNode,\n  NetworkNodeType,\n  Port,\n  PortBlock,\n  PortPosition,\n  PortPositionInput,\n  PortType,\n} from '../types';\nimport {\n  applyFaceAliasHandleId,\n  buildBreakerWayHandleId,\n  buildPortHandleId,\n  isPowerPortType,\n  normalizeDirectionalHandleId,\n  normalizeHandleId,\n  parseHandleId,\n} from './handle-ids';\nimport { normalizePortPositionInput } from './port-position';\nimport { getPDUPortType } from './power-connectors';\nimport { getResolvedPortBlocks } from './render-ports';\n\nexport interface NetworkGraphSnapshot {\n  nodes: NetworkNode[];\n  edges: NetworkEdge[];\n}\n\n/**\n * Hard ceiling on how many power outlets a PDU may allocate. A physical PDU has\n * at most a few dozen outlets; this generous cap keeps a hostile persisted\n * `powerPorts` (e.g. `2_000_000_000`) from triggering an unbounded\n * `Array.from({ length })` allocation on either the serialize path here or the\n * client render in {@link file://../components/VerticalPDU.tsx}. Both sinks\n * share this single constant.\n */\nexport const MAX_POWER_PORTS = 256;\n\n/**\n * Lenient input accepted by {@link diagramDocumentToNetworkGraph}. Everything is\n * optional so partial / legacy documents degrade gracefully instead of throwing:\n * a missing `version` is treated as legacy v0 (identical to `1`), and missing\n * `nodes`/`edges` default to empty. A full {@link DiagramDocument} is assignable.\n */\nexport type DiagramDocumentInput = {\n  version?: number;\n  nodes?: DiagramNode[];\n  edges?: DiagramEdge[];\n  ui?: DiagramUiState;\n  metadata?: Record<string, unknown>;\n};\n\nconst KNOWN_DIAGRAM_NODE_KINDS: ReadonlySet<DiagramNodeKind> = new Set<DiagramNodeKind>([\n  'rack',\n  'equipment',\n  'pdu',\n  'distribution-board',\n  'fiber',\n  'tube',\n  'cable',\n  'multi-tube-cable',\n  'splice',\n  'coupler',\n  'splitter',\n  'container',\n  'fibre-flow-cable',\n  'fibre-flow-closure',\n]);\n\nconst CONTAINER_SUBTYPES = new Set(['patch-panel', 'closure', 'splice-tray']);\nconst IMAGE_RENDER_KEYS = [\n  'frontImageUrl',\n  'rearImageUrl',\n  'frontImageAttribution',\n  'rearImageAttribution',\n  'imageFit',\n  'imagePosition',\n  'imageRepeat',\n  'stretchToFit',\n] as const;\nconst DIMENSION_RENDER_KEYS = [\n  'panelWidth',\n  'panelHeight',\n  'closureWidth',\n  'closureHeight',\n  'headerHeight',\n] as const;\nconst GENERAL_RENDER_KEYS = [\n  'status',\n  'width',\n  'widthPx',\n  'height',\n  'header',\n  'color',\n  'colorMode',\n  'showUMarkers',\n  'customHeaderText',\n  'powerSide',\n  'rotation',\n  'rackWidthPx',\n  'rackFaceGapPx',\n] as const;\nconst UI_NODE_STATE_KEYS = ['face', 'handleSideMap'] as const;\nconst EXCLUDED_CONFIG_KEYS = new Set<string>([\n  'label',\n  'ports',\n  'portGroups',\n  'rearPortGroups',\n  // `rearPorts` is a documented alias for `rearPortGroups` (resolved by\n  // getResolvedPortBlocks). Exclude it too, else a rearPorts-only node leaks its\n  // whole port array into the opaque config blob.\n  'rearPorts',\n  'pduPortGroups',\n  'connections',\n  'netboxId',\n  'netboxType',\n  'inventoryId',\n  'inventoryType',\n  'uPosition',\n  'mountMode',\n  'rackLayout',\n  'slotPosition',\n  'holderPosition',\n  // Derived on the reverse pass from placement.rack; never re-persist it into\n  // config or round-trips accumulate a stale copy.\n  'isRackMounted',\n  ...IMAGE_RENDER_KEYS,\n  ...DIMENSION_RENDER_KEYS,\n  ...GENERAL_RENDER_KEYS,\n  ...UI_NODE_STATE_KEYS,\n]);\n\n/**\n * Assign an own, enumerable property — safe for keys such as `__proto__` that\n * plain assignment would treat as a prototype mutation.\n */\nfunction setOwn(target: Record<string, unknown>, key: string, value: unknown): void {\n  Object.defineProperty(target, key, { value, enumerable: true, writable: true, configurable: true });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction clonePosition(position: { x?: number; y?: number } | undefined): { x: number; y: number } {\n  // Number.isFinite rejects NaN/Infinity (which typeof reports as 'number'),\n  // so a malformed coordinate degrades to 0 instead of corrupting layout.\n  return {\n    x: Number.isFinite(position?.x) ? (position!.x as number) : 0,\n    y: Number.isFinite(position?.y) ? (position!.y as number) : 0,\n  };\n}\n\nfunction clonePortPosition(position: PortPositionInput | undefined): PortPosition | undefined {\n  const normalized = normalizePortPositionInput(position);\n  if (!normalized) {\n    return undefined;\n  }\n\n  return { ...normalized };\n}\n\nfunction toDiagramNodeKind(node: NetworkNode): { kind: DiagramNodeKind; subtype?: string } {\n  if (node.type === 'vertical-pdu') {\n    return { kind: 'pdu' };\n  }\n\n  if (node.type === 'server' || node.type === 'switch' || node.type === 'router' || node.type === 'device') {\n    // `subtype` carries the runtime node TYPE so the reverse pass restores it\n    // exactly. The free-text `data.deviceType` (often a vendor model, e.g.\n    // `Catalyst 9300`) travels in `config` like any other data field — using it\n    // as the subtype made a `switch` with a model name come back as `device`.\n    return { kind: 'equipment', subtype: node.type };\n  }\n\n  if (node.type === 'patch-panel' || node.type === 'closure' || node.type === 'splice-tray') {\n    return { kind: 'container', subtype: node.type };\n  }\n\n  if (node.type === 'fibre-split') {\n    return { kind: 'splitter' };\n  }\n\n  if (node.type === 'fibre-flow-cable' || node.type === 'fibre-flow-closure') {\n    return { kind: node.type };\n  }\n\n  return { kind: node.type as DiagramNodeKind };\n}\n\nfunction toLegacyNodeType(node: DiagramNode): NetworkNodeType {\n  if (node.kind === 'equipment') {\n    const normalizedSubtype = (node.subtype ?? 'device').toLowerCase().replace(/_/g, '-');\n    if (normalizedSubtype === 'server' || normalizedSubtype === 'switch' || normalizedSubtype === 'router') {\n      return normalizedSubtype;\n    }\n    return 'device';\n  }\n\n  if (node.kind === 'pdu') {\n    return 'vertical-pdu';\n  }\n\n  if (node.kind === 'container') {\n    const subtype = node.subtype ?? 'patch-panel';\n    if (CONTAINER_SUBTYPES.has(subtype)) {\n      return subtype as NetworkNodeType;\n    }\n    return 'patch-panel';\n  }\n\n  if (node.kind === 'splitter') {\n    return 'fibre-split';\n  }\n\n  return node.kind as NetworkNodeType;\n}\n\nfunction toDiagramPlacement(node: NetworkNode): DiagramNodePlacement {\n  const placement: DiagramNodePlacement = {\n    position: clonePosition(node.position),\n  };\n\n  const parentId = node.parentId;\n  if (parentId) {\n    placement.parentId = parentId;\n  }\n  if (node.extent === 'parent') {\n    placement.extent = 'parent';\n  }\n\n  const data = node.data ?? {};\n  if (\n    typeof data.uPosition === 'number' ||\n    data.mountMode === 'front' ||\n    data.mountMode === 'rear' ||\n    data.mountMode === 'both' ||\n    data.rackLayout === 'single-face' ||\n    data.rackLayout === 'split-face'\n  ) {\n    placement.rack = {\n      ...(typeof data.uPosition === 'number' ? { uPosition: data.uPosition } : {}),\n      ...(data.mountMode ? { mountMode: data.mountMode } : {}),\n      ...(data.rackLayout ? { rackLayout: data.rackLayout } : {}),\n      ...(typeof data.rackWidthPx === 'number' ? { rackWidthPx: data.rackWidthPx } : {}),\n      ...(typeof data.rackFaceGapPx === 'number' ? { rackFaceGapPx: data.rackFaceGapPx } : {}),\n    };\n  }\n\n  if (typeof data.slotPosition === 'number' || typeof data.holderPosition === 'number') {\n    placement.container = {\n      ...(typeof data.slotPosition === 'number' ? { slotPosition: data.slotPosition } : {}),\n      ...(typeof data.holderPosition === 'number' ? { holderPosition: data.holderPosition } : {}),\n    };\n  }\n\n  return placement;\n}\n\nfunction toDiagramRender(node: NetworkNode): DiagramNodeRender | undefined {\n  const data = node.data ?? {};\n  const render: DiagramNodeRender = {};\n\n  GENERAL_RENDER_KEYS.forEach((key) => {\n    const value = data[key];\n    if (value !== undefined) {\n      (render as Record<string, unknown>)[key] = value;\n    }\n  });\n\n  const images = IMAGE_RENDER_KEYS.reduce<Record<string, unknown>>((acc, key) => {\n    const value = data[key];\n    if (value !== undefined) {\n      acc[key] = value;\n    }\n    return acc;\n  }, {});\n  if (Object.keys(images).length > 0) {\n    render.images = images as DiagramNodeRender['images'];\n  }\n\n  const dimensions = DIMENSION_RENDER_KEYS.reduce<Record<string, number | undefined>>((acc, key) => {\n    const value = data[key];\n    if (typeof value === 'number') {\n      acc[key] = value;\n    }\n    return acc;\n  }, {});\n  if (Object.keys(dimensions).length > 0) {\n    render.dimensions = dimensions;\n  }\n\n  return Object.keys(render).length > 0 ? render : undefined;\n}\n\nfunction toDiagramExternalRefs(node: NetworkNode): DiagramExternalRef[] | undefined {\n  const refs: DiagramExternalRef[] = [];\n  const data = node.data ?? {};\n\n  if (data.netboxId !== undefined) {\n    refs.push({\n      source: 'netbox',\n      id: data.netboxId,\n      type: data.netboxType,\n    });\n  }\n\n  if (data.inventoryId !== undefined) {\n    refs.push({\n      source: 'inventory',\n      id: data.inventoryId,\n      type: data.inventoryType,\n    });\n  }\n\n  return refs.length > 0 ? refs : undefined;\n}\n\nfunction toDiagramConfig(node: NetworkNode): Record<string, unknown> | undefined {\n  const data = node.data ?? {};\n  const config: Record<string, unknown> = {};\n\n  Object.entries(data).forEach(([key, value]) => {\n    // `ports` is excluded because port ARRAYS serialize as endpoints. A numeric\n    // `ports` (the rack header's port count) has no endpoint form, so it is\n    // kept as plain config instead of being dropped.\n    if (EXCLUDED_CONFIG_KEYS.has(key) && !(key === 'ports' && !Array.isArray(value))) {\n      return;\n    }\n    if ((key === 'rackWidthPx' || key === 'rackFaceGapPx') && node.type !== 'rack') {\n      return;\n    }\n    setOwn(config, key, value);\n  });\n\n  // Equipment without a device type is normalised to `deviceType = node.type`,\n  // the value the reverse pass has always filled in. Writing it here keeps\n  // re-serialisation idempotent (graph → doc → graph → doc is stable).\n  if (\n    (node.type === 'server' || node.type === 'switch' || node.type === 'router' || node.type === 'device') &&\n    !(typeof config.deviceType === 'string' && config.deviceType.trim().length > 0)\n  ) {\n    config.deviceType = node.type;\n  }\n\n  return Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction extractUiState(nodes: NetworkNode[], previousUi?: DiagramUiState): DiagramUiState | undefined {\n  const nodeState: NonNullable<DiagramUiState['nodeState']> = {};\n\n  nodes.forEach((node) => {\n    const data = node.data ?? {};\n    const entry: NonNullable<DiagramUiState['nodeState']>[string] = {};\n    const resolvedFace =\n      data.face === 'front' || data.face === 'rear'\n        ? data.face\n        : undefined;\n\n    if (resolvedFace) {\n      entry.face = resolvedFace;\n    }\n    if (data.handleSideMap && typeof data.handleSideMap === 'object') {\n      entry.handleSideMap = { ...data.handleSideMap };\n    }\n\n    if (Object.keys(entry).length > 0) {\n      setOwn(nodeState, node.id, entry);\n    }\n  });\n\n  if (Object.keys(nodeState).length === 0 && !previousUi?.metadata) {\n    return undefined;\n  }\n\n  return {\n    ...(Object.keys(nodeState).length > 0 ? { nodeState } : {}),\n    ...(previousUi?.metadata ? { metadata: previousUi.metadata } : {}),\n  };\n}\n\nfunction inferEndpointMedium(portType: string | undefined): DiagramEndpoint['medium'] {\n  const normalizedType = String(portType ?? '').toLowerCase();\n  if (normalizedType.includes('power')) return 'power';\n  if (normalizedType.includes('fiber')) return 'fiber';\n  if (normalizedType.includes('console') || normalizedType.includes('mgmt') || normalizedType.includes('usb')) {\n    return 'generic';\n  }\n  if (normalizedType.includes('ethernet') || normalizedType.includes('rj45')) return 'ethernet';\n  return 'generic';\n}\n\nfunction buildPduPortBlocks(node: NetworkNode): PortBlock[] {\n  const data = node.data ?? {};\n  if (Array.isArray(data.pduPortGroups) && data.pduPortGroups.length > 0) {\n    return data.pduPortGroups;\n  }\n\n  const { ports, rearPorts } = getResolvedPortBlocks(data);\n  if (rearPorts.length > 0) {\n    return rearPorts;\n  }\n  if (ports.length > 0) {\n    return ports;\n  }\n\n  const powerPorts = Math.min(\n    MAX_POWER_PORTS,\n    typeof data.powerPorts === 'number' && Number.isFinite(data.powerPorts) && data.powerPorts > 0\n      ? data.powerPorts\n      : 36\n  );\n\n  return [\n    {\n      name: 'Power',\n      ports: Array.from({ length: powerPorts }, (_, index) => {\n        const connectorType = getPDUPortType(index);\n        return {\n          id: `${index + 1}`,\n          label: `${index + 1}`,\n          connected: false,\n          type: connectorType === 'C19' ? 'power_c19' : 'power_c13',\n          connectorType: connectorType as Port['connectorType'],\n        };\n      }),\n    },\n  ];\n}\n\nfunction toPortBasedEndpoints(node: NetworkNode): DiagramEndpoint[] {\n  // Read the resolved blocks directly. Endpoint construction below already\n  // clones compatibility and normalized layout, so an intermediate deep clone\n  // of every block/port only duplicates work on each document round trip.\n  const blocksByFace: Array<{ face: 'front' | 'rear'; blocks: PortBlock[] }> =\n    node.type === 'vertical-pdu'\n      ? [{ face: 'rear', blocks: buildPduPortBlocks(node) }]\n      : (() => {\n          const { ports, rearPorts } = getResolvedPortBlocks(node.data);\n          return [\n            { face: 'front' as const, blocks: ports },\n            { face: 'rear' as const, blocks: rearPorts },\n          ];\n        })();\n\n  const endpoints: DiagramEndpoint[] = blocksByFace.flatMap(({ face, blocks }) =>\n    blocks.flatMap((block) =>\n      block.ports.map((port) => ({\n        // Mint the endpoint id through the same grammar the renderer mounts\n        // handles with ({@link buildPortHandleId}): an explicit `port.id`\n        // verbatim, a bare label for power ports, else a percent-escaped\n        // `<group>-<label>` join. Edges persist\n        // `endpointId = normalizeHandleId(edge.sourceHandle)` — i.e. this exact\n        // builder output — so a raw `${block.name}-${port.label}` join would\n        // diverge for any label/group containing `-` or `%` (or a label that\n        // collides with a fixed grammar shape, e.g. group `fiber` + label `3`),\n        // and the edge would dangle after a document round-trip. Building the id\n        // the same way keeps persisted endpoint ids and edge endpointIds equal\n        // by construction.\n        id: buildPortHandleId(block.name, port),\n        label: port.label,\n        face,\n        group: block.name,\n        role: isPowerPortType(port.type) ? 'power' : 'network',\n        medium: inferEndpointMedium(port.type),\n        type: port.type,\n        cableType: port.cableType,\n        connector: port.connectorType,\n        sourceTypeLabel: port.sourceTypeLabel,\n        compatibility: Array.isArray(port.compatibility) ? [...port.compatibility] : undefined,\n        layout: clonePortPosition(port.position),\n      }))\n    )\n  );\n\n  // A front and a rear port that resolve to the same handle id (same explicit\n  // `port.id`, or same group + label on both faces) are disambiguated by the\n  // renderer with the split-face alias: the rear handle mounts as\n  // `<id>--rear`. Mint the rear endpoint with that same alias so endpoint ids\n  // stay unique and an edge on the rear port keeps pointing at the rear port\n  // after a round trip (instead of collapsing onto the front one).\n  const frontIds = new Set(\n    endpoints.filter((endpoint) => endpoint.face === 'front').map((endpoint) => endpoint.id)\n  );\n  if (frontIds.size > 0) {\n    for (const endpoint of endpoints) {\n      if (endpoint.face === 'rear' && frontIds.has(endpoint.id)) {\n        endpoint.id = applyFaceAliasHandleId(endpoint.id, 'rear');\n      }\n    }\n  }\n  return endpoints;\n}\n\n/**\n * Whether `endpoint` is the rear twin of a front endpoint, minted by\n * {@link toPortBasedEndpoints} as `<frontId>--rear`. Returns the shared base id\n * (what the port's own `id` is) when it is, else `undefined`.\n */\nfunction rearAliasBase(\n  endpoint: DiagramEndpoint,\n  frontIds: ReadonlySet<string>\n): string | undefined {\n  if (endpoint.face !== 'rear' || !endpoint.id.endsWith('--rear')) return undefined;\n  const base = endpoint.id.slice(0, -'--rear'.length);\n  return frontIds.has(base) ? base : undefined;\n}\n\nfunction collectFrontEndpointIds(endpoints: readonly DiagramEndpoint[]): Set<string> {\n  return new Set(\n    endpoints.filter((endpoint) => endpoint.face !== 'rear').map((endpoint) => endpoint.id)\n  );\n}\n\nfunction createSyntheticEndpoints(node: NetworkNode): DiagramEndpoint[] {\n  const data = node.data ?? {};\n\n  if (node.type === 'distribution-board') {\n    const ways = Array.isArray(data.ways) ? data.ways : [];\n    return ways\n      .filter((way): way is { id: string; label?: string } => !!way && typeof way.id === 'string')\n      .map((way) => ({\n        id: buildBreakerWayHandleId(way.id),\n        label: typeof way.label === 'string' ? way.label : way.id,\n        role: 'power' as const,\n        medium: 'power' as const,\n      }));\n  }\n\n  if (node.type === 'tube') {\n    const fibersPerTube = Math.max(1, Math.min(24, Number(data.fibersPerTube) || 12));\n    const tubeIndex = Math.max(1, Number(data.tubeIndex) || 1);\n    return [\n      ...Array.from({ length: fibersPerTube }, (_, index) => ({\n        id: `fiber-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'fiber' as const,\n        medium: 'fiber' as const,\n      })),\n      {\n        id: `tube-${tubeIndex}`,\n        label: `Tube ${tubeIndex}`,\n        role: 'tube',\n        medium: 'tube',\n      },\n    ];\n  }\n\n  if (node.type === 'cable') {\n    const fiberCount = Math.max(1, Math.min(24, Number(data.fiberCount) || 12));\n    return [\n      ...Array.from({ length: fiberCount }, (_, index) => ({\n        id: `fiber-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'fiber' as const,\n        medium: 'fiber' as const,\n      })),\n      {\n        id: 'cable-button',\n        label: 'Cable',\n        role: 'cable',\n      },\n    ];\n  }\n\n  if (node.type === 'multi-tube-cable') {\n    const tubeCount = Math.max(1, Math.min(24, Number(data.tubeCount) || 12));\n    const tubeStartIndex = Math.max(1, Number(data.tubeStartIndex) || 1);\n    return [\n      ...Array.from({ length: tubeCount }, (_, index) => ({\n        id: `tube-${tubeStartIndex + index}`,\n        label: `${tubeStartIndex + index}`,\n        role: 'tube' as const,\n        medium: 'tube' as const,\n      })),\n      {\n        id: 'cable-button',\n        label: 'Cable',\n        role: 'cable',\n      },\n    ];\n  }\n\n  if (node.type === 'splice') {\n    return [\n      { id: 'left', label: 'Left', role: 'splice', medium: 'fiber' },\n      { id: 'right', label: 'Right', role: 'splice', medium: 'fiber' },\n    ];\n  }\n\n  if (node.type === 'coupler') {\n    const portCount = typeof data.couplerFiberCount === 'number' && [1, 2, 4].includes(data.couplerFiberCount)\n      ? data.couplerFiberCount\n      : 2;\n    return [\n      ...Array.from({ length: portCount }, (_, index) => ({\n        id: `left-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'coupler' as const,\n        medium: 'fiber' as const,\n      })),\n      ...Array.from({ length: portCount }, (_, index) => ({\n        id: `right-${index + 1}`,\n        label: `${index + 1}`,\n        role: 'coupler' as const,\n        medium: 'fiber' as const,\n      })),\n    ];\n  }\n\n  if (node.type === 'fibre-split') {\n    const inputCount = Math.max(1, Math.min(2, Number(data.inputCount) || 1));\n    const outputCount = Math.max(1, Math.min(64, Number(data.outputCount) || 8));\n    return [\n      ...Array.from({ length: inputCount }, (_, index) => ({\n        id: `input-${index + 1}`,\n        label: `Input ${index + 1}`,\n        role: 'splitter' as const,\n        medium: 'fiber' as const,\n      })),\n      ...Array.from({ length: outputCount }, (_, index) => ({\n        id: `output-${index + 1}`,\n        label: `Output ${index + 1}`,\n        role: 'splitter' as const,\n        medium: 'fiber' as const,\n      })),\n    ];\n  }\n\n  return [];\n}\n\nfunction toDiagramEndpoints(node: NetworkNode): DiagramEndpoint[] | undefined {\n  const endpoints = node.type === 'vertical-pdu' || Array.isArray(node.data?.ports) || Array.isArray(node.data?.portGroups) || Array.isArray(node.data?.rearPortGroups) || Array.isArray(node.data?.rearPorts) || Array.isArray(node.data?.pduPortGroups)\n    ? toPortBasedEndpoints(node)\n    : createSyntheticEndpoints(node);\n\n  return endpoints.length > 0 ? endpoints : undefined;\n}\n\n/**\n * `node.data` fields the endpoint derivation reads. The cache fingerprints the\n * identity (and, for arrays, the length) of each, so replacing or growing a\n * port collection — even on the same `data` object — invalidates the entry.\n */\nconst ENDPOINT_SOURCE_KEYS = [\n  'ports',\n  'portGroups',\n  'rearPortGroups',\n  'rearPorts',\n  'pduPortGroups',\n  'powerPorts',\n  'ways',\n  'fibersPerTube',\n  'tubeIndex',\n  'fiberCount',\n  'tubeCount',\n  'tubeStartIndex',\n  'couplerFiberCount',\n  'inputCount',\n  'outputCount',\n] as const;\n\nfunction endpointSourceFingerprint(data: Record<string, unknown>): unknown[] {\n  const fingerprint: unknown[] = [];\n  for (const key of ENDPOINT_SOURCE_KEYS) {\n    const value = data[key];\n    fingerprint.push(value, Array.isArray(value) ? value.length : -1);\n  }\n  return fingerprint;\n}\n\nfunction sameFingerprint(a: readonly unknown[], b: readonly unknown[]): boolean {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i += 1) {\n    if (!Object.is(a[i], b[i])) return false;\n  }\n  return true;\n}\n\n/**\n * One derivation of a node's endpoints, cached against the `node.data` object\n * that produced it. `nodeId`/`nodeType` are stored alongside so a data object\n * shared by two different nodes (a hazard, not the norm) is detected and\n * recomputed rather than aliased, and `fingerprint` catches an in-place\n * replacement of a port collection on the same data object.\n */\ninterface EndpointDerivation {\n  nodeId: string;\n  nodeType: NetworkNodeType;\n  fingerprint: unknown[];\n  endpoints: DiagramEndpoint[] | undefined;\n}\n\n/**\n * Module-scoped memo of {@link toDiagramEndpoints}, keyed on `node.data`\n * identity. A position-only drag reframes `node.position` but keeps `node.data`\n * referentially stable, so the (potentially large) endpoint derivation runs\n * once and later frames only copy the cached records. Keys are consumer\n * `node.data` objects, so entries die with them (no unbounded growth).\n *\n * Limitation: editing a port object's fields in place (e.g. `port.label = …`\n * on the same port object inside the same array) is not detected — replace\n * the port, its array, or `data` (the React immutable-update convention).\n */\nconst endpointDerivationCache = new WeakMap<object, EndpointDerivation>();\n\n/** Detached copy of an endpoint list so callers can never mutate the cache. */\nfunction copyEndpoints(endpoints: DiagramEndpoint[] | undefined): DiagramEndpoint[] | undefined {\n  return endpoints?.map((endpoint) => ({\n    ...endpoint,\n    ...(endpoint.compatibility ? { compatibility: [...endpoint.compatibility] } : {}),\n    ...(endpoint.layout ? { layout: { ...endpoint.layout } } : {}),\n  }));\n}\n\n/**\n * Memoized {@link toDiagramEndpoints}. Recomputes on any data-identity change,\n * on a port-collection change detected by the fingerprint, and when the same\n * data object surfaces under a different node id/type. Always returns a fresh\n * copy, so a consumer mutating an emitted document cannot corrupt later ones.\n * Nodes with no object `data` bypass the cache (nothing stable to key on).\n */\nfunction toDiagramEndpointsCached(node: NetworkNode): DiagramEndpoint[] | undefined {\n  const data: unknown = node.data;\n  if (!data || typeof data !== 'object') {\n    return toDiagramEndpoints(node);\n  }\n\n  const fingerprint = endpointSourceFingerprint(data as Record<string, unknown>);\n  const cached = endpointDerivationCache.get(data);\n  if (\n    cached &&\n    cached.nodeId === node.id &&\n    cached.nodeType === node.type &&\n    sameFingerprint(cached.fingerprint, fingerprint)\n  ) {\n    return copyEndpoints(cached.endpoints);\n  }\n\n  const endpoints = toDiagramEndpoints(node);\n  endpointDerivationCache.set(data, { nodeId: node.id, nodeType: node.type, fingerprint, endpoints });\n  return copyEndpoints(endpoints);\n}\n\nfunction mergeEndpoints(\n  previousEndpoints: DiagramEndpoint[] | undefined,\n  nextEndpoints: DiagramEndpoint[] | undefined\n): DiagramEndpoint[] | undefined {\n  if (!nextEndpoints || nextEndpoints.length === 0) {\n    return nextEndpoints;\n  }\n\n  const previousById = new Map((previousEndpoints ?? []).map((endpoint) => [endpoint.id, endpoint]));\n  return nextEndpoints.map((endpoint) => {\n    const previous = previousById.get(endpoint.id);\n    if (!previous) {\n      return endpoint;\n    }\n\n    return {\n      ...previous,\n      ...endpoint,\n      compatibility: endpoint.compatibility ?? previous.compatibility,\n      layout: endpoint.layout ?? previous.layout,\n    };\n  });\n}\n\nfunction mergeDocumentNode(previousNode: DiagramNode | undefined, nextNode: DiagramNode): DiagramNode {\n  if (!previousNode) {\n    return nextNode;\n  }\n\n  return {\n    ...previousNode,\n    ...nextNode,\n    placement: nextNode.placement,\n    render: nextNode.render,\n    config: nextNode.config,\n    externalRefs: nextNode.externalRefs,\n    metadata: nextNode.metadata ?? previousNode.metadata,\n    endpoints: mergeEndpoints(previousNode.endpoints, nextNode.endpoints),\n  };\n}\n\n/**\n * Convert a runtime {@link NetworkGraphSnapshot} into the canonical, persistable\n * {@link DiagramDocument}. The emitted document always carries `version: 1`.\n *\n * Container kinds (`patch-panel`/`closure`/`splice-tray`) serialize their ports\n * as face-tagged `endpoints`, which {@link diagramDocumentToNetworkGraph}\n * reconstructs back into `portGroups`/`rearPortGroups`.\n */\nexport function networkGraphToDiagramDocument(\n  snapshot: NetworkGraphSnapshot,\n  previousDocument?: DiagramDocument\n): DiagramDocument {\n  const previousNodesById = new Map((previousDocument?.nodes ?? []).map((node) => [node.id, node]));\n\n  const nodes = snapshot.nodes.map((node) =>\n    mergeDocumentNode(previousNodesById.get(node.id), {\n      id: node.id,\n      ...toDiagramNodeKind(node),\n      label: node.data?.label ?? node.id,\n      placement: toDiagramPlacement(node),\n      render: toDiagramRender(node),\n      config: toDiagramConfig(node),\n      externalRefs: toDiagramExternalRefs(node),\n      metadata: previousNodesById.get(node.id)?.metadata,\n      endpoints: toDiagramEndpointsCached(node),\n    })\n  );\n\n  // Per node: the endpoint ids that exist on both faces, i.e. whose rear twin\n  // was minted as `<id>--rear` (see toPortBasedEndpoints).\n  const aliasedIdsByNodeId = new Map<string, Set<string>>();\n  nodes.forEach((node) => {\n    const endpoints = node.endpoints ?? [];\n    const frontIds = collectFrontEndpointIds(endpoints);\n    const aliased = new Set<string>();\n    for (const endpoint of endpoints) {\n      const base = rearAliasBase(endpoint, frontIds);\n      if (base !== undefined) aliased.add(base);\n    }\n    if (aliased.size > 0) aliasedIdsByNodeId.set(node.id, aliased);\n  });\n\n  /**\n   * Endpoint id an edge handle persists as: the handle with its directional\n   * suffix and split-face alias stripped — except a `--rear` alias on a port id\n   * that exists on both faces, which names the rear twin endpoint.\n   */\n  const toEndpointId = (nodeId: string, handle: string | null | undefined): string | undefined => {\n    if (!handle) return undefined;\n    const normalized = normalizeHandleId(handle);\n    const aliased = aliasedIdsByNodeId.get(nodeId);\n    if (aliased?.has(normalized) && parseHandleId(normalizeDirectionalHandleId(handle)).faceAlias === 'rear') {\n      return applyFaceAliasHandleId(normalized, 'rear');\n    }\n    return normalized;\n  };\n\n  const previousEdgesById = new Map((previousDocument?.edges ?? []).map((edge) => [edge.id, edge]));\n  const edges = snapshot.edges.map((edge) => {\n    const nextEdge: DiagramEdge = {\n      id: edge.id,\n      source: {\n        nodeId: edge.source,\n        endpointId: toEndpointId(edge.source, edge.sourceHandle),\n      },\n      target: {\n        nodeId: edge.target,\n        endpointId: toEndpointId(edge.target, edge.targetHandle),\n      },\n      type: edge.type,\n      hidden: edge.hidden === true ? true : undefined,\n      deletable: typeof edge.deletable === 'boolean' ? edge.deletable : undefined,\n      interactionWidth:\n        typeof edge.interactionWidth === 'number' && Number.isFinite(edge.interactionWidth)\n          ? edge.interactionWidth\n          : undefined,\n      data: edge.data ? { ...edge.data } : undefined,\n      metadata: previousEdgesById.get(edge.id)?.metadata,\n    };\n\n    const previous = previousEdgesById.get(edge.id);\n    return previous ? { ...previous, ...nextEdge } : nextEdge;\n  });\n\n  return {\n    version: 1,\n    nodes,\n    edges,\n    ui: extractUiState(snapshot.nodes, previousDocument?.ui),\n    metadata: previousDocument?.metadata,\n  };\n}\n\n/**\n * Port type for a rebuilt port. An endpoint's own `type` wins. Without one,\n * only power endpoints get an inferred type (their connector family drives\n * power semantics); every other untyped endpoint stays untyped — inventing\n * `ethernet` changed the port's icon and compatibility on every load and made\n * re-serialisation non-idempotent.\n */\nfunction endpointToPortType(endpoint: DiagramEndpoint): PortType | undefined {\n  if (endpoint.type) {\n    return endpoint.type;\n  }\n\n  if (endpoint.role === 'power' || endpoint.medium === 'power') {\n    const connector = String(endpoint.connector ?? '').toLowerCase();\n    if (connector.includes('c19') || connector.includes('c20')) return 'power_c19';\n    if (connector.includes('c13') || connector.includes('c14')) return 'power_c13';\n    return 'power_ac';\n  }\n\n  // Legacy hand-authored documents that name a medium but no type.\n  if (endpoint.medium === 'fiber') return 'fiber';\n  if (endpoint.medium === 'ethernet') return 'ethernet';\n  return undefined;\n}\n\n/** Shared per-port fields of a rebuilt {@link DevicePort} / {@link Port}. */\nfunction endpointPortFields(\n  endpoint: DiagramEndpoint,\n  frontIds: ReadonlySet<string>,\n  connectedEndpointIds: ReadonlySet<string>\n) {\n  const type = endpointToPortType(endpoint);\n  return {\n    // The rear twin of a both-faces port id was persisted as `<id>--rear`; the\n    // port itself keeps the shared id (the renderer re-applies the alias).\n    id: rearAliasBase(endpoint, frontIds) ?? endpoint.id,\n    label: endpoint.label,\n    connected: connectedEndpointIds.has(endpoint.id),\n    ...(type ? { type } : {}),\n    cableType: endpoint.cableType,\n    connectorType: endpoint.connector as DevicePort['connectorType'],\n    sourceTypeLabel: endpoint.sourceTypeLabel,\n    compatibility:\n      endpoint.compatibility && endpoint.compatibility.length > 0\n        ? [...endpoint.compatibility]\n        : undefined,\n    position: clonePortPosition(endpoint.layout),\n  };\n}\n\nfunction endpointsToDevicePorts(\n  endpoints: DiagramEndpoint[],\n  frontIds: ReadonlySet<string>,\n  connectedEndpointIds: ReadonlySet<string>\n): DevicePort[] {\n  return endpoints.map((endpoint) => {\n    const fields = endpointPortFields(endpoint, frontIds, connectedEndpointIds);\n    return {\n      ...fields,\n      face: endpoint.face === 'rear' ? 'rear' : endpoint.face === 'front' ? 'front' : undefined,\n      group: endpoint.group,\n    } as DevicePort;\n  });\n}\n\nfunction endpointsToPortBlocks(\n  endpoints: DiagramEndpoint[],\n  frontIds: ReadonlySet<string>,\n  connectedEndpointIds: ReadonlySet<string>\n): PortBlock[] {\n  const grouped = new Map<string, Port[]>();\n\n  endpoints.forEach((endpoint) => {\n    const groupName = endpoint.group ?? 'Ports';\n    const port = endpointPortFields(endpoint, frontIds, connectedEndpointIds) as Port;\n    const existing = grouped.get(groupName);\n    if (existing) {\n      existing.push(port);\n      return;\n    }\n    grouped.set(groupName, [port]);\n  });\n\n  return [...grouped.entries()].map(([name, ports]) => ({ name, ports }));\n}\n\nfunction applyRenderToLegacyData(\n  data: NetworkNode['data'],\n  render: DiagramNodeRender | undefined,\n  uiEntry: NonNullable<DiagramUiState['nodeState']>[string] | undefined\n): NetworkNode['data'] {\n  if (!render && !uiEntry) {\n    return data;\n  }\n\n  const nextData = { ...data };\n  if (render) {\n    GENERAL_RENDER_KEYS.forEach((key) => {\n      const value = render[key];\n      if (value !== undefined) {\n        (nextData as Record<string, unknown>)[key] = value;\n      }\n    });\n\n    if (render.images) {\n      IMAGE_RENDER_KEYS.forEach((key) => {\n        const value = render.images?.[key];\n        if (value !== undefined) {\n          (nextData as Record<string, unknown>)[key] = value;\n        }\n      });\n    }\n\n    if (render.dimensions) {\n      DIMENSION_RENDER_KEYS.forEach((key) => {\n        const value = render.dimensions?.[key];\n        if (typeof value === 'number') {\n          (nextData as Record<string, unknown>)[key] = value;\n        }\n      });\n    }\n  }\n\n  const resolvedFace = uiEntry?.face;\n  if (resolvedFace) {\n    nextData.face = resolvedFace;\n  }\n  if (uiEntry?.handleSideMap) {\n    nextData.handleSideMap = { ...uiEntry.handleSideMap };\n  }\n\n  return nextData;\n}\n\nfunction applyExternalRefsToLegacyData(data: NetworkNode['data'], refs: DiagramExternalRef[] | undefined): NetworkNode['data'] {\n  if (!refs || refs.length === 0) {\n    return data;\n  }\n\n  const nextData = { ...data };\n  refs.forEach((ref) => {\n    if (ref.source === 'netbox') {\n      nextData.netboxId = ref.id;\n      if (ref.type) {\n        nextData.netboxType = ref.type;\n      }\n    }\n    if (ref.source === 'inventory') {\n      nextData.inventoryId = ref.id;\n      if (ref.type) {\n        nextData.inventoryType = ref.type;\n      }\n    }\n  });\n\n  return nextData;\n}\n\n/**\n * Build a per-node `endpointId -> endpoint` index for O(1) resolution. First\n * write wins so the lookup mirrors `Array.prototype.find` (first match) exactly;\n * nodes with no endpoints are omitted. Computing this once before the edge loop\n * turns {@link resolveEndpointHandleId} from an O(edges x endpoints) linear scan\n * into O(edges + endpoints).\n */\nfunction buildEndpointIndex(nodes: DiagramNode[]): Map<string, Map<string, DiagramEndpoint>> {\n  const index = new Map<string, Map<string, DiagramEndpoint>>();\n  for (const node of nodes) {\n    if (!node.endpoints || node.endpoints.length === 0) {\n      continue;\n    }\n    const byId = new Map<string, DiagramEndpoint>();\n    for (const endpoint of node.endpoints) {\n      if (!byId.has(endpoint.id)) {\n        byId.set(endpoint.id, endpoint);\n      }\n    }\n    index.set(node.id, byId);\n  }\n  return index;\n}\n\n/**\n * Resolve an edge's endpoint reference to the endpoint RECORD it names: by raw\n * id first (opaque ids, and the `<id>--rear` twin of a both-faces port), then\n * by the normalized id (a hand-authored edge may carry a directional/face\n * suffix the persisted endpoint id lacks).\n */\nfunction findEndpointRecord(\n  endpointId: string,\n  endpointById: Map<string, DiagramEndpoint> | undefined\n): DiagramEndpoint | undefined {\n  if (!endpointById) return undefined;\n  return endpointById.get(endpointId) ?? endpointById.get(normalizeHandleId(endpointId));\n}\n\n/**\n * The device type the renderer reads for an equipment node: `config.deviceType`\n * (what `DeviceNode` reads from `data.deviceType`), falling back to the subtype\n * for legacy/hand-authored documents that carried the device type there.\n */\nfunction effectiveEquipmentDeviceType(node: DiagramNode): string {\n  const configured = node.config?.deviceType;\n  const deviceType =\n    typeof configured === 'string' && configured.trim().length > 0 ? configured : node.subtype;\n  return (deviceType ?? '').toLowerCase().replace(/_/g, '-');\n}\n\n/**\n * Resolve a document edge's endpoint reference into the React Flow\n * `sourceHandle`/`targetHandle` the reconstructed port actually mounts.\n *\n * The endpoint {@link DiagramEndpoint.id} is treated as an *opaque* key, not a\n * handle-grammar string: the edge is resolved to its endpoint RECORD (via the\n * per-node index — {@link buildEndpointIndex}), and the handle base is the\n * record's own id, which is exactly what `endpointsToDevicePorts` /\n * `endpointsToPortBlocks` stamp onto `port.id` and therefore what\n * {@link buildPortHandleId} mounts (for the `<id>--rear` twin of a both-faces\n * port, the id IS the rear handle's split-face alias). Directional\n * (`-source`/`-target`) decisions are then derived from the record's\n * STRUCTURED fields (`face`/`role`/`medium`) plus the node's rendered device\n * type — never from the id text.\n *\n * This is the single resolution path for both id styles: opaque consumer keys\n * (uuid-like, including ones whose text coincidentally ends in a reserved\n * suffix) and the handle-grammar strings the shipped adapters mint round-trip\n * identically. When an edge references a missing endpoint, the base falls back\n * to the normalized edge id so a dangling legacy reference still resolves\n * byte-for-byte as before.\n */\nfunction resolveEndpointHandleId(\n  node: DiagramNode | undefined,\n  endpointId: string | null | undefined,\n  role: 'source' | 'target',\n  endpointById?: Map<string, DiagramEndpoint>\n): string | undefined {\n  if (!endpointId) {\n    return undefined;\n  }\n\n  const endpoint = endpointById\n    ? findEndpointRecord(endpointId, endpointById)\n    : node?.endpoints?.find(\n        (candidate) =>\n          candidate.id === endpointId || candidate.id === normalizeHandleId(endpointId)\n      );\n\n  // Base = the endpoint's own id (what the rebuilt port mounts its handle with),\n  // NOT the normalized edge string. Falls back to the normalized edge id only\n  // for a dangling reference with no matching endpoint.\n  const base = endpoint ? endpoint.id : normalizeHandleId(endpointId);\n\n  if (!node || node.kind !== 'equipment') {\n    return base;\n  }\n\n  const deviceType = effectiveEquipmentDeviceType(node);\n  const isPowerEndpoint = endpoint?.role === 'power' || endpoint?.medium === 'power';\n\n  if ((deviceType === 'switch' || deviceType === 'router') && role === 'target' && !isPowerEndpoint) {\n    return `${base}-target`;\n  }\n\n  if (deviceType === 'patch-panel') {\n    const face = endpoint?.face === 'rear' ? 'rear' : 'front';\n    if (role === 'source' && face === 'front') {\n      return `${base}-source`;\n    }\n    if (role === 'target' && face === 'rear') {\n      return `${base}-target`;\n    }\n  }\n\n  return base;\n}\n\n/**\n * Resolve a document node's placement defensively. Missing placement or a\n * missing/partial position degrades to `{ x: 0, y: 0 }` (with a warning) instead\n * of throwing on a partial document.\n */\nfunction resolveNodePlacement(node: DiagramNode): DiagramNodePlacement {\n  const placement = isRecord(node.placement) ? (node.placement as DiagramNodePlacement) : undefined;\n  if (!placement) {\n    console.warn(\n      `[react-networks] Document node \"${node.id}\" is missing placement; defaulting position to { x: 0, y: 0 }.`\n    );\n    return { position: { x: 0, y: 0 } };\n  }\n  const position = placement.position;\n  // Number.isFinite (not typeof === 'number') so NaN/Infinity coordinates from a\n  // malformed document are rejected rather than corrupting React Flow layout.\n  if (!position || !Number.isFinite(position.x) || !Number.isFinite(position.y)) {\n    console.warn(\n      `[react-networks] Document node \"${node.id}\" is missing a valid placement.position; defaulting to { x: 0, y: 0 }.`\n    );\n    return { ...placement, position: clonePosition(position) };\n  }\n  return placement;\n}\n\n/**\n * Coerce untrusted document nodes: drop non-record entries and entries without\n * a string id or a known kind (with a warning each), drop malformed endpoint\n * entries, and keep the first node for a duplicated id.\n */\nfunction sanitizeDocumentNodes(rawNodes: unknown): DiagramNode[] {\n  if (rawNodes === undefined || rawNodes === null) return [];\n  if (!Array.isArray(rawNodes)) {\n    console.warn('[react-networks] DiagramDocument `nodes` is not an array; treating it as empty.');\n    return [];\n  }\n  const out: DiagramNode[] = [];\n  const seenIds = new Set<string>();\n  rawNodes.forEach((raw, index) => {\n    if (!isRecord(raw) || typeof raw.id !== 'string' || raw.id.length === 0) {\n      console.warn(`[react-networks] Skipping malformed document node at index ${index} (not an object with a string id).`);\n      return;\n    }\n    const node = raw as unknown as DiagramNode;\n    if (!KNOWN_DIAGRAM_NODE_KINDS.has(node.kind)) {\n      console.warn(\n        `[react-networks] Skipping document node \"${node.id}\" with unknown kind \"${String(node.kind)}\".`\n      );\n      return;\n    }\n    if (seenIds.has(node.id)) {\n      console.warn(`[react-networks] Skipping duplicate document node id \"${node.id}\".`);\n      return;\n    }\n    seenIds.add(node.id);\n    const endpoints = Array.isArray(node.endpoints)\n      ? node.endpoints.filter(\n          (endpoint): endpoint is DiagramEndpoint =>\n            isRecord(endpoint) && typeof endpoint.id === 'string' && endpoint.id.length > 0\n        )\n      : undefined;\n    const label = typeof node.label === 'string' ? node.label : node.id;\n    out.push({\n      ...node,\n      label,\n      config: isRecord(node.config) ? node.config : undefined,\n      endpoints: endpoints && endpoints.length > 0 ? endpoints : undefined,\n      externalRefs: Array.isArray(node.externalRefs)\n        ? node.externalRefs.filter((ref): ref is DiagramExternalRef => isRecord(ref))\n        : undefined,\n    });\n  });\n  return out;\n}\n\nfunction isEdgeEnd(value: unknown): value is DiagramEdgeEndpoint {\n  return isRecord(value) && typeof value.nodeId === 'string' && value.nodeId.length > 0;\n}\n\n/**\n * Convert a persisted {@link DiagramDocument} (or a lenient\n * {@link DiagramDocumentInput}) back into a runtime graph.\n *\n * Untrusted input never throws: malformed nodes/endpoints/edges are skipped\n * with a `console.warn`, and edges whose source or target node is absent from\n * the produced graph (unknown kind, malformed, or simply missing) are dropped\n * instead of being emitted dangling.\n */\nexport function diagramDocumentToNetworkGraph(document: DiagramDocumentInput): NetworkGraphSnapshot {\n  const input: DiagramDocumentInput = isRecord(document) ? document : {};\n  if (typeof input.version === 'number' && input.version > 1) {\n    console.warn(\n      `[react-networks] Unknown DiagramDocument version ${input.version}; parsing best-effort as version 1.`\n    );\n  }\n\n  const documentNodes = sanitizeDocumentNodes(input.nodes);\n  const rawEdges: unknown[] = Array.isArray(input.edges) ? input.edges : [];\n  if (input.edges !== undefined && input.edges !== null && !Array.isArray(input.edges)) {\n    console.warn('[react-networks] DiagramDocument `edges` is not an array; treating it as empty.');\n  }\n  const documentEdges = rawEdges.filter((edge, index): edge is DiagramEdge => {\n    if (isRecord(edge) && typeof edge.id === 'string' && isEdgeEnd(edge.source) && isEdgeEnd(edge.target)) {\n      return true;\n    }\n    console.warn(\n      `[react-networks] Skipping malformed document edge at index ${index} (needs an id and source/target node ids).`\n    );\n    return false;\n  });\n\n  const nodesById = new Map(documentNodes.map((node) => [node.id, node]));\n  const endpointsByNodeId = buildEndpointIndex(documentNodes);\n\n  // Mark the endpoint RECORDS edges land on, so a both-faces port's front and\n  // rear twins are tracked independently.\n  const connectedByNodeId = new Map<string, Set<string>>();\n  const markConnected = (ref: DiagramEdgeEndpoint) => {\n    if (!ref.endpointId) return;\n    const record = findEndpointRecord(ref.endpointId, endpointsByNodeId.get(ref.nodeId));\n    if (!record) return;\n    let set = connectedByNodeId.get(ref.nodeId);\n    if (!set) {\n      set = new Set();\n      connectedByNodeId.set(ref.nodeId, set);\n    }\n    set.add(record.id);\n  };\n  documentEdges.forEach((edge) => {\n    markConnected(edge.source);\n    markConnected(edge.target);\n  });\n\n  const uiNodeState = isRecord(input.ui?.nodeState) ? input.ui!.nodeState! : {};\n  const noConnections: ReadonlySet<string> = new Set();\n\n  const nodes: NetworkNode[] = [];\n  for (const node of documentNodes) {\n    const placement = resolveNodePlacement(node);\n    const legacyType = toLegacyNodeType(node);\n    const connected = connectedByNodeId.get(node.id) ?? noConnections;\n    let data: NetworkNode['data'] = {\n      label: node.label,\n      ...(node.config ?? {}),\n    } as NetworkNode['data'];\n\n    const uiEntry = Object.prototype.hasOwnProperty.call(uiNodeState, node.id)\n      ? uiNodeState[node.id]\n      : undefined;\n    data = applyRenderToLegacyData(data, isRecord(node.render) ? node.render : undefined, isRecord(uiEntry) ? uiEntry : undefined);\n    data = applyExternalRefsToLegacyData(data, node.externalRefs);\n\n    if (legacyType === 'device' || legacyType === 'server' || legacyType === 'switch' || legacyType === 'router') {\n      // `config.deviceType` (already spread above) is authoritative. Legacy\n      // documents carried the device type in `subtype` instead.\n      if (typeof data.deviceType !== 'string' || data.deviceType.trim().length === 0) {\n        data.deviceType = node.subtype ?? (legacyType === 'device' ? 'device' : legacyType);\n      }\n      if (node.endpoints && node.endpoints.length > 0) {\n        data.ports = endpointsToDevicePorts(node.endpoints, collectFrontEndpointIds(node.endpoints), connected);\n      }\n    } else if (legacyType === 'vertical-pdu') {\n      const rearEndpoints = (node.endpoints ?? []).filter((endpoint) => endpoint.face !== 'front');\n      if (rearEndpoints.length > 0) {\n        data.pduPortGroups = endpointsToPortBlocks(rearEndpoints, noConnections, connected);\n        data.powerPorts = rearEndpoints.length;\n      }\n    } else if (legacyType === 'patch-panel' || legacyType === 'closure' || legacyType === 'splice-tray') {\n      // Mirror the forward pass, which serializes container ports as face-tagged\n      // endpoints, by reconstructing front -> portGroups and rear -> rearPortGroups.\n      const endpoints = node.endpoints ?? [];\n      const frontIds = collectFrontEndpointIds(endpoints);\n      const frontEndpoints = endpoints.filter((endpoint) => endpoint.face !== 'rear');\n      const rearEndpoints = endpoints.filter((endpoint) => endpoint.face === 'rear');\n      if (frontEndpoints.length > 0) {\n        data.portGroups = endpointsToPortBlocks(frontEndpoints, frontIds, connected);\n      }\n      if (rearEndpoints.length > 0) {\n        data.rearPortGroups = endpointsToPortBlocks(rearEndpoints, frontIds, connected);\n      }\n    }\n\n    if (placement.rack) {\n      if (placement.rack.rackLayout) {\n        data.rackLayout = placement.rack.rackLayout;\n      }\n      if (typeof placement.rack.rackWidthPx === 'number') {\n        data.rackWidthPx = placement.rack.rackWidthPx;\n      }\n      if (typeof placement.rack.rackFaceGapPx === 'number') {\n        data.rackFaceGapPx = placement.rack.rackFaceGapPx;\n      }\n    }\n\n    if (legacyType !== 'rack' && placement.rack) {\n      data.isRackMounted = true;\n      if (typeof placement.rack.uPosition === 'number') {\n        data.uPosition = placement.rack.uPosition;\n      }\n      if (placement.rack.mountMode) {\n        data.mountMode = placement.rack.mountMode;\n      }\n    }\n\n    if (placement.container) {\n      if (typeof placement.container.slotPosition === 'number') {\n        data.slotPosition = placement.container.slotPosition;\n      }\n      if (typeof placement.container.holderPosition === 'number') {\n        data.holderPosition = placement.container.holderPosition;\n      }\n    }\n\n    nodes.push({\n      id: node.id,\n      type: legacyType,\n      position: clonePosition(placement.position),\n      ...(placement.parentId ? { parentId: placement.parentId } : {}),\n      ...(placement.extent ? { extent: placement.extent } : {}),\n      data,\n    } as NetworkNode);\n  }\n\n  const producedNodeIds = new Set(nodes.map((node) => node.id));\n  const danglingEdgeIds: string[] = [];\n  const edges: NetworkEdge[] = [];\n  for (const edge of documentEdges) {\n    if (!producedNodeIds.has(edge.source.nodeId) || !producedNodeIds.has(edge.target.nodeId)) {\n      danglingEdgeIds.push(edge.id);\n      continue;\n    }\n    edges.push({\n      id: edge.id,\n      source: edge.source.nodeId,\n      target: edge.target.nodeId,\n      ...(edge.source.endpointId ? { sourceHandle: resolveEndpointHandleId(nodesById.get(edge.source.nodeId), edge.source.endpointId, 'source', endpointsByNodeId.get(edge.source.nodeId)) } : {}),\n      ...(edge.target.endpointId ? { targetHandle: resolveEndpointHandleId(nodesById.get(edge.target.nodeId), edge.target.endpointId, 'target', endpointsByNodeId.get(edge.target.nodeId)) } : {}),\n      ...(edge.type ? { type: edge.type } : {}),\n      ...(edge.hidden === true ? { hidden: true } : {}),\n      ...(typeof edge.deletable === 'boolean' ? { deletable: edge.deletable } : {}),\n      ...(typeof edge.interactionWidth === 'number' && Number.isFinite(edge.interactionWidth)\n        ? { interactionWidth: edge.interactionWidth }\n        : {}),\n      ...(isRecord(edge.data) ? { data: { ...edge.data } } : {}),\n    });\n  }\n  if (danglingEdgeIds.length > 0) {\n    console.warn(\n      `[react-networks] Dropped ${danglingEdgeIds.length} document edge(s) whose source or target node is not in the document: ${danglingEdgeIds\n        .slice(0, 5)\n        .map((id) => `\"${id}\"`)\n        .join(', ')}${danglingEdgeIds.length > 5 ? ', …' : ''}.`\n    );\n  }\n\n  return { nodes, edges };\n}\n","import type { DeviceFace, DevicePort, NetworkNodeDataBase, Port, PortBlock, PortCableType, PortType } from '../types';\nimport { buildPortHandleId, isPowerPortType, normalizeHandleId } from './handle-ids';\nimport { normalizePortPositionInput } from './port-position';\nimport { buildPortBlocksFromPorts, getResolvedPortBlocks } from './render-ports';\n\nconst DEFAULT_FRONT_GROUP_NAME = 'Network Ports';\nconst DEFAULT_REAR_GROUP_NAME = 'Rear Network Ports';\nconst DEFAULT_POWER_GROUP_NAME = 'Power Ports';\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst toText = (value: unknown): string | undefined => {\n  if (typeof value !== 'string') {\n    return undefined;\n  }\n  const trimmed = value.trim();\n  return trimmed.length > 0 ? trimmed : undefined;\n};\n\nconst normalizeFaceInput = (value: unknown): DeviceFace | undefined => {\n  const normalized = toText(value)?.toLowerCase();\n  if (normalized === 'front') {\n    return 'front';\n  }\n  if (normalized === 'rear' || normalized === 'back') {\n    return 'rear';\n  }\n  return undefined;\n};\n\n/**\n * Every IEC 60320 designation the mate table in `validation/connectors` knows.\n * Narrower whitelists silently dropped C15/C16/C21/C22, so those pairings could\n * never be checked.\n */\nconst CONNECTOR_TYPES: readonly NonNullable<DevicePort['connectorType']>[] = [\n  'C13',\n  'C14',\n  'C15',\n  'C16',\n  'C19',\n  'C20',\n  'C21',\n  'C22',\n];\n\nconst normalizeConnectorType = (\n  value: unknown,\n): NonNullable<DevicePort['connectorType']> | undefined => {\n  const normalized = toText(value)?.toUpperCase();\n  if (\n    normalized !== undefined &&\n    (CONNECTOR_TYPES as readonly string[]).includes(normalized)\n  ) {\n    return normalized as NonNullable<DevicePort['connectorType']>;\n  }\n  return undefined;\n};\n\nconst resolveDefaultFace = (portType: string | undefined): DeviceFace =>\n  isPowerPortType(portType) ? 'rear' : 'front';\n\nconst resolveDefaultGroup = ({\n  face,\n  type,\n}: {\n  face: DeviceFace;\n  type: string | undefined;\n}): string => {\n  if (isPowerPortType(type)) {\n    return DEFAULT_POWER_GROUP_NAME;\n  }\n  return face === 'rear' ? DEFAULT_REAR_GROUP_NAME : DEFAULT_FRONT_GROUP_NAME;\n};\n\n/**\n * Resolve the effective device face for a flat port input.\n * @public\n */\nexport function resolveDevicePortFace(\n  port: Pick<DevicePort, 'face' | 'type'>,\n): DeviceFace {\n  return normalizeFaceInput(port.face) ?? resolveDefaultFace(port.type);\n}\n\n/**\n * Resolve the effective port group name for a flat port input.\n * @public\n */\nexport function resolveDevicePortGroup(\n  port: Pick<DevicePort, 'face' | 'group' | 'type'>,\n): string {\n  return (\n    toText(port.group) ??\n    resolveDefaultGroup({\n      face: resolveDevicePortFace(port),\n      type: port.type,\n    })\n  );\n}\n\n/**\n * Normalize unknown **flat** port array input into canonical {@link DevicePort} entries.\n *\n * This handles a flat `DevicePort[]` only; grouped `PortBlock[]` input returns\n * `undefined` (blocks have no `label`). To resolve flat ports from any supported\n * form — including grouped `PortBlock[]` — use {@link resolveNodeDevicePorts}.\n * @public\n */\nexport function normalizeDevicePorts(value: unknown): DevicePort[] | undefined {\n  if (!Array.isArray(value)) {\n    return undefined;\n  }\n\n  const ports = value\n    .map((entry): DevicePort | null => {\n      if (!isRecord(entry)) {\n        return null;\n      }\n\n      const label = toText(entry.label);\n      if (!label) {\n        return null;\n      }\n\n      const type = toText(entry.type) as PortType | undefined;\n      const face = resolveDevicePortFace({\n        face: normalizeFaceInput(entry.face),\n        type,\n      });\n      const group = resolveDevicePortGroup({\n        face,\n        group: toText(entry.group),\n        type,\n      });\n      const position = normalizePortPositionInput(entry.position ?? entry);\n      const order =\n        typeof entry.order === 'number' && Number.isFinite(entry.order) ? entry.order : undefined;\n\n      const port: DevicePort = {\n        ...(toText(entry.id) ? { id: toText(entry.id) } : {}),\n        label,\n        face,\n        group,\n        connected: typeof entry.connected === 'boolean' ? entry.connected : undefined,\n        ...(type ? { type } : {}),\n        ...(toText(entry.cableType) ? { cableType: toText(entry.cableType) as PortCableType } : {}),\n        ...(normalizeConnectorType(entry.connectorType)\n          ? { connectorType: normalizeConnectorType(entry.connectorType) }\n          : {}),\n        ...(typeof order === 'number' ? { order } : {}),\n        ...(position ? { position } : {}),\n      };\n\n      return port;\n    })\n    .filter((entry): entry is DevicePort => entry !== null);\n\n  return ports.length > 0 ? ports : undefined;\n}\n\nfunction normalizePortBlocks(value: unknown, face: DeviceFace): DevicePort[] {\n  if (!Array.isArray(value)) {\n    return [];\n  }\n\n  return value.flatMap((entry) => {\n    if (!isRecord(entry) || !Array.isArray(entry.ports)) {\n      return [];\n    }\n\n    return entry.ports\n      .map((rawPort, index): DevicePort | null => {\n        if (!isRecord(rawPort)) {\n          return null;\n        }\n\n        const label = toText(rawPort.label);\n        if (!label) {\n          return null;\n        }\n\n        const type = toText(rawPort.type) as PortType | undefined;\n        const group = resolveDevicePortGroup({\n          face,\n          group: toText(entry.name),\n          type,\n        });\n        const position = normalizePortPositionInput(rawPort.position ?? rawPort);\n        const order =\n          typeof rawPort.order === 'number' && Number.isFinite(rawPort.order)\n            ? rawPort.order\n            : index;\n\n        return {\n          ...(toText(rawPort.id) ? { id: toText(rawPort.id) } : {}),\n          label,\n          face,\n          group,\n          connected: typeof rawPort.connected === 'boolean' ? rawPort.connected : undefined,\n          ...(type ? { type } : {}),\n          ...(toText(rawPort.cableType)\n            ? { cableType: toText(rawPort.cableType) as PortCableType }\n            : {}),\n          ...(normalizeConnectorType(rawPort.connectorType)\n            ? { connectorType: normalizeConnectorType(rawPort.connectorType) }\n            : {}),\n          order,\n          ...(position ? { position } : {}),\n        };\n      })\n      .filter((port): port is DevicePort => port !== null);\n  });\n}\n\n/**\n * Resolve flat device ports from any supported port input.\n *\n * Accepts every form the renderer accepts (`getResolvedPortBlocks`): a node-data\n * record (`ports` / `portGroups` / `rearPortGroups` / `rearPorts`), a flat\n * `DevicePort[]`, or a grouped `PortBlock[]`. Grouped `PortBlock[]` carried on\n * `ports` are treated as front groups (taking precedence over `portGroups`, matching\n * the renderer); rear groups come from `rearPortGroups`, falling back to `rearPorts`.\n * @public\n */\nexport function resolveNodeDevicePorts(value: unknown): DevicePort[] | undefined {\n  if (isRecord(value)) {\n    const flatPorts = normalizeDevicePorts(value.ports);\n    if (flatPorts) {\n      return flatPorts;\n    }\n\n    // `ports` may instead carry grouped PortBlock[] (front). It wins over\n    // `portGroups`, mirroring getResolvedPortBlocks' precedence.\n    const frontFromPorts = normalizePortBlocks(value.ports, 'front');\n    const frontPorts =\n      frontFromPorts.length > 0\n        ? frontFromPorts\n        : normalizePortBlocks(value.portGroups, 'front');\n    const rearFromGroups = normalizePortBlocks(value.rearPortGroups, 'rear');\n    const rearPorts =\n      rearFromGroups.length > 0\n        ? rearFromGroups\n        : normalizePortBlocks(value.rearPorts, 'rear');\n    const ports = [...frontPorts, ...rearPorts];\n    return ports.length > 0 ? ports : undefined;\n  }\n\n  // Bare array input: a flat DevicePort[] or a grouped PortBlock[] (front groups).\n  const flatPorts = normalizeDevicePorts(value);\n  if (flatPorts) {\n    return flatPorts;\n  }\n  const blockPorts = normalizePortBlocks(value, 'front');\n  return blockPorts.length > 0 ? blockPorts : undefined;\n}\n\n/**\n * Flatten grouped port blocks into canonical flat {@link DevicePort} entries.\n * @public\n */\nexport function flattenDevicePortsFromGroups({\n  portGroups,\n  rearPortGroups,\n}: {\n  portGroups?: PortBlock[];\n  rearPortGroups?: PortBlock[];\n}): DevicePort[] {\n  const ports: DevicePort[] = [];\n\n  const appendFace = (blocks: PortBlock[] | undefined, face: DeviceFace) => {\n    blocks?.forEach((block) => {\n      block.ports.forEach((port, index) => {\n        ports.push({\n          ...(typeof port.id === 'string' && port.id.trim().length > 0 ? { id: port.id } : {}),\n          label: port.label,\n          face,\n          group: block.name,\n          connected: port.connected,\n          ...(port.type ? { type: port.type } : {}),\n          ...(port.cableType ? { cableType: port.cableType } : {}),\n          ...(port.connectorType ? { connectorType: port.connectorType } : {}),\n          order: index,\n          ...(port.position ? { position: port.position } : {}),\n        });\n      });\n    });\n  };\n\n  appendFace(portGroups, 'front');\n  appendFace(rearPortGroups, 'rear');\n  return ports;\n}\n\n/**\n * Build grouped port blocks from flat device port input.\n * @public\n */\nexport function buildDevicePortGroupsFromPorts(\n  ports: DevicePort[] | undefined,\n): {\n  portGroups: PortBlock[];\n  rearPortGroups: PortBlock[];\n} {\n  if (!Array.isArray(ports) || ports.length === 0) {\n    return { portGroups: [], rearPortGroups: [] };\n  }\n\n  const { ports: portGroups, rearPorts: rearPortGroups } = buildPortBlocksFromPorts(ports);\n  return {\n    portGroups,\n    rearPortGroups,\n  };\n}\n\nexport interface ListPossiblePortHandleIdsOptions {\n  deviceType?: string;\n  face?: DeviceFace;\n}\n\n/**\n * List the **canonical** handle ids that may exist on a rendered node for a port.\n *\n * The returned ids are in canonical form. When a device renders both faces and a\n * front/rear handle id collides, the renderer disambiguates the hidden face with a\n * `--rear` / `--front` suffix (see `normalizeHandleId`). This function does not emit\n * those split-face aliases, so callers reconciling stored/rendered handles should\n * compare through {@link normalizeHandleId} (which strips the suffix) rather than by\n * raw string equality. {@link validatePortHandleOnNodeData} does this for you.\n * @public\n */\nexport function listPossiblePortHandleIds(\n  portBlockName: string,\n  port: Pick<Port, 'id' | 'label' | 'type'>,\n  options: ListPossiblePortHandleIdsOptions = {},\n): string[] {\n  const baseHandle = buildPortHandleId(portBlockName, port);\n  // Power-ness must be decided with the same rule the renderer and handle-id\n  // builder use (`isPowerPortType`, a substring match), otherwise this list\n  // diverges from the mounted handles for types like `power` / `power_dc` and\n  // reports a phantom `-target`/`-source` handle that was never rendered.\n  if (isPowerPortType(port.type)) {\n    return [baseHandle];\n  }\n\n  const normalizedDeviceType = String(options.deviceType ?? '').toLowerCase();\n  const face = options.face ?? 'front';\n\n  if (normalizedDeviceType === 'switch' || normalizedDeviceType === 'router') {\n    return [baseHandle, `${baseHandle}-target`];\n  }\n\n  if (normalizedDeviceType === 'patch-panel' || normalizedDeviceType === 'patch_panel') {\n    return face === 'rear'\n      ? [baseHandle, `${baseHandle}-target`]\n      : [baseHandle, `${baseHandle}-source`];\n  }\n\n  return [baseHandle];\n}\n\n/**\n * A rendered/stored handle id resolved back to the port that owns it.\n * @public\n */\nexport interface DescribedPortHandle {\n  /** The port the handle belongs to. */\n  port: DevicePort;\n  /** The port block (group) the port renders under. */\n  blockName: string;\n  /** The device face the block renders on. */\n  face: DeviceFace;\n}\n\n/**\n * Resolve a handle id back to the port that owns it — the inverse of\n * {@link buildPortHandleId} / {@link listPossiblePortHandleIds} over a node's\n * resolved port blocks. Directional suffixes and split-face aliases are\n * normalized before comparison (via {@link normalizeHandleId}), so ids taken\n * straight from edges match. Pass `options.sanitizeHandle` to match under an\n * additional app-side normalization (e.g. a `+`-insensitive comparison). Prefer\n * {@link handlesMatch} / {@link findEdgesForPortHandle} for whole-handle matching.\n * Returns `undefined` when no port on the node renders that handle.\n * @public\n */\nexport function describePortHandle(\n  nodeData: Pick<NetworkNodeDataBase, 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n  handle: string,\n  options: {\n    deviceType?: string;\n    sanitizeHandle?: (value: string) => string;\n  } = {},\n): DescribedPortHandle | undefined {\n  const sanitize = options.sanitizeHandle ?? ((value: string) => normalizeHandleId(value));\n  const normalizedRequestedHandle = sanitize(handle);\n  const { ports, rearPorts } = getResolvedPortBlocks(nodeData);\n\n  const findInBlocks = (blocks: PortBlock[], face: DeviceFace): DescribedPortHandle | undefined => {\n    for (const portBlock of blocks) {\n      for (const port of portBlock.ports) {\n        const possibleHandles = listPossiblePortHandleIds(portBlock.name, port, {\n          deviceType: options.deviceType,\n          face,\n        });\n        if (\n          possibleHandles.some(\n            (candidate) => sanitize(candidate) === normalizedRequestedHandle,\n          )\n        ) {\n          return { port, blockName: portBlock.name, face };\n        }\n      }\n    }\n    return undefined;\n  };\n\n  return findInBlocks(ports, 'front') ?? findInBlocks(rearPorts, 'rear');\n}\n\n/**\n * Validate that a handle id exists on node data after grouping resolution.\n * @public\n */\nexport function validatePortHandleOnNodeData(\n  nodeData: Pick<NetworkNodeDataBase, 'ports' | 'portGroups' | 'rearPortGroups'> | undefined,\n  handle: string,\n  options: {\n    deviceType?: string;\n    sanitizeHandle?: (value: string) => string;\n  } = {},\n): boolean {\n  return describePortHandle(nodeData, handle, options) !== undefined;\n}\n\n/**\n * Convert flat ports to grouped blocks using the same rules as renderers.\n * @public\n */\nexport function resolveDevicePortBlocksFromFlatPorts(\n  ports: DevicePort[],\n): { portGroups: PortBlock[]; rearPortGroups: PortBlock[] } {\n  return buildDevicePortGroupsFromPorts(ports);\n}\n","import {\n  DevicePort,\n  DiagramDocument,\n  NetworkNode,\n  NetworkEdge,\n  NetBoxDevice,\n  NetBoxRack,\n  NetBoxCable,\n  NetBoxInterface,\n  NetBoxDeviceType,\n  NetBoxDeviceTransformOptions,\n  NetBoxMappedPort,\n  PortCableType,\n  PortPosition,\n  PortType,\n} from '../types';\nimport { lengthToCentimetres, cableTypeToEdgeType, readChoiceLabel, readChoiceValue } from './cable-type';\nimport { networkGraphToDiagramDocument } from './document-adapter';\nimport { buildPortHandleId } from './handle-ids';\nimport { resolveDevicePortGroup } from './port-contract';\nimport { normalizePortPositionInput, parseSemanticPortPosition } from './port-position';\nimport { generateSimpleLayout } from './simple-layout';\n\nconst resolveDeviceNodeId = (device: { id?: number; name: string }): string => {\n  if (typeof device.id === 'number') {\n    return `device-${device.id}`;\n  }\n  return `device-${device.name}`;\n};\n\ntype NodeStatus = 'active' | 'inactive' | 'maintenance';\ntype PortFace = 'front' | 'rear';\n\nconst DEFAULT_INTERFACE_GROUP = 'Interfaces';\nconst DEFAULT_MANAGEMENT_GROUP = 'Management';\nconst DEFAULT_POWER_GROUP = 'Power';\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n  typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst toNonEmptyString = (value: unknown): string | undefined => {\n  if (typeof value !== 'string') {\n    return undefined;\n  }\n  const trimmed = value.trim();\n  return trimmed.length > 0 ? trimmed : undefined;\n};\n\nconst normalizeConnectorType = (value: unknown): DevicePort['connectorType'] | undefined => {\n  if (typeof value !== 'string') {\n    return undefined;\n  }\n  const normalized = value.trim().toUpperCase();\n  if (normalized === 'C13' || normalized === 'C14' || normalized === 'C19' || normalized === 'C20') {\n    return normalized;\n  }\n  return undefined;\n};\n\nconst normalizePortFace = (value: unknown): PortFace => {\n  if (typeof value === 'string' && value.toLowerCase() === 'rear') {\n    return 'rear';\n  }\n  return 'front';\n};\n\nconst getInterfaceCustomFields = (iface: NetBoxInterface): Record<string, unknown> | undefined =>\n  isRecord(iface.custom_fields) ? iface.custom_fields : undefined;\n\nconst getNestedLayoutHints = (value: unknown): Record<string, unknown> | undefined =>\n  isRecord(value) ? value : undefined;\n\nconst resolvePortFaceHint = (\n  iface: NetBoxInterface,\n  mappedFace?: NetBoxMappedPort['face']\n): PortFace => {\n  const customFields = getInterfaceCustomFields(iface);\n  const nestedLayout = getNestedLayoutHints(customFields?.layout);\n  return normalizePortFace(\n    mappedFace ??\n      iface.layout?.face ??\n      iface.face ??\n      nestedLayout?.face ??\n      customFields?.port_face ??\n      customFields?.portFace ??\n      customFields?.face\n  );\n};\n\nconst resolvePortGroupHint = (\n  iface: NetBoxInterface,\n  type: PortType,\n  mappedGroup?: string\n): string => {\n  const customFields = getInterfaceCustomFields(iface);\n  const nestedLayout = getNestedLayoutHints(customFields?.layout);\n  const explicitGroup =\n    toNonEmptyString(mappedGroup) ??\n    toNonEmptyString(iface.layout?.group) ??\n    toNonEmptyString(iface.port_group) ??\n    toNonEmptyString(nestedLayout?.group) ??\n    toNonEmptyString(customFields?.port_group) ??\n    toNonEmptyString(customFields?.portGroup) ??\n    toNonEmptyString(customFields?.interface_group) ??\n    toNonEmptyString(customFields?.interfaceGroup) ??\n    toNonEmptyString(customFields?.group);\n\n  if (explicitGroup) {\n    return explicitGroup;\n  }\n  if (type === 'power_ac' || type === 'power_c13' || type === 'power_c19') {\n    return DEFAULT_POWER_GROUP;\n  }\n  if (type === 'console' || type === 'mgmt') {\n    return DEFAULT_MANAGEMENT_GROUP;\n  }\n  return DEFAULT_INTERFACE_GROUP;\n};\n\nconst resolveConnectorTypeHint = (\n  iface: NetBoxInterface,\n  rawType?: string,\n  mappedConnectorType?: NetBoxMappedPort['connectorType']\n): DevicePort['connectorType'] | undefined => {\n  const customFields = getInterfaceCustomFields(iface);\n  const nestedLayout = getNestedLayoutHints(customFields?.layout);\n  return (\n    normalizeConnectorType(mappedConnectorType) ??\n    normalizeConnectorType(iface.layout?.connectorType) ??\n    normalizeConnectorType((iface.layout as unknown as Record<string, unknown> | undefined)?.connector_type) ??\n    normalizeConnectorType(iface.connector_type) ??\n    normalizeConnectorType(nestedLayout?.connectorType) ??\n    normalizeConnectorType(nestedLayout?.connector_type) ??\n    normalizeConnectorType(customFields?.connectorType) ??\n    normalizeConnectorType(customFields?.connector_type) ??\n    normalizeConnectorType(rawType)\n  );\n};\n\nconst normalizePortType = (\n  rawType: string | undefined,\n  connectorType?: DevicePort['connectorType'],\n): PortType => {\n  if (connectorType === 'C13') {\n    return 'power_c13';\n  }\n  if (connectorType === 'C19') {\n    return 'power_c19';\n  }\n\n  const normalized = (rawType ?? '').toLowerCase();\n  if (normalized.includes('c13')) return 'power_c13';\n  if (normalized.includes('c19')) return 'power_c19';\n  if (normalized.includes('power') || normalized.includes('pdu')) return 'power_ac';\n  if (\n    normalized.includes('fiber') ||\n    normalized.includes('sfp') ||\n    normalized.includes('qsfp') ||\n    normalized.includes('dac')\n  ) return 'fiber';\n  if (normalized.includes('console')) return 'console';\n  if (normalized.includes('mgmt') || normalized.includes('management') || normalized.includes('oob')) return 'mgmt';\n  if (normalized.includes('usb')) return 'usb';\n  if (normalized.includes('vga') || normalized.includes('hd15') || normalized.includes('hd-15')) return 'vga';\n  if (\n    normalized.includes('serial') ||\n    normalized.includes('rs232') ||\n    normalized.includes('rs-232') ||\n    normalized.includes('db9') ||\n    normalized.includes('db-9')\n  ) {\n    return 'serial';\n  }\n  if (normalized.includes('rj45') || normalized.includes('rj-45')) return 'rj45';\n  return 'ethernet';\n};\n\nconst normalizeCableType = (rawType: string | undefined): PortCableType | undefined => {\n  const normalized = (rawType ?? '').toLowerCase();\n  if (!normalized) return undefined;\n  if (normalized.includes('om3')) return 'om3';\n  if (normalized.includes('om4')) return 'om4';\n  if (normalized.includes('om5')) return 'om5';\n  if (normalized.includes('smf') || normalized.includes('singlemode')) return 'smf';\n  if (normalized.includes('cat5e')) return 'cat5e';\n  if (normalized.includes('cat6a')) return 'cat6a';\n  if (normalized.includes('cat6')) return 'cat6';\n  if (normalized.includes('cat7')) return 'cat7';\n  if (normalized.includes('fiber') || normalized.includes('sfp') || normalized.includes('qsfp')) return 'fiber';\n  if (\n    normalized.includes('ethernet') ||\n    normalized.includes('rj45') ||\n    normalized.includes('1000base') ||\n    normalized.includes('10gbase') ||\n    normalized.includes('base-t')\n  ) return 'ethernet';\n  return undefined;\n};\n\nconst parsePortPosition = (value: unknown): PortPosition | undefined => {\n  if (typeof value === 'string') {\n    return parseSemanticPortPosition(value);\n  }\n  return normalizePortPositionInput(value);\n};\n\nconst extractMappedPosition = (\n  iface: NetBoxInterface,\n  mappedPort: NetBoxMappedPort | null | undefined,\n  includePortPositions: boolean\n): PortPosition | undefined => {\n  if (!includePortPositions) {\n    return undefined;\n  }\n\n  const customFields = getInterfaceCustomFields(iface);\n  const nestedLayout = getNestedLayoutHints(customFields?.layout);\n  const candidates: unknown[] = [\n    mappedPort?.position,\n    mappedPort,\n    iface.layout?.position,\n    iface.layout,\n    nestedLayout?.position,\n    nestedLayout,\n    customFields?.port_position,\n    customFields?.portPosition,\n    customFields?.position,\n    customFields,\n    iface as unknown as Record<string, unknown>,\n  ];\n\n  for (const candidate of candidates) {\n    const parsed = parsePortPosition(candidate);\n    if (parsed) {\n      return parsed;\n    }\n  }\n\n  return undefined;\n};\n\nconst defaultMapInterfaceToPort = (\n  iface: NetBoxInterface,\n  includePortPositions: boolean\n): NetBoxMappedPort => {\n  const rawType = iface.type?.value ?? iface.type?.label ?? iface.layout?.type ?? undefined;\n  const connectorType = resolveConnectorTypeHint(iface, rawType);\n  const portType = normalizePortType(rawType, connectorType);\n  const group = resolvePortGroupHint(iface, portType);\n\n  return {\n    id: iface.name,\n    label: iface.name,\n    face: resolvePortFaceHint(iface),\n    group,\n    type: portType,\n    cableType: normalizeCableType(rawType),\n    connected: Boolean(iface.connected_endpoint || iface.cable),\n    connectorType,\n    position: extractMappedPosition(iface, undefined, includePortPositions),\n  };\n};\n\nconst sortInterfaces = (interfaces: NetBoxInterface[]): NetBoxInterface[] => {\n  return [...interfaces].sort((a, b) => {\n    const aPosition = typeof a.position === 'number' ? a.position : Number.MAX_SAFE_INTEGER;\n    const bPosition = typeof b.position === 'number' ? b.position : Number.MAX_SAFE_INTEGER;\n    if (aPosition !== bPosition) {\n      return aPosition - bPosition;\n    }\n    return a.name.localeCompare(b.name);\n  });\n};\n\nconst buildInterfacePorts = (\n  device: NetBoxDevice,\n  options: NetBoxDeviceTransformOptions\n): DevicePort[] => {\n  const { includePortPositions = false, mapInterfaceToPort } = options;\n  const ports: DevicePort[] = [];\n  const sortedInterfaces = sortInterfaces(\n    (device.interfaces ?? []).filter(\n      (iface): iface is NetBoxInterface => isRecord(iface) && typeof iface.name === 'string',\n    ),\n  );\n\n  sortedInterfaces.forEach((iface, index) => {\n    const mappedPort = mapInterfaceToPort\n      ? mapInterfaceToPort({ device, iface, index })\n      : defaultMapInterfaceToPort(iface, includePortPositions);\n    if (!mappedPort) {\n      return;\n    }\n\n    const rawType = mappedPort.type ?? iface.type?.value ?? iface.type?.label ?? undefined;\n    const connectorType = resolveConnectorTypeHint(iface, rawType, mappedPort.connectorType);\n    const type = normalizePortType(rawType, connectorType);\n    const targetFace = resolvePortFaceHint(iface, mappedPort.face);\n    const groupName = resolvePortGroupHint(iface, type, mappedPort.group);\n    ports.push({\n      id: mappedPort.id,\n      label: mappedPort.label ?? iface.name,\n      face: targetFace,\n      group: groupName,\n      connected: mappedPort.connected ?? Boolean(iface.connected_endpoint || iface.cable),\n      type,\n      cableType: normalizeCableType(mappedPort.cableType ?? iface.type?.value ?? iface.type?.label ?? undefined),\n      connectorType,\n      position: extractMappedPosition(iface, mappedPort, includePortPositions),\n    });\n  });\n\n  return ports;\n};\n\n/** Read a NetBox `{ value }` choice (or bare string) as a status union. */\nconst readStatus = (value: unknown): NodeStatus | undefined =>\n  readChoiceValue(value) as NodeStatus | undefined;\n\n/** Name of a nested `{ name }` reference, tolerating `null`/absent refs. */\nconst readName = (ref: { name?: unknown } | null | undefined): string | undefined =>\n  ref && typeof ref.name === 'string' ? ref.name : undefined;\n\n/**\n * Convert NetBox rack data to NetworkNode\n */\nexport const netboxRackToNetworkNode = (rack: NetBoxRack): NetworkNode => {\n  return {\n    id: `rack-${rack.id}`,\n    type: 'rack',\n    position: { x: 0, y: 0 }, // Will be set by layout\n    data: {\n      label: rack.name,\n      status: readStatus(rack.status),\n      uHeight: typeof rack.u_height === 'number' && rack.u_height > 0 ? rack.u_height : 42,\n      // NetBox `desc_units` numbers units from the top of the rack down.\n      ...(rack.desc_units === true ? { uNumberingDirection: 'top-down' as const } : {}),\n      netboxId: rack.id,\n      netboxType: 'rack',\n      // Additional NetBox data\n      // NetBox serializes unset scalars as null; surface them as absent instead\n      // (same normalization the cable transform applies to `length`).\n      facilityId: rack.facility_id ?? undefined,\n      serial: rack.serial ?? undefined,\n      assetTag: rack.asset_tag ?? undefined,\n      site: readName(rack.site),\n      role: readName(rack.role),\n      tenant: readName(rack.tenant),\n    },\n  };\n};\n\n/**\n * Convert NetBox device data to NetworkNode.\n *\n * Accepts NetBox 4.x (`role`) and legacy (`device_role`) payloads. `uPosition`\n * is only set for a racked device with a real position: an unracked or 0U\n * device (`position: null`) carries no `uPosition` rather than a fabricated U1.\n */\nexport const netboxDeviceToNetworkNode = (\n  device: NetBoxDevice,\n  options: NetBoxDeviceTransformOptions = {}\n): NetworkNode => {\n  const position =\n    device.rack && typeof device.position === 'number' && Number.isFinite(device.position) && device.position > 0\n      ? device.position\n      : undefined;\n  const face = readChoiceValue(device.face);\n  const uHeight = device.device_type?.u_height;\n  const data: NetworkNode['data'] = {\n    label: device.name,\n    status: readStatus(device.status),\n    deviceType: device.device_type?.model,\n    manufacturer: readName(device.device_type?.manufacturer),\n    role: readName(device.role) ?? readName(device.device_role),\n    uHeight: typeof uHeight === 'number' && Number.isFinite(uHeight) && uHeight >= 0 ? uHeight : 1,\n    ...(position !== undefined ? { uPosition: position } : {}),\n    face: face === 'rear' ? 'rear' : 'front',\n    netboxId: device.id,\n    netboxType: 'device',\n    // Additional NetBox data\n    primaryIp4: device.primary_ip4?.address,\n    primaryIp6: device.primary_ip6?.address,\n    site: readName(device.site),\n    rack: device.rack?.name,\n  };\n\n  if (options.includeInterfacePorts && Array.isArray(device.interfaces) && device.interfaces.length > 0) {\n    const ports = buildInterfacePorts(device, options);\n    if (ports.length > 0) {\n      data.ports = ports;\n    }\n  }\n\n  return {\n    id: `device-${device.id}`,\n    type: 'device',\n    position: { x: 0, y: 0 }, // Will be set by layout\n    parentId: device.rack ? `rack-${device.rack.id}` : undefined,\n    extent: device.rack ? 'parent' : undefined,\n    data,\n  };\n};\n\ninterface ResolvedCableEnd {\n  device: { id?: number; name: string };\n  /** Name of the terminating port/interface, when known. */\n  name?: string;\n  /** NetBox object type of the termination (e.g. `dcim.interface`). */\n  objectType?: string;\n}\n\nconst isDeviceRef = (value: unknown): value is { id?: number; name: string } =>\n  !!value &&\n  typeof value === 'object' &&\n  (typeof (value as { id?: unknown }).id === 'number' ||\n    typeof (value as { name?: unknown }).name === 'string');\n\n/**\n * Resolve one end of a cable to its device + port name. NetBox ≥ 3.3\n * multi-termination cables (`a_terminations`/`b_terminations`) win; the first\n * device-bound termination is used. Legacy `termination_a`/`termination_b`\n * is the fallback. Circuit terminations, power feeds and other non-device\n * ends resolve to `undefined`.\n */\nconst resolveCableEnd = (cable: NetBoxCable, side: 'a' | 'b'): ResolvedCableEnd | undefined => {\n  const terminations = side === 'a' ? cable.a_terminations : cable.b_terminations;\n  if (Array.isArray(terminations)) {\n    for (const termination of terminations) {\n      const object = termination?.object;\n      if (object && isDeviceRef(object.device)) {\n        return {\n          device: { id: object.device.id, name: String(object.device.name ?? '') },\n          name: typeof object.name === 'string' ? object.name : undefined,\n          objectType: termination.object_type,\n        };\n      }\n    }\n  }\n  const legacy = side === 'a' ? cable.termination_a : cable.termination_b;\n  if (legacy && isDeviceRef(legacy.device)) {\n    return {\n      device: { id: legacy.device.id, name: String(legacy.device.name ?? '') },\n      name: typeof legacy.name === 'string' ? legacy.name : undefined,\n      objectType: side === 'a' ? cable.termination_a_type : cable.termination_b_type,\n    };\n  }\n  return undefined;\n};\n\nconst buildCableEdgeData = (\n  cable: NetBoxCable,\n  a: ResolvedCableEnd | undefined,\n  b: ResolvedCableEnd | undefined,\n): NetworkEdge['data'] => {\n  const lengthUnit = readChoiceValue(cable.length_unit);\n  const lengthCm = lengthToCentimetres(cable.length, cable.length_unit);\n  return {\n    bandwidth: readChoiceLabel(cable.type),\n    length: cable.length || 0,\n    ...(lengthUnit ? { lengthUnit } : {}),\n    ...(lengthCm !== undefined ? { lengthCm } : {}),\n    // NetBox sends '' for \"no colour\"; an empty string is not a paintable colour.\n    color: cable.color || undefined,\n    netboxId: cable.id,\n    netboxType: 'cable',\n    status: readChoiceValue(cable.status),\n    terminationA: a?.name,\n    terminationB: b?.name,\n  };\n};\n\n/**\n * Convert NetBox cable data to NetworkEdge.\n *\n * Accepts NetBox ≥ 3.3 `a_terminations`/`b_terminations` and the legacy\n * `termination_a`/`termination_b`. The edge `type` follows the cable type\n * (`power` → `power`, fibre/AOC → `fiber`, any other named type → `ethernet`;\n * a blank type keeps the historical `fiber`). Lengths with a known\n * `length_unit` also carry `data.lengthCm` / `data.lengthUnit`.\n *\n * This standalone form cannot see the device nodes, so it sets\n * `sourceHandle`/`targetHandle` to the termination names verbatim — those only\n * resolve when the devices mount ports with those ids\n * (`includeInterfacePorts`). {@link netboxToNetworkDiagram} emits handles only\n * when the port exists. A cable with no device termination on a side gets an\n * empty `source`/`target`; `netboxToNetworkDiagram` drops such cables.\n */\nexport const netboxCableToNetworkEdge = (cable: NetBoxCable): NetworkEdge => {\n  const a = resolveCableEnd(cable, 'a');\n  const b = resolveCableEnd(cable, 'b');\n  return {\n    id: `cable-${cable.id}`,\n    source: a ? resolveDeviceNodeId(a.device) : '',\n    ...(a?.name ? { sourceHandle: a.name } : {}),\n    target: b ? resolveDeviceNodeId(b.device) : '',\n    ...(b?.name ? { targetHandle: b.name } : {}),\n    type: cableTypeToEdgeType(cable.type) ?? 'fiber',\n    data: buildCableEdgeData(cable, a, b),\n  };\n};\n\n/**\n * Index a device node's mounted port handle ids by the names a cable\n * termination may use (the port's explicit id and its label).\n */\nconst indexDevicePortHandles = (node: NetworkNode): Map<string, string> => {\n  const index = new Map<string, string>();\n  const ports = Array.isArray(node.data?.ports) ? (node.data.ports as DevicePort[]) : [];\n  for (const port of ports) {\n    if (!port || typeof port.label !== 'string') continue;\n    const handle = buildPortHandleId(resolveDevicePortGroup(port), {\n      id: port.id,\n      label: port.label.trim(),\n      type: port.type,\n    });\n    if (typeof port.id === 'string' && !index.has(port.id)) index.set(port.id, handle);\n    if (!index.has(port.label)) index.set(port.label, handle);\n  }\n  return index;\n};\n\n/**\n * Input to {@link netboxToNetworkDiagram} / {@link netboxToDiagramDocument}:\n * the NetBox 4.5 REST lists, as returned by `GET /api/dcim/<endpoint>/`.\n */\nexport interface NetBoxDiagramInput {\n  /** `GET /api/dcim/racks/` */\n  racks: NetBoxRack[];\n  /** `GET /api/dcim/devices/` */\n  devices: NetBoxDevice[];\n  /** `GET /api/dcim/cables/` */\n  cables: NetBoxCable[];\n  /**\n   * `GET /api/dcim/device-types/`. A device listing only carries a brief\n   * device type (no `u_height`), so without this every device is drawn 1U.\n   */\n  deviceTypes?: NetBoxDeviceType[];\n  /**\n   * `GET /api/dcim/interfaces/`. Grouped onto their devices by\n   * `interface.device.id`; used with `includeInterfacePorts`. Interfaces\n   * already nested in `device.interfaces` take precedence.\n   */\n  interfaces?: NetBoxInterface[];\n}\n\n/** Attach looked-up device-type heights and interfaces to devices (no input mutation). */\nconst createNetBoxDeviceEnricher = (data: NetBoxDiagramInput) => {\n  const heightByTypeId = new Map<number, number>();\n  for (const type of data.deviceTypes ?? []) {\n    if (isRecord(type) && typeof type.id === 'number' && typeof type.u_height === 'number') {\n      heightByTypeId.set(type.id, type.u_height);\n    }\n  }\n  const interfacesByDeviceId = new Map<number, NetBoxInterface[]>();\n  for (const iface of data.interfaces ?? []) {\n    const deviceId = isRecord(iface) ? iface.device?.id : undefined;\n    if (typeof deviceId !== 'number') continue;\n    const list = interfacesByDeviceId.get(deviceId);\n    if (list) list.push(iface);\n    else interfacesByDeviceId.set(deviceId, [iface]);\n  }\n\n  return (device: NetBoxDevice): NetBoxDevice => {\n    if (!isRecord(device)) return device;\n    const typeId = device.device_type?.id;\n    const lookedUpHeight = typeof typeId === 'number' ? heightByTypeId.get(typeId) : undefined;\n    const needsHeight =\n      lookedUpHeight !== undefined && typeof device.device_type?.u_height !== 'number';\n    const lookedUpInterfaces = interfacesByDeviceId.get(device.id);\n    const needsInterfaces =\n      lookedUpInterfaces !== undefined && !(Array.isArray(device.interfaces) && device.interfaces.length > 0);\n    if (!needsHeight && !needsInterfaces) return device;\n    return {\n      ...device,\n      ...(needsHeight ? { device_type: { ...device.device_type, u_height: lookedUpHeight } } : {}),\n      ...(needsInterfaces ? { interfaces: lookedUpInterfaces } : {}),\n    };\n  };\n};\n\n/**\n * Convert NetBox data to network diagram format.\n *\n * Cable ends resolve by device id first, then by device name. A cable whose\n * either end has no device (circuit termination, power feed) or names a device\n * that is not in `devices` is dropped rather than left dangling. Handles are\n * only emitted for terminations the device node actually mounts as a port, so\n * cables stay visible (on the device's fallback anchors) when\n * `includeInterfacePorts` is off or the termination is not an interface.\n */\nexport const netboxToNetworkDiagram = (data: NetBoxDiagramInput, options: NetBoxDeviceTransformOptions = {}): { nodes: NetworkNode[]; edges: NetworkEdge[] } => {\n  const nodes: NetworkNode[] = [];\n  const edges: NetworkEdge[] = [];\n  const deviceNodeById = new Map<string, NetworkNode>();\n  const deviceNodeIdByName = new Map<string, string>();\n  const portHandlesByNodeId = new Map<string, Map<string, string>>();\n\n  // Convert racks\n  (data.racks ?? []).forEach(rack => {\n    nodes.push(netboxRackToNetworkNode(rack));\n  });\n\n  // Convert devices (enriched with the separately fetched device types and\n  // interfaces NetBox 4.5 does not nest inside a device).\n  const enrich = createNetBoxDeviceEnricher(data);\n  (data.devices ?? []).forEach(rawDevice => {\n    const device = enrich(rawDevice);\n    const deviceNode = netboxDeviceToNetworkNode(device, options);\n    nodes.push(deviceNode);\n    deviceNodeById.set(deviceNode.id, deviceNode);\n    if (typeof device.name === 'string' && !deviceNodeIdByName.has(device.name)) {\n      deviceNodeIdByName.set(device.name, deviceNode.id);\n    }\n  });\n\n  const resolveNodeId = (end: ResolvedCableEnd | undefined): string | undefined => {\n    if (!end) return undefined;\n    if (typeof end.device.id === 'number') {\n      const byId = `device-${end.device.id}`;\n      if (deviceNodeById.has(byId)) return byId;\n    }\n    return end.device.name ? deviceNodeIdByName.get(end.device.name) : undefined;\n  };\n\n  const resolveHandle = (nodeId: string, end: ResolvedCableEnd | undefined): string | undefined => {\n    if (!end?.name) return undefined;\n    let handles = portHandlesByNodeId.get(nodeId);\n    if (!handles) {\n      const node = deviceNodeById.get(nodeId);\n      handles = node ? indexDevicePortHandles(node) : new Map();\n      portHandlesByNodeId.set(nodeId, handles);\n    }\n    return handles.get(end.name);\n  };\n\n  // Convert cables\n  (data.cables ?? []).forEach(cable => {\n    const a = resolveCableEnd(cable, 'a');\n    const b = resolveCableEnd(cable, 'b');\n    const source = resolveNodeId(a);\n    const target = resolveNodeId(b);\n    if (!source || !target) return;\n    const sourceHandle = resolveHandle(source, a);\n    const targetHandle = resolveHandle(target, b);\n    edges.push({\n      id: `cable-${cable.id}`,\n      source,\n      ...(sourceHandle ? { sourceHandle } : {}),\n      target,\n      ...(targetHandle ? { targetHandle } : {}),\n      type: cableTypeToEdgeType(cable.type) ?? 'fiber',\n      data: buildCableEdgeData(cable, a, b),\n    });\n  });\n\n  return { nodes, edges };\n};\n\n/**\n * Convert NetBox data directly into the canonical DiagramDocument model.\n */\nexport const netboxToDiagramDocument = (\n  data: NetBoxDiagramInput,\n  options: NetBoxDeviceTransformOptions = {}\n): DiagramDocument => {\n  return networkGraphToDiagramDocument(netboxToNetworkDiagram(data, options));\n};\n\n/**\n * Calculate U position for devices in a rack\n */\nexport const calculateUPositions = (devices: NetBoxDevice[], _rackHeight: number): Map<number, number> => {\n  const positions = new Map<number, number>();\n  const usedPositions = new Set<number>();\n\n  // Sort devices by position\n  const sortedDevices = devices\n    .filter(d => d.position !== null)\n    .sort((a, b) => (a.position || 0) - (b.position || 0));\n\n  sortedDevices.forEach(device => {\n    if (device.position) {\n      positions.set(device.id, device.position);\n      usedPositions.add(device.position);\n    }\n  });\n\n  return positions;\n};\n\n/**\n * Generate layout positions for nodes\n */\nexport const generateLayout = (nodes: NetworkNode[]): NetworkNode[] => {\n  return generateSimpleLayout(nodes);\n};\n"]}