feat(fls): import a .fls topology into the editor model

importProject builds what the simulation core runs from Java's view of a
file: nodes with kinds and ports, links between port ids, device and
interface IP configurations, documentation items with Swift's derived
ids. The core gains the model types it needs (node, link, graph,
documentation item), ported from filius-ipad.

Checked against the Swift import, which the Swift oracle now writes in
full (corpus.swift.json, "state"), on the 13 files Swift reads
completely. Two differences are intended and named in the test:
- a device the file leaves unnamed has Java's default name
  ("Switch / WLAN"), not Swift's;
- multicast_server.fls: useIPAsName refers to a shared Boolean that
  Swift cannot follow; Java shows the IP address, and so do we.
All 23 files import with as many devices and cables as Java has, and an
imported example pings across its router in the core.

Port counts of switches and routers follow Java's lists instead of
Swift's guess from references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 20:47:47 +00:00
co-authored by Claude Opus 5.5
parent a02bb62413
commit da2ec1dae2
16 changed files with 14023 additions and 1 deletions
+9
View File
@@ -18,6 +18,15 @@ npm test # vergleicht mit den geschriebenen *.swift.json
* Vergleich: `packages/core/test/oracle.test.ts`. Er meldet das **erste**
abweichende Spurereignis mit Nummer.
## Korpus
`--corpus` importiert jede `.fls` unter `corpus/` und schreibt
`corpus/corpus.swift.json`: je Datei die Zählwerte und Warnungen des Imports
und unter `state` Swifts ganzes importiertes Modell ohne die zufälligen UUIDs
(Geräte, Ports und Kabel als Indizes, IP-Konfiguration, Doku-Elemente mit
ihren abgeleiteten IDs). Daran misst `packages/fls/test/importProject.test.ts`
den Umbau — auf den 13 Dateien, die Swift vollständig liest.
## Wie es gebaut wird
`fetch-upstream.sh` holt `filius-ipad` am festen Stand `3b5ded0` und kopiert
@@ -283,6 +283,7 @@ func readCorpus(root: URL) throws -> JSON {
"warnings": imported.report.warnings,
"residual": imported.opaqueContent.residualCount,
"kinds": kinds,
"state": describeImportedState(state),
] as JSON
} catch {
result[name] = ["error": String(describing: error)] as JSON
@@ -291,6 +292,61 @@ func readCorpus(root: URL) throws -> JSON {
return result
}
/// The imported model without its random UUIDs: nodes, ports and links by
/// index; the IP configurations the runtime starts from; documentation items
/// with their deterministic ids.
func describeImportedState(_ state: TopologyEditorState) -> JSON {
let nodeIndex = Dictionary(uniqueKeysWithValues: state.graph.nodes.enumerated().map { ($1.id, $0) })
func portIndex(_ nodeID: UUID, _ portID: UUID) -> Int {
state.graph.node(withID: nodeID)?.ports.firstIndex { $0.id == portID } ?? -1
}
let nodes: [JSON] = state.graph.nodes.map { node in
let device: Any = state.runtimeDeviceConfigurations[node.id].map { configuration -> JSON in
[
"ipAddress": configuration.ipAddress,
"subnetMask": configuration.subnetMask,
"defaultGateway": configuration.defaultGateway,
"dnsServer": configuration.dnsServer,
]
} ?? NSNull()
let interfaces: [Any] = node.ports.map { port in
state.runtimeInterfaceConfigurations[TopologyRuntimeInterfaceKey(nodeID: node.id, portID: port.id)]
.map { ["ipAddress": $0.ipAddress, "subnetMask": $0.subnetMask] as JSON } ?? NSNull()
}
return [
"kind": node.kind.rawValue,
"displayName": node.displayName,
"hostLabelPolicy": node.hostLabelPolicy.rawValue,
"position": [Double(node.position.x), Double(node.position.y)],
"ports": node.ports.map { port -> JSON in
["label": port.label, "mac": port.importedMACAddress ?? NSNull(), "occupied": port.isOccupied]
},
"device": device,
"interfaces": interfaces,
]
}
let links: [[Int]] = state.graph.links.map { link in
[
nodeIndex[link.sourceNodeID] ?? -1, portIndex(link.sourceNodeID, link.sourcePortID),
nodeIndex[link.targetNodeID] ?? -1, portIndex(link.targetNodeID, link.targetPortID),
]
}
let documentation: [JSON] = state.documentationItems.map { item in
[
"id": item.id.uuidString,
"kind": item.kind.rawValue,
"frame": [item.frame.origin.x, item.frame.origin.y, item.frame.width, item.frame.height].map { Double($0) },
"text": item.text,
"color": [item.color.red, item.color.green, item.color.blue, item.color.alpha],
"fontName": item.fontName,
"fontSize": Double(item.fontSize),
"isBold": item.isBold,
"order": item.order,
]
}
return ["nodes": nodes, "links": links, "documentation": documentation]
}
// MARK: - Archive path folding
struct PathCase: Decodable { let name: String; let first: String; let second: String }