import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; import { DEFAULT_GATEWAY_SESSION_ID, DEFAULT_PROJECT_ID } from "../sim/model.mjs"; export const PATCH_PANEL_SERVICE_ID = "hwlab-patch-panel"; const KNOWN_OUTPUT_FAMILIES = Object.freeze(["DO", "AO", "FREQ_OUT"]); const SIGNAL_TARGET_FAMILY = Object.freeze({ DO: "DI", AO: "AI", FREQ_OUT: "FREQ_IN" }); const SIGNAL_VALUE_TYPE = Object.freeze({ "DO->DI": "boolean", "AO->AI": "number", "FREQ_OUT->FREQ_IN": "number" }); export function createWiringConfig({ wiringConfigId = "wir_mvp_patch_panel", projectId = DEFAULT_PROJECT_ID, gatewaySessionId = DEFAULT_GATEWAY_SESSION_ID, name = "MVP simulated patch panel wiring", status = "active", connections, constraints, now = new Date().toISOString() }) { if (!Array.isArray(connections) || connections.length === 0) { throw new Error("wiring config requires at least one connection"); } return { wiringConfigId, projectId, gatewaySessionId, name, status, connections, constraints: constraints ?? { propagation: "patch-panel-only", localLoopbackSubstituteAllowed: false }, createdAt: now, updatedAt: now }; } export function createPatchPanelState({ patchPanelStatusId = "pps_mvp_patch_panel", gatewaySessionId = null, wiringConfig, state = "active", metadata = {}, now = new Date().toISOString() }) { const effectiveGatewaySessionId = gatewaySessionId ?? wiringConfig.gatewaySessionId ?? DEFAULT_GATEWAY_SESSION_ID; return { patchPanelStatusId, projectId: wiringConfig.projectId ?? DEFAULT_PROJECT_ID, gatewaySessionId: effectiveGatewaySessionId, wiringConfigId: wiringConfig.wiringConfigId, serviceId: PATCH_PANEL_SERVICE_ID, state, environment: ENVIRONMENT_DEV, activeConnections: wiringConfig.connections.map((connection) => ({ fromResourceId: connection.from.resourceId, fromPort: connection.from.port, toResourceId: connection.to.resourceId, toPort: connection.to.port })), observedAt: now, metadata: { propagation: "patch-panel-only", connectionCount: wiringConfig.connections.length, ...metadata } }; } export function endpointKey(resourceId, port) { return `${resourceId}:${port}`; } export function classifySignalPort(port) { const normalized = String(port ?? "").trim().toUpperCase(); if (!normalized) { return null; } const freqOut = normalized.match(/^FREQ_OUT(?[A-Z0-9_-]*)$/); if (freqOut) { return { family: "FREQ_OUT", direction: "output", valueType: "number", suffix: freqOut.groups.suffix ?? "" }; } const freqIn = normalized.match(/^FREQ_IN(?[A-Z0-9_-]*)$/); if (freqIn) { return { family: "FREQ_IN", direction: "input", valueType: "number", suffix: freqIn.groups.suffix ?? "" }; } const discrete = normalized.match(/^(?DO|DI|AO|AI)(?[A-Z0-9_-]*)$/); if (!discrete) { return null; } const family = discrete.groups.family; return { family, direction: family === "DO" || family === "AO" ? "output" : "input", valueType: family === "DO" || family === "DI" ? "boolean" : "number", suffix: discrete.groups.suffix ?? "" }; } function issue(level, code, message, path, extra = {}) { return { level, code, message, path, ...extra }; } function endpointDiagnostic(endpoint, path) { if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint)) { return issue("error", "endpoint_invalid", "wiring endpoint must be an object", path); } if (!endpoint.resourceId || typeof endpoint.resourceId !== "string") { return issue("error", "endpoint_resource_missing", "wiring endpoint requires resourceId", `${path}.resourceId`); } if (!endpoint.port || typeof endpoint.port !== "string") { return issue("error", "endpoint_port_missing", "wiring endpoint requires port", `${path}.port`); } return null; } function describeExpectedTarget(fromFamily) { const targetFamily = SIGNAL_TARGET_FAMILY[fromFamily]; if (!targetFamily) { return `one of ${KNOWN_OUTPUT_FAMILIES.join(", ")}`; } return targetFamily; } function normalizeConnection(connection, index) { const path = `$.connections[${index}]`; const diagnostics = []; if (!connection || typeof connection !== "object" || Array.isArray(connection)) { return { diagnostics: [issue("error", "connection_invalid", "wiring connection must be an object", path)], normalized: null }; } for (const side of ["from", "to"]) { const endpointIssue = endpointDiagnostic(connection[side], `${path}.${side}`); if (endpointIssue) { diagnostics.push(endpointIssue); } } if (diagnostics.some((item) => item.level === "error")) { return { diagnostics, normalized: null }; } const fromKind = classifySignalPort(connection.from.port); const toKind = classifySignalPort(connection.to.port); let syncKind = "generic"; let valueType = "any"; let syncable = false; if (fromKind || toKind) { if (!fromKind || !toKind) { diagnostics.push( issue( "error", "typed_signal_endpoint_mismatch", `typed signal wiring requires both endpoints to use signal ports; got ${connection.from.port} -> ${connection.to.port}`, path ) ); } else if (fromKind.direction !== "output") { diagnostics.push( issue( "error", "source_port_not_output", `${connection.from.port} is an input port and cannot drive ${connection.to.port}; expected source family ${KNOWN_OUTPUT_FAMILIES.join(", ")}`, `${path}.from.port` ) ); } else if (toKind.direction !== "input") { diagnostics.push( issue( "error", "target_port_not_input", `${connection.to.port} is an output port and cannot be driven by ${connection.from.port}; expected target family ${describeExpectedTarget(fromKind.family)}`, `${path}.to.port` ) ); } else if (SIGNAL_TARGET_FAMILY[fromKind.family] !== toKind.family) { diagnostics.push( issue( "error", "unsupported_signal_wiring", `${fromKind.family} output port ${connection.from.port} can only drive ${describeExpectedTarget(fromKind.family)} input ports; got ${connection.to.port}`, path ) ); } else { syncKind = `${fromKind.family}->${toKind.family}`; valueType = SIGNAL_VALUE_TYPE[syncKind]; syncable = true; } } return { diagnostics, normalized: { index, from: connection.from, to: connection.to, mode: connection.mode ?? "exclusive", syncKind, valueType, syncable, bidirectionalRoute: syncKind === "generic" } }; } export function validateWiringConfig(wiringConfig) { const diagnostics = []; const normalizedConnections = []; if (!wiringConfig || typeof wiringConfig !== "object" || Array.isArray(wiringConfig)) { return { diagnostics: [ issue("error", "wiring_config_invalid", "wiring config must be an object", "$") ], normalizedConnections }; } for (const field of ["wiringConfigId", "projectId", "gatewaySessionId", "status"]) { if (!wiringConfig[field] || typeof wiringConfig[field] !== "string") { diagnostics.push( issue("error", "wiring_config_field_missing", `wiring config requires string ${field}`, `$.${field}`) ); } } if (wiringConfig.status !== "active") { diagnostics.push( issue( "error", "wiring_config_not_active", `patch-panel runtime can only apply active wiring configs; got ${JSON.stringify(wiringConfig.status)}`, "$.status" ) ); } if (wiringConfig.constraints?.propagation !== "patch-panel-only") { diagnostics.push( issue( "error", "wiring_propagation_invalid", "wiring config constraints.propagation must be patch-panel-only", "$.constraints.propagation" ) ); } if (wiringConfig.constraints?.localLoopbackSubstituteAllowed !== false) { diagnostics.push( issue( "error", "local_loopback_substitute_invalid", "wiring config must set constraints.localLoopbackSubstituteAllowed=false", "$.constraints.localLoopbackSubstituteAllowed" ) ); } if (!Array.isArray(wiringConfig.connections) || wiringConfig.connections.length === 0) { diagnostics.push( issue("error", "connections_missing", "wiring config requires at least one connection", "$.connections") ); } else { wiringConfig.connections.forEach((connection, index) => { const result = normalizeConnection(connection, index); diagnostics.push(...result.diagnostics); if (result.normalized) { normalizedConnections.push(result.normalized); } }); } return { diagnostics, normalizedConnections }; } function diagnosticSummary(diagnostics) { return { errorCount: diagnostics.filter((item) => item.level === "error").length, warningCount: diagnostics.filter((item) => item.level === "warning").length, infoCount: diagnostics.filter((item) => item.level === "info").length }; } function validateValueForConnection(value, connection) { if (connection.valueType === "boolean" && typeof value !== "boolean") { return issue( "error", "signal_value_type_mismatch", `${connection.syncKind} requires a boolean value; got ${typeof value}`, `$.connections[${connection.index}]` ); } if (connection.valueType === "number" && typeof value !== "number") { return issue( "error", "signal_value_type_mismatch", `${connection.syncKind} requires a number value; got ${typeof value}`, `$.connections[${connection.index}]` ); } return null; } export function createPatchPanel({ wiringConfig, now = () => new Date().toISOString() }) { const signals = new Map(); const validation = validateWiringConfig(wiringConfig); const summary = diagnosticSummary(validation.diagnostics); function routeSignal({ fromResourceId, fromPort, value }) { const observedAt = now(); const deliveries = []; const routeDiagnostics = []; if (summary.errorCount > 0) { return { accepted: false, propagatedBy: PATCH_PANEL_SERVICE_ID, deliveryCount: 0, deliveries, diagnostics: validation.diagnostics, observedAt }; } for (const connection of validation.normalizedConnections) { const forwardMatch = connection.from.resourceId === fromResourceId && connection.from.port === fromPort; const reverseMatch = connection.bidirectionalRoute && connection.to.resourceId === fromResourceId && connection.to.port === fromPort; if (!forwardMatch && !reverseMatch) { continue; } const valueIssue = validateValueForConnection(value, connection); if (valueIssue) { routeDiagnostics.push(valueIssue); continue; } const target = forwardMatch ? connection.to : connection.from; const key = endpointKey(target.resourceId, target.port); const delivery = { resourceId: target.resourceId, port: target.port, value, sourceResourceId: fromResourceId, sourcePort: fromPort, observedAt }; signals.set(key, delivery); deliveries.push(delivery); } if (routeDiagnostics.some((item) => item.level === "error")) { return { accepted: false, propagatedBy: PATCH_PANEL_SERVICE_ID, deliveryCount: deliveries.length, deliveries, diagnostics: routeDiagnostics, observedAt }; } const response = { accepted: true, propagatedBy: PATCH_PANEL_SERVICE_ID, deliveryCount: deliveries.length, deliveries, observedAt }; if (routeDiagnostics.length > 0) { response.diagnostics = routeDiagnostics; } return response; } function status() { return createPatchPanelState({ wiringConfig, state: summary.errorCount > 0 ? "faulted" : "active", metadata: { diagnostics: summary, syncableConnectionCount: validation.normalizedConnections.filter((connection) => connection.syncable).length }, now: now() }); } return { wiringConfig, diagnostics: validation.diagnostics, diagnosticSummary: summary, normalizedConnections: validation.normalizedConnections, routeSignal, status, signals }; }