{"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/simple-layout.ts","../src/utils/inventory.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","generateSimpleLayout","rackNodes","n","deviceNodes","otherNodes","rack","device","parentRack","r","uPosition","uHeight","normalizeInventoryDeviceNodeId","getInventoryCableDeviceRef","term","dev","id","name","ref","getInventoryTerminationName","readRef","readStatus","getDeviceRackId","inventoryRackToNetworkNode","inventoryDeviceToNetworkNode","rackId","ip","buildInventoryCableEdge","cable","source","lengthUnit","lengthCm","inventoryCableToNetworkEdge","resolvedEndpoints","sourceRef","targetRef","inventoryToNetworkDiagram","deviceNodeIds","deviceNodeIdByName","rackNodeIds","rackNodeIdByName","rackName","byName","unrackedData","resolve","byId","inventoryToDiagramDocument","generateLayout"],"mappings":"AASO,SAASA,EAAgBC,CAAAA,CAAoC,CAClE,GAAI,OAAOA,CAAAA,EAAU,SAAU,CAC7B,IAAMC,CAAAA,CAAUD,CAAAA,CAAM,MAAK,CAC3B,OAAOC,EAAQ,MAAA,CAAS,CAAA,CAAIA,EAAU,MACxC,CACA,GAAID,CAAAA,EAAS,OAAOA,CAAAA,EAAU,QAAA,CAAU,CACtC,IAAME,CAAAA,CAASF,EACf,OAAOD,CAAAA,CAAgBG,CAAAA,CAAO,KAAK,GAAKH,CAAAA,CAAgBG,CAAAA,CAAO,KAAK,CACtE,CAEF,CAGO,SAASC,CAAAA,CAAgBH,CAAAA,CAAoC,CAClE,GAAIA,CAAAA,EAAS,OAAOA,GAAU,QAAA,CAAU,CACtC,IAAME,CAAAA,CAASF,CAAAA,CACf,OAAOD,CAAAA,CAAgBG,EAAO,KAAK,CAAA,EAAKH,EAAgBG,CAAAA,CAAO,KAAK,CACtE,CACA,OAAOH,CAAAA,CAAgBC,CAAK,CAC9B,CAeO,SAASI,KAAuBC,CAAAA,CAAoD,CACzF,IAAMC,CAAAA,CAASD,CAAAA,CACZ,GAAA,CAAKE,CAAAA,EAAcR,EAAgBQ,CAAS,CAAC,EAC7C,MAAA,CAAQC,CAAAA,EAA2BA,IAAU,MAAS,CAAA,CACtD,GAAA,CAAKA,CAAAA,EAAUA,EAAM,WAAA,EAAa,EACrC,GAAIF,CAAAA,CAAO,SAAW,CAAA,CACtB,OAAIA,CAAAA,CAAO,IAAA,CAAME,GAAUA,CAAAA,CAAM,QAAA,CAAS,OAAO,CAAC,CAAA,CAAU,QAE1DF,CAAAA,CAAO,IAAA,CACJE,CAAAA,EACC,kBAAA,CAAmB,KAAKA,CAAK,CAAA,EAC7B,uBAAuB,IAAA,CAAKA,CAAK,GACjC,kBAAA,CAAmB,IAAA,CAAKA,CAAK,CACjC,EAEO,OAAA,CAEF,UACT,CAEA,IAAMC,CAAAA,CAAyD,CAC7D,EAAA,CAAI,GAAA,CACJ,SAAA,CAAW,GAAA,CACX,WAAY,GAAA,CACZ,SAAA,CAAW,IACX,UAAA,CAAY,GAAA,CACZ,EAAG,GAAA,CACH,KAAA,CAAO,GAAA,CACP,MAAA,CAAQ,IACR,KAAA,CAAO,GAAA,CACP,OAAQ,GAAA,CACR,EAAA,CAAI,EACJ,UAAA,CAAY,CAAA,CACZ,WAAA,CAAa,CAAA,CACb,WAAY,CAAA,CACZ,WAAA,CAAa,EACb,EAAA,CAAI,QAAA,CACJ,KAAM,QAAA,CACN,KAAA,CAAO,SACP,EAAA,CAAI,KAAA,CACJ,KAAM,KAAA,CACN,IAAA,CAAM,MACN,EAAA,CAAI,IAAA,CACJ,KAAM,IAAA,CACN,MAAA,CAAQ,IACV,CAAA,CAOO,SAASC,CAAAA,CAAoBC,CAAAA,CAAiBC,EAAmC,CACtF,GAAI,OAAOD,CAAAA,EAAW,QAAA,EAAY,CAAC,MAAA,CAAO,SAASA,CAAM,CAAA,EAAKA,GAAU,CAAA,CAAG,OAC3E,IAAME,CAAAA,CAAMd,CAAAA,CAAgBa,CAAI,CAAA,EAAG,aAAY,CAC/C,GAAI,CAACC,CAAAA,EAAO,CAAC,OAAO,SAAA,CAAU,cAAA,CAAe,IAAA,CAAKJ,CAAAA,CAAsBI,CAAG,CAAA,CAAG,OAC9E,IAAMC,CAAAA,CAASL,CAAAA,CAAqBI,CAAG,CAAA,CACvC,OAAO,IAAA,CAAK,KAAA,CAAMF,EAASG,CAAAA,CAAS,GAAG,EAAI,GAC7C,CCnGA,IAAMC,CAAAA,CAA4B,oBAAA,CAC5BC,CAAAA,CAA2B,kBAAA,CAM1B,SAASC,CAAAA,CACdC,CAAAA,CACQ,CACR,OAAKA,CAAAA,CACEA,EAAS,OAAA,CAAQH,CAAAA,CAA2B,EAAE,CAAA,CAD/B,EAExB,CAiBO,SAASI,EAAkBD,CAAAA,CAA6C,CAC7E,OAAOD,CAAAA,CAA6BC,CAAQ,CAAA,CAAE,OAAA,CAC5CF,EACA,EACF,CACF,CAKO,SAASI,CAAAA,CAAgBC,EAAuC,CACrE,OACE,OAAOA,CAAAA,EAAa,UAAYA,CAAAA,CAAS,WAAA,GAAc,QAAA,CAAS,OAAO,CAE3E,CAoDA,IAAMC,EAAAA,CAAW,kBAAA,CAGXC,GAAwB,MAAA,CAM9B,SAASC,EAAoBC,CAAAA,CAAyB,CACpD,OAAKF,EAAAA,CAAsB,IAAA,CAAKE,CAAO,CAAA,CAGhCA,EAAQ,OAAA,CAAQ,IAAA,CAAM,KAAK,CAAA,CAAE,OAAA,CAAQ,KAAM,KAAK,CAAA,CAF9CA,CAGX,CAOA,SAASC,CAAAA,CAAoBD,CAAAA,CAAyB,CACpD,GAAI,CAACA,EAAQ,QAAA,CAAS,GAAG,CAAA,CACvB,OAAOA,EAET,IAAIE,CAAAA,CAAU,GACd,IAAA,IAASC,CAAAA,CAAI,EAAGA,CAAAA,CAAIH,CAAAA,CAAQ,MAAA,CAAQG,CAAAA,EAAK,EAErCH,CAAAA,CAAQG,CAAC,IAAM,GAAA,EACfA,CAAAA,CAAI,EAAIH,CAAAA,CAAQ,MAAA,EAChBH,EAAAA,CAAS,IAAA,CAAKG,EAAQ,KAAA,CAAMG,CAAAA,CAAI,EAAGA,CAAAA,CAAI,CAAC,CAAC,CAAA,EAEzCD,CAAAA,EAAW,OAAO,YAAA,CAAa,MAAA,CAAO,SAASF,CAAAA,CAAQ,KAAA,CAAMG,EAAI,CAAA,CAAGA,CAAAA,CAAI,CAAC,CAAA,CAAG,EAAE,CAAC,CAAA,CAC/EA,GAAK,CAAA,EAELD,CAAAA,EAAWF,EAAQG,CAAC,CAAA,CAGxB,OAAOD,CACT,CAGA,SAASE,EAAAA,CAAkBC,EAAoB,CAC7C,IAAMC,EAAMD,CAAAA,CAAG,UAAA,CAAW,CAAC,CAAA,CAAE,QAAA,CAAS,EAAE,CAAA,CAAE,aAAY,CACtD,OAAO,IAAIC,CAAAA,CAAI,MAAA,GAAW,EAAI,CAAA,CAAA,EAAIA,CAAG,CAAA,CAAA,CAAKA,CAAG,EAC/C,CAOA,SAASC,GAAwBC,CAAAA,CAAuB,CACtD,OACEC,CAAAA,CAAmBD,CAAI,CAAA,CAAE,IAAA,GAAS,QAClClB,CAAAA,CAA0B,IAAA,CAAKkB,CAAI,CAAA,EACnCjB,CAAAA,CAAyB,KAAKiB,CAAI,CAEtC,CASA,SAASE,GAAqBC,CAAAA,CAAuB,CACnD,OAAOP,EAAAA,CAAkBO,CAAAA,CAAM,CAAC,CAAC,CAAA,CAAIZ,CAAAA,CAAoBY,CAAAA,CAAM,MAAM,CAAC,CAAC,CACzE,CAOA,SAASC,GAAoBC,CAAAA,CAAeF,CAAAA,CAAuB,CACjE,IAAMG,EAAef,CAAAA,CAAoBc,CAAK,EACxCL,CAAAA,CAAO,CAAA,EAAGM,CAAY,CAAA,CAAA,EAAIf,CAAAA,CAAoBY,CAAK,CAAC,GAC1D,OAAIA,CAAAA,CAAM,OAAS,CAAA,EAAKJ,EAAAA,CAAwBC,CAAI,CAAA,CAC3C,CAAA,EAAGM,CAAY,CAAA,CAAA,EAAIJ,GAAqBC,CAAK,CAAC,GAEhDH,CACT,CAqBO,SAASO,CAAAA,CACdC,CAAAA,CACAC,CAAAA,CACQ,CACR,OAAI,OAAOA,CAAAA,CAAK,IAAO,QAAA,EAAYA,CAAAA,CAAK,GAAG,MAAA,CAAS,CAAA,CAC3CA,CAAAA,CAAK,EAAA,CAEVtB,EAAgBsB,CAAAA,CAAK,IAAI,EACpBA,CAAAA,CAAK,KAAA,CAEPL,GAAoBI,CAAAA,CAAeC,CAAAA,CAAK,KAAK,CACtD,CAeO,SAASC,EAAAA,CAAmBzB,EAAoD,CACrF,IAAM0B,EAAY1B,CAAAA,CAAS,OAAA,CAAQ,GAAG,CAAA,CACtC,OAAI0B,CAAAA,GAAc,EAAA,CACT,CAAE,KAAA,CAAOlB,CAAAA,CAAoBR,CAAQ,CAAA,CAAG,KAAA,CAAO,EAAG,CAAA,CAEpD,CACL,KAAA,CAAOQ,CAAAA,CAAoBR,EAAS,KAAA,CAAM,CAAA,CAAG0B,CAAS,CAAC,CAAA,CACvD,MAAOlB,CAAAA,CAAoBR,CAAAA,CAAS,MAAM0B,CAAAA,CAAY,CAAC,CAAC,CAC1D,CACF,CA0KA,IAAMC,EAAAA,CAAc,OAAA,CACdC,EAAAA,CAAkB,sBAClBC,EAAAA,CAAa,eAAA,CACbC,GAAY,cAAA,CACZC,EAAAA,CAAiB,uBACjBC,EAAAA,CAAgB,wBAAA,CAChBC,EAAAA,CAAmB,YAAA,CAEzB,SAASjB,CAAAA,CAAmBD,CAAAA,CAA2B,CACrD,GAAIY,EAAAA,CAAY,KAAKZ,CAAI,CAAA,CACvB,OAAO,CAAE,KAAM,QAAA,CAAU,MAAA,CAAQ,OAAOA,CAAI,CAAE,EAEhD,IAAMmB,CAAAA,CAAYN,EAAAA,CAAgB,IAAA,CAAKb,CAAI,CAAA,CAC3C,GAAImB,EACF,OAAO,CAAE,KAAM,OAAA,CAAS,IAAA,CAAM,MAAA,CAAOA,CAAAA,CAAU,CAAC,CAAC,CAAA,CAAG,MAAO,MAAA,CAAOA,CAAAA,CAAU,CAAC,CAAC,CAAE,CAAA,CAElF,IAAMC,EAAQN,EAAAA,CAAW,IAAA,CAAKd,CAAI,CAAA,CAClC,GAAIoB,EACF,OAAO,CAAE,IAAA,CAAM,OAAA,CAAS,MAAO,MAAA,CAAOA,CAAAA,CAAM,CAAC,CAAC,CAAE,EAElD,IAAMC,CAAAA,CAAON,GAAU,IAAA,CAAKf,CAAI,EAChC,GAAIqB,CAAAA,CACF,OAAO,CAAE,IAAA,CAAM,OAAQ,IAAA,CAAM,MAAA,CAAOA,CAAAA,CAAK,CAAC,CAAC,CAAE,CAAA,CAE/C,GAAIrB,CAAAA,GAAS,MAAA,EAAUA,IAAS,OAAA,CAC9B,OAAO,CAAE,IAAA,CAAM,cAAe,IAAA,CAAMA,CAAK,EAE3C,IAAMsB,CAAAA,CAAWN,GAAe,IAAA,CAAKhB,CAAI,CAAA,CACzC,GAAIsB,EACF,OAAO,CACL,KAAM,WAAA,CACN,IAAA,CAAMA,EAAS,CAAC,CAAA,CAChB,KAAA,CAAO,MAAA,CAAOA,EAAS,CAAC,CAAC,CAC3B,CAAA,CAEF,IAAMC,EAAUN,EAAAA,CAAc,IAAA,CAAKjB,CAAI,CAAA,CACvC,GAAIuB,CAAAA,CACF,OAAO,CACL,IAAA,CAAM,UAAA,CACN,GAAIA,CAAAA,CAAQ,CAAC,CAAA,CACb,KAAA,CAAO,OAAOA,CAAAA,CAAQ,CAAC,CAAC,CAC1B,CAAA,CAEF,GAAIvB,CAAAA,GAAS,cAAA,CACX,OAAO,CAAE,KAAM,cAAe,CAAA,CAEhC,IAAMwB,CAAAA,CAAaN,EAAAA,CAAiB,KAAKlB,CAAI,CAAA,CAC7C,GAAIwB,CAAAA,CACF,OAAO,CAAE,IAAA,CAAM,aAAA,CAAe,MAAO/B,CAAAA,CAAoB+B,CAAAA,CAAW,CAAC,CAAC,CAAE,CAAA,CAK1E,GAAIxB,EAAK,QAAA,CAAS,GAAG,EAAG,CACtB,GAAM,CAAE,KAAA,CAAAK,CAAAA,CAAO,KAAA,CAAAF,CAAM,EAAIO,EAAAA,CAAmBV,CAAI,EAChD,OAAO,CAAE,KAAM,MAAA,CAAQ,KAAA,CAAAK,CAAAA,CAAO,KAAA,CAAAF,CAAM,CACtC,CACA,OAAO,CAAE,IAAA,CAAM,OAAQ,QAAA,CAAU,IAAK,CACxC,CAOA,SAASsB,EAAAA,CAAgBC,CAAAA,CAAmC,CAC1D,OAAIA,CAAAA,CAAM,OAAS,MAAA,CACVA,CAAAA,CAAM,QAAA,CAAW,GAAK,CAAC,CAAE,KAAM,MAAA,CAAQ,QAAA,CAAU,IAAK,CAAC,CAAA,CAEzD,CAAC,CAAE,KAAM,MAAA,CAAQ,QAAA,CAAU,IAAK,CAAC,CAC1C,CAQO,SAASC,CAAAA,CAAc1C,CAAAA,CAAqD,CACjF,IAAM2C,CAAAA,CAAM3C,CAAAA,EAAY,GAClB4C,CAAAA,CAAiB/C,CAAAA,CAA0B,KAAK8C,CAAG,CAAA,CACnDE,CAAAA,CAAiBD,CAAAA,CACnBD,EAAI,KAAA,CAAM,CAAA,CAAGA,EAAI,MAAA,CAASC,CAAAA,CAAe,CAAC,CAAA,CAAE,MAAM,CAAA,CAClDD,CAAAA,CACEG,EAAYhD,CAAAA,CAAyB,IAAA,CAAK+C,CAAc,CAAA,CACxD9B,CAAAA,CAAO+B,EACTD,CAAAA,CAAe,KAAA,CAAM,CAAA,CAAGA,CAAAA,CAAe,OAASC,CAAAA,CAAU,CAAC,EAAE,MAAM,CAAA,CACnED,EACEJ,CAAAA,CAAQzB,CAAAA,CAAmBD,CAAI,CAAA,CAC/BgC,EAAyB,CAAE,GAAA,CAAAJ,EAAK,IAAA,CAAA5B,CAAAA,CAAM,MAAA0B,CAAM,CAAA,CAC9CG,CAAAA,GACFG,CAAAA,CAAO,UAAYH,CAAAA,CAAe,CAAC,EAAE,WAAA,EAAY,CAAA,CAE/CE,IACFC,CAAAA,CAAO,SAAA,CAAYD,CAAAA,CAAU,CAAC,EAAE,WAAA,EAAY,CAAA,CAE9C,IAAME,CAAAA,CAAaR,EAAAA,CAAgBC,CAAK,CAAA,CACxC,OAAIO,CAAAA,CAAW,MAAA,CAAS,IACtBD,CAAAA,CAAO,UAAA,CAAaC,GAEfD,CACT,CAwDO,SAASE,CAAAA,CAAwBC,CAAAA,CAAuB,CAC7D,OAAO,OAAO5C,CAAAA,CAAoB4C,CAAK,CAAC,CAAA,CAC1C,CAQO,SAASC,CAAAA,CACdnD,CAAAA,CACAoD,EACQ,CACR,OAAO,GAAGpD,CAAAA,CAAS,OAAA,CAAQF,EAA0B,EAAE,CAAC,KAAKsD,CAAI,CAAA,CACnE,CCxjBA,IAAMC,GAAYvE,CAAAA,EAChB,OAAOA,GAAU,QAAA,EAAYA,CAAAA,GAAU,MAAQ,CAAC,KAAA,CAAM,OAAA,CAAQA,CAAK,EAE/DwE,CAAAA,CAAYxE,CAAAA,EAAuC,CACvD,GAAI,OAAOA,GAAU,QAAA,EAAY,MAAA,CAAO,QAAA,CAASA,CAAK,EACpD,OAAOA,CAAAA,CAET,GAAI,OAAOA,CAAAA,EAAU,UAAYA,CAAAA,CAAM,IAAA,EAAK,CAAE,MAAA,CAAS,EAAG,CACxD,IAAMiE,EAAS,MAAA,CAAOjE,CAAK,EAC3B,GAAI,MAAA,CAAO,QAAA,CAASiE,CAAM,EACxB,OAAOA,CAEX,CAEF,CAAA,CAEMQ,EAAAA,CAAmCzE,GACvCA,CAAAA,CACG,IAAA,EAAK,CACL,WAAA,GACA,KAAA,CAAM,YAAY,EAClB,MAAA,CAAQQ,CAAAA,EAAUA,EAAM,MAAA,CAAS,CAAC,CAAA,CAEjCkE,CAAAA,CAAyBlE,GAAiE,CAC9F,GAAIA,IAAU,KAAA,EAASA,CAAAA,GAAU,KAC/B,OAAO,KAAA,CAET,GAAIA,CAAAA,GAAU,QACZ,OAAO,OAAA,CAET,GAAIA,CAAAA,GAAU,QAAA,EAAYA,IAAU,MAAA,CAClC,OAAO,QAAA,CAET,GAAIA,IAAU,MAAA,CACZ,OAAO,MAGX,CAAA,CAEMmE,CAAAA,CAAkBrE,GACtBA,CAAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,EAAKA,EAAO,QAAA,CAAS,QAAQ,GAAKA,CAAAA,CAAO,QAAA,CAAS,QAAQ,CAAA,EAAKA,CAAAA,CAAO,QAAA,CAAS,KAAK,EAExGsE,EAAAA,CAA2B,CAC/BC,EACAvE,CAAAA,GACuB,CACvB,IAAMwE,CAAAA,CAAUxE,CAAAA,CAAO,QAAA,CAAS,MAAM,EAChCyE,CAAAA,CAAWzE,CAAAA,CAAO,SAAS,OAAO,CAAA,CAClC0E,EAAS1E,CAAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAKA,EAAO,QAAA,CAAS,IAAI,EACvD2E,CAAAA,CAAY3E,CAAAA,CAAO,SAAS,QAAQ,CAAA,EAAKA,CAAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAC/D4E,CAAAA,CAAYP,EAAerE,CAAM,CAAA,CAEvC,GAAIuE,CAAAA,GAAgB,KAAA,EAASA,CAAAA,GAAgB,QAAA,CAC3C,OAAIC,CAAAA,EAAWI,CAAAA,EAAa,CAACH,CAAAA,CAAiB,EAAA,CAC1CA,GAAYG,CAAAA,EAAa,CAACJ,EAAgB,EAAA,CAC1CA,CAAAA,EAAW,CAACC,CAAAA,CAAiB,EAAA,CAC7BA,GAAY,CAACD,CAAAA,CAAgB,GAC7BI,CAAAA,CAAkB,EAAA,CACtB,MAAA,CAGF,GAAIF,GAAUE,CAAAA,EAAa,CAACD,EAAW,OAAO,EAAA,CAC9C,GAAIA,CAAAA,EAAaC,CAAAA,EAAa,CAACF,CAAAA,CAAQ,OAAO,EAAA,CAC9C,GAAIA,GAAU,CAACC,CAAAA,CAAW,OAAO,EAAA,CACjC,GAAIA,CAAAA,EAAa,CAACD,EAAQ,OAAO,EAAA,CACjC,GAAIE,CAAAA,CAAW,SAEjB,CAAA,CAEMC,EAAAA,CAAmC7E,CAAAA,EAA+C,CAEtF,GADsBA,CAAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAKA,CAAAA,CAAO,SAAS,MAAM,CAAA,EAAKA,CAAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAEjG,OAGF,IAAMuE,CAAAA,CAAcvE,CAAAA,CACjB,IAAIoE,CAAqB,CAAA,CACzB,IAAA,CAAMU,CAAAA,EAAoDA,IAAS,MAAS,CAAA,CAE/E,GAAI,CAACP,CAAAA,CACH,OAGF,IAAMQ,CAAAA,CAAgBT,EAAAA,CAAyBC,CAAAA,CAAavE,CAAM,CAAA,CAClE,GAAI+E,IAAkB,MAAA,CAItB,OAAO,CACL,IAAA,CAAMR,CAAAA,CACN,aAAA,CAAAQ,CACF,CACF,CAAA,CAEMC,EAAAA,CAAwChF,GAA+C,CAC3F,GAAIA,EAAO,MAAA,GAAW,CAAA,CACpB,OAGF,IAAMwE,EAAUxE,CAAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAChCyE,CAAAA,CAAWzE,EAAO,QAAA,CAAS,OAAO,CAAA,CAClC4E,CAAAA,CAAYP,EAAerE,CAAM,CAAA,CACjCiF,EAAWjF,CAAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAKA,CAAAA,CAAO,QAAA,CAAS,KAAK,GAAKA,CAAAA,CAAO,QAAA,CAAS,IAAI,CAAA,CACrFkF,CAAAA,CAAUlF,EAAO,QAAA,CAAS,MAAM,CAAA,EAAKA,CAAAA,CAAO,SAAS,MAAM,CAAA,EAAKA,EAAO,QAAA,CAAS,QAAQ,GAAKA,CAAAA,CAAO,QAAA,CAAS,MAAM,CAAA,CAErHmF,EACAX,CAAAA,EAAWI,CAAAA,EAAa,CAACH,CAAAA,CAC3BU,CAAAA,CAAW,GACFV,CAAAA,EAAYG,CAAAA,EAAa,CAACJ,CAAAA,CACnCW,EAAW,EAAA,CACFX,CAAAA,EAAW,CAACC,CAAAA,CACrBU,CAAAA,CAAW,GACFV,CAAAA,EAAY,CAACD,CAAAA,CACtBW,CAAAA,CAAW,GACFP,CAAAA,GACTO,CAAAA,CAAW,IAGb,IAAIC,CAAAA,CASJ,GARIH,CAAAA,EAAY,CAACC,EACfE,CAAAA,CAAW,EAAA,CACFF,GAAW,CAACD,CAAAA,CACrBG,EAAW,EAAA,CACFR,CAAAA,GACTQ,EAAW,EAAA,CAAA,CAGT,EAAAD,CAAAA,GAAa,MAAA,EAAaC,IAAa,MAAA,CAAA,CAI3C,OAAO,CAAE,QAAA,CAAAD,CAAAA,CAAU,SAAAC,CAAS,CAC9B,CAAA,CAEaC,CAAAA,CAA6B3F,GAA4C,CACpF,IAAMM,EAASmE,EAAAA,CAAgCzE,CAAK,EACpD,GAAIM,CAAAA,CAAO,MAAA,GAAW,CAAA,CAItB,IAAIA,CAAAA,CAAO,MAAA,GAAW,EAAG,CACvB,IAAM8E,EAAOV,CAAAA,CAAsBpE,CAAAA,CAAO,CAAC,CAAC,EAC5C,OAAO8E,CAAAA,CAAO,CAAE,IAAA,CAAAA,CAAK,EAAI,MAC3B,CAEA,OAAOD,EAAAA,CAAgC7E,CAAM,CAAA,EAAKgF,EAAAA,CAAqChF,CAAM,CAAA,CAC/F,CAAA,CAEasF,EAA8B5F,CAAAA,EAA6C,CACtF,GAAI,OAAOA,GAAU,QAAA,CACnB,OAAO2F,EAA0B3F,CAAK,CAAA,CAGxC,GAAI,CAACuE,EAAAA,CAASvE,CAAK,CAAA,CACjB,OAGF,IAAM6F,CAAAA,CAAU7F,EAAM,IAAA,EAAQA,CAAAA,CAAM,WAAaA,CAAAA,CAAM,QAAA,CACjD8F,EAAmB,OAAOD,CAAAA,EAAY,SAAWF,CAAAA,CAA0BE,CAAO,EAAI,MAAA,CAGtFT,CAAAA,CAAAA,CAFa,OAAOS,CAAAA,EAAY,QAAA,CAAWnB,CAAAA,CAAsBmB,CAAAA,CAAQ,MAAK,CAAE,WAAA,EAAa,CAAA,CAAI,MAAA,GAE5EC,GAAkB,IAAA,CACvCT,CAAAA,CAAgBb,CAAAA,CACpBxE,CAAAA,CAAM,eACNA,CAAAA,CAAM,cAAA,EACNA,EAAM,SAAA,EACNA,CAAAA,CAAM,UACR,CAAA,EAAK8F,CAAAA,EAAkB,aAAA,CACjBC,CAAAA,CAAWvB,EACfxE,CAAAA,CAAM,QAAA,EACNA,EAAM,SAAA,EACNA,CAAAA,CAAM,cACNA,CAAAA,CAAM,aACR,CAAA,EAAK8F,CAAAA,EAAkB,SACjBL,CAAAA,CAAWjB,CAAAA,CACfxE,EAAM,QAAA,EACNA,CAAAA,CAAM,WACNA,CAAAA,CAAM,IAAA,EACNA,CAAAA,CAAM,KAAA,EACNA,EAAM,iBAAA,EACNA,CAAAA,CAAM,oBACNA,CAAAA,CAAM,CACR,GAAK8F,CAAAA,EAAkB,QAAA,CACjBJ,CAAAA,CAAWlB,CAAAA,CACfxE,EAAM,QAAA,EACNA,CAAAA,CAAM,WACNA,CAAAA,CAAM,IAAA,EACNA,EAAM,KAAA,EACNA,CAAAA,CAAM,eAAA,EACNA,CAAAA,CAAM,kBACNA,CAAAA,CAAM,CACR,GAAK8F,CAAAA,EAAkB,QAAA,CACjBE,EAAexB,CAAAA,CACnBxE,CAAAA,CAAM,cACNA,CAAAA,CAAM,aAAA,EACNA,EAAM,QAAA,EACNA,CAAAA,CAAM,WACNA,CAAAA,CAAM,QAAA,EACNA,EAAM,SACR,CAAA,CACMiG,CAAAA,CAAgBzB,CAAAA,CACpBxE,EAAM,aAAA,EACNA,CAAAA,CAAM,gBACNA,CAAAA,CAAM,SAAA,EACNA,EAAM,UAAA,EACNA,CAAAA,CAAM,QAAA,EACNA,CAAAA,CAAM,SACR,CAAA,CAEA,GACE,EAAAoF,CAAAA,GAAS,MAAA,EACTC,IAAkB,MAAA,EAClBU,CAAAA,GAAa,MAAA,EACbN,CAAAA,GAAa,QACbC,CAAAA,GAAa,MAAA,EACbM,IAAiB,MAAA,EACjBC,CAAAA,GAAkB,QAKpB,OAAO,CACL,IAAA,CAAAb,CAAAA,CACA,cAAAC,CAAAA,CACA,QAAA,CAAAU,EACA,QAAA,CAAAN,CAAAA,CACA,SAAAC,CAAAA,CACA,YAAA,CAAAM,CAAAA,CACA,aAAA,CAAAC,CACF,CACF,CAAA,CC1EO,SAASC,CAAAA,CAAeC,CAAAA,CAA2B,CAExD,OADsBA,CAAAA,CAAY,EAAA,CACX,CAAA,CAAI,MAAQ,KACrC,CClJA,IAAMC,EAAAA,CAA2B,eAAA,CAC3BC,GAA0B,oBAAA,CAC1BC,EAAAA,CAA2B,aAAA,CAC3BC,CAAAA,CAAiC,EACjCC,EAAAA,CAAiC,EAAA,CAEvC,SAASC,CAAAA,CAAazG,CAAAA,CAAuB,CAC3C,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,CAAG,KAAK,GAAA,CAAI,GAAA,CAAKA,CAAK,CAAC,CACzC,CAEA,SAAS0G,CAAAA,CAAoB1G,CAAAA,CAA2E,CACtG,GAAIA,CAAAA,GAAU,KAAA,EAASA,IAAU,OAAA,EAAWA,CAAAA,GAAU,UAAYA,CAAAA,GAAU,MAAA,CAC1E,OAAOA,CAGX,CAEA,SAAS2G,CAAAA,CAA0BC,EAA6C,CAC9E,OAAOhB,EAA2BgB,CAAQ,CAC5C,CAEA,SAASC,EAA+BD,CAAAA,CAA6C,CACnF,OACE,OAAOA,CAAAA,EAAU,UAAa,QAAA,EAC9B,OAAOA,CAAAA,EAAU,QAAA,EAAa,QAElC,CAEA,SAASE,EAAkBF,CAAAA,CAA6C,CACtE,OACE,OAAOA,CAAAA,EAAU,aAAA,EAAkB,QAAA,EACnC,OAAOA,CAAAA,EAAU,QAAA,EAAa,QAElC,CAEA,SAASG,EAAsBrE,CAAAA,CAA8B,CAC3D,OAAIA,CAAAA,CAAK,OAAS,MAAA,EAAUA,CAAAA,CAAK,OAAS,MAAA,CACjC,MAAA,CAELA,EAAK,IAAA,GAAS,OAAA,CACT,OAAA,CAEFtB,CAAAA,CAAgBsB,EAAK,IAAI,CAAA,CAAI,OAAS,OAC/C,CAEA,SAASsE,EAAAA,CAA2BtE,CAAAA,CAAkB4B,EAA0B,CAC9E,OAAI,OAAO5B,CAAAA,CAAK,KAAA,EAAU,UAAYA,CAAAA,CAAK,KAAA,CAAM,MAAK,CAAE,MAAA,CAAS,CAAA,CACxDA,CAAAA,CAAK,MAAM,IAAA,EAAK,CAErBtB,EAAgBsB,CAAAA,CAAK,IAAI,EACpB4D,EAAAA,CAEFhC,CAAAA,GAAS,MAAA,CAAS+B,EAAAA,CAA0BD,EACrD,CAEA,SAASa,EAAkBC,CAAAA,CAA4B,CACrD,OAAO,OAAOA,CAAAA,CAAM,IAAA,CAAK,KAAA,EAAU,UAAY,MAAA,CAAO,QAAA,CAASA,EAAM,IAAA,CAAK,KAAK,EAC3EA,CAAAA,CAAM,IAAA,CAAK,KAAA,CACXA,CAAAA,CAAM,KACZ,CAEA,SAASC,EAA2BC,CAAAA,CAAeC,CAAAA,CAAuB,CACxE,OAAIA,CAAAA,EAAS,CAAA,CACJ,EAAA,CAGPd,EACCa,CAAAA,EAASZ,EAAAA,CAAiCD,IAAoCc,CAAAA,CAAQ,CAAA,CAE3F,CAEA,SAASC,CAAAA,CAA0BC,CAAAA,CAA6C,CAC9E,IAAMC,CAAAA,CAAqBD,CAAAA,CAAQ,OAAQL,CAAAA,EAElC,CAAC,CADOR,CAAAA,CAAoBQ,CAAAA,CAAM,QAAA,EAAU,IAAI,GACpC,CAACL,CAAAA,CAA+BK,EAAM,QAAQ,CAAA,EAAK,CAACJ,CAAAA,CAAkBI,CAAAA,CAAM,QAAQ,CACxG,EACKO,CAAAA,CAAuB,IAAI,IAC3BC,CAAAA,CAAkB,IAAI,IAE5B,OAAAF,CAAAA,CAAmB,OAAA,CAASN,CAAAA,EAAU,CACpC,IAAMS,CAAAA,CAASjB,EAAoBQ,CAAAA,CAAM,QAAA,EAAU,IAAI,CAAA,CACvD,GAAI,CAACS,CAAAA,CACH,OAEF,IAAMrF,CAAAA,CAAQoF,EAAgB,GAAA,CAAIC,CAAM,EACxC,GAAIrF,CAAAA,CAAO,CACTA,CAAAA,CAAM,KAAK4E,CAAK,CAAA,CAChB,MACF,CACAQ,CAAAA,CAAgB,IAAIC,CAAAA,CAAQ,CAACT,CAAK,CAAC,EACrC,CAAC,CAAA,CAEDQ,EAAgB,OAAA,CAASE,CAAAA,EAAkB,CACzC,IAAMC,CAAAA,CAAgB,CAAC,GAAGD,CAAa,CAAA,CAAE,IAAA,CAAK,CAACE,CAAAA,CAAMC,CAAAA,GAAU,CAC7D,IAAMC,CAAAA,CAAkBf,CAAAA,CAAkBa,CAAI,EAAIb,CAAAA,CAAkBc,CAAK,EACzE,OAAIC,CAAAA,GAAoB,EACfA,CAAAA,CAEFF,CAAAA,CAAK,KAAA,CAAQC,CAAAA,CAAM,KAC5B,CAAC,CAAA,CACDF,EAAc,OAAA,CAAQ,CAACX,EAAOE,CAAAA,GAAU,CACtCK,EAAqB,GAAA,CAAIP,CAAAA,CAAM,MAAOC,CAAAA,CAA2BC,CAAAA,CAAOS,EAAc,MAAM,CAAC,EAC/F,CAAC,EACH,CAAC,CAAA,CAEMJ,CACT,CAEA,SAASQ,GACPC,CAAAA,CACAC,CAAAA,CAC0B,CAC1B,IAAMR,CAAAA,CAASjB,CAAAA,CAAoBwB,CAAAA,EAAoB,IAAI,CAAA,CAC3D,GAAIrB,EAA+BqB,CAAkB,CAAA,CAAG,CACtD,IAAMtB,CAAAA,CAAyB,CAC7B,QAAA,CAAUH,EAAayB,CAAAA,EAAoB,QAAkB,EAC7D,QAAA,CAAUzB,CAAAA,CAAayB,GAAoB,QAAkB,CAC/D,CAAA,CACA,OAAI,OAAOA,CAAAA,EAAoB,YAAA,EAAiB,UAAY,MAAA,CAAO,QAAA,CAASA,EAAmB,YAAY,CAAA,GACzGtB,CAAAA,CAAS,YAAA,CAAeH,EAAayB,CAAAA,CAAmB,YAAY,GAElE,OAAOA,CAAAA,EAAoB,eAAkB,QAAA,EAAY,MAAA,CAAO,QAAA,CAASA,CAAAA,CAAmB,aAAa,CAAA,GAC3GtB,CAAAA,CAAS,cAAgBH,CAAAA,CAAayB,CAAAA,CAAmB,aAAa,CAAA,CAAA,CAEpEP,CAAAA,GACFf,CAAAA,CAAS,IAAA,CAAOe,GAEXf,CACT,CAEA,GAAI,CAACe,CAAAA,CACH,OAGF,IAAMf,CAAAA,CAAyB,CAAE,IAAA,CAAMe,CAAO,CAAA,CAC9C,GAAI,OAAOO,CAAAA,EAAoB,QAAA,EAAa,UAAY,MAAA,CAAO,QAAA,CAASA,CAAAA,CAAmB,QAAQ,EACjG,OAAAtB,CAAAA,CAAS,SAAWsB,CAAAA,CAAmB,QAAA,CAChCtB,EAOT,IAAMwB,CAAAA,CAAAA,CAHJ,OAAOF,CAAAA,EAAoB,eAAkB,QAAA,EAAY,MAAA,CAAO,SAASA,CAAAA,CAAmB,aAAa,EACrGzB,CAAAA,CAAayB,CAAAA,CAAmB,aAAa,CAAA,CAC7C,UAEsB,OAAOC,CAAAA,EAA6B,SAC1D1B,CAAAA,CAAa0B,CAAwB,EACrC,MAAA,CAAA,CAEN,OAAI,OAAOC,CAAAA,EAA0B,WACnCxB,CAAAA,CAAS,aAAA,CAAgBwB,GAGpBxB,CACT,CAEA,SAASyB,EAAAA,CAAOnB,CAAAA,CAAoBiB,CAAAA,CAAyC,CAC3E,IAAMvB,CAAAA,CAAWqB,EAAAA,CAAef,EAAM,QAAA,CAAUiB,CAAwB,EACxE,OAAO,CACL,EAAA,CAAIjB,CAAAA,CAAM,KAAK,EAAA,CACf,KAAA,CAAOA,EAAM,IAAA,CAAK,KAAA,CAClB,gBAAiBA,CAAAA,CAAM,IAAA,CAAK,eAAA,CAC5B,SAAA,CAAWA,EAAM,IAAA,CAAK,SAAA,EAAa,MACnC,IAAA,CAAMA,CAAAA,CAAM,KAAK,IAAA,CACjB,SAAA,CAAWA,EAAM,IAAA,CAAK,SAAA,CACtB,cAAeA,CAAAA,CAAM,IAAA,CAAK,cAC1B,aAAA,CAAe,KAAA,CAAM,QAAQA,CAAAA,CAAM,IAAA,CAAK,aAAa,CAAA,CAAI,CAAC,GAAGA,CAAAA,CAAM,KAAK,aAAa,CAAA,CAAI,OACzF,GAAIN,CAAAA,CAAW,CAAE,QAAA,CAAAA,CAAS,CAAA,CAAI,EAChC,CACF,CAEA,SAAS0B,CAAAA,CAA6BC,CAAAA,CAAsC,CAC1E,IAAMC,EAAmBD,CAAAA,CAAW,GAAA,CAAKE,IAAW,CAClD,GAAGA,EACH,KAAA,CAAOA,CAAAA,CAAM,KAAA,CAAM,GAAA,CAAK/F,GAAS,CAC/B,IAAMkE,EAAWD,CAAAA,CAA0BjE,CAAAA,CAAK,QAAQ,CAAA,CACxD,OAAO,CACL,GAAGA,EACH,GAAIkE,CAAAA,CAAW,CAAE,QAAA,CAAAA,CAAS,EAAI,CAAE,QAAA,CAAU,MAAU,CACtD,CACF,CAAC,CACH,EAAE,CAAA,CAII8B,CAAAA,CAA0B,IAAI,GAAA,CAIpC,OAAAF,CAAAA,CAAiB,OAAA,CAASC,GAAU,CAClCA,CAAAA,CAAM,MAAM,OAAA,CAAS/F,CAAAA,EAAS,CAC5B,IAAMkE,CAAAA,CAAWlE,EAAK,QAAA,CAChB0C,CAAAA,CAAOsB,EAAoBE,CAAAA,EAAU,IAAI,EAC/C,GACE,CAACxB,GACDyB,CAAAA,CAA+BD,CAAQ,CAAA,EACvCE,CAAAA,CAAkBF,CAAQ,CAAA,CAE1B,OAEF,IAAM+B,CAAAA,CAAQD,CAAAA,CAAwB,IAAItD,CAAI,CAAA,CAC1CuD,CAAAA,CACFA,CAAAA,CAAM,KAAKjG,CAAI,CAAA,CAEfgG,EAAwB,GAAA,CAAItD,CAAAA,CAAM,CAAC1C,CAAI,CAAC,EAE5C,CAAC,EACH,CAAC,CAAA,CAIDgG,EAAwB,OAAA,CAASC,CAAAA,EAAU,CACzCA,CAAAA,CAAM,OAAA,CAAQ,CAACjG,CAAAA,CAAM0E,IAAU,CAC7B1E,CAAAA,CAAK,SAAW,CACd,GAAIA,EAAK,QAAA,CACT,aAAA,CAAeyE,CAAAA,CAA2BC,CAAAA,CAAOuB,EAAM,MAAM,CAC/D,EACF,CAAC,EACH,CAAC,CAAA,CAEMH,CACT,CAEA,SAASI,EAAwB5I,CAAAA,CAAsC,CACrE,OACE,KAAA,CAAM,OAAA,CAAQA,CAAK,CAAA,EACnBA,CAAAA,CAAM,MAAA,CAAS,CAAA,EACfA,EAAM,KAAA,CACHkH,CAAAA,EACCA,IAAU,IAAA,EACV,OAAOA,GAAU,QAAA,EACjB,OAAQA,EAAoB,IAAA,EAAS,QAAA,EACrC,MAAM,OAAA,CAASA,CAAAA,CAAoB,KAAK,CAC5C,CAEJ,CAEA,SAAS2B,EAAAA,CAAsB7I,CAAAA,CAAuC,CACpE,OACE,KAAA,CAAM,OAAA,CAAQA,CAAK,CAAA,EACnBA,CAAAA,CAAM,OAAS,CAAA,EACfA,CAAAA,CAAM,KAAA,CACHkH,CAAAA,EACCA,IAAU,IAAA,EACV,OAAOA,GAAU,QAAA,EACjB,OAAQA,EAAqB,KAAA,EAAU,QAAA,EACvC,EACE,OAAQA,EAA6B,IAAA,EAAS,QAAA,EAC9C,MAAM,OAAA,CAASA,CAAAA,CAA8B,KAAK,CAAA,CAExD,CAEJ,CAQO,SAAS4B,GACdC,CAAAA,CACgD,CAChD,IAAMC,CAAAA,CAAe,IAAI,IAGnBC,CAAAA,CAA8B,EAAC,CACrCF,CAAAA,CAAY,QAAQ,CAACrG,CAAAA,CAAM0E,IAAU,CAC/B,OAAO1E,EAAK,KAAA,EAAU,QAAA,EAAYA,CAAAA,CAAK,KAAA,CAAM,MAAK,CAAE,MAAA,GAAW,GACnEuG,CAAAA,CAAa,IAAA,CAAK,CAChB,KAAA,CAAA7B,CAAAA,CACA,IAAA,CAAM,CAAE,GAAG1E,CAAAA,CAAM,KAAA,CAAOA,EAAK,KAAA,CAAM,IAAA,EAAO,CAAA,CAC1C,QAAA,CAAUiE,CAAAA,CAA0BjE,CAAAA,CAAK,QAAQ,CACnD,CAAC,EACH,CAAC,CAAA,CACD,IAAMwG,CAAAA,CAA2B,CAC/B,KAAA,CAAO5B,CAAAA,CACL2B,EAAa,MAAA,CAAO,CAAC,CAAE,IAAA,CAAAvG,CAAK,IAAMqE,CAAAA,CAAsBrE,CAAI,CAAA,GAAM,OAAO,CAC3E,CAAA,CACA,IAAA,CAAM4E,EACJ2B,CAAAA,CAAa,MAAA,CAAO,CAAC,CAAE,IAAA,CAAAvG,CAAK,CAAA,GAAMqE,EAAsBrE,CAAI,CAAA,GAAM,MAAM,CAC1E,CACF,EAEAuG,CAAAA,CAAa,OAAA,CAAS/B,CAAAA,EAAU,CAC9B,IAAM5C,CAAAA,CAAOyC,CAAAA,CAAsBG,EAAM,IAAI,CAAA,CACvCiC,EAAYnC,EAAAA,CAA2BE,CAAAA,CAAM,IAAA,CAAM5C,CAAI,EACvDzD,CAAAA,CAAM,CAAA,EAAGyD,CAAI,CAAA,CAAA,EAAI6E,CAAS,GAC1BC,CAAAA,CAAiBJ,CAAAA,CAAa,GAAA,CAAInI,CAAG,EACvCuI,CAAAA,CACFA,CAAAA,CAAe,QAAQ,IAAA,CAAKlC,CAAK,EAEjC8B,CAAAA,CAAa,GAAA,CAAInI,CAAAA,CAAK,CAAE,KAAAyD,CAAAA,CAAM,SAAA,CAAA6E,EAAW,OAAA,CAAS,CAACjC,CAAK,CAAE,CAAC,EAE/D,CAAC,CAAA,CAED,IAAMyB,CAAAA,CAAqB,GACrBU,CAAAA,CAAyB,GAE/B,OAAAL,CAAAA,CAAa,OAAA,CAASM,CAAAA,EAAW,CAC/B,IAAMzB,CAAAA,CAAgB,CAAC,GAAGyB,CAAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,CAACxB,CAAAA,CAAMC,IAAU,CAC9D,IAAMC,EAAkBf,CAAAA,CAAkBa,CAAI,EAAIb,CAAAA,CAAkBc,CAAK,CAAA,CACzE,OAAIC,IAAoB,CAAA,CACfA,CAAAA,CAEFF,EAAK,KAAA,CAAQC,CAAAA,CAAM,KAC5B,CAAC,CAAA,CACKwB,CAAAA,CAA2BL,CAAAA,CAAyBI,EAAO,IAAI,CAAA,CAC/Db,EAAmB,CACvB,IAAA,CAAMa,EAAO,SAAA,CACb,KAAA,CAAOzB,CAAAA,CAAc,GAAA,CAAKX,GACxBmB,EAAAA,CAAOnB,CAAAA,CAAOqC,EAAyB,GAAA,CAAIrC,CAAAA,CAAM,KAAK,CAAC,CACzD,CACF,CAAA,CACA,GAAIoC,CAAAA,CAAO,IAAA,GAAS,OAAQ,CAC1BD,CAAAA,CAAU,KAAKZ,CAAK,CAAA,CACpB,MACF,CACAE,EAAM,IAAA,CAAKF,CAAK,EAClB,CAAC,CAAA,CAEM,CAAE,KAAA,CAAAE,CAAAA,CAAO,UAAAU,CAAU,CAC5B,CAqBO,SAASG,CAAAA,CACdC,EACgD,CAChD,OAAIb,EAAwBa,CAAAA,EAAM,KAAK,CAAA,CAC9B,CACL,MAAOnB,CAAAA,CAA6BmB,CAAAA,CAAK,KAAK,CAAA,CAC9C,SAAA,CAAWnB,EACT,KAAA,CAAM,OAAA,CAAQmB,CAAAA,EAAM,cAAc,EAC9BA,CAAAA,CAAK,cAAA,CACLb,EAAwBa,CAAAA,EAAM,SAAS,EACrCA,CAAAA,CAAK,SAAA,CACL,EACR,CACF,CAAA,CAGEZ,EAAAA,CAAsBY,GAAM,KAAK,CAAA,CAC5BX,GAAyBW,CAAAA,CAAK,KAAK,CAAA,CAGrC,CACL,MAAOnB,CAAAA,CAA6B,KAAA,CAAM,QAAQmB,CAAAA,EAAM,UAAU,EAAIA,CAAAA,CAAK,UAAA,CAAa,EAAE,EAC1F,SAAA,CAAWnB,CAAAA,CACT,MAAM,OAAA,CAAQmB,CAAAA,EAAM,cAAc,CAAA,CAC9BA,CAAAA,CAAK,cAAA,CACLb,CAAAA,CAAwBa,GAAM,SAAS,CAAA,CACrCA,EAAK,SAAA,CACL,EACR,CACF,CACF,CC7VO,IAAMC,GAAkB,GAAA,CAkC/B,IAAMC,GAAoB,CACxB,eAAA,CACA,eACA,uBAAA,CACA,sBAAA,CACA,WACA,eAAA,CACA,aAAA,CACA,cACF,CAAA,CACMC,EAAAA,CAAwB,CAC5B,YAAA,CACA,aAAA,CACA,eACA,eAAA,CACA,cACF,CAAA,CACMC,EAAAA,CAAsB,CAC1B,QAAA,CACA,OAAA,CACA,UACA,QAAA,CACA,QAAA,CACA,QACA,WAAA,CACA,cAAA,CACA,kBAAA,CACA,WAAA,CACA,WACA,aAAA,CACA,eACF,EACMC,EAAAA,CAAqB,CAAC,OAAQ,eAAe,CAAA,CAC7CC,EAAAA,CAAuB,IAAI,IAAY,CAC3C,OAAA,CACA,QACA,YAAA,CACA,gBAAA,CAIA,YACA,eAAA,CACA,aAAA,CACA,UAAA,CACA,YAAA,CACA,cACA,eAAA,CACA,WAAA,CACA,YACA,YAAA,CACA,cAAA,CACA,iBAGA,eAAA,CACA,GAAGJ,EAAAA,CACH,GAAGC,GACH,GAAGC,EAAAA,CACH,GAAGC,EACL,CAAC,EAMD,SAASE,EAAAA,CAAOC,CAAAA,CAAiCpJ,CAAAA,CAAab,EAAsB,CAClF,MAAA,CAAO,eAAeiK,CAAAA,CAAQpJ,CAAAA,CAAK,CAAE,KAAA,CAAAb,CAAAA,CAAO,UAAA,CAAY,IAAA,CAAM,SAAU,IAAA,CAAM,YAAA,CAAc,IAAK,CAAC,EACpG,CAMA,SAASkK,EAAAA,CAActD,CAAAA,CAA4E,CAGjG,OAAO,CACL,CAAA,CAAG,OAAO,QAAA,CAASA,CAAAA,EAAU,CAAC,CAAA,CAAKA,CAAAA,CAAU,CAAA,CAAe,CAAA,CAC5D,EAAG,MAAA,CAAO,QAAA,CAASA,GAAU,CAAC,CAAA,CAAKA,EAAU,CAAA,CAAe,CAC9D,CACF,CAEA,SAASuD,EAAAA,CAAkBvD,CAAAA,CAAmE,CAC5F,IAAMwD,CAAAA,CAAaxE,EAA2BgB,CAAQ,CAAA,CACtD,GAAKwD,CAAAA,CAIL,OAAO,CAAE,GAAGA,CAAW,CACzB,CAEA,SAASC,EAAAA,CAAkBC,CAAAA,CAAgE,CACzF,OAAIA,EAAK,IAAA,GAAS,cAAA,CACT,CAAE,IAAA,CAAM,KAAM,EAGnBA,CAAAA,CAAK,IAAA,GAAS,QAAA,EAAYA,CAAAA,CAAK,OAAS,QAAA,EAAYA,CAAAA,CAAK,OAAS,QAAA,EAAYA,CAAAA,CAAK,OAAS,QAAA,CAKvF,CAAE,IAAA,CAAM,WAAA,CAAa,QAASA,CAAAA,CAAK,IAAK,EAG7CA,CAAAA,CAAK,IAAA,GAAS,eAAiBA,CAAAA,CAAK,IAAA,GAAS,SAAA,EAAaA,CAAAA,CAAK,OAAS,aAAA,CACnE,CAAE,KAAM,WAAA,CAAa,OAAA,CAASA,EAAK,IAAK,CAAA,CAG7CA,EAAK,IAAA,GAAS,aAAA,CACT,CAAE,IAAA,CAAM,UAAW,EAGxBA,CAAAA,CAAK,IAAA,GAAS,oBAAsBA,CAAAA,CAAK,IAAA,GAAS,oBAAA,CAC7C,CAAE,KAAMA,CAAAA,CAAK,IAAK,EAGpB,CAAE,IAAA,CAAMA,EAAK,IAAwB,CAC9C,CA8BA,SAASC,GAAmBD,CAAAA,CAAyC,CACnE,IAAME,CAAAA,CAAkC,CACtC,SAAUN,EAAAA,CAAcI,CAAAA,CAAK,QAAQ,CACvC,EAEMG,CAAAA,CAAWH,CAAAA,CAAK,SAClBG,CAAAA,GACFD,CAAAA,CAAU,SAAWC,CAAAA,CAAAA,CAEnBH,CAAAA,CAAK,MAAA,GAAW,QAAA,GAClBE,EAAU,MAAA,CAAS,QAAA,CAAA,CAGrB,IAAMf,CAAAA,CAAOa,CAAAA,CAAK,MAAQ,EAAC,CAC3B,OAAA,CACE,OAAOb,EAAK,SAAA,EAAc,QAAA,EAC1BA,EAAK,SAAA,GAAc,OAAA,EACnBA,EAAK,SAAA,GAAc,MAAA,EACnBA,CAAAA,CAAK,SAAA,GAAc,QACnBA,CAAAA,CAAK,UAAA,GAAe,eACpBA,CAAAA,CAAK,UAAA,GAAe,gBAEpBe,CAAAA,CAAU,IAAA,CAAO,CACf,GAAI,OAAOf,CAAAA,CAAK,SAAA,EAAc,SAAW,CAAE,SAAA,CAAWA,EAAK,SAAU,CAAA,CAAI,EAAC,CAC1E,GAAIA,EAAK,SAAA,CAAY,CAAE,UAAWA,CAAAA,CAAK,SAAU,EAAI,EAAC,CACtD,GAAIA,CAAAA,CAAK,WAAa,CAAE,UAAA,CAAYA,EAAK,UAAW,CAAA,CAAI,EAAC,CACzD,GAAI,OAAOA,CAAAA,CAAK,aAAgB,QAAA,CAAW,CAAE,YAAaA,CAAAA,CAAK,WAAY,EAAI,EAAC,CAChF,GAAI,OAAOA,EAAK,aAAA,EAAkB,QAAA,CAAW,CAAE,aAAA,CAAeA,CAAAA,CAAK,aAAc,CAAA,CAAI,EACvF,CAAA,CAAA,CAAA,CAGE,OAAOA,CAAAA,CAAK,YAAA,EAAiB,UAAY,OAAOA,CAAAA,CAAK,gBAAmB,QAAA,IAC1Ee,CAAAA,CAAU,SAAA,CAAY,CACpB,GAAI,OAAOf,CAAAA,CAAK,cAAiB,QAAA,CAAW,CAAE,aAAcA,CAAAA,CAAK,YAAa,CAAA,CAAI,GAClF,GAAI,OAAOA,EAAK,cAAA,EAAmB,QAAA,CAAW,CAAE,cAAA,CAAgBA,CAAAA,CAAK,cAAe,CAAA,CAAI,EAC1F,CAAA,CAAA,CAGKe,CACT,CAEA,SAASE,GAAgBJ,CAAAA,CAAkD,CACzE,IAAMb,CAAAA,CAAOa,CAAAA,CAAK,MAAQ,EAAC,CACrBK,EAA4B,EAAC,CAEnCd,GAAoB,OAAA,CAAShJ,CAAAA,EAAQ,CACnC,IAAMb,EAAQyJ,CAAAA,CAAK5I,CAAG,EAClBb,CAAAA,GAAU,MAAA,GACX2K,EAAmC9J,CAAG,CAAA,CAAIb,CAAAA,EAE/C,CAAC,EAED,IAAM4K,CAAAA,CAASjB,GAAkB,MAAA,CAAgC,CAACkB,EAAKhK,CAAAA,GAAQ,CAC7E,IAAMb,CAAAA,CAAQyJ,EAAK5I,CAAG,CAAA,CACtB,OAAIb,CAAAA,GAAU,MAAA,GACZ6K,EAAIhK,CAAG,CAAA,CAAIb,CAAAA,CAAAA,CAEN6K,CACT,EAAG,EAAE,EACD,MAAA,CAAO,IAAA,CAAKD,CAAM,CAAA,CAAE,MAAA,CAAS,CAAA,GAC/BD,CAAAA,CAAO,OAASC,CAAAA,CAAAA,CAGlB,IAAME,EAAalB,EAAAA,CAAsB,MAAA,CAA2C,CAACiB,CAAAA,CAAKhK,CAAAA,GAAQ,CAChG,IAAMb,EAAQyJ,CAAAA,CAAK5I,CAAG,EACtB,OAAI,OAAOb,GAAU,QAAA,GACnB6K,CAAAA,CAAIhK,CAAG,CAAA,CAAIb,GAEN6K,CACT,CAAA,CAAG,EAAE,CAAA,CACL,OAAI,MAAA,CAAO,IAAA,CAAKC,CAAU,CAAA,CAAE,OAAS,CAAA,GACnCH,CAAAA,CAAO,WAAaG,CAAAA,CAAAA,CAGf,MAAA,CAAO,KAAKH,CAAM,CAAA,CAAE,MAAA,CAAS,CAAA,CAAIA,EAAS,MACnD,CAEA,SAASI,EAAAA,CAAsBT,CAAAA,CAAqD,CAClF,IAAMU,CAAAA,CAA6B,EAAC,CAC9BvB,EAAOa,CAAAA,CAAK,IAAA,EAAQ,EAAC,CAE3B,OAAIb,EAAK,QAAA,GAAa,MAAA,EACpBuB,CAAAA,CAAK,IAAA,CAAK,CACR,MAAA,CAAQ,QAAA,CACR,GAAIvB,CAAAA,CAAK,QAAA,CACT,KAAMA,CAAAA,CAAK,UACb,CAAC,CAAA,CAGCA,EAAK,WAAA,GAAgB,MAAA,EACvBuB,EAAK,IAAA,CAAK,CACR,OAAQ,WAAA,CACR,EAAA,CAAIvB,CAAAA,CAAK,WAAA,CACT,KAAMA,CAAAA,CAAK,aACb,CAAC,CAAA,CAGIuB,CAAAA,CAAK,OAAS,CAAA,CAAIA,CAAAA,CAAO,MAClC,CAEA,SAASC,EAAAA,CAAgBX,CAAAA,CAAwD,CAC/E,IAAMb,CAAAA,CAAOa,EAAK,IAAA,EAAQ,EAAC,CACrBY,CAAAA,CAAkC,EAAC,CAEzC,OAAA,MAAA,CAAO,QAAQzB,CAAI,CAAA,CAAE,QAAQ,CAAC,CAAC5I,EAAKb,CAAK,CAAA,GAAM,CAIzC+J,EAAAA,CAAqB,GAAA,CAAIlJ,CAAG,CAAA,EAAK,EAAEA,IAAQ,OAAA,EAAW,CAAC,KAAA,CAAM,OAAA,CAAQb,CAAK,CAAA,CAAA,EAAA,CAGzEa,CAAAA,GAAQ,eAAiBA,CAAAA,GAAQ,eAAA,GAAoByJ,EAAK,IAAA,GAAS,MAAA,EAGxEN,EAAAA,CAAOkB,CAAAA,CAAQrK,EAAKb,CAAK,EAC3B,CAAC,CAAA,CAAA,CAMEsK,CAAAA,CAAK,OAAS,QAAA,EAAYA,CAAAA,CAAK,IAAA,GAAS,QAAA,EAAYA,EAAK,IAAA,GAAS,QAAA,EAAYA,EAAK,IAAA,GAAS,QAAA,GAC7F,EAAE,OAAOY,CAAAA,CAAO,UAAA,EAAe,QAAA,EAAYA,EAAO,UAAA,CAAW,IAAA,GAAO,MAAA,CAAS,CAAA,CAAA,GAE7EA,EAAO,UAAA,CAAaZ,CAAAA,CAAK,IAAA,CAAA,CAGpB,MAAA,CAAO,KAAKY,CAAM,CAAA,CAAE,OAAS,CAAA,CAAIA,CAAAA,CAAS,MACnD,CAEA,SAASC,EAAAA,CAAeC,CAAAA,CAAsBC,EAAyD,CACrG,IAAMC,EAAsD,EAAC,CAsB7D,GApBAF,CAAAA,CAAM,OAAA,CAASd,CAAAA,EAAS,CACtB,IAAMb,CAAAA,CAAOa,CAAAA,CAAK,MAAQ,EAAC,CACrBpD,EAA0D,EAAC,CAC3DqE,CAAAA,CACJ9B,CAAAA,CAAK,OAAS,OAAA,EAAWA,CAAAA,CAAK,OAAS,MAAA,CACnCA,CAAAA,CAAK,KACL,MAAA,CAEF8B,CAAAA,GACFrE,CAAAA,CAAM,IAAA,CAAOqE,GAEX9B,CAAAA,CAAK,aAAA,EAAiB,OAAOA,CAAAA,CAAK,aAAA,EAAkB,WACtDvC,CAAAA,CAAM,aAAA,CAAgB,CAAE,GAAGuC,EAAK,aAAc,CAAA,CAAA,CAG5C,OAAO,IAAA,CAAKvC,CAAK,EAAE,MAAA,CAAS,CAAA,EAC9B8C,EAAAA,CAAOsB,CAAAA,CAAWhB,EAAK,EAAA,CAAIpD,CAAK,EAEpC,CAAC,CAAA,CAEG,SAAO,IAAA,CAAKoE,CAAS,CAAA,CAAE,MAAA,GAAW,GAAK,MAI3C,OAAO,CACL,GAAI,MAAA,CAAO,IAAA,CAAKA,CAAS,CAAA,CAAE,OAAS,CAAA,CAAI,CAAE,UAAAA,CAAU,CAAA,CAAI,EAAC,CACzD,GAA+D,EACjE,CACF,CAEA,SAASE,EAAAA,CAAoBnK,EAAyD,CACpF,IAAMoK,EAAiB,MAAA,CAAOpK,CAAAA,EAAY,EAAE,CAAA,CAAE,WAAA,GAC9C,OAAIoK,CAAAA,CAAe,SAAS,OAAO,CAAA,CAAU,QACzCA,CAAAA,CAAe,QAAA,CAAS,OAAO,CAAA,CAAU,OAAA,CACzCA,CAAAA,CAAe,QAAA,CAAS,SAAS,CAAA,EAAKA,CAAAA,CAAe,SAAS,MAAM,CAAA,EAAKA,EAAe,QAAA,CAAS,KAAK,CAAA,CACjG,SAAA,CAELA,EAAe,QAAA,CAAS,UAAU,GAAKA,CAAAA,CAAe,QAAA,CAAS,MAAM,CAAA,CAAU,UAAA,CAC5E,SACT,CAEA,SAASC,EAAAA,CAAmBpB,CAAAA,CAAgC,CAC1D,IAAMb,CAAAA,CAAOa,EAAK,IAAA,EAAQ,EAAC,CAC3B,GAAI,MAAM,OAAA,CAAQb,CAAAA,CAAK,aAAa,CAAA,EAAKA,CAAAA,CAAK,cAAc,MAAA,CAAS,CAAA,CACnE,OAAOA,CAAAA,CAAK,cAGd,GAAM,CAAE,MAAAd,CAAAA,CAAO,SAAA,CAAAU,CAAU,CAAA,CAAIG,CAAAA,CAAsBC,CAAI,CAAA,CACvD,GAAIJ,CAAAA,CAAU,MAAA,CAAS,EACrB,OAAOA,CAAAA,CAET,GAAIV,CAAAA,CAAM,MAAA,CAAS,CAAA,CACjB,OAAOA,EAGT,IAAMgD,CAAAA,CAAa,KAAK,GAAA,CACtBjC,EAAAA,CACA,OAAOD,CAAAA,CAAK,UAAA,EAAe,QAAA,EAAY,MAAA,CAAO,SAASA,CAAAA,CAAK,UAAU,GAAKA,CAAAA,CAAK,UAAA,CAAa,EACzFA,CAAAA,CAAK,UAAA,CACL,EACN,CAAA,CAEA,OAAO,CACL,CACE,KAAM,OAAA,CACN,KAAA,CAAO,MAAM,IAAA,CAAK,CAAE,MAAA,CAAQkC,CAAW,EAAG,CAACC,CAAAA,CAAGxE,IAAU,CACtD,IAAMyE,EAAgB3F,CAAAA,CAAekB,CAAK,CAAA,CAC1C,OAAO,CACL,EAAA,CAAI,CAAA,EAAGA,EAAQ,CAAC,CAAA,CAAA,CAChB,MAAO,CAAA,EAAGA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CACnB,UAAW,KAAA,CACX,IAAA,CAAMyE,IAAkB,KAAA,CAAQ,WAAA,CAAc,YAC9C,aAAA,CAAeA,CACjB,CACF,CAAC,CACH,CACF,CACF,CAEA,SAASC,EAAAA,CAAqBxB,EAAsC,CAelE,IAAMyB,CAAAA,CAAAA,CAVJzB,CAAAA,CAAK,OAAS,cAAA,CACV,CAAC,CAAE,IAAA,CAAM,MAAA,CAAQ,OAAQoB,EAAAA,CAAmBpB,CAAI,CAAE,CAAC,GAClD,IAAM,CACL,GAAM,CAAE,KAAA,CAAA3B,EAAO,SAAA,CAAAU,CAAU,EAAIG,CAAAA,CAAsBc,CAAAA,CAAK,IAAI,CAAA,CAC5D,OAAO,CACL,CAAE,IAAA,CAAM,QAAkB,MAAA,CAAQ3B,CAAM,CAAA,CACxC,CAAE,KAAM,MAAA,CAAiB,MAAA,CAAQU,CAAU,CAC7C,CACF,IAAG,EAEyC,OAAA,CAAQ,CAAC,CAAE,KAAA/E,CAAAA,CAAM,MAAA,CAAA0H,CAAO,CAAA,GACxEA,CAAAA,CAAO,QAASvD,CAAAA,EACdA,CAAAA,CAAM,KAAA,CAAM,GAAA,CAAK/F,IAAU,CAYzB,EAAA,CAAIF,EAAkBiG,CAAAA,CAAM,IAAA,CAAM/F,CAAI,CAAA,CACtC,KAAA,CAAOA,CAAAA,CAAK,KAAA,CACZ,KAAA4B,CAAAA,CACA,KAAA,CAAOmE,EAAM,IAAA,CACb,IAAA,CAAMrH,EAAgBsB,CAAAA,CAAK,IAAI,CAAA,CAAI,OAAA,CAAU,UAC7C,MAAA,CAAQ8I,EAAAA,CAAoB9I,EAAK,IAAI,CAAA,CACrC,KAAMA,CAAAA,CAAK,IAAA,CACX,SAAA,CAAWA,CAAAA,CAAK,UAChB,SAAA,CAAWA,CAAAA,CAAK,cAChB,eAAA,CAAiBA,CAAAA,CAAK,gBACtB,aAAA,CAAe,KAAA,CAAM,OAAA,CAAQA,CAAAA,CAAK,aAAa,CAAA,CAAI,CAAC,GAAGA,CAAAA,CAAK,aAAa,EAAI,MAAA,CAC7E,MAAA,CAAQyH,GAAkBzH,CAAAA,CAAK,QAAQ,CACzC,CAAA,CAAE,CACJ,CACF,CAAA,CAQMuJ,CAAAA,CAAW,IAAI,GAAA,CACnBF,CAAAA,CAAU,MAAA,CAAQG,CAAAA,EAAaA,EAAS,IAAA,GAAS,OAAO,EAAE,GAAA,CAAKA,CAAAA,EAAaA,EAAS,EAAE,CACzF,CAAA,CACA,GAAID,EAAS,IAAA,CAAO,CAAA,CAClB,QAAWC,CAAAA,IAAYH,CAAAA,CACjBG,EAAS,IAAA,GAAS,MAAA,EAAUD,CAAAA,CAAS,GAAA,CAAIC,EAAS,EAAE,CAAA,GACtDA,EAAS,EAAA,CAAK7H,CAAAA,CAAuB6H,EAAS,EAAA,CAAI,MAAM,CAAA,CAAA,CAI9D,OAAOH,CACT,CAOA,SAASI,GACPD,CAAAA,CACAD,CAAAA,CACoB,CACpB,GAAIC,CAAAA,CAAS,IAAA,GAAS,MAAA,EAAU,CAACA,CAAAA,CAAS,EAAA,CAAG,SAAS,QAAQ,CAAA,CAAG,OACjE,IAAMjK,CAAAA,CAAOiK,CAAAA,CAAS,EAAA,CAAG,MAAM,CAAA,CAAG,EAAgB,EAClD,OAAOD,CAAAA,CAAS,IAAIhK,CAAI,CAAA,CAAIA,CAAAA,CAAO,MACrC,CAEA,SAASmK,EAAAA,CAAwBL,EAAoD,CACnF,OAAO,IAAI,GAAA,CACTA,CAAAA,CAAU,OAAQG,CAAAA,EAAaA,CAAAA,CAAS,OAAS,MAAM,CAAA,CAAE,IAAKA,CAAAA,EAAaA,CAAAA,CAAS,EAAE,CACxF,CACF,CAEA,SAASG,GAAyB/B,CAAAA,CAAsC,CACtE,IAAMb,CAAAA,CAAOa,CAAAA,CAAK,MAAQ,EAAC,CAE3B,GAAIA,CAAAA,CAAK,OAAS,oBAAA,CAEhB,OAAA,CADa,MAAM,OAAA,CAAQb,CAAAA,CAAK,IAAI,CAAA,CAAIA,CAAAA,CAAK,IAAA,CAAO,IAEjD,MAAA,CAAQ6C,CAAAA,EAA+C,CAAC,CAACA,CAAAA,EAAO,OAAOA,CAAAA,CAAI,EAAA,EAAO,QAAQ,CAAA,CAC1F,IAAKA,CAAAA,GAAS,CACb,GAAInI,CAAAA,CAAwBmI,CAAAA,CAAI,EAAE,CAAA,CAClC,KAAA,CAAO,OAAOA,CAAAA,CAAI,OAAU,QAAA,CAAWA,CAAAA,CAAI,MAAQA,CAAAA,CAAI,EAAA,CACvD,KAAM,OAAA,CACN,MAAA,CAAQ,OACV,CAAA,CAAE,EAGN,GAAIhC,CAAAA,CAAK,OAAS,MAAA,CAAQ,CACxB,IAAMiC,CAAAA,CAAgB,IAAA,CAAK,GAAA,CAAI,CAAA,CAAG,KAAK,GAAA,CAAI,EAAA,CAAI,OAAO9C,CAAAA,CAAK,aAAa,GAAK,EAAE,CAAC,CAAA,CAC1E+C,CAAAA,CAAY,KAAK,GAAA,CAAI,CAAA,CAAG,OAAO/C,CAAAA,CAAK,SAAS,GAAK,CAAC,CAAA,CACzD,OAAO,CACL,GAAG,KAAA,CAAM,IAAA,CAAK,CAAE,MAAA,CAAQ8C,CAAc,EAAG,CAACX,CAAAA,CAAGxE,CAAAA,IAAW,CACtD,GAAI,CAAA,MAAA,EAASA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CACtB,KAAA,CAAO,GAAGA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CACnB,IAAA,CAAM,QACN,MAAA,CAAQ,OACV,EAAE,CAAA,CACF,CACE,GAAI,CAAA,KAAA,EAAQoF,CAAS,CAAA,CAAA,CACrB,KAAA,CAAO,QAAQA,CAAS,CAAA,CAAA,CACxB,KAAM,MAAA,CACN,MAAA,CAAQ,MACV,CACF,CACF,CAEA,GAAIlC,EAAK,IAAA,GAAS,OAAA,CAAS,CACzB,IAAMmC,CAAAA,CAAa,KAAK,GAAA,CAAI,CAAA,CAAG,IAAA,CAAK,GAAA,CAAI,GAAI,MAAA,CAAOhD,CAAAA,CAAK,UAAU,CAAA,EAAK,EAAE,CAAC,CAAA,CAC1E,OAAO,CACL,GAAG,MAAM,IAAA,CAAK,CAAE,OAAQgD,CAAW,CAAA,CAAG,CAACb,CAAAA,CAAGxE,CAAAA,IAAW,CACnD,EAAA,CAAI,CAAA,MAAA,EAASA,EAAQ,CAAC,CAAA,CAAA,CACtB,MAAO,CAAA,EAAGA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CACnB,IAAA,CAAM,OAAA,CACN,MAAA,CAAQ,OACV,CAAA,CAAE,CAAA,CACF,CACE,EAAA,CAAI,cAAA,CACJ,MAAO,OAAA,CACP,IAAA,CAAM,OACR,CACF,CACF,CAEA,GAAIkD,EAAK,IAAA,GAAS,kBAAA,CAAoB,CACpC,IAAMoC,CAAAA,CAAY,IAAA,CAAK,GAAA,CAAI,EAAG,IAAA,CAAK,GAAA,CAAI,GAAI,MAAA,CAAOjD,CAAAA,CAAK,SAAS,CAAA,EAAK,EAAE,CAAC,CAAA,CAClEkD,EAAiB,IAAA,CAAK,GAAA,CAAI,EAAG,MAAA,CAAOlD,CAAAA,CAAK,cAAc,CAAA,EAAK,CAAC,CAAA,CACnE,OAAO,CACL,GAAG,KAAA,CAAM,KAAK,CAAE,MAAA,CAAQiD,CAAU,CAAA,CAAG,CAACd,CAAAA,CAAGxE,CAAAA,IAAW,CAClD,EAAA,CAAI,CAAA,KAAA,EAAQuF,EAAiBvF,CAAK,CAAA,CAAA,CAClC,MAAO,CAAA,EAAGuF,CAAAA,CAAiBvF,CAAK,CAAA,CAAA,CAChC,KAAM,MAAA,CACN,MAAA,CAAQ,MACV,CAAA,CAAE,CAAA,CACF,CACE,EAAA,CAAI,cAAA,CACJ,MAAO,OAAA,CACP,IAAA,CAAM,OACR,CACF,CACF,CAEA,GAAIkD,CAAAA,CAAK,OAAS,QAAA,CAChB,OAAO,CACL,CAAE,GAAI,MAAA,CAAQ,KAAA,CAAO,OAAQ,IAAA,CAAM,QAAA,CAAU,OAAQ,OAAQ,CAAA,CAC7D,CAAE,EAAA,CAAI,QAAS,KAAA,CAAO,OAAA,CAAS,KAAM,QAAA,CAAU,MAAA,CAAQ,OAAQ,CACjE,CAAA,CAGF,GAAIA,CAAAA,CAAK,OAAS,SAAA,CAAW,CAC3B,IAAMsC,CAAAA,CAAY,OAAOnD,EAAK,iBAAA,EAAsB,QAAA,EAAY,CAAC,CAAA,CAAG,EAAG,CAAC,CAAA,CAAE,SAASA,CAAAA,CAAK,iBAAiB,EACrGA,CAAAA,CAAK,iBAAA,CACL,CAAA,CACJ,OAAO,CACL,GAAG,KAAA,CAAM,KAAK,CAAE,MAAA,CAAQmD,CAAU,CAAA,CAAG,CAAChB,CAAAA,CAAGxE,CAAAA,IAAW,CAClD,EAAA,CAAI,CAAA,KAAA,EAAQA,EAAQ,CAAC,CAAA,CAAA,CACrB,MAAO,CAAA,EAAGA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CACnB,KAAM,SAAA,CACN,MAAA,CAAQ,OACV,CAAA,CAAE,CAAA,CACF,GAAG,KAAA,CAAM,IAAA,CAAK,CAAE,MAAA,CAAQwF,CAAU,EAAG,CAAChB,CAAAA,CAAGxE,KAAW,CAClD,EAAA,CAAI,SAASA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CACtB,KAAA,CAAO,GAAGA,CAAAA,CAAQ,CAAC,GACnB,IAAA,CAAM,SAAA,CACN,OAAQ,OACV,CAAA,CAAE,CACJ,CACF,CAEA,GAAIkD,CAAAA,CAAK,OAAS,aAAA,CAAe,CAC/B,IAAMuC,CAAAA,CAAa,IAAA,CAAK,GAAA,CAAI,CAAA,CAAG,KAAK,GAAA,CAAI,CAAA,CAAG,OAAOpD,CAAAA,CAAK,UAAU,GAAK,CAAC,CAAC,CAAA,CAClEqD,CAAAA,CAAc,KAAK,GAAA,CAAI,CAAA,CAAG,KAAK,GAAA,CAAI,EAAA,CAAI,OAAOrD,CAAAA,CAAK,WAAW,CAAA,EAAK,CAAC,CAAC,CAAA,CAC3E,OAAO,CACL,GAAG,KAAA,CAAM,KAAK,CAAE,MAAA,CAAQoD,CAAW,CAAA,CAAG,CAACjB,CAAAA,CAAGxE,CAAAA,IAAW,CACnD,EAAA,CAAI,CAAA,MAAA,EAASA,EAAQ,CAAC,CAAA,CAAA,CACtB,KAAA,CAAO,CAAA,MAAA,EAASA,EAAQ,CAAC,CAAA,CAAA,CACzB,KAAM,UAAA,CACN,MAAA,CAAQ,OACV,CAAA,CAAE,CAAA,CACF,GAAG,KAAA,CAAM,KAAK,CAAE,MAAA,CAAQ0F,CAAY,CAAA,CAAG,CAAClB,EAAGxE,CAAAA,IAAW,CACpD,EAAA,CAAI,CAAA,OAAA,EAAUA,EAAQ,CAAC,CAAA,CAAA,CACvB,MAAO,CAAA,OAAA,EAAUA,CAAAA,CAAQ,CAAC,CAAA,CAAA,CAC1B,IAAA,CAAM,UAAA,CACN,MAAA,CAAQ,OACV,CAAA,CAAE,CACJ,CACF,CAEA,OAAO,EACT,CAEA,SAAS2F,CAAAA,CAAmBzC,EAAkD,CAC5E,IAAMyB,EAAYzB,CAAAA,CAAK,IAAA,GAAS,gBAAkB,KAAA,CAAM,OAAA,CAAQA,CAAAA,CAAK,IAAA,EAAM,KAAK,CAAA,EAAK,KAAA,CAAM,QAAQA,CAAAA,CAAK,IAAA,EAAM,UAAU,CAAA,EAAK,KAAA,CAAM,OAAA,CAAQA,CAAAA,CAAK,MAAM,cAAc,CAAA,EAAK,MAAM,OAAA,CAAQA,CAAAA,CAAK,MAAM,SAAS,CAAA,EAAK,KAAA,CAAM,OAAA,CAAQA,EAAK,IAAA,EAAM,aAAa,EAClPwB,EAAAA,CAAqBxB,CAAI,EACzB+B,EAAAA,CAAyB/B,CAAI,CAAA,CAEjC,OAAOyB,EAAU,MAAA,CAAS,CAAA,CAAIA,EAAY,MAC5C,CAOA,IAAMiB,EAAAA,CAAuB,CAC3B,QACA,YAAA,CACA,gBAAA,CACA,YACA,eAAA,CACA,YAAA,CACA,OACA,eAAA,CACA,WAAA,CACA,aACA,WAAA,CACA,gBAAA,CACA,mBAAA,CACA,YAAA,CACA,aACF,CAAA,CAEA,SAASC,GAA0BxD,CAAAA,CAA0C,CAC3E,IAAMyD,CAAAA,CAAyB,EAAC,CAChC,IAAA,IAAWrM,KAAOmM,EAAAA,CAAsB,CACtC,IAAMhN,CAAAA,CAAQyJ,CAAAA,CAAK5I,CAAG,CAAA,CACtBqM,CAAAA,CAAY,IAAA,CAAKlN,CAAAA,CAAO,MAAM,OAAA,CAAQA,CAAK,EAAIA,CAAAA,CAAM,MAAA,CAAS,EAAE,EAClE,CACA,OAAOkN,CACT,CAEA,SAASC,EAAAA,CAAgBC,EAAuBC,CAAAA,CAAgC,CAC9E,GAAID,CAAAA,CAAE,MAAA,GAAWC,CAAAA,CAAE,MAAA,CAAQ,OAAO,MAAA,CAClC,IAAA,IAASzL,EAAI,CAAA,CAAGA,CAAAA,CAAIwL,EAAE,MAAA,CAAQxL,CAAAA,EAAK,CAAA,CACjC,GAAI,CAAC,MAAA,CAAO,EAAA,CAAGwL,EAAExL,CAAC,CAAA,CAAGyL,EAAEzL,CAAC,CAAC,CAAA,CAAG,OAAO,OAErC,OAAO,KACT,CA2BA,IAAM0L,EAAAA,CAA0B,IAAI,OAAA,CAGpC,SAASC,GAAcxB,CAAAA,CAAyE,CAC9F,OAAOA,CAAAA,EAAW,GAAA,CAAKG,IAAc,CACnC,GAAGA,EACH,GAAIA,CAAAA,CAAS,aAAA,CAAgB,CAAE,cAAe,CAAC,GAAGA,EAAS,aAAa,CAAE,EAAI,EAAC,CAC/E,GAAIA,CAAAA,CAAS,OAAS,CAAE,MAAA,CAAQ,CAAE,GAAGA,CAAAA,CAAS,MAAO,CAAE,CAAA,CAAI,EAC7D,EAAE,CACJ,CASA,SAASsB,EAAAA,CAAyBlD,CAAAA,CAAkD,CAClF,IAAMb,CAAAA,CAAgBa,CAAAA,CAAK,IAAA,CAC3B,GAAI,CAACb,CAAAA,EAAQ,OAAOA,CAAAA,EAAS,QAAA,CAC3B,OAAOsD,CAAAA,CAAmBzC,CAAI,CAAA,CAGhC,IAAM4C,EAAcD,EAAAA,CAA0BxD,CAA+B,EACvEgE,CAAAA,CAASH,EAAAA,CAAwB,IAAI7D,CAAI,CAAA,CAC/C,GACEgE,CAAAA,EACAA,EAAO,MAAA,GAAWnD,CAAAA,CAAK,IACvBmD,CAAAA,CAAO,QAAA,GAAanD,EAAK,IAAA,EACzB6C,EAAAA,CAAgBM,CAAAA,CAAO,WAAA,CAAaP,CAAW,CAAA,CAE/C,OAAOK,GAAcE,CAAAA,CAAO,SAAS,EAGvC,IAAM1B,CAAAA,CAAYgB,EAAmBzC,CAAI,CAAA,CACzC,OAAAgD,EAAAA,CAAwB,GAAA,CAAI7D,EAAM,CAAE,MAAA,CAAQa,EAAK,EAAA,CAAI,QAAA,CAAUA,CAAAA,CAAK,IAAA,CAAM,YAAA4C,CAAAA,CAAa,SAAA,CAAAnB,CAAU,CAAC,CAAA,CAC3FwB,GAAcxB,CAAS,CAChC,CAEA,SAAS2B,GACPC,CAAAA,CACAC,CAAAA,CAC+B,CAC/B,GAAI,CAACA,GAAiBA,CAAAA,CAAc,MAAA,GAAW,CAAA,CAC7C,OAAOA,EAGT,IAAMC,CAAAA,CAAe,IAAI,GAAA,CAAA,CAAKF,CAAAA,EAAqB,EAAC,EAAG,GAAA,CAAKzB,CAAAA,EAAa,CAACA,EAAS,EAAA,CAAIA,CAAQ,CAAC,CAAC,CAAA,CACjG,OAAO0B,CAAAA,CAAc,GAAA,CAAK1B,CAAAA,EAAa,CACrC,IAAM4B,CAAAA,CAAWD,CAAAA,CAAa,IAAI3B,CAAAA,CAAS,EAAE,EAC7C,OAAK4B,CAAAA,CAIE,CACL,GAAGA,EACH,GAAG5B,CAAAA,CACH,cAAeA,CAAAA,CAAS,aAAA,EAAiB4B,EAAS,aAAA,CAClD,MAAA,CAAQ5B,CAAAA,CAAS,MAAA,EAAU4B,EAAS,MACtC,CAAA,CARS5B,CASX,CAAC,CACH,CAEA,SAAS6B,EAAAA,CAAkBC,CAAAA,CAAuCC,CAAAA,CAAoC,CACpG,OAAKD,CAAAA,CAIE,CACL,GAAGA,CAAAA,CACH,GAAGC,CAAAA,CACH,SAAA,CAAWA,CAAAA,CAAS,SAAA,CACpB,OAAQA,CAAAA,CAAS,MAAA,CACjB,OAAQA,CAAAA,CAAS,MAAA,CACjB,aAAcA,CAAAA,CAAS,YAAA,CACvB,QAAA,CAAUA,CAAAA,CAAS,UAAYD,CAAAA,CAAa,QAAA,CAC5C,UAAWN,EAAAA,CAAeM,CAAAA,CAAa,UAAWC,CAAAA,CAAS,SAAS,CACtE,CAAA,CAZSA,CAaX,CAUO,SAASC,GACdC,CAAAA,CACAC,CAAAA,CACiB,CACjB,IAAMC,CAAAA,CAAoB,IAAI,GAAA,CAAA,CAAgC,IAAI,GAAA,CAAK/D,CAAAA,EAAS,CAACA,CAAAA,CAAK,EAAA,CAAIA,CAAI,CAAC,CAAC,CAAA,CAE1Fc,CAAAA,CAAQ+C,EAAS,KAAA,CAAM,GAAA,CAAK7D,GAChCyD,EAAAA,CAAkBM,CAAAA,CAAkB,GAAA,CAAI/D,CAAAA,CAAK,EAAE,CAAA,CAAG,CAChD,GAAIA,CAAAA,CAAK,EAAA,CACT,GAAGD,EAAAA,CAAkBC,CAAI,CAAA,CACzB,KAAA,CAAOA,EAAK,IAAA,EAAM,KAAA,EAASA,EAAK,EAAA,CAChC,SAAA,CAAWC,GAAmBD,CAAI,CAAA,CAClC,OAAQI,EAAAA,CAAgBJ,CAAI,EAC5B,MAAA,CAAQW,EAAAA,CAAgBX,CAAI,CAAA,CAC5B,YAAA,CAAcS,GAAsBT,CAAI,CAAA,CACxC,QAAA,CAAU+D,CAAAA,CAAkB,IAAI/D,CAAAA,CAAK,EAAE,GAAG,QAAA,CAC1C,SAAA,CAAWkD,GAAyBlD,CAAI,CAC1C,CAAC,CACH,EAIMgE,CAAAA,CAAqB,IAAI,IAC/BlD,CAAAA,CAAM,OAAA,CAASd,GAAS,CACtB,IAAMyB,CAAAA,CAAYzB,CAAAA,CAAK,WAAa,EAAC,CAC/B2B,EAAWG,EAAAA,CAAwBL,CAAS,EAC5CwC,CAAAA,CAAU,IAAI,GAAA,CACpB,IAAA,IAAWrC,KAAYH,CAAAA,CAAW,CAChC,IAAM9J,CAAAA,CAAOkK,EAAAA,CAAcD,EAAUD,CAAQ,CAAA,CACzChK,CAAAA,GAAS,MAAA,EAAWsM,EAAQ,GAAA,CAAItM,CAAI,EAC1C,CACIsM,CAAAA,CAAQ,KAAO,CAAA,EAAGD,CAAAA,CAAmB,GAAA,CAAIhE,CAAAA,CAAK,GAAIiE,CAAO,EAC/D,CAAC,CAAA,CAOD,IAAMC,EAAe,CAACC,CAAAA,CAAgBC,CAAAA,GAA0D,CAC9F,GAAI,CAACA,CAAAA,CAAQ,OACb,IAAMtE,CAAAA,CAAajJ,EAAkBuN,CAAM,CAAA,CAE3C,OADgBJ,CAAAA,CAAmB,IAAIG,CAAM,CAAA,EAChC,IAAIrE,CAAU,CAAA,EAAKxG,EAAc3C,CAAAA,CAA6ByN,CAAM,CAAC,CAAA,CAAE,YAAc,MAAA,CACzFrK,CAAAA,CAAuB+F,EAAY,MAAM,CAAA,CAE3CA,CACT,CAAA,CAEMuE,CAAAA,CAAoB,IAAI,GAAA,CAAA,CAAgC,IAAI,GAAA,CAAKC,CAAAA,EAAS,CAACA,CAAAA,CAAK,EAAA,CAAIA,CAAI,CAAC,CAAC,CAAA,CAC1FC,CAAAA,CAAQV,EAAS,KAAA,CAAM,GAAA,CAAKS,GAAS,CACzC,IAAME,CAAAA,CAAwB,CAC5B,GAAIF,CAAAA,CAAK,EAAA,CACT,OAAQ,CACN,MAAA,CAAQA,EAAK,MAAA,CACb,UAAA,CAAYJ,CAAAA,CAAaI,CAAAA,CAAK,OAAQA,CAAAA,CAAK,YAAY,CACzD,CAAA,CACA,MAAA,CAAQ,CACN,MAAA,CAAQA,CAAAA,CAAK,MAAA,CACb,UAAA,CAAYJ,EAAaI,CAAAA,CAAK,MAAA,CAAQA,EAAK,YAAY,CACzD,EACA,IAAA,CAAMA,CAAAA,CAAK,IAAA,CACX,MAAA,CAAQA,EAAK,MAAA,GAAW,IAAA,CAAO,KAAO,MAAA,CACtC,SAAA,CAAW,OAAOA,CAAAA,CAAK,SAAA,EAAc,UAAYA,CAAAA,CAAK,SAAA,CAAY,OAClE,gBAAA,CACE,OAAOA,EAAK,gBAAA,EAAqB,QAAA,EAAY,OAAO,QAAA,CAASA,CAAAA,CAAK,gBAAgB,CAAA,CAC9EA,EAAK,gBAAA,CACL,MAAA,CACN,KAAMA,CAAAA,CAAK,IAAA,CAAO,CAAE,GAAGA,CAAAA,CAAK,IAAK,CAAA,CAAI,OACrC,QAAA,CAAUD,CAAAA,CAAkB,IAAIC,CAAAA,CAAK,EAAE,GAAG,QAC5C,CAAA,CAEMd,CAAAA,CAAWa,CAAAA,CAAkB,IAAIC,CAAAA,CAAK,EAAE,EAC9C,OAAOd,CAAAA,CAAW,CAAE,GAAGA,CAAAA,CAAU,GAAGgB,CAAS,EAAIA,CACnD,CAAC,EAED,OAAO,CACL,QAAS,CAAA,CACT,KAAA,CAAA1D,CAAAA,CACA,KAAA,CAAAyD,EACA,EAAA,CAAI1D,EAAAA,CAAegD,EAAS,KAA2B,CAAA,CACvD,QAAA,CAAUC,CAAAA,EAAkB,QAC9B,CACF,CCj3BO,IAAMW,GAAwB3D,CAAAA,EAAwC,CAC3E,IAAM4D,CAAAA,CAAY5D,CAAAA,CAAM,MAAA,CAAO6D,CAAAA,EAAKA,EAAE,IAAA,GAAS,MAAM,EAC/CC,CAAAA,CAAc9D,CAAAA,CAAM,OAAO6D,CAAAA,EAAKA,CAAAA,CAAE,IAAA,GAAS,QAAQ,EACnDE,CAAAA,CAAa/D,CAAAA,CAAM,OAAO6D,CAAAA,EAAK,CAAC,CAAC,MAAA,CAAQ,QAAQ,CAAA,CAAE,QAAA,CAASA,EAAE,IAAI,CAAC,EAEzE,OAAAD,CAAAA,CAAU,QAAQ,CAACI,CAAAA,CAAMhI,CAAAA,GAAU,CACjCgI,EAAK,QAAA,CAAW,CAAE,EAAGhI,CAAAA,CAAQ,GAAA,CAAK,EAAG,GAAI,EAC3C,CAAC,CAAA,CAED8H,EAAY,OAAA,CAAQG,CAAAA,EAAU,CAC5B,GAAIA,CAAAA,CAAO,SAAU,CACnB,IAAMC,CAAAA,CAAaN,CAAAA,CAAU,KAAKO,CAAAA,EAAKA,CAAAA,CAAE,KAAOF,CAAAA,CAAO,QAAQ,EAC/D,GAAIC,CAAAA,CAAY,CACd,IAAME,EAAYH,CAAAA,CAAO,IAAA,CAAK,WAAa,CAAA,CACrCI,CAAAA,CAAUJ,EAAO,IAAA,CAAK,OAAA,EAAW,CAAA,CACvCA,CAAAA,CAAO,SAAW,CAChB,CAAA,CAAGC,EAAW,QAAA,CAAS,CAAA,CAAI,GAC3B,CAAA,CAAGA,CAAAA,CAAW,QAAA,CAAS,CAAA,CAAKE,EAAY,EAAA,CAAOC,CAAAA,CAAU,EAC3D,EACF,CACF,MACEJ,CAAAA,CAAO,QAAA,CAAW,CAAE,CAAA,CAAG,IAAA,CAAK,QAAO,CAAI,GAAA,CAAM,IAAK,CAAA,CAAG,IAAA,CAAK,QAAO,CAAI,GAAA,CAAM,EAAG,EAElF,CAAC,CAAA,CAEDF,CAAAA,CAAW,QAAQ,CAAC7E,CAAAA,CAAMlD,IAAU,CAClCkD,CAAAA,CAAK,QAAA,CAAW,CAAE,EAAGlD,CAAAA,CAAQ,GAAA,CAAM,IAAK,CAAA,CAAG,GAAI,EACjD,CAAC,CAAA,CAEMgE,CACT,CAAA,CC2BA,SAASsE,CAAAA,CAA+B1P,CAAAA,CAAwD,CAC9F,GAA2BA,CAAAA,EAAU,KACnC,OAGF,GAAI,OAAOA,CAAAA,EAAU,SACnB,OAAO,MAAA,CAAO,SAASA,CAAK,CAAA,CAAI,UAAUA,CAAK,CAAA,CAAA,CAAK,MAAA,CAGtD,GAAI,OAAOA,CAAAA,EAAU,QAAA,CACnB,OAEF,IAAMC,CAAAA,CAAUD,EAAM,IAAA,EAAK,CAC3B,GAAKC,CAAAA,CAGL,OAAIA,CAAAA,CAAQ,UAAA,CAAW,SAAS,CAAA,CACvBA,CAAAA,CAEF,UAAUA,CAAO,CAAA,CAC1B,CAOA,SAAS0P,EAA2BC,CAAAA,CAA0E,CAC5G,GAAI,CAACA,CAAAA,CAAM,OACX,GAAI,OAAOA,GAAS,QAAA,CAAU,OAAO,CAAE,EAAA,CAAIA,CAAAA,CAAM,KAAMA,CAAK,CAAA,CAC5D,GAAI,OAAOA,CAAAA,EAAS,QAAA,CAAU,OAC9B,IAAMC,CAAAA,CAAOD,CAAAA,CAA8B,OAC3C,GAAIC,CAAAA,EAAO,OAAOA,CAAAA,EAAQ,QAAA,CAAU,CAClC,GAAM,CAAE,EAAA,CAAAC,CAAAA,CAAI,KAAAC,CAAK,CAAA,CAAIF,EACfG,CAAAA,CAA0B,EAAC,CACjC,OAAA,CAAI,OAAOF,CAAAA,EAAO,QAAA,EAAY,OAAOA,CAAAA,EAAO,QAAA,IAAUE,EAAI,EAAA,CAAKF,CAAAA,CAAAA,CAC3D,OAAOC,CAAAA,EAAS,WAAUC,CAAAA,CAAI,IAAA,CAAOD,GAClCC,CAAAA,CAAI,EAAA,GAAO,QAAaA,CAAAA,CAAI,IAAA,GAAS,MAAA,CAAYA,CAAAA,CAAM,MAChE,CACA,GAAI,OAAOH,CAAAA,EAAQ,QAAA,EAAY,OAAOA,CAAAA,EAAQ,QAAA,CAAU,OAAO,CAAE,GAAIA,CAAI,CAE3E,CAEA,SAASI,EAAAA,CAA4BL,EAA8D,CACjG,GAAI,OAAOA,CAAAA,EAAS,SAAU,OAAOA,CAAAA,CACrC,GAAIA,CAAAA,EAAQ,OAAOA,GAAS,QAAA,EAAY,OAAOA,EAAK,IAAA,EAAS,QAAA,CAAU,OAAOA,CAAAA,CAAK,IAErF,CAEA,SAASM,CAAAA,CAAQlQ,EAAoC,CACnD,GAAIA,CAAAA,EAAS,OAAOA,GAAU,QAAA,CAAU,CACtC,IAAM+P,CAAAA,CAAQ/P,CAAAA,CAA6B,KAC3C,OAAO,OAAO+P,CAAAA,EAAS,QAAA,CAAWA,EAAO,MAC3C,CACA,OAAO,OAAO/P,CAAAA,EAAU,SAAWA,CAAAA,CAAQ,MAC7C,CAEA,SAASmQ,GAAWnQ,CAAAA,CAAmE,CACrF,OAAOD,CAAAA,CAAgBC,CAAK,CAC9B,CAGA,SAASoQ,EAAAA,CAAgBhB,CAAAA,CAA+D,CACtF,GAA0BA,CAAAA,EAAS,KACnC,CAAA,GAAI,OAAOA,GAAS,QAAA,CAAU,CAC5B,IAAMU,CAAAA,CAAMV,EAA0B,EAAA,CACtC,OAAO,OAAOU,CAAAA,EAAO,QAAA,EAAY,OAAOA,CAAAA,EAAO,QAAA,CAAWA,CAAAA,CAAK,MACjE,CACA,OAAO,OAAOV,GAAS,QAAA,EAAY,OAAOA,GAAS,QAAA,CAAWA,CAAAA,CAAO,MAAA,CACvE,KAKaiB,EAAAA,CAA8BjB,CAAAA,GAClC,CACL,EAAA,CAAI,CAAA,KAAA,EAAQA,EAAK,EAAE,CAAA,CAAA,CACnB,IAAA,CAAM,MAAA,CACN,SAAU,CAAE,CAAA,CAAG,EAAG,CAAA,CAAG,CAAE,EACvB,IAAA,CAAM,CACJ,KAAA,CAAOA,CAAAA,CAAK,KACZ,MAAA,CAAQe,EAAAA,CAAWf,EAAK,MAAM,CAAA,CAC9B,QAASA,CAAAA,CAAK,QAAA,EAAYA,CAAAA,CAAK,KAAA,EAAS,GACxC,WAAA,CAAaA,CAAAA,CAAK,GAClB,aAAA,CAAe,MAAA,CACf,WAAYA,CAAAA,CAAK,WAAA,CACjB,MAAA,CAAQA,CAAAA,CAAK,OACb,QAAA,CAAUA,CAAAA,CAAK,UACf,IAAA,CAAMc,CAAAA,CAAQd,EAAK,IAAI,CAAA,CACvB,IAAA,CAAMc,CAAAA,CAAQd,EAAK,IAAI,CAAA,CACvB,OAAQc,CAAAA,CAAQd,CAAAA,CAAK,MAAM,CAC7B,CACF,CAAA,CAAA,CAWWkB,EAAAA,CAAgCjB,GAA4C,CACvF,IAAMkB,EAASH,EAAAA,CAAgBf,CAAAA,CAAO,IAAI,CAAA,CACpC5E,CAAAA,CAAW8F,CAAAA,GAAW,MAAA,CAAY,QAAQA,CAAM,CAAA,CAAA,CAAK,OACrD3J,CAAAA,CACJyI,CAAAA,CAAO,MAAQ,OAAOA,CAAAA,CAAO,QAAA,EAAa,QAAA,EAAY,OAAO,QAAA,CAASA,CAAAA,CAAO,QAAQ,CAAA,EAAKA,CAAAA,CAAO,SAAW,CAAA,CACxGA,CAAAA,CAAO,SACP,MAAA,CACAmB,CAAAA,CAAMxQ,GACVA,CAAAA,EAAS,OAAOA,GAAU,QAAA,CAAWA,CAAAA,CAAM,QAAUA,CAAAA,CACvD,OAAO,CACL,EAAA,CAAI,UAAUqP,CAAAA,CAAO,EAAE,GACvB,IAAA,CAAM,QAAA,CACN,SAAU,CAAE,CAAA,CAAG,CAAA,CAAG,CAAA,CAAG,CAAE,CAAA,CACvB,QAAA,CAAA5E,EACA,MAAA,CAAQA,CAAAA,CAAW,SAAW,MAAA,CAC9B,IAAA,CAAM,CACJ,KAAA,CAAO4E,EAAO,IAAA,CACd,MAAA,CAAQc,GAAWd,CAAAA,CAAO,MAAM,EAChC,UAAA,CAAYA,CAAAA,CAAO,WAAA,EAAa,KAAA,EAASA,EAAO,KAAA,CAChD,YAAA,CAAeA,EAAO,WAAA,EAAa,YAAA,EAAc,MAAQA,CAAAA,CAAO,YAAA,CAChE,IAAA,CAAMa,CAAAA,CAAQb,EAAO,WAAW,CAAA,EAAKa,EAASb,CAAAA,CAA8B,IAAI,EAChF,OAAA,CAASA,CAAAA,CAAO,WAAA,EAAa,QAAA,EAAYA,EAAO,QAAA,EAAY,CAAA,CAC5D,GAAIzI,CAAAA,GAAa,MAAA,CAAY,CAAE,SAAA,CAAWA,CAAS,CAAA,CAAI,GACvD,IAAA,CAAMyI,CAAAA,CAAO,OAAS,MAAA,CAAS,MAAA,CAAS,QACxC,WAAA,CAAaA,CAAAA,CAAO,GACpB,aAAA,CAAe,QAAA,CACf,WAAYmB,CAAAA,CAAGnB,CAAAA,CAAO,WAAW,CAAA,CACjC,UAAA,CAAYmB,EAAGnB,CAAAA,CAAO,WAAW,CAAA,CACjC,IAAA,CAAMa,EAAQb,CAAAA,CAAO,IAAI,EACzB,IAAA,CACEA,CAAAA,CAAO,MAAQ,OAAOA,CAAAA,CAAO,IAAA,EAAS,QAAA,CAClCA,EAAO,IAAA,CAAK,IAAA,CACXA,EAAO,IAChB,CACF,CACF,CAAA,CAEMoB,EAAAA,CAA0B,CAC9BC,CAAAA,CACAC,EACA1G,CAAAA,GACgB,CAChB,IAAM2G,CAAAA,CAAa7Q,CAAAA,CAAgB2Q,EAAM,WAAW,CAAA,CAC9CG,CAAAA,CAAWnQ,CAAAA,CAAoBgQ,EAAM,MAAA,CAAQA,CAAAA,CAAM,WAAW,CAAA,CACpE,OAAO,CACL,EAAA,CAAI,CAAA,MAAA,EAASA,CAAAA,CAAM,EAAE,GACrB,MAAA,CAAAC,CAAAA,CACA,OAAA1G,CAAAA,CACA,IAAA,CAAM7J,EAAoBsQ,CAAAA,CAAM,IAAI,CAAA,EAAK,OAAA,CACzC,KAAM,CACJ,SAAA,CAAWvQ,EAAgBuQ,CAAAA,CAAM,IAAI,GAAKA,CAAAA,CAAM,SAAA,CAChD,MAAA,CAAQA,CAAAA,CAAM,QAAU,CAAA,CACxB,GAAIE,EAAa,CAAE,UAAA,CAAAA,CAAW,CAAA,CAAI,GAClC,GAAIC,CAAAA,GAAa,OAAY,CAAE,QAAA,CAAAA,CAAS,CAAA,CAAI,GAC5C,KAAA,CAAOH,CAAAA,CAAM,KAAA,EAAS,MAAA,CACtB,YAAaA,CAAAA,CAAM,EAAA,CACnB,cAAe,OAAA,CACf,MAAA,CAAQ3Q,EAAgB2Q,CAAAA,CAAM,MAAM,CAAA,CACpC,YAAA,CAAcT,GAA4BS,CAAAA,CAAM,aAAa,EAC7D,YAAA,CAAcT,EAAAA,CAA4BS,EAAM,aAAa,CAC/D,CACF,CACF,EAWaI,EAAAA,CAA8B,CACzCJ,EACAK,CAAAA,GACuB,CACvB,IAAMC,CAAAA,CAAYrB,CAAAA,CAA2Be,CAAAA,CAAM,aAAa,EAC1DO,CAAAA,CAAYtB,CAAAA,CAA2Be,EAAM,aAAa,CAAA,CAC1DC,EAASI,CAAAA,EAAmB,MAAA,EAAUrB,CAAAA,CAA+BsB,CAAAA,EAAW,EAAE,CAAA,CAClF/G,CAAAA,CAAS8G,GAAmB,MAAA,EAAUrB,CAAAA,CAA+BuB,GAAW,EAAE,CAAA,CACxF,OAAI,CAACN,GAAU,CAAC1G,CAAAA,CACP,KAEFwG,EAAAA,CAAwBC,CAAAA,CAAOC,EAAQ1G,CAAM,CACtD,CAAA,CAWaiH,EAAAA,CAA6BzH,GAIY,CACpD,IAAM2B,EAAuB,EAAC,CACxByD,EAAuB,EAAC,CACxBsC,CAAAA,CAAgB,IAAI,IACpBC,CAAAA,CAAqB,IAAI,IACzBC,CAAAA,CAAc,IAAI,IAClBC,CAAAA,CAAmB,IAAI,GAAA,CAAA,CAE5B7H,CAAAA,CAAK,OAAS,EAAC,EAAG,QAAS2F,CAAAA,EAAS,CACnC,IAAM9E,CAAAA,CAAO+F,EAAAA,CAA2BjB,CAAI,CAAA,CAC5ChE,EAAM,IAAA,CAAKd,CAAI,EACf+G,CAAAA,CAAY,GAAA,CAAI/G,EAAK,EAAE,CAAA,CACnB,OAAO8E,CAAAA,CAAK,MAAS,QAAA,EAAY,CAACkC,EAAiB,GAAA,CAAIlC,CAAAA,CAAK,IAAI,CAAA,EAClEkC,CAAAA,CAAiB,GAAA,CAAIlC,CAAAA,CAAK,KAAM9E,CAAAA,CAAK,EAAE,EAE3C,CAAC,CAAA,CAAA,CAEAb,EAAK,OAAA,EAAW,EAAC,EAAG,OAAA,CAAS4F,GAAW,CACvC,IAAI/E,EAAOgG,EAAAA,CAA6BjB,CAAM,EACxCkC,CAAAA,CACJlC,CAAAA,CAAO,IAAA,EAAQ,OAAOA,EAAO,IAAA,EAAS,QAAA,CAAWA,EAAO,IAAA,CAAK,IAAA,CAAO,OAEtE,GAAI,EADgB/E,CAAAA,CAAK,QAAA,GAAa,QAAa+G,CAAAA,CAAY,GAAA,CAAI/G,EAAK,QAAQ,CAAA,CAAA,CAC9D,CAIhB,IAAMkH,CAAAA,CAAAA,CACH,OAAOD,CAAAA,EAAa,QAAA,CAAWD,EAAiB,GAAA,CAAIC,CAAQ,EAAI,MAAA,IAChE,OAAOlC,EAAO,IAAA,EAAS,QAAA,CAAWiC,CAAAA,CAAiB,GAAA,CAAIjC,EAAO,IAAI,CAAA,CAAI,QAMzE,GALA/E,CAAAA,CAAO,CACL,GAAGA,CAAAA,CACH,QAAA,CAAUkH,CAAAA,CACV,OAAQA,CAAAA,CAAS,QAAA,CAAW,MAC9B,CAAA,CACI,CAACA,GAAUlH,CAAAA,CAAK,IAAA,CAAK,SAAA,GAAc,MAAA,CAAW,CAChD,IAAMmH,CAAAA,CAAe,CAAE,GAAGnH,CAAAA,CAAK,IAAK,CAAA,CACpC,OAAOmH,CAAAA,CAAa,SAAA,CACpBnH,EAAO,CAAE,GAAGA,EAAM,IAAA,CAAMmH,CAAa,EACvC,CACF,CACArG,CAAAA,CAAM,IAAA,CAAKd,CAAI,CAAA,CACf6G,CAAAA,CAAc,IAAI7G,CAAAA,CAAK,EAAE,EACrB,OAAO+E,CAAAA,CAAO,IAAA,EAAS,QAAA,EAAY,CAAC+B,CAAAA,CAAmB,GAAA,CAAI/B,EAAO,IAAI,CAAA,EACxE+B,EAAmB,GAAA,CAAI/B,CAAAA,CAAO,IAAA,CAAM/E,CAAAA,CAAK,EAAE,EAE/C,CAAC,EAED,IAAMoH,CAAAA,CAAW1B,GAA4D,CAC3E,GAAI,CAACA,CAAAA,CAAK,OACV,IAAM2B,CAAAA,CAAOjC,CAAAA,CAA+BM,EAAI,EAAE,CAAA,CAClD,GAAI2B,CAAAA,EAAQR,CAAAA,CAAc,GAAA,CAAIQ,CAAI,EAAG,OAAOA,CAAAA,CAC5C,GAAI,OAAO3B,CAAAA,CAAI,MAAS,QAAA,CAAU,CAChC,IAAMwB,CAAAA,CAASJ,EAAmB,GAAA,CAAIpB,CAAAA,CAAI,IAAI,CAAA,CAC9C,GAAIwB,EAAQ,OAAOA,CACrB,CACA,GAAI,OAAOxB,CAAAA,CAAI,EAAA,EAAO,SACpB,OAAOoB,CAAAA,CAAmB,IAAIpB,CAAAA,CAAI,EAAE,CAGxC,CAAA,CAEA,QAACvG,CAAAA,CAAK,MAAA,EAAU,EAAC,EAAG,OAAA,CAASiH,GAAU,CACrC,IAAMC,CAAAA,CAASe,CAAAA,CAAQ/B,EAA2Be,CAAAA,CAAM,aAAa,CAAC,CAAA,CAChEzG,CAAAA,CAASyH,EAAQ/B,CAAAA,CAA2Be,CAAAA,CAAM,aAAa,CAAC,EAClEC,CAAAA,EAAU1G,CAAAA,EACZ4E,EAAM,IAAA,CAAK4B,EAAAA,CAAwBC,EAAOC,CAAAA,CAAQ1G,CAAM,CAAC,EAE7D,CAAC,CAAA,CAEM,CAAE,MAAAmB,CAAAA,CAAO,KAAA,CAAAyD,CAAM,CACxB,CAAA,CAKa+C,GAA8BnI,CAAAA,EAKlCyE,EAAAA,CAA8BgD,GAA0BzH,CAAI,CAAC,EAMzDoI,EAAAA,CAAkBzG,CAAAA,EACtB2D,GAAqB3D,CAAK","file":"inventory.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 { NetworkNode } from '../types';\n\n/**\n * Generate simple positions for imported rack/device graphs.\n *\n * Mutates and returns the input array to preserve the legacy adapter behavior.\n */\nexport const generateSimpleLayout = (nodes: NetworkNode[]): NetworkNode[] => {\n  const rackNodes = nodes.filter(n => n.type === 'rack');\n  const deviceNodes = nodes.filter(n => n.type === 'device');\n  const otherNodes = nodes.filter(n => !['rack', 'device'].includes(n.type));\n\n  rackNodes.forEach((rack, index) => {\n    rack.position = { x: index * 300, y: 100 };\n  });\n\n  deviceNodes.forEach(device => {\n    if (device.parentId) {\n      const parentRack = rackNodes.find(r => r.id === device.parentId);\n      if (parentRack) {\n        const uPosition = device.data.uPosition || 1;\n        const uHeight = device.data.uHeight || 1;\n        device.position = {\n          x: parentRack.position.x + 50,\n          y: parentRack.position.y + (uPosition * 20) - (uHeight * 10),\n        };\n      }\n    } else {\n      device.position = { x: Math.random() * 400 + 100, y: Math.random() * 200 + 50 };\n    }\n  });\n\n  otherNodes.forEach((node, index) => {\n    node.position = { x: index * 150 + 100, y: 300 };\n  });\n\n  return nodes;\n};\n","import { DiagramDocument, NetworkNode, NetworkEdge } from '../types';\nimport { cableTypeToEdgeType, lengthToCentimetres, readChoiceLabel, readChoiceValue } from './cable-type';\nimport { networkGraphToDiagramDocument } from './document-adapter';\nimport { generateSimpleLayout } from './simple-layout';\n\nexport interface InventoryRackDTO {\n  id: string | number;\n  name: string;\n  status?: { value?: string } | string;\n  u_height?: number;\n  units?: number;\n  facility_id?: string | number;\n  serial?: string;\n  asset_tag?: string;\n  site?: { name?: string } | string;\n  role?: { name?: string } | string;\n  tenant?: { name?: string } | string;\n}\n\nexport interface InventoryDeviceDTO {\n  id: string | number;\n  name: string;\n  status?: { value?: string } | string;\n  device_type?: { u_height?: number; model?: string; manufacturer?: { name?: string } };\n  model?: string;\n  manufacturer?: string;\n  device_role?: { name?: string } | string;\n  u_height?: number;\n  position?: number;\n  face?: 'front' | 'rear';\n  /**\n   * Rack reference: `{ id }`, `{ name }` (resolved against the racks by name in\n   * {@link inventoryToNetworkDiagram}), or a bare rack id.\n   */\n  rack?: { id?: string | number; name?: string } | string | number;\n  primary_ip4?: { address?: string } | string;\n  primary_ip6?: { address?: string } | string;\n  site?: { name?: string } | string;\n}\n\nexport interface InventoryCableDTO {\n  id: string | number;\n  /** Cable type, e.g. `{ label: 'Cat6' }`, `{ value: 'smf' }` or `'power'`. */\n  type?: { label?: string; value?: string } | string;\n  bandwidth?: string;\n  length?: number;\n  /** Unit of `length` (`m`, `cm`, `ft`, `in`, `km`, `mi`); enables `data.lengthCm`. */\n  length_unit?: { value?: string; label?: string } | string;\n  color?: string;\n  status?: { value?: string } | string;\n  /**\n   * A-side termination: `{ device, name }` where `device` is `{ id }`,\n   * `{ name }`, or a bare id; or a bare device reference string.\n   */\n  termination_a?: { device?: { id?: string | number; name?: string } | string | number; name?: string } | string;\n  termination_b?: { device?: { id?: string | number; name?: string } | string | number; name?: string } | string;\n}\n\n/**\n * Mint the node id for a device reference the same way\n * {@link inventoryDeviceToNetworkNode} mints device node ids (`device-<id>`),\n * so string/UUID ids resolve to the node rather than dangling. A reference\n * already carrying the `device-` prefix is kept verbatim.\n */\nfunction normalizeInventoryDeviceNodeId(value: string | number | undefined): string | undefined {\n  if (value === undefined || value === null) {\n    return undefined;\n  }\n\n  if (typeof value === 'number') {\n    return Number.isFinite(value) ? `device-${value}` : undefined;\n  }\n\n  if (typeof value !== 'string') {\n    return undefined;\n  }\n  const trimmed = value.trim();\n  if (!trimmed) {\n    return undefined;\n  }\n  if (trimmed.startsWith('device-')) {\n    return trimmed;\n  }\n  return `device-${trimmed}`;\n}\n\ninterface InventoryDeviceRef {\n  id?: string | number;\n  name?: string;\n}\n\nfunction getInventoryCableDeviceRef(term: InventoryCableDTO['termination_a']): InventoryDeviceRef | undefined {\n  if (!term) return undefined;\n  if (typeof term === 'string') return { id: term, name: term };\n  if (typeof term !== 'object') return undefined;\n  const dev = (term as { device?: unknown }).device;\n  if (dev && typeof dev === 'object') {\n    const { id, name } = dev as { id?: unknown; name?: unknown };\n    const ref: InventoryDeviceRef = {};\n    if (typeof id === 'string' || typeof id === 'number') ref.id = id;\n    if (typeof name === 'string') ref.name = name;\n    return ref.id !== undefined || ref.name !== undefined ? ref : undefined;\n  }\n  if (typeof dev === 'string' || typeof dev === 'number') return { id: dev };\n  return undefined;\n}\n\nfunction getInventoryTerminationName(term: InventoryCableDTO['termination_a']): string | undefined {\n  if (typeof term === 'string') return term;\n  if (term && typeof term === 'object' && typeof term.name === 'string') return term.name;\n  return undefined;\n}\n\nfunction readRef(value: unknown): string | undefined {\n  if (value && typeof value === 'object') {\n    const name = (value as { name?: unknown }).name;\n    return typeof name === 'string' ? name : undefined;\n  }\n  return typeof value === 'string' ? value : undefined;\n}\n\nfunction readStatus(value: unknown): 'active' | 'inactive' | 'maintenance' | undefined {\n  return readChoiceValue(value) as 'active' | 'inactive' | 'maintenance' | undefined;\n}\n\n/** Rack id a device references by id (object `{ id }` or a bare id), if any. */\nfunction getDeviceRackId(rack: InventoryDeviceDTO['rack']): string | number | undefined {\n  if (rack === undefined || rack === null) return undefined;\n  if (typeof rack === 'object') {\n    const id = (rack as { id?: unknown }).id;\n    return typeof id === 'string' || typeof id === 'number' ? id : undefined;\n  }\n  return typeof rack === 'string' || typeof rack === 'number' ? rack : undefined;\n}\n\n/**\n * Convert inventory rack data to NetworkNode\n */\nexport const inventoryRackToNetworkNode = (rack: InventoryRackDTO): NetworkNode => {\n  return {\n    id: `rack-${rack.id}`,\n    type: 'rack',\n    position: { x: 0, y: 0 },\n    data: {\n      label: rack.name,\n      status: readStatus(rack.status),\n      uHeight: rack.u_height ?? rack.units ?? 42,\n      inventoryId: rack.id,\n      inventoryType: 'rack',\n      facilityId: rack.facility_id,\n      serial: rack.serial,\n      assetTag: rack.asset_tag,\n      site: readRef(rack.site),\n      role: readRef(rack.role),\n      tenant: readRef(rack.tenant),\n    },\n  };\n};\n\n/**\n * Convert inventory device data to NetworkNode.\n *\n * `parentId` is only set when the rack is referenced by id (`{ id }` or a bare\n * id); a name-only `{ name }` rack reference is resolved against the racks by\n * {@link inventoryToNetworkDiagram}, never minted as `rack-undefined`.\n * `uPosition` is only set for a racked device with a positive `position`.\n */\nexport const inventoryDeviceToNetworkNode = (device: InventoryDeviceDTO): NetworkNode => {\n  const rackId = getDeviceRackId(device.rack);\n  const parentId = rackId !== undefined ? `rack-${rackId}` : undefined;\n  const position =\n    device.rack && typeof device.position === 'number' && Number.isFinite(device.position) && device.position > 0\n      ? device.position\n      : undefined;\n  const ip = (value: InventoryDeviceDTO['primary_ip4']): string | undefined =>\n    value && typeof value === 'object' ? value.address : value;\n  return {\n    id: `device-${device.id}`,\n    type: 'device',\n    position: { x: 0, y: 0 },\n    parentId,\n    extent: parentId ? 'parent' : undefined,\n    data: {\n      label: device.name,\n      status: readStatus(device.status),\n      deviceType: device.device_type?.model ?? device.model,\n      manufacturer: (device.device_type?.manufacturer?.name ?? device.manufacturer),\n      role: readRef(device.device_role) ?? readRef((device as { role?: unknown }).role),\n      uHeight: device.device_type?.u_height ?? device.u_height ?? 1,\n      ...(position !== undefined ? { uPosition: position } : {}),\n      face: device.face === 'rear' ? 'rear' : 'front',\n      inventoryId: device.id,\n      inventoryType: 'device',\n      primaryIp4: ip(device.primary_ip4),\n      primaryIp6: ip(device.primary_ip6),\n      site: readRef(device.site),\n      rack:\n        device.rack && typeof device.rack === 'object'\n          ? device.rack.name\n          : (device.rack as string | number | undefined),\n    },\n  };\n};\n\nconst buildInventoryCableEdge = (\n  cable: InventoryCableDTO,\n  source: string,\n  target: string,\n): NetworkEdge => {\n  const lengthUnit = readChoiceValue(cable.length_unit);\n  const lengthCm = lengthToCentimetres(cable.length, cable.length_unit);\n  return {\n    id: `cable-${cable.id}`,\n    source,\n    target,\n    type: cableTypeToEdgeType(cable.type) ?? 'fiber',\n    data: {\n      bandwidth: readChoiceLabel(cable.type) ?? cable.bandwidth,\n      length: cable.length || 0,\n      ...(lengthUnit ? { lengthUnit } : {}),\n      ...(lengthCm !== undefined ? { lengthCm } : {}),\n      color: cable.color || undefined,\n      inventoryId: cable.id,\n      inventoryType: 'cable',\n      status: readChoiceValue(cable.status),\n      terminationA: getInventoryTerminationName(cable.termination_a),\n      terminationB: getInventoryTerminationName(cable.termination_b),\n    },\n  };\n};\n\n/**\n * Convert inventory cable data to NetworkEdge.\n *\n * Device references are minted as node ids exactly like\n * {@link inventoryDeviceToNetworkNode} (`device-<id>`, including string/UUID\n * ids). The edge `type` follows the cable type (`power` → `power`,\n * fibre/AOC → `fiber`, any other named type → `ethernet`; a missing type keeps\n * the historical `fiber`). Returns `null` when either end has no device.\n */\nexport const inventoryCableToNetworkEdge = (\n  cable: InventoryCableDTO,\n  resolvedEndpoints?: { source: string; target: string },\n): NetworkEdge | null => {\n  const sourceRef = getInventoryCableDeviceRef(cable.termination_a);\n  const targetRef = getInventoryCableDeviceRef(cable.termination_b);\n  const source = resolvedEndpoints?.source ?? normalizeInventoryDeviceNodeId(sourceRef?.id);\n  const target = resolvedEndpoints?.target ?? normalizeInventoryDeviceNodeId(targetRef?.id);\n  if (!source || !target) {\n    return null;\n  }\n  return buildInventoryCableEdge(cable, source, target);\n};\n\n/**\n * Convert inventory data to network diagram format.\n *\n * Cable ends resolve against the converted devices — by id (number, numeric\n * string, string/UUID, or an explicit `device-` node id) and then by device\n * name. A cable whose either end does not resolve to a device in `devices` is\n * dropped rather than emitted dangling. Devices whose rack is referenced by\n * name only are parented to the rack with that name.\n */\nexport const inventoryToNetworkDiagram = (data: {\n  racks: InventoryRackDTO[];\n  devices: InventoryDeviceDTO[];\n  cables: InventoryCableDTO[];\n}): { nodes: NetworkNode[]; edges: NetworkEdge[] } => {\n  const nodes: NetworkNode[] = [];\n  const edges: NetworkEdge[] = [];\n  const deviceNodeIds = new Set<string>();\n  const deviceNodeIdByName = new Map<string, string>();\n  const rackNodeIds = new Set<string>();\n  const rackNodeIdByName = new Map<string, string>();\n\n  (data.racks ?? []).forEach((rack) => {\n    const node = inventoryRackToNetworkNode(rack);\n    nodes.push(node);\n    rackNodeIds.add(node.id);\n    if (typeof rack.name === 'string' && !rackNodeIdByName.has(rack.name)) {\n      rackNodeIdByName.set(rack.name, node.id);\n    }\n  });\n\n  (data.devices ?? []).forEach((device) => {\n    let node = inventoryDeviceToNetworkNode(device);\n    const rackName =\n      device.rack && typeof device.rack === 'object' ? device.rack.name : undefined;\n    const parentKnown = node.parentId !== undefined && rackNodeIds.has(node.parentId);\n    if (!parentKnown) {\n      // Resolve a name-only (or unknown-id) rack reference by rack name; a\n      // reference that matches no rack leaves the device free-floating rather\n      // than pointing at a parent that does not exist.\n      const byName =\n        (typeof rackName === 'string' ? rackNodeIdByName.get(rackName) : undefined) ??\n        (typeof device.rack === 'string' ? rackNodeIdByName.get(device.rack) : undefined);\n      node = {\n        ...node,\n        parentId: byName,\n        extent: byName ? 'parent' : undefined,\n      } as NetworkNode;\n      if (!byName && node.data.uPosition !== undefined) {\n        const unrackedData = { ...node.data };\n        delete unrackedData.uPosition;\n        node = { ...node, data: unrackedData } as NetworkNode;\n      }\n    }\n    nodes.push(node);\n    deviceNodeIds.add(node.id);\n    if (typeof device.name === 'string' && !deviceNodeIdByName.has(device.name)) {\n      deviceNodeIdByName.set(device.name, node.id);\n    }\n  });\n\n  const resolve = (ref: InventoryDeviceRef | undefined): string | undefined => {\n    if (!ref) return undefined;\n    const byId = normalizeInventoryDeviceNodeId(ref.id);\n    if (byId && deviceNodeIds.has(byId)) return byId;\n    if (typeof ref.name === 'string') {\n      const byName = deviceNodeIdByName.get(ref.name);\n      if (byName) return byName;\n    }\n    if (typeof ref.id === 'string') {\n      return deviceNodeIdByName.get(ref.id);\n    }\n    return undefined;\n  };\n\n  (data.cables ?? []).forEach((cable) => {\n    const source = resolve(getInventoryCableDeviceRef(cable.termination_a));\n    const target = resolve(getInventoryCableDeviceRef(cable.termination_b));\n    if (source && target) {\n      edges.push(buildInventoryCableEdge(cable, source, target));\n    }\n  });\n\n  return { nodes, edges };\n};\n\n/**\n * Convert inventory data directly into the canonical DiagramDocument model.\n */\nexport const inventoryToDiagramDocument = (data: {\n  racks: InventoryRackDTO[];\n  devices: InventoryDeviceDTO[];\n  cables: InventoryCableDTO[];\n}): DiagramDocument => {\n  return networkGraphToDiagramDocument(inventoryToNetworkDiagram(data));\n};\n\n/**\n * Generate simple layout positions for nodes array\n */\nexport const generateLayout = (nodes: NetworkNode[]): NetworkNode[] => {\n  return generateSimpleLayout(nodes);\n};\n"]}