/* Cortex-OS Umbrella panel — Cytoscape.js render of /api/umbrella graph. * Consumes UI-stable schema v1.0 per CORTEX-OS-UMBRELLA-VIZ-PRD.md. * Curator-maintained graph artifact at graph/umbrella.json. */ (function () { "use strict"; const TYPE_COLOR = { doc: "#64748b", profile: "#6ee7b7", skill: "#a78bfa", mcp_server: "#f97316", sovereign_api: "#f97316", cortex_tool: "#60a5fa", external_dep: "#f87171", credential: "#be647a", }; const TYPE_SHAPE = { doc: "round-rectangle", profile: "hexagon", skill: "ellipse", mcp_server: "diamond", sovereign_api: "rectangle", cortex_tool: "round-rectangle", external_dep: "triangle", credential: "vee", }; const EDGE_STYLE = { depends_on: { color: "#475569", style: "solid" }, governs: { color: "#fbbf24", style: "solid" }, consumes: { color: "#6ee7b7", style: "solid" }, produces: { color: "#a78bfa", style: "dashed" }, supersedes: { color: "#f87171", style: "dashed" }, cites: { color: "#38bdf8", style: "dotted" }, derives_from: { color: "#f59e0b", style: "dashed" }, }; const SPINE_LAYERS = [ { key: "system", label: "System Spine", color: "#e7eaf0", baseWidth: 920 }, { key: "governance", label: "Governance / Protocols", color: "#fbbf24", baseWidth: 1240 }, { key: "runtime", label: "Runtime / WebUI / Services", color: "#60a5fa", baseWidth: 880 }, { key: "profiles", label: "Agents / Profiles", color: "#6ee7b7", baseWidth: 680 }, { key: "capabilities", label: "Skills / Tools / APIs", color: "#a78bfa", baseWidth: 2200 }, { key: "projects", label: "Projects / Domain Systems", color: "#14b8a6", baseWidth: 760 }, { key: "knowledge", label: "SOT / Evidence / Outputs", color: "#94a3b8", baseWidth: 2200 }, ]; let cy = null; let graph = null; let layerOverlayState = null; const activeTypes = new Set(); const quietTypes = new Set(["credential"]); const quietLayers = new Set(["knowledge"]); const disclosedLayers = new Set(); window.__svrntyUmbrella = { ready: false, error: null, nodes: 0, edges: 0, view: "force" }; async function loadGraph() { const res = await fetch("/api/umbrella", { cache: "no-store" }); if (!res.ok) throw new Error("graph load failed: " + res.status); return await res.json(); } function renderStats(g) { const s = g.stats || {}; const byType = s.by_type || {}; document.getElementById("stats").textContent = `nodes ${s.total_nodes || g.nodes.length} · edges ${s.total_edges || g.edges.length} · ` + Object.entries(byType).map(([k, v]) => `${k}:${v}`).join(" "); document.getElementById("genInfo").textContent = `generated ${g.generated_at || "?"} by ${g.generated_by || "?"}`; } function renderFilters(g) { const types = [...new Set(g.nodes.map(n => n.type))].sort(); const wrap = document.getElementById("filters"); const disclosure = document.getElementById("disclosure"); wrap.innerHTML = ""; disclosure.innerHTML = ""; types.forEach(t => { activeTypes.add(t); const chip = document.createElement("button"); chip.className = "chip active" + (quietTypes.has(t) ? " disclosure-chip" : ""); chip.dataset.type = t; chip.style.borderColor = TYPE_COLOR[t] || "#475569"; chip.style.color = TYPE_COLOR[t] || "#e7eaf0"; chip.textContent = `${quietTypes.has(t) ? "show " : ""}${t} (${g.nodes.filter(n => n.type === t).length})`; chip.addEventListener("click", () => toggleType(t, chip)); (quietTypes.has(t) ? disclosure : wrap).appendChild(chip); }); quietLayers.forEach((layer) => { const count = g.nodes.filter((n) => _spineLayerKey(n) === layer).length; const chip = document.createElement("button"); chip.className = "chip disclosure-chip"; chip.dataset.layerDisclosure = layer; chip.textContent = `show ${_layerLabel(layer)} (${count})`; chip.addEventListener("click", () => toggleLayerDisclosure(layer, chip)); disclosure.appendChild(chip); }); } function applyDefaultDisclosure() { quietTypes.forEach((t) => activeTypes.delete(t)); disclosedLayers.clear(); document.querySelectorAll(".chip[data-type]").forEach((chip) => { chip.classList.toggle("active", activeTypes.has(chip.dataset.type)); }); document.querySelectorAll(".chip[data-layer-disclosure]").forEach((chip) => { chip.classList.toggle("active", disclosedLayers.has(chip.dataset.layerDisclosure)); }); applyFilter(); } function _layerLabel(layer) { if (layer === "knowledge") return "SOT / evidence"; return layer; } function toggleLayerDisclosure(layer, chipEl) { if (disclosedLayers.has(layer)) { disclosedLayers.delete(layer); chipEl.classList.remove("active"); } else { disclosedLayers.add(layer); chipEl.classList.add("active"); } applyFilter(); if (window.__svrntyUmbrella.view === "spine") runPresetView("spine"); } function toggleType(t, chipEl) { if (activeTypes.has(t)) { activeTypes.delete(t); chipEl.classList.remove("active"); } else { activeTypes.add(t); chipEl.classList.add("active"); } applyFilter(); } function applyFilter() { if (!cy) return; cy.batch(() => { cy.nodes().forEach(n => { const layer = n.data("layer"); const visible = activeTypes.has(n.data("type")) && (!quietLayers.has(layer) || disclosedLayers.has(layer)); n.style("display", visible ? "element" : "none"); }); cy.edges().forEach(e => { const s = e.source().style("display"); const t = e.target().style("display"); e.style("display", (s === "element" && t === "element") ? "element" : "none"); }); }); } function buildCyElements(g) { const nodes = g.nodes.map(n => ({ data: { id: n.id, label: n.label || n.id, shortLabel: _shortLabel(n.label || n.id), type: n.type, raw: n, ..._visualMeta(n), }, })); const edges = g.edges.map((e, i) => ({ data: { id: `e${i}`, source: e.source, target: e.target, type: e.type, raw: e }, })); return [...nodes, ...edges]; } function renderGraph(g) { cy = cytoscape({ container: document.getElementById("cy"), elements: buildCyElements(g), wheelSensitivity: 0.2, style: [ { selector: "node", style: { "background-color": ele => TYPE_COLOR[ele.data("type")] || "#475569", "shape": ele => TYPE_SHAPE[ele.data("type")] || "ellipse", "label": ele => ele.data("important") ? ele.data("shortLabel") : "", "color": "#e7eaf0", "text-valign": "bottom", "text-margin-y": 4, "font-size": ele => ele.data("fontSize") || 10, "font-family": "ui-monospace, monospace", "width": ele => ele.data("nodeWidth") || 28, "height": ele => ele.data("nodeHeight") || 28, "border-width": 1, "border-color": "#0f1115", "text-wrap": "wrap", "text-max-width": ele => Math.max(90, (ele.data("nodeWidth") || 28) + 44), }, }, { selector: "node:selected", style: { "border-width": 3, "border-color": "#fbbf24", "label": "data(label)" }, }, { selector: "node.hover", style: { "label": "data(label)", "z-index": 20 }, }, { selector: "edge", style: { "width": 1, "line-color": ele => (EDGE_STYLE[ele.data("type")] || {}).color || "#475569", "line-style": ele => (EDGE_STYLE[ele.data("type")] || {}).style || "solid", "curve-style": "bezier", "target-arrow-shape": "triangle", "target-arrow-color": ele => (EDGE_STYLE[ele.data("type")] || {}).color || "#475569", "opacity": 0.5, }, }, { selector: "edge:selected", style: { "opacity": 1, "width": 2 }, }, ], layout: { name: "cose", animate: false, idealEdgeLength: 80, nodeRepulsion: 12000 }, }); window.cy = cy; cy.on("tap", "node", (evt) => openSidePanel(evt.target.data("raw"))); cy.on("tap", (evt) => { if (evt.target === cy) closeSidePanel(); }); cy.on("mouseover", "node", (evt) => focusNode(evt.target)); cy.on("mouseout", "node", (evt) => { evt.target.removeClass("hover"); resetGraphEmphasis(); }); cy.on("zoom", () => applyLabelDensity()); cy.on("pan zoom render", () => updateLayerOverlay()); } function _shortLabel(value) { const label = String(value || ""); if (label.length <= 24) return label; return label.slice(0, 21) + "..."; } function _estimatedLabelWidth(node) { const label = String(node.label || node.id || ""); const layer = node.layer || _spineLayerKey(node); const priority = node.labelPriority || _labelPriority(node, layer); const size = node.nodeWidth ? node : _nodeSize(node, layer, priority); return Math.min(320, Math.max(size.nodeWidth || size.width || 92, label.length * 7 + 44)); } function _isImportantNode(node) { const id = node.id || ""; return node.type === "profile" || ["readme", "architecture", "cortex-os-framework", "cortex-os-roadmap"].includes(id); } function _visualMeta(node) { const layer = _spineLayerKey(node); const priority = _labelPriority(node, layer); const size = _nodeSize(node, layer, priority); return { layer, labelPriority: priority, important: priority <= 1, nodeWidth: size.width, nodeHeight: size.height, fontSize: size.fontSize, }; } function _labelPriority(node, layer) { if (layer === "system") return 0; if (node.type === "profile") return 0; if (layer === "runtime" || layer === "projects") return 1; if (["skill", "mcp_server", "cortex_tool", "sovereign_api", "external_dep"].includes(node.type)) return 2; return 3; } function _nodeSize(node, layer, priority) { if (layer === "system") return { width: 148, height: 54, fontSize: 12 }; if (node.type === "profile") return { width: 118, height: 48, fontSize: 11 }; if (layer === "runtime" || layer === "projects") return { width: 108, height: 42, fontSize: 10 }; if (priority === 2) return { width: 82, height: 34, fontSize: 9 }; return { width: 34, height: 28, fontSize: 9 }; } function applyLabelDensity() { if (!cy) return; const zoom = cy.zoom(); cy.batch(() => { cy.nodes().forEach((node) => { if (node.selected() || node.hasClass("hover")) return; const priority = node.data("labelPriority"); const label = priority <= 1 || (priority === 2 && zoom >= 0.85) || zoom >= 1.25 ? node.data("shortLabel") : ""; node.style("label", label); }); }); } function resetGraphEmphasis() { if (!cy) return; const inSpine = window.__svrntyUmbrella.view === "spine"; cy.batch(() => { cy.nodes().style("opacity", 1); cy.edges().style({ "opacity": inSpine ? 0.14 : 0.5, "width": 1 }); }); applyLabelDensity(); } function focusNode(node) { if (!cy) return; node.addClass("hover"); const neighborhood = node.closedNeighborhood(); cy.batch(() => { cy.elements().not(neighborhood).style("opacity", 0.16); neighborhood.nodes().style("opacity", 1); cy.edges().style({ "opacity": 0.05, "width": 1 }); node.connectedEdges().style({ "opacity": 0.9, "width": 2 }); }); } function _sourcePath(node) { return String((node && node.source_path) || ""); } function _docCategory(node) { const path = _sourcePath(node); if (path.includes("/01-ROADMAP/")) return "roadmap"; if (path.includes("/02-FRAMEWORK/")) return "framework"; if (path.includes("/03-PROTOCOLS/")) return "protocols"; if (path.includes("/04-STANDARDS/")) return "standards"; if (path.includes("/05-RECIPES/")) return "recipes"; if (path.includes("/06-REGISTRY/")) return "registry"; if (path.includes("/07-BRAND/")) return "brand"; if (path.includes("/08-OUTPUTS/")) return "outputs"; if (path.includes("/99-RAW/")) return "raw"; return "docs"; } function _spineLayerKey(node) { const id = node.id || ""; const source = _sourcePath(node); if (["readme", "architecture", "cortex-os-framework", "cortex-os-roadmap"].includes(id)) return "system"; if (["hermes-webui", "hermes-agent", "llm-gateway"].includes(id) || source.startsWith("hermes-webui/")) return "runtime"; if (node.type === "profile") return "profiles"; if (node.type === "skill") return "capabilities"; if (["mcp_server", "cortex_tool", "sovereign_api", "external_dep", "credential"].includes(node.type)) return "capabilities"; if (["bte", "bte-mcp", "adwright", "adwright-mcp", "svrnty-vision", "deep-research", "sandcastle", "pi-code"].includes(id)) return "projects"; if (source && !source.startsWith("sot/")) return "projects"; if (node.type === "doc") { const cat = _docCategory(node); if (["framework", "protocols", "standards"].includes(cat)) return "governance"; return "knowledge"; } return "knowledge"; } function _groupRows(nodes, keyFn) { const groups = new Map(); nodes.forEach((node) => { const key = keyFn(node); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(node); }); groups.forEach((items) => items.sort((a, b) => String(a.label || a.id).localeCompare(String(b.label || b.id)))); return [...groups.entries()].sort((a, b) => String(a[0]).localeCompare(String(b[0]))); } function _wrapGroupRows(groups, width, minGap) { const rows = []; groups.forEach(([group, items]) => { let row = []; let used = 0; items.forEach((node) => { const need = _estimatedLabelWidth(node) + minGap; if (row.length && used + need > width) { rows.push([group, row]); row = []; used = 0; } row.push(node); used += need; }); if (row.length) rows.push([group, row]); }); return rows; } function _rowPositions(groups, opts) { const positions = {}; const width = Math.max(opts.width || 1200, 900); const top = opts.top || 80; const rowGap = opts.rowGap || 120; const minGap = opts.minGap || 78; const rows = _wrapGroupRows(groups, Math.max(width - 180, 720), minGap); rows.forEach(([_, items], rowIndex) => { const totalLabelWidth = items.reduce((sum, node) => sum + _estimatedLabelWidth(node), 0); const rowWidth = Math.max(width - 180, totalLabelWidth + Math.max(0, items.length - 1) * minGap); const startX = (width - rowWidth) / 2; const availableGap = items.length > 1 ? (rowWidth - totalLabelWidth) / (items.length - 1) : 0; let cursor = startX; items.forEach((node, i) => { const labelWidth = _estimatedLabelWidth(node); positions[node.id] = { x: items.length === 1 ? width / 2 : cursor + labelWidth / 2, y: top + rowIndex * rowGap, }; cursor += labelWidth + Math.max(minGap, availableGap); }); }); return positions; } function _layerRows(items, layerWidth) { const innerWidth = Math.max(layerWidth - 120, 360); const rows = []; let row = []; let used = 0; items.forEach((node) => { const need = _estimatedLabelWidth(node) + 56; if (row.length && used + need > innerWidth) { rows.push(row); row = []; used = 0; } row.push(node); used += need; }); if (row.length) rows.push(row); return rows; } function spineLayout(g) { const layerMap = new Map(); SPINE_LAYERS.forEach((layer) => layerMap.set(layer.key, [])); g.nodes.forEach((node) => { const layer = _spineLayerKey(node); if (!layerMap.has(layer)) layerMap.set("knowledge", []); layerMap.get(layerMap.has(layer) ? layer : "knowledge").push(node); }); layerMap.forEach((items) => { items.sort((a, b) => { const av = _labelPriority(a, _spineLayerKey(a)); const bv = _labelPriority(b, _spineLayerKey(b)); return av - bv || String(a.label || a.id).localeCompare(String(b.label || b.id)); }); }); const positions = {}; const bands = []; let y = 80; SPINE_LAYERS.forEach((layer, layerIndex) => { const allItems = layerMap.get(layer.key) || []; if (!allItems.length) return; const collapsed = quietLayers.has(layer.key) && !disclosedLayers.has(layer.key); const items = collapsed ? [] : allItems; if (collapsed) { const layerWidth = Math.max(900, Math.min(1800, layer.baseWidth || 900)); const x1 = -layerWidth / 2; const x2 = layerWidth / 2; const y1 = y; const y2 = y + 52; bands.push({ key: layer.key, label: layer.label, count: allItems.length, color: layer.color, x1, x2, y1, y2, collapsed: true }); y = y2 + 42; return; } const density = items.reduce((sum, node) => sum + _estimatedLabelWidth(node), 0); const layerWidth = Math.min(4200, Math.max(layer.baseWidth || 900, density * 0.72)); const rows = _layerRows(items, layerWidth); const bandHeight = Math.max(138, rows.length * 92 + 74); const x1 = -layerWidth / 2; const x2 = layerWidth / 2; const y1 = y; const y2 = y + bandHeight; bands.push({ key: layer.key, label: layer.label, count: items.length, color: layer.color, x1, x2, y1, y2, collapsed: false }); rows.forEach((row, rowIndex) => { const rowWidth = row.reduce((sum, node) => sum + _estimatedLabelWidth(node), 0) + Math.max(0, row.length - 1) * 58; let cursor = -rowWidth / 2; row.forEach((node) => { const w = _estimatedLabelWidth(node); positions[node.id] = { x: cursor + w / 2, y: y1 + 78 + rowIndex * 92, }; cursor += w + 58; }); }); y = y2 + 70; }); return { positions, bands }; } function spinePositions(g) { return spineLayout(g).positions; } function orgPositions(g) { const bounds = document.getElementById("cy").getBoundingClientRect(); const profiles = g.nodes.filter((n) => n.type === "profile"); const profileIds = new Set(profiles.map((p) => p.id.replace(/-planb$/, ""))); const groups = [["system", g.nodes.filter((n) => ["readme", "architecture", "cortex-os-framework"].includes(n.id))]]; groups.push(["profiles", profiles]); profiles.forEach((profile) => { const shortId = profile.id.replace(/-planb$/, ""); groups.push([profile.id + "-skills", g.nodes.filter((n) => n.type === "skill" && (n.id.startsWith(profile.id + "::") || n.id.startsWith(shortId + "-")) )]); }); groups.push(["tools", g.nodes.filter((n) => ["mcp_server", "cortex_tool", "sovereign_api", "external_dep"].includes(n.type))]); groups.push(["credentials", g.nodes.filter((n) => n.type === "credential")]); groups.push(["sot", g.nodes.filter((n) => n.type === "doc" && !["readme", "architecture", "cortex-os-framework"].includes(n.id))]); return _rowPositions(groups.filter(([, items]) => items.length), { width: bounds.width, top: 70, rowGap: 120, minGap: 86, }); } function mindmapPositions(g) { const bounds = document.getElementById("cy").getBoundingClientRect(); const width = Math.max(bounds.width, 900); const height = Math.max(bounds.height, 700); const center = { x: width / 2, y: height / 2 }; const positions = {}; const rootIds = new Set(["readme", "architecture", "cortex-os-framework"]); const roots = g.nodes.filter((n) => rootIds.has(n.id)); roots.forEach((node, i) => { positions[node.id] = { x: center.x - 80 + i * 80, y: center.y }; }); const groups = _groupRows(g.nodes.filter((n) => !rootIds.has(n.id)), (n) => n.type || "other"); const maxRadius = Math.min(width, height) * 0.42; groups.forEach(([_, items], groupIndex) => { const angleStart = (Math.PI * 2 * groupIndex) / groups.length; const angleEnd = (Math.PI * 2 * (groupIndex + 1)) / groups.length; items.forEach((node, i) => { const t = (i + 1) / (items.length + 1); const angle = angleStart + (angleEnd - angleStart) * t; const ring = Math.floor(i / Math.max(1, Math.ceil(items.length / 4))); const radius = maxRadius * (0.35 + 0.16 * Math.min(ring, 4)); positions[node.id] = { x: center.x + Math.cos(angle) * radius, y: center.y + Math.sin(angle) * radius, }; }); }); return positions; } function clearLayerOverlay() { layerOverlayState = null; const overlay = document.getElementById("layerOverlay"); if (overlay) overlay.innerHTML = ""; } function renderLayerOverlay(bands) { const overlay = document.getElementById("layerOverlay"); if (!overlay || !cy) return; layerOverlayState = bands; overlay.innerHTML = bands.map((band) => '