diff --git a/clade-portal/rescript.json b/clade-portal/rescript.json deleted file mode 100644 index e1d96252..00000000 --- a/clade-portal/rescript.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@hyperpolymath/clade-portal", - "sources": [ - { - "dir": "src", - "subdirs": true - } - ], - "package-specs": [ - { - "module": "esmodule", - "in-source": true - } - ], - "suffix": ".res.mjs", - "bs-dependencies": [ - "@rescript/core" - ], - "bsc-flags": [ - "-open RescriptCore" - ], - "warnings": { - "number": "+a-4-9-20-40-41-42-50-61", - "error": "+5+6+101+109" - }, - "jsx": { - "version": 4, - "mode": "automatic" - } -} diff --git a/clade-portal/src/App.affine b/clade-portal/src/App.affine new file mode 100644 index 00000000..eb92faa1 --- /dev/null +++ b/clade-portal/src/App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module App; + +// TODO: Complete semantic implementation diff --git a/clade-portal/src/App.res b/clade-portal/src/App.res deleted file mode 100644 index fcfec14a..00000000 --- a/clade-portal/src/App.res +++ /dev/null @@ -1,1013 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// App — TEA (The Elm Architecture) entry point for the Clade Portal. -/// -/// The THIRD application built natively for the Gossamer webview shell. -/// Provides a taxonomy browser for PanLL's 100+ panel clades with three -/// view modes (tree, list, graph), full-text search, relationship mapping, -/// and live health indicators. -/// -/// Architecture: -/// - Model.res — State types -/// - Msg.res — Message types -/// - App.res — init, update, view (this file) -/// - CladeCmd.res — IPC commands to read clade metadata -/// - Capabilities.res — Gossamer capability token management -/// - RuntimeBridge.res — Gossamer-native IPC bridge -/// -/// Layout: -/// +--------------------------------------------------+ -/// | [Search bar] [Tree|List|Graph] [Cap] | -/// +----------+---------------------------------------+ -/// | Tree/ | Detail panel: clade metadata, | -/// | List | traits, capabilities, relationships, | -/// | sidebar | health status, panel integration | -/// | | | -/// +----------+---------------------------------------+ - -// --------------------------------------------------------------------------- -// TEA command helpers -// --------------------------------------------------------------------------- - -/// Wrap an async operation as a TEA command. -/// Runs the promise and dispatches the resulting message. -let cmdFromPromise = ( - promiseFn: unit => promise, - onOk: string => Msg.msg, - onErr: string => Msg.msg, -): Tea_Cmd.t => { - Tea_Cmd.call(dispatch => { - promiseFn() - ->Promise.thenResolve(result => dispatch(onOk(result))) - ->Promise.catch(err => { - let errMsg = switch err { - | JsExn(jsErr) => - switch JsExn.message(jsErr) { - | Some(m) => m - | None => "Unknown error" - } - | _ => "Unknown error" - } - dispatch(onErr(errMsg)) - Promise.resolve() - }) - ->ignore - }) -} - -/// Extract a filesystem capability token from the model. -/// Returns None if the filesystem capability has not been granted. -let getFilesystemToken = (model: Model.model): option => { - switch model.filesystemCap { - | Granted(token) => Some(token) - | _ => None - } -} - -/// Extract a network capability token from the model. -/// Returns None if the network capability has not been granted. -let getNetworkToken = (model: Model.model): option => { - switch model.networkCap { - | Granted(token) => Some(token) - | _ => None - } -} - -// --------------------------------------------------------------------------- -// Init -// --------------------------------------------------------------------------- - -/// Initialise the application. Starts with the capability grant panel -/// visible and no clades loaded (filesystem token required first). -let init = (): (Model.model, Tea_Cmd.t) => { - (Model.initial, Tea_Cmd.none) -} - -// --------------------------------------------------------------------------- -// Update -// --------------------------------------------------------------------------- - -/// Process a message and return the new state plus any commands to execute. -let update = (model: Model.model, msg: Msg.msg): (Model.model, Tea_Cmd.t) => { - switch msg { - // --- Clade loading --- - | LoadClades => - switch getFilesystemToken(model) { - | Some(token) => - let cmd = cmdFromPromise( - () => CladeCmd.loadAllCladeSummaries(token), - result => Msg.CladesLoaded(Ok(result)), - err => Msg.CladesLoaded(Error(err)), - ) - ({...model, isLoading: true}, cmd) - | None => ( - {...model, error: Some("Filesystem capability required. Grant it in the capability panel.")}, - Tea_Cmd.none, - ) - } - - | CladesLoaded(Ok(_response)) => - // In a full implementation, parse the JSON response into cladeSummary records. - // For now, mark loading complete and clear errors. - ({...model, isLoading: false, error: None}, Tea_Cmd.none) - - | CladesLoaded(Error(err)) => - ({...model, isLoading: false, error: Some(`Failed to load clades: ${err}`)}, Tea_Cmd.none) - - // --- Clade selection --- - | SelectClade(cladeId) => - switch getFilesystemToken(model) { - | Some(token) => - let cmd = cmdFromPromise( - () => CladeCmd.getCladeDetail(cladeId, token), - result => Msg.CladeDetailLoaded(Ok(result)), - err => Msg.CladeDetailLoaded(Error(err)), - ) - (model, cmd) - | None => ({...model, error: Some("Filesystem capability required.")}, Tea_Cmd.none) - } - - | CladeDetailLoaded(Ok(_response)) => - // In a full implementation, parse JSON into cladeDetail and set selectedClade. - ({...model, error: None}, Tea_Cmd.none) - - | CladeDetailLoaded(Error(err)) => - ({...model, error: Some(`Failed to load clade detail: ${err}`)}, Tea_Cmd.none) - - | DeselectClade => - ({...model, selectedClade: None}, Tea_Cmd.none) - - // --- Relationships --- - | LoadRelationships(cladeId) => - switch getFilesystemToken(model) { - | Some(token) => - let cmd = cmdFromPromise( - () => CladeCmd.getCladeRelationships(cladeId, token), - result => Msg.RelationshipsLoaded(Ok(result)), - err => Msg.RelationshipsLoaded(Error(err)), - ) - (model, cmd) - | None => ({...model, error: Some("Filesystem capability required.")}, Tea_Cmd.none) - } - - | RelationshipsLoaded(Ok(_response)) => - // In a full implementation, merge relationship data into selectedClade. - ({...model, error: None}, Tea_Cmd.none) - - | RelationshipsLoaded(Error(err)) => - ({...model, error: Some(`Failed to load relationships: ${err}`)}, Tea_Cmd.none) - - // --- Search --- - | UpdateSearchQuery(query) => - ({...model, searchQuery: query}, Tea_Cmd.none) - - | PerformSearch => - if String.length(model.searchQuery) == 0 { - ({...model, searchResults: []}, Tea_Cmd.none) - } else { - switch getFilesystemToken(model) { - | Some(token) => - let cmd = cmdFromPromise( - () => CladeCmd.searchClades(model.searchQuery, token), - result => Msg.SearchResultsLoaded(Ok(result)), - err => Msg.SearchResultsLoaded(Error(err)), - ) - (model, cmd) - | None => ({...model, error: Some("Filesystem capability required.")}, Tea_Cmd.none) - } - } - - | SearchResultsLoaded(Ok(_response)) => - // In a full implementation, parse JSON into searchResult records. - ({...model, error: None}, Tea_Cmd.none) - - | SearchResultsLoaded(Error(err)) => - ({...model, error: Some(`Search failed: ${err}`)}, Tea_Cmd.none) - - | ClearSearch => - ({...model, searchQuery: "", searchResults: []}, Tea_Cmd.none) - - // --- View mode --- - | SetViewMode(mode) => - ({...model, viewMode: mode}, Tea_Cmd.none) - - // --- Tree expansion --- - | ExpandNode(nodeId) => - let alreadyExpanded = Array.some(model.expandedNodes, n => n == nodeId) - if alreadyExpanded { - (model, Tea_Cmd.none) - } else { - ({...model, expandedNodes: Array.concat(model.expandedNodes, [nodeId])}, Tea_Cmd.none) - } - - | CollapseNode(nodeId) => - ( - {...model, expandedNodes: Array.filter(model.expandedNodes, n => n != nodeId)}, - Tea_Cmd.none, - ) - - // --- Health --- - | CheckCladeHealth(cladeId) => - switch getNetworkToken(model) { - | Some(token) => - let cmd = cmdFromPromise( - () => CladeCmd.getCladeHealth(cladeId, token), - result => Msg.CladeHealthLoaded(cladeId, Ok(result)), - err => Msg.CladeHealthLoaded(cladeId, Error(err)), - ) - (model, cmd) - | None => ({...model, error: Some("Network capability required for health checks.")}, Tea_Cmd.none) - } - - | CladeHealthLoaded(_cladeId, Ok(_response)) => - // In a full implementation, parse health status and update healthMap. - ({...model, error: None}, Tea_Cmd.none) - - | CladeHealthLoaded(_cladeId, Error(err)) => - ({...model, error: Some(`Health check failed: ${err}`)}, Tea_Cmd.none) - - | CheckAllHealth => - switch getNetworkToken(model) { - | Some(token) => - let cmd = cmdFromPromise( - () => CladeCmd.checkAllCladeHealth(token), - result => Msg.AllHealthLoaded(Ok(result)), - err => Msg.AllHealthLoaded(Error(err)), - ) - (model, cmd) - | None => ({...model, error: Some("Network capability required for health checks.")}, Tea_Cmd.none) - } - - | AllHealthLoaded(Ok(_response)) => - // In a full implementation, parse batch health data into healthMap. - ({...model, error: None}, Tea_Cmd.none) - - | AllHealthLoaded(Error(err)) => - ({...model, error: Some(`Batch health check failed: ${err}`)}, Tea_Cmd.none) - - // --- Gossamer capability tokens --- - | RequestCapability(kind) => - let kindInt = switch kind { - | "filesystem" => Capabilities.Kind.filesystem - | "network" => Capabilities.Kind.network - | _ => 0 - } - let updatedModel = switch kind { - | "filesystem" => {...model, filesystemCap: Pending} - | "network" => {...model, networkCap: Pending} - | _ => model - } - let cmd = cmdFromPromise( - () => Capabilities.requestCapability(kindInt)->Promise.thenResolve(token => Float.toString(token)), - tokenStr => { - switch Float.fromString(tokenStr) { - | Some(token) => Msg.CapGranted(kind, token) - | None => Msg.ClearError - } - }, - _err => Msg.CapRevoked(kind), - ) - (updatedModel, cmd) - - | CapGranted(kind, token) => - let updatedModel = switch kind { - | "filesystem" => {...model, filesystemCap: Granted(token), error: None} - | "network" => {...model, networkCap: Granted(token), error: None} - | _ => model - } - // Auto-load clades once filesystem token is granted. - let autoLoadCmd = switch kind { - | "filesystem" => - cmdFromPromise( - () => CladeCmd.loadAllCladeSummaries(token), - result => Msg.CladesLoaded(Ok(result)), - err => Msg.CladesLoaded(Error(err)), - ) - | _ => Tea_Cmd.none - } - ({...updatedModel, isLoading: kind == "filesystem"}, autoLoadCmd) - - | CapRevoked(kind) => - switch kind { - | "filesystem" => ({...model, filesystemCap: Denied}, Tea_Cmd.none) - | "network" => ({...model, networkCap: Denied}, Tea_Cmd.none) - | _ => (model, Tea_Cmd.none) - } - - | DismissCapPanel => - ({...model, showCapPanel: false}, Tea_Cmd.none) - - | ShowCapPanel => - ({...model, showCapPanel: true}, Tea_Cmd.none) - - // --- UI --- - | ClearError => - ({...model, error: None}, Tea_Cmd.none) - - | NoOp => - (model, Tea_Cmd.none) - } -} - -// --------------------------------------------------------------------------- -// View helpers -// --------------------------------------------------------------------------- - -/// Render a health indicator dot with the appropriate colour. -let healthDot = (status: Model.healthStatus): Tea_Html.t => { - let (label, className) = switch status { - | Healthy => ("Healthy", "health-healthy") - | Degraded => ("Degraded", "health-degraded") - | Unhealthy => ("Unhealthy", "health-unhealthy") - | Unknown => ("Unknown", "health-unknown") - } - Tea_Html.span( - [ - Tea_Html.Attributes.class(`health-dot ${className}`), - Tea_Html.Attributes.title(label), - ], - [], - ) -} - -/// Render a capability row in the grant panel. -let capabilityRow = ( - kindName: string, - kindInt: int, - status: Model.capabilityStatus, -): Tea_Html.t => { - let statusText = switch status { - | NotRequested => "Not requested" - | Pending => "Requesting..." - | Granted(_) => "Granted" - | Denied => "Denied" - } - let statusClass = switch status { - | NotRequested => "cap-not-requested" - | Pending => "cap-pending" - | Granted(_) => "cap-granted" - | Denied => "cap-denied" - } - let button = switch status { - | NotRequested | Denied => - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.RequestCapability(kindName))], - [Tea_Html.text("Grant")], - ) - | Pending => - Tea_Html.button( - [Tea_Html.Attributes.disabled(true)], - [Tea_Html.text("Pending...")], - ) - | Granted(_) => - Tea_Html.button( - [Tea_Html.Attributes.disabled(true)], - [Tea_Html.text("Active")], - ) - } - Tea_Html.div( - [Tea_Html.Attributes.class("cap-row")], - [ - Tea_Html.div( - [Tea_Html.Attributes.class("cap-info")], - [ - Tea_Html.strong([], [Tea_Html.text(Capabilities.Kind.toString(kindInt))]), - Tea_Html.p([], [Tea_Html.text(Capabilities.Kind.description(kindInt))]), - Tea_Html.span([Tea_Html.Attributes.class(statusClass)], [Tea_Html.text(statusText)]), - ], - ), - button, - ], - ) -} - -/// Render a single clade entry in the sidebar tree/list. -let cladeEntry = (clade: Model.cladeSummary, isExpanded: bool): Tea_Html.t => { - Tea_Html.div( - [ - Tea_Html.Attributes.class("clade-entry"), - Tea_Html.Events.onClick(Msg.SelectClade(clade.id)), - ], - [ - Tea_Html.div( - [Tea_Html.Attributes.class("clade-entry-header")], - [ - // Expand/collapse toggle for tree view. - Tea_Html.button( - [ - Tea_Html.Attributes.class("tree-toggle"), - Tea_Html.Events.onClick( - if isExpanded { - Msg.CollapseNode(clade.id) - } else { - Msg.ExpandNode(clade.id) - }, - ), - ], - [Tea_Html.text(if isExpanded { "v" } else { ">" })], - ), - healthDot(clade.health), - Tea_Html.span( - [Tea_Html.Attributes.class("clade-name")], - [Tea_Html.text(clade.name)], - ), - Tea_Html.span( - [Tea_Html.Attributes.class("clade-kind-badge")], - [Tea_Html.text(clade.kind)], - ), - ], - ), - if isExpanded { - Tea_Html.div( - [Tea_Html.Attributes.class("clade-entry-detail")], - [ - Tea_Html.p( - [Tea_Html.Attributes.class("clade-description")], - [Tea_Html.text(clade.description)], - ), - Tea_Html.span( - [Tea_Html.Attributes.class("panel-count")], - [Tea_Html.text(`${Int.toString(clade.panelCount)} panels`)], - ), - ], - ) - } else { - Tea_Html.noNode - }, - ], - ) -} - -/// Render the view mode toggle buttons. -let viewModeToggle = (current: Model.viewMode): Tea_Html.t => { - let modeButton = (mode: Model.viewMode, label: string) => { - let isActive = current == mode - Tea_Html.button( - [ - Tea_Html.Attributes.class( - if isActive { - "view-mode-btn active" - } else { - "view-mode-btn" - }, - ), - Tea_Html.Events.onClick(Msg.SetViewMode(mode)), - ], - [Tea_Html.text(label)], - ) - } - Tea_Html.div( - [Tea_Html.Attributes.class("view-mode-toggle")], - [ - modeButton(Tree, "Tree"), - modeButton(List, "List"), - modeButton(Graph, "Graph"), - ], - ) -} - -/// Render the search bar. -let searchBar = (query: string): Tea_Html.t => { - Tea_Html.div( - [Tea_Html.Attributes.class("search-bar")], - [ - Tea_Html.input( - [ - Tea_Html.Attributes.type_("text"), - Tea_Html.Attributes.placeholder("Search clades by name, kind, or description..."), - Tea_Html.Attributes.value(query), - Tea_Html.Events.onInput(value => Msg.UpdateSearchQuery(value)), - ], - ), - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.PerformSearch)], - [Tea_Html.text("Search")], - ), - if String.length(query) > 0 { - Tea_Html.button( - [ - Tea_Html.Attributes.class("search-clear"), - Tea_Html.Events.onClick(Msg.ClearSearch), - ], - [Tea_Html.text("Clear")], - ) - } else { - Tea_Html.noNode - }, - ], - ) -} - -/// Render search results list. -let searchResultsList = (results: array): Tea_Html.t => { - if Array.length(results) == 0 { - Tea_Html.noNode - } else { - Tea_Html.div( - [Tea_Html.Attributes.class("search-results")], - [ - Tea_Html.h3([], [Tea_Html.text(`${Int.toString(Array.length(results))} results`)]), - Tea_Html.div( - [Tea_Html.Attributes.class("results-list")], - Array.map(results, result => - Tea_Html.div( - [ - Tea_Html.Attributes.class("search-result-item"), - Tea_Html.Events.onClick(Msg.SelectClade(result.cladeId)), - ], - [ - Tea_Html.strong([], [Tea_Html.text(result.name)]), - Tea_Html.span( - [Tea_Html.Attributes.class("result-kind")], - [Tea_Html.text(result.kind)], - ), - Tea_Html.p( - [Tea_Html.Attributes.class("result-snippet")], - [Tea_Html.text(result.matchSnippet)], - ), - Tea_Html.span( - [Tea_Html.Attributes.class("result-field")], - [Tea_Html.text(`Matched: ${result.matchField}`)], - ), - ], - ) - ) - ->Array.toList - ->List.toArray, - ), - ], - ) - } -} - -/// Render trait badges for the detail panel. -let traitBadge = (label: string, isActive: bool): Tea_Html.t => { - Tea_Html.span( - [ - Tea_Html.Attributes.class( - if isActive { - "trait-badge trait-active" - } else { - "trait-badge trait-inactive" - }, - ), - ], - [Tea_Html.text(label)], - ) -} - -/// Render the detail panel for a selected clade. -let detailPanel = (detail: Model.cladeDetail): Tea_Html.t => { - Tea_Html.div( - [Tea_Html.Attributes.class("detail-panel")], - [ - // Header with name, kind badge, and health. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-header")], - [ - Tea_Html.h2([], [Tea_Html.text(detail.name)]), - Tea_Html.span( - [Tea_Html.Attributes.class("detail-short-name")], - [Tea_Html.text(`[${detail.shortName}]`)], - ), - Tea_Html.span( - [Tea_Html.Attributes.class("clade-kind-badge detail-kind")], - [Tea_Html.text(detail.kind)], - ), - healthDot(detail.health), - Tea_Html.span( - [Tea_Html.Attributes.class("detail-version")], - [Tea_Html.text(`v${detail.version}`)], - ), - ], - ), - // Description. - Tea_Html.p( - [Tea_Html.Attributes.class("detail-description")], - [Tea_Html.text(detail.description)], - ), - // Traits grid. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-section")], - [ - Tea_Html.h3([], [Tea_Html.text("Traits")]), - Tea_Html.div( - [Tea_Html.Attributes.class("traits-grid")], - [ - traitBadge("Backend", detail.traits.hasBackend), - traitBadge("Scanning", detail.traits.hasScanning), - traitBadge("Persistence", detail.traits.hasPersistence), - traitBadge("Work Items", detail.traits.hasWorkItems), - traitBadge("Priority Ordering", detail.traits.hasPriorityOrdering), - traitBadge("Customisation", detail.traits.hasCustomisation), - traitBadge("Directive", detail.traits.isDirective), - traitBadge("Read-only", detail.traits.isReadonly), - ], - ), - ], - ), - // Capabilities. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-section")], - [ - Tea_Html.h3([], [Tea_Html.text("Capabilities")]), - Tea_Html.div( - [Tea_Html.Attributes.class("capabilities-list")], - Array.map(detail.capabilities, cap => - Tea_Html.span( - [Tea_Html.Attributes.class("capability-tag")], - [Tea_Html.text(cap)], - ) - ) - ->Array.toList - ->List.toArray, - ), - ], - ), - // Panel integration. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-section")], - [ - Tea_Html.h3([], [Tea_Html.text("Panel Integration")]), - Tea_Html.dl( - [Tea_Html.Attributes.class("integration-list")], - [ - switch detail.panelId { - | Some(pid) => - Tea_Html.div( - [], - [ - Tea_Html.dt([], [Tea_Html.text("Panel ID")]), - Tea_Html.dd([], [Tea_Html.text(pid)]), - ], - ) - | None => Tea_Html.noNode - }, - switch detail.modelModule { - | Some(m) => - Tea_Html.div( - [], - [ - Tea_Html.dt([], [Tea_Html.text("Model Module")]), - Tea_Html.dd([], [Tea_Html.text(m)]), - ], - ) - | None => Tea_Html.noNode - }, - switch detail.componentModule { - | Some(c) => - Tea_Html.div( - [], - [ - Tea_Html.dt([], [Tea_Html.text("Component Module")]), - Tea_Html.dd([], [Tea_Html.text(c)]), - ], - ) - | None => Tea_Html.noNode - }, - switch detail.commandModule { - | Some(c) => - Tea_Html.div( - [], - [ - Tea_Html.dt([], [Tea_Html.text("Command Module")]), - Tea_Html.dd([], [Tea_Html.text(c)]), - ], - ) - | None => Tea_Html.noNode - }, - ], - ), - ], - ), - // Relationships. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-section")], - [ - Tea_Html.h3([], [Tea_Html.text("Relationships")]), - switch detail.relationships.parent { - | Some(parent) => - Tea_Html.div( - [Tea_Html.Attributes.class("relationship-row")], - [ - Tea_Html.span([Tea_Html.Attributes.class("rel-label")], [Tea_Html.text("Parent:")]), - Tea_Html.a( - [Tea_Html.Events.onClick(Msg.SelectClade(parent))], - [Tea_Html.text(parent)], - ), - ], - ) - | None => Tea_Html.noNode - }, - if Array.length(detail.relationships.siblings) > 0 { - Tea_Html.div( - [Tea_Html.Attributes.class("relationship-row")], - [ - Tea_Html.span( - [Tea_Html.Attributes.class("rel-label")], - [Tea_Html.text(`Siblings (${Int.toString(Array.length(detail.relationships.siblings))})`)], - ), - Tea_Html.div( - [Tea_Html.Attributes.class("rel-links")], - Array.map(detail.relationships.siblings, sib => - Tea_Html.a( - [ - Tea_Html.Attributes.class("rel-link"), - Tea_Html.Events.onClick(Msg.SelectClade(sib)), - ], - [Tea_Html.text(sib)], - ) - ) - ->Array.toList - ->List.toArray, - ), - ], - ) - } else { - Tea_Html.noNode - }, - if Array.length(detail.relationships.children) > 0 { - Tea_Html.div( - [Tea_Html.Attributes.class("relationship-row")], - [ - Tea_Html.span( - [Tea_Html.Attributes.class("rel-label")], - [Tea_Html.text(`Children (${Int.toString(Array.length(detail.relationships.children))})`)], - ), - Tea_Html.div( - [Tea_Html.Attributes.class("rel-links")], - Array.map(detail.relationships.children, child => - Tea_Html.a( - [ - Tea_Html.Attributes.class("rel-link"), - Tea_Html.Events.onClick(Msg.SelectClade(child)), - ], - [Tea_Html.text(child)], - ) - ) - ->Array.toList - ->List.toArray, - ), - ], - ) - } else { - Tea_Html.noNode - }, - ], - ), - // File presence indicators. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-section")], - [ - Tea_Html.h3([], [Tea_Html.text("Files")]), - Tea_Html.div( - [Tea_Html.Attributes.class("file-indicators")], - [ - Tea_Html.span( - [ - Tea_Html.Attributes.class( - if detail.hasK9Config { - "file-present" - } else { - "file-absent" - }, - ), - ], - [Tea_Html.text("config.k9.ncl")], - ), - Tea_Html.span( - [ - Tea_Html.Attributes.class( - if detail.hasReadme { - "file-present" - } else { - "file-absent" - }, - ), - ], - [Tea_Html.text("README.adoc")], - ), - ], - ), - ], - ), - // Actions. - Tea_Html.div( - [Tea_Html.Attributes.class("detail-actions")], - [ - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.CheckCladeHealth(detail.id))], - [Tea_Html.text("Check Health")], - ), - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.LoadRelationships(detail.id))], - [Tea_Html.text("Refresh Relationships")], - ), - Tea_Html.button( - [ - Tea_Html.Attributes.class("detail-close"), - Tea_Html.Events.onClick(Msg.DeselectClade), - ], - [Tea_Html.text("Close")], - ), - ], - ), - ], - ) -} - -// --------------------------------------------------------------------------- -// View -// --------------------------------------------------------------------------- - -/// Render the complete Clade Portal UI. -let view = (model: Model.model): Tea_Html.t => { - Tea_Html.div( - [Tea_Html.Attributes.class("clade-portal")], - [ - // --- Header --- - Tea_Html.header( - [Tea_Html.Attributes.class("portal-header")], - [ - Tea_Html.h1([], [Tea_Html.text("Clade Portal")]), - Tea_Html.div( - [Tea_Html.Attributes.class("header-controls")], - [ - searchBar(model.searchQuery), - viewModeToggle(model.viewMode), - Tea_Html.span( - [Tea_Html.Attributes.class("runtime-badge")], - [Tea_Html.text(`Runtime: ${RuntimeBridge.runtimeName()}`)], - ), - Tea_Html.span( - [Tea_Html.Attributes.class("clade-count")], - [Tea_Html.text(`${Int.toString(Array.length(model.clades))} clades`)], - ), - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.CheckAllHealth)], - [Tea_Html.text("Health Check")], - ), - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.ShowCapPanel)], - [Tea_Html.text("Capabilities")], - ), - ], - ), - ], - ), - - // --- Error bar --- - switch model.error { - | Some(err) => - Tea_Html.div( - [Tea_Html.Attributes.class("error-bar")], - [ - Tea_Html.text(err), - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.ClearError)], - [Tea_Html.text("Dismiss")], - ), - ], - ) - | None => Tea_Html.noNode - }, - - // --- Capability grant panel --- - if model.showCapPanel { - Tea_Html.div( - [Tea_Html.Attributes.class("cap-panel")], - [ - Tea_Html.h2([], [Tea_Html.text("Gossamer Capability Tokens")]), - Tea_Html.p( - [Tea_Html.Attributes.class("cap-description")], - [ - Tea_Html.text( - "Clade Portal runs in a sandboxed Gossamer webview. " ++ - "Grant capabilities below to enable clade browsing and health monitoring. " ++ - "Filesystem access is required to read clade metadata. " ++ - "Network access enables live health indicators.", - ), - ], - ), - capabilityRow("filesystem", Capabilities.Kind.filesystem, model.filesystemCap), - capabilityRow("network", Capabilities.Kind.network, model.networkCap), - Tea_Html.button( - [ - Tea_Html.Attributes.class("cap-dismiss"), - Tea_Html.Events.onClick(Msg.DismissCapPanel), - ], - [Tea_Html.text("Continue to Clade Portal")], - ), - ], - ) - } else { - Tea_Html.noNode - }, - - // --- Search results overlay --- - searchResultsList(model.searchResults), - - // --- Loading indicator --- - if model.isLoading { - Tea_Html.div( - [Tea_Html.Attributes.class("loading-indicator")], - [Tea_Html.text("Loading clade taxonomy...")], - ) - } else { - Tea_Html.noNode - }, - - // --- Main content --- - Tea_Html.main( - [Tea_Html.Attributes.class("portal-main")], - [ - // Sidebar: clade tree/list. - Tea_Html.aside( - [Tea_Html.Attributes.class("clade-sidebar")], - [ - Tea_Html.div( - [Tea_Html.Attributes.class("sidebar-header")], - [ - Tea_Html.h2( - [], - [ - Tea_Html.text( - switch model.viewMode { - | Tree => "Clade Tree" - | List => "Clade List" - | Graph => "Clade Graph" - }, - ), - ], - ), - Tea_Html.button( - [Tea_Html.Events.onClick(Msg.LoadClades)], - [Tea_Html.text("Refresh")], - ), - ], - ), - Tea_Html.div( - [Tea_Html.Attributes.class("clade-list")], - Array.map(model.clades, clade => { - let isExpanded = Array.some(model.expandedNodes, n => n == clade.id) - cladeEntry(clade, isExpanded) - }) - ->Array.toList - ->List.toArray, - ), - ], - ), - - // Detail panel: selected clade or placeholder. - Tea_Html.section( - [Tea_Html.Attributes.class("content-panel")], - [ - switch model.selectedClade { - | Some(detail) => detailPanel(detail) - | None => - Tea_Html.div( - [Tea_Html.Attributes.class("placeholder")], - [ - Tea_Html.h2([], [Tea_Html.text("PanLL Clade Taxonomy")]), - Tea_Html.p( - [], - [ - Tea_Html.text( - "Select a clade from the sidebar to view its metadata, " ++ - "traits, capabilities, panel integration details, and " ++ - "relationships to other clades.", - ), - ], - ), - Tea_Html.p( - [Tea_Html.Attributes.class("placeholder-stats")], - [ - Tea_Html.text( - `${Int.toString(Array.length(model.clades))} clades loaded`, - ), - ], - ), - ], - ) - }, - ], - ), - ], - ), - ], - ) -} - -// --------------------------------------------------------------------------- -// Main — TEA program registration -// --------------------------------------------------------------------------- - -/// Start the Clade Portal TEA application. -/// Mounts into the #app element in public/index.html. -let main = Tea_App.standardProgram({ - init: () => init(), - update: update, - view: view, - subscriptions: _model => Tea_Sub.none, -}) diff --git a/clade-portal/src/Capabilities.affine b/clade-portal/src/Capabilities.affine new file mode 100644 index 00000000..418221e5 --- /dev/null +++ b/clade-portal/src/Capabilities.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Capabilities; + +// TODO: Complete semantic implementation diff --git a/clade-portal/src/Capabilities.res b/clade-portal/src/Capabilities.res deleted file mode 100644 index bb8caa01..00000000 --- a/clade-portal/src/Capabilities.res +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// Capabilities — Gossamer capability token management for the Clade Portal. -/// -/// The Clade Portal requires two capabilities: -/// - filesystem (kind 2): Read clade directories and A2ML metadata files -/// from the panel-clades path on disk. -/// - network (kind 1): Fetch health status from running panel services -/// to display live health indicators in the portal. -/// -/// Flow: -/// 1. App starts with NO capabilities (sandbox by default) -/// 2. User sees the capability grant panel -/// 3. User clicks "Grant Filesystem" -> Gossamer shows consent dialog -/// 4. Runtime returns a token (float) valid for TTL seconds -/// 5. All subsequent file reads include the token in the IPC payload -/// 6. Token expires -> app must re-request or operations fail - -/// Capability kind identifiers matching the Gossamer runtime's internal enum. -module Kind = { - /// Filesystem access — read clade directories and A2ML files. - let filesystem = 2 - - /// Network access — fetch panel health status from running services. - let network = 1 - - /// Human-readable name for a capability kind. - let toString = (kind: int): string => { - switch kind { - | 1 => "network" - | 2 => "filesystem" - | k => `unknown(${Int.toString(k)})` - } - } - - /// Description of why the Clade Portal needs this capability. - let description = (kind: int): string => { - switch kind { - | 1 => "Check health of running panel services to display live status indicators." - | 2 => "Read clade directories and A2ML metadata files from the panel-clades path." - | _ => "Unknown capability." - } - } -} - -/// Request a capability token from the Gossamer runtime. -/// -/// This triggers Gossamer's consent dialog. The user must approve the -/// request before the runtime issues a token. Returns a promise that -/// resolves to the token value (float) on success. -/// -/// @param kind - The capability kind (use Kind.filesystem, Kind.network) -let requestCapability = (kind: int): promise => { - RuntimeBridge.invoke("__gossamer_cap_grant", {"kind": kind}) -} - -/// Request filesystem capability — needed to read clade A2ML files. -/// -/// Without this token, no clade metadata can be loaded from disk. -/// This is the first capability users should grant. -let requestFilesystemAccess = (): promise => { - requestCapability(Kind.filesystem) -} - -/// Request network capability — needed for panel health checks. -/// -/// Health indicators show which panels are running, degraded, or offline. -/// This capability is optional but recommended for full portal features. -let requestNetworkAccess = (): promise => { - requestCapability(Kind.network) -} - -/// Revoke a previously granted capability. -/// -/// After revocation, any IPC calls using the old token will fail. -/// -/// @param kind - The capability kind to revoke -let revokeCapability = (kind: int): promise => { - RuntimeBridge.invoke("__gossamer_cap_revoke", {"kind": kind}) -} - -/// Check whether a token is still valid. -/// -/// Tokens expire after the TTL defined in gossamer.conf.json (default: -/// 3600 seconds). This lets the app proactively check and re-request -/// before a critical operation fails. -/// -/// @param token - The capability token to validate -let validateToken = (token: float): promise => { - RuntimeBridge.invoke("__gossamer_cap_validate", {"token": token}) -} diff --git a/clade-portal/src/CladeCmd.affine b/clade-portal/src/CladeCmd.affine new file mode 100644 index 00000000..6bb98e4c --- /dev/null +++ b/clade-portal/src/CladeCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CladeCmd; + +// TODO: Complete semantic implementation diff --git a/clade-portal/src/CladeCmd.res b/clade-portal/src/CladeCmd.res deleted file mode 100644 index 9a372374..00000000 --- a/clade-portal/src/CladeCmd.res +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// CladeCmd — Backend command dispatch for the Clade Portal. -/// -/// Each function wraps a Gossamer IPC call to read clade metadata from the -/// filesystem or check panel health over the network. Filesystem commands -/// require a valid filesystem capability token; health commands require a -/// network capability token. -/// -/// The commands operate on the PanLL panel-clades directory structure: -/// panel-clades/clades/{clade-id}/ -/// *.a2ml — Clade metadata (A2ML format) -/// config.k9.ncl — K9 kennel configuration (Nickel) -/// README.adoc — Human-readable documentation -/// -/// Gossamer acts as the filesystem proxy — the webview never makes direct -/// file I/O calls. Instead, each command goes through IPC to the Gossamer -/// Zig runtime, which holds the filesystem capability and reads from disk. - -/// The base path to the panel-clades directory on disk. -/// Resolved by the Gossamer backend via IPC — this constant is a hint only. -/// The backend uses PANLL_ROOT env var or its own exe-relative discovery. -let _cladesBasePath = "panel-clades/clades" - -// --------------------------------------------------------------------------- -// Directory listing -// --------------------------------------------------------------------------- - -/// List all clade directories under the panel-clades path. -/// -/// Returns a JSON array of directory entry objects, each containing: -/// - name: string — the directory name (clade ID) -/// - isDirectory: bool — always true for valid clades -/// -/// Excludes hidden files and the _basics template directory. -/// Requires a filesystem capability token. -let listClades = (token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_list_directories", - {"path": _cladesBasePath, "excludePatterns": ["_basics", "README.adoc"]}, - token, - ) -} - -// --------------------------------------------------------------------------- -// Clade detail -// --------------------------------------------------------------------------- - -/// Read the full detail of a specific clade. -/// -/// Reads and parses all A2ML files in the clade directory, returning -/// the combined metadata as a JSON string. The response includes: -/// - id, name, shortName, version, kind, icon, description -/// - traits: hasBackend, hasScanning, hasPersistence, etc. -/// - capabilities: array of capability strings -/// - panelIntegration: panelId, modelModule, componentModule, etc. -/// - hasK9Config: whether a config.k9.ncl file exists -/// - hasReadme: whether a README.adoc file exists -/// -/// @param cladeId - The clade directory name (e.g. "ai", "databases") -let getCladeDetail = (cladeId: string, token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_read_detail", - {"path": `${_cladesBasePath}/${cladeId}`, "cladeId": cladeId}, - token, - ) -} - -// --------------------------------------------------------------------------- -// Relationships -// --------------------------------------------------------------------------- - -/// Get parent/child/sibling relationships for a clade. -/// -/// Analyses the clade's kind, capabilities, and connections sections to -/// determine its position in the taxonomy. Returns a JSON object with: -/// - parent: option — parent clade ID if hierarchical -/// - children: array — child clade IDs -/// - siblings: array — clades of the same kind -/// - dependencies: array — clades this one depends on -/// - dependents: array — clades that depend on this one -/// -/// @param cladeId - The clade directory name -let getCladeRelationships = (cladeId: string, token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_get_relationships", - {"path": _cladesBasePath, "cladeId": cladeId}, - token, - ) -} - -// --------------------------------------------------------------------------- -// Search -// --------------------------------------------------------------------------- - -/// Full-text search across all clade metadata. -/// -/// Searches clade names, descriptions, capability lists, and kind fields -/// for the given query string. Returns a JSON array of match objects: -/// - cladeId: string — the matching clade -/// - name: string — clade display name -/// - kind: string — clade kind -/// - matchField: string — which field matched (name, description, etc.) -/// - matchSnippet: string — context around the match -/// - score: float — relevance score (higher = better match) -/// -/// @param query - The search query string (case-insensitive substring match) -let searchClades = (query: string, token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_search", - {"path": _cladesBasePath, "query": query}, - token, - ) -} - -// --------------------------------------------------------------------------- -// Health -// --------------------------------------------------------------------------- - -/// Check the health of panels belonging to a clade. -/// -/// For each panel registered in the clade's panel-integration section, -/// this command attempts to contact the panel's health endpoint (if the -/// panel has a backend). Returns a JSON object with: -/// - cladeId: string — the clade checked -/// - panelHealth: array of { panelId, status, latencyMs, lastCheck } -/// - overallStatus: "healthy" | "degraded" | "unhealthy" | "unknown" -/// -/// Requires a network capability token (panels may run on localhost or -/// remote services). -/// -/// @param cladeId - The clade to check health for -let getCladeHealth = (cladeId: string, token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_check_health", - {"cladeId": cladeId}, - token, - ) -} - -// --------------------------------------------------------------------------- -// Batch operations -// --------------------------------------------------------------------------- - -/// Load summary data for all clades in a single IPC call. -/// -/// This is an optimised path for the initial portal load. Instead of -/// calling listClades + getCladeDetail for each clade (100+ IPC calls), -/// this command reads all clade directories and extracts summary fields -/// (id, name, kind, icon, description, panelCount) in one pass. -/// -/// Returns a JSON array of clade summary objects. -let loadAllCladeSummaries = (token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_load_all_summaries", - {"path": _cladesBasePath}, - token, - ) -} - -/// Batch health check for all clades with backends. -/// -/// Checks health of all clades that have hasBackend = true in their -/// traits. Returns a JSON object mapping clade IDs to health status. -/// -/// Requires a network capability token. -let checkAllCladeHealth = (token: float): promise => { - RuntimeBridge.invokeWithToken( - "clade_check_all_health", - {"path": _cladesBasePath}, - token, - ) -} diff --git a/clade-portal/src/Model.affine b/clade-portal/src/Model.affine new file mode 100644 index 00000000..cd116033 --- /dev/null +++ b/clade-portal/src/Model.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Model; + +// TODO: Complete semantic implementation diff --git a/clade-portal/src/Model.res b/clade-portal/src/Model.res deleted file mode 100644 index b6284f54..00000000 --- a/clade-portal/src/Model.res +++ /dev/null @@ -1,216 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// Model — Application state for the Clade Portal. -/// -/// Holds the complete UI state for browsing PanLL's 100+ panel clades. -/// The portal provides three view modes (tree, list, graph), a search -/// facility, and live health indicators for each clade's panels. -/// -/// Capability tokens are the central security mechanism: filesystem access -/// is required to read clade metadata, and network access is required to -/// check panel health. - -// --------------------------------------------------------------------------- -// View mode -// --------------------------------------------------------------------------- - -/// How the clade taxonomy is displayed in the main content area. -type viewMode = - | /// Hierarchical tree with expandable nodes grouped by kind. - Tree - | /// Flat alphabetical list with sortable columns. - List - | /// Force-directed graph showing relationships between clades. - Graph - -// --------------------------------------------------------------------------- -// Health status -// --------------------------------------------------------------------------- - -/// Health status of a clade's panels. -type healthStatus = - | /// All panels in the clade are responding normally. - Healthy - | /// Some panels are slow or partially available. - Degraded - | /// No panels are responding or all have errors. - Unhealthy - | /// Health has not been checked yet (no network token, or not loaded). - Unknown - -// --------------------------------------------------------------------------- -// Clade types -// --------------------------------------------------------------------------- - -/// Summary information for a single clade, displayed in lists and trees. -type cladeSummary = { - /// Clade identifier (directory name, e.g. "ai", "databases"). - id: string, - /// Human-readable display name. - name: string, - /// Clade kind (directive, scanner, builder, database, network, etc.). - kind: string, - /// Icon identifier (maps to Lucide icon set). - icon: string, - /// Short description of what the clade does. - description: string, - /// Number of panels registered in this clade. - panelCount: int, - /// Current health status of the clade's panels. - health: healthStatus, -} - -/// Trait flags from the clade's [clade-traits] section. -type cladeTraits = { - /// Whether the clade connects to a backend service. - hasBackend: bool, - /// Whether the clade performs scanning/analysis. - hasScanning: bool, - /// Whether the clade persists data across sessions. - hasPersistence: bool, - /// Whether the clade generates work items. - hasWorkItems: bool, - /// Whether the clade defines priority ordering for other panels. - hasPriorityOrdering: bool, - /// Whether the clade supports user customisation. - hasCustomisation: bool, - /// Whether the clade is a directive (controls other panels). - isDirective: bool, - /// Whether the clade is read-only (reports but does not modify). - isReadonly: bool, -} - -/// Relationship data between clades. -type cladeRelationships = { - /// Parent clade ID, if this clade is part of a hierarchy. - parent: option, - /// Child clade IDs that inherit from this clade. - children: array, - /// Sibling clades of the same kind. - siblings: array, - /// Clades that this one depends on. - dependencies: array, - /// Clades that depend on this one. - dependents: array, -} - -/// Full detail for a single clade, shown in the detail panel. -type cladeDetail = { - /// Core metadata from [clade-metadata]. - id: string, - /// Human-readable display name. - name: string, - /// Short name for compact UI elements. - shortName: string, - /// Semantic version of the clade definition. - version: string, - /// Clade kind (directive, scanner, builder, etc.). - kind: string, - /// Icon identifier. - icon: string, - /// Full description. - description: string, - /// Trait flags from [clade-traits]. - traits: cladeTraits, - /// Capability strings from [clade-capabilities]. - capabilities: array, - /// Panel integration identifiers from [clade-panel-integration]. - panelId: option, - /// Model module name. - modelModule: option, - /// Component module name. - componentModule: option, - /// Command module name. - commandModule: option, - /// Relationships to other clades. - relationships: cladeRelationships, - /// Whether a config.k9.ncl file exists for this clade. - hasK9Config: bool, - /// Whether a README.adoc file exists for this clade. - hasReadme: bool, - /// Current health status. - health: healthStatus, -} - -/// A search result entry returned by full-text clade search. -type searchResult = { - /// Matching clade ID. - cladeId: string, - /// Clade display name. - name: string, - /// Clade kind. - kind: string, - /// Which field matched the query. - matchField: string, - /// Context snippet around the match. - matchSnippet: string, - /// Relevance score (higher = better match). - score: float, -} - -// --------------------------------------------------------------------------- -// Capability status -// --------------------------------------------------------------------------- - -/// Capability token status for Gossamer security. -/// Each capability must be explicitly granted by the runtime before use. -type capabilityStatus = - | /// Not yet requested from the runtime. - NotRequested - | /// Request sent, awaiting runtime grant. - Pending - | /// Granted with a token. The float is the token value. - Granted(float) - | /// Runtime denied the capability request. - Denied - -// --------------------------------------------------------------------------- -// Application model -// --------------------------------------------------------------------------- - -/// Complete application state for the Clade Portal. -type model = { - /// All clade summaries loaded from disk. - clades: array, - /// Currently selected clade with full detail, if any. - selectedClade: option, - /// Current search query string (empty = no active search). - searchQuery: string, - /// Search results from the last query. - searchResults: array, - /// Current view mode for the main content area. - viewMode: viewMode, - /// Set of clade IDs whose tree nodes are expanded. - expandedNodes: array, - /// Whether the initial clade list has been loaded. - isLoading: bool, - /// Filesystem capability token — required to read clade files. - filesystemCap: capabilityStatus, - /// Network capability token — required for health checks. - networkCap: capabilityStatus, - /// Error message to display in the UI, if any. - error: option, - /// Whether the capability grant panel is visible. - showCapPanel: bool, - /// Health status map: clade ID -> health status (from batch check). - healthMap: array<(string, healthStatus)>, -} - -/// Initial application state. Starts with no capabilities granted, -/// forcing the user to explicitly authorise filesystem and network -/// access through the Gossamer capability token system. -let initial: model = { - clades: [], - selectedClade: None, - searchQuery: "", - searchResults: [], - viewMode: Tree, - expandedNodes: [], - isLoading: false, - filesystemCap: NotRequested, - networkCap: NotRequested, - error: None, - showCapPanel: true, - healthMap: [], -} diff --git a/clade-portal/src/Msg.affine b/clade-portal/src/Msg.affine new file mode 100644 index 00000000..b1a59b85 --- /dev/null +++ b/clade-portal/src/Msg.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Msg; + +// TODO: Complete semantic implementation diff --git a/clade-portal/src/Msg.res b/clade-portal/src/Msg.res deleted file mode 100644 index 6311b22c..00000000 --- a/clade-portal/src/Msg.res +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// Msg — Message type for the Clade Portal TEA architecture. -/// -/// Every user interaction and async result flows through this type. -/// Messages are dispatched by the view and processed by the update -/// function in App.res. - -/// All messages that can occur in the Clade Portal. -type msg = - // --- Clade loading --- - | /// Load all clade summaries from disk (initial load). - LoadClades - | /// All clade summaries arrived from the filesystem. - CladesLoaded(result) - - // --- Clade selection --- - | /// User clicked a clade in the tree/list/graph to see its detail. - SelectClade(string) - | /// Clade detail response arrived from the filesystem. - CladeDetailLoaded(result) - | /// User closed the detail panel. - DeselectClade - - // --- Relationships --- - | /// Load relationship data for the selected clade. - LoadRelationships(string) - | /// Relationship data arrived. - RelationshipsLoaded(result) - - // --- Search --- - | /// User typed in the search bar. - UpdateSearchQuery(string) - | /// User submitted the search (pressed Enter or clicked Search). - PerformSearch - | /// Search results arrived. - SearchResultsLoaded(result) - | /// User cleared the search bar. - ClearSearch - - // --- View mode --- - | /// User switched to a different view mode (Tree, List, Graph). - SetViewMode(Model.viewMode) - - // --- Tree expansion --- - | /// User expanded a node in the tree view. - ExpandNode(string) - | /// User collapsed a node in the tree view. - CollapseNode(string) - - // --- Health --- - | /// Check health for a single clade. - CheckCladeHealth(string) - | /// Health check result for a single clade. - CladeHealthLoaded(string, result) - | /// Batch health check for all clades. - CheckAllHealth - | /// Batch health results arrived. - AllHealthLoaded(result) - - // --- Gossamer capability tokens --- - | /// User clicked "Grant" on a capability in the cap panel. - RequestCapability(string) - | /// Gossamer runtime granted a capability token. - CapGranted(string, float) - | /// Gossamer runtime revoked a capability token. - CapRevoked(string) - | /// User dismissed the capability panel. - DismissCapPanel - | /// User reopened the capability panel. - ShowCapPanel - - // --- UI --- - | /// Clear the current error message. - ClearError - | /// No-op message (used for commands that have no followup). - NoOp diff --git a/clade-portal/src/RuntimeBridge.affine b/clade-portal/src/RuntimeBridge.affine new file mode 100644 index 00000000..585655fa --- /dev/null +++ b/clade-portal/src/RuntimeBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RuntimeBridge; + +// TODO: Complete semantic implementation diff --git a/clade-portal/src/RuntimeBridge.res b/clade-portal/src/RuntimeBridge.res deleted file mode 100644 index 44bb875d..00000000 --- a/clade-portal/src/RuntimeBridge.res +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// RuntimeBridge — Gossamer-native IPC bridge for the Clade Portal. -/// -/// Gossamer-only bridge for the third Gossamer-native application. The Clade -/// Portal reads clade A2ML files from the PanLL panel-clades directory via -/// filesystem capability tokens, and checks panel health via network tokens. -/// -/// The bridge communicates with the Gossamer runtime via the injected -/// `window.__gossamer_invoke` function. All IPC uses JSON protocol as -/// configured in gossamer.conf.json. -/// -/// Capability tokens: -/// - filesystem: Required to read clade directories and A2ML files -/// - network: Required to fetch panel health status from running services - -// --------------------------------------------------------------------------- -// Gossamer runtime detection -// --------------------------------------------------------------------------- - -/// Check whether the Gossamer runtime is available in this webview. -/// Returns true when `window.__gossamer_invoke` has been injected by -/// the gossamer_channel_open() call during webview initialisation. -%%raw(` -function isGossamerRuntime() { - return typeof window !== 'undefined' - && typeof window.__gossamer_invoke === 'function'; -} -`) -@val external isGossamerRuntime: unit => bool = "isGossamerRuntime" - -/// Raw Gossamer IPC call. Sends a command name and JSON payload to the -/// Gossamer runtime and returns a promise with the response. -%%raw(` -function gossamerInvoke(cmd, args) { - return window.__gossamer_invoke(cmd, args); -} -`) -@val external gossamerInvoke: (string, 'a) => promise<'b> = "gossamerInvoke" - -// --------------------------------------------------------------------------- -// Runtime type (Gossamer-only, no Tauri path) -// --------------------------------------------------------------------------- - -/// The runtime environment. For the Clade Portal, this is always Gossamer -/// or an error state (dev browser without the runtime). -type runtime = - | /// Running inside the Gossamer webview shell (production). - Gossamer - | /// Running in a plain browser (development only — most features disabled). - BrowserDev - -/// Detect the current runtime environment. -let detectRuntime = (): runtime => { - if isGossamerRuntime() { - Gossamer - } else { - BrowserDev - } -} - -// --------------------------------------------------------------------------- -// Unified invoke — Gossamer-native with dev fallback -// --------------------------------------------------------------------------- - -/// Invoke a Gossamer IPC command. -/// -/// In production (Gossamer runtime), this calls `window.__gossamer_invoke`. -/// In development (browser), this rejects with a descriptive error so the -/// developer knows to run inside Gossamer. -/// -/// All command modules (CladeCmd, Capabilities) use this function. -let invoke = (cmd: string, args: 'a): promise<'b> => { - if isGossamerRuntime() { - gossamerInvoke(cmd, args) - } else { - Promise.reject( - JsError.throwWithMessage( - `Gossamer runtime required — "${cmd}" cannot run in a plain browser. ` ++ - `Launch via: gossamer run --config gossamer.conf.json`, - ), - ) - } -} - -/// Invoke a command that requires a capability token. -/// -/// This is the security-critical path. The token is included in the IPC -/// payload so the Gossamer runtime can verify the caller holds the -/// required capability before executing the command. -/// -/// @param cmd - The IPC command name -/// @param args - The command payload -/// @param token - The capability token (obtained from __gossamer_cap_grant) -let invokeWithToken = (cmd: string, args: 'a, token: float): promise<'b> => { - if isGossamerRuntime() { - gossamerInvoke(cmd, {"__cap_token": token, "payload": args}) - } else { - Promise.reject( - JsError.throwWithMessage( - `Gossamer runtime required — "${cmd}" needs a capability token`, - ), - ) - } -} - -/// Check whether the Gossamer runtime is available. -let hasRuntime = (): bool => isGossamerRuntime() - -/// Human-readable runtime name for display in the UI. -let runtimeName = (): string => { - switch detectRuntime() { - | Gossamer => "Gossamer" - | BrowserDev => "Browser (dev)" - } -} diff --git a/rescript.json b/rescript.json deleted file mode 100644 index 83e44f48..00000000 --- a/rescript.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "panll", - "sources": [ - { - "dir": "src", - "subdirs": true - } - ], - "package-specs": [ - { - "module": "esmodule", - "in-source": true - } - ], - "suffix": ".res.js", - "dependencies": [ - "@rescript/core" - ], - "compiler-flags": [ - "-open RescriptCore" - ], - "warnings": { - "error": "+101-33-44", - "number": "-44-45" - } -} diff --git a/src/App.affine b/src/App.affine new file mode 100644 index 00000000..eb92faa1 --- /dev/null +++ b/src/App.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module App; + +// TODO: Complete semantic implementation diff --git a/src/App.res b/src/App.res deleted file mode 100644 index 79e2b82d..00000000 --- a/src/App.res +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Application Entry Point -/// -/// Initializes the TEA application with the Binary Star co-orbit model. - -/// Initialize the application -/// Attempts to restore persisted state then probes panic-attacker capability so the UI -/// knows whether ambush/panll exports are available. -let init = (): (Model.model, Tea_Cmd.t) => { - // Try to load persisted state - let model = switch Storage.load() { - | Some(loadedModel) => loadedModel - | None => Model.init() // Use default if no saved state - } - - // Register OS colour scheme change listener for System theme mode. - let colorSchemeCmd = AccessibilityEngine.listenColorSchemeChange(prefersLight => { - let osTheme: AccessibilityModel.themeMode = if prefersLight { - ThemeLight - } else { - ThemeDark - } - Msg.AccessibilityCtrl(OsColorSchemeChanged(osTheme)) - }) - - // Apply saved font size on startup (sets font-size for rem scaling). - let fontSizeCmd = AccessibilityEngine.applyFontSizeCmd(model.accessibility.fontSize) - - // Probe Burble groove endpoint at startup for capability discovery. - // Non-blocking — if Burble is not running, the error is silently recorded. - let grooveCmd = BurbleCmd.checkGroove(result => - switch result { - | Ok(_json) => Msg.Burble(BurbleModel.ConnectionChanged(BurbleModel.Connected)) - | Error(err) => Msg.Burble(BurbleModel.ErrorOccurred(err)) - } - ) - - // Attempt async VeriSimDB state restore (Connected Workbench v0.2.0). - // Fires after the synchronous localStorage load above — if VeriSimDB holds - // newer/richer state, the VeriSimDBStateLoaded handler will merge it in. - let verisimdbStateCmd = GossamerCmd.loadStateFromVeriSimDB(result => - Msg.VeriSimDBStateLoaded(result) - ) - - // Probe all registered services at startup (Connected Workbench v0.2.0). - // Non-blocking — populates the service registry with current health status. - let serviceRefreshCmd = ServiceCmd.refreshAll(result => - Msg.Service(RefreshAllResult(result)) - ) - - // Load user settings from ~/.panll/config.json (Connected Workbench v0.2.0). - let settingsCmd = SettingsCmd.getSettings(result => - Msg.Settings(SettingsLoaded(result)) - ) - - ( - model, - Tea_Cmd.batch(list{ - colorSchemeCmd, - fontSizeCmd, - grooveCmd, - verisimdbStateCmd, - serviceRefreshCmd, - settingsCmd, - }), - ) -} - -/// Main TEA program -/// Bootstraps the standard TEA pipeline (init/update/view/subscriptions). -let main = Tea_App.standardProgram( - ~init, - ~update=Update.update, - ~view=View.view, - ~subscriptions=SubscriptionsFixed.all, - (), -) diff --git a/src/Model.affine b/src/Model.affine new file mode 100644 index 00000000..cd116033 --- /dev/null +++ b/src/Model.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Model; + +// TODO: Complete semantic implementation diff --git a/src/Model.res b/src/Model.res deleted file mode 100644 index 95b23544..00000000 --- a/src/Model.res +++ /dev/null @@ -1,1533 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Model — Composition root for the eNSAID environment state. -/// -/// This module re-exports all domain types from the four sub-modules -/// (PaneModel, EchidnaModel, VeriSimModel, GovernanceModel) and defines -/// the unified `model` record and its initial state. -/// -/// Downstream code using `open Model` or `Model.typeName` continues to -/// work unchanged — `include` re-exports types AND variant constructors. -/// -/// Dependency graph (no cycles): -/// PaneModel ← no deps (leaf) -/// EchidnaModel ← no deps (leaf) -/// VeriSimModel ← no deps (leaf) -/// GovernanceModel ← PaneModel (for neuralToken in antiCrashState) -/// Model ← all four (this file) -/// -/// NOTE ON TYPE COMPOSITION: This `model` record contains 18 domain slices, -/// each with its own variant types and records. ReScript's `include` mechanism -/// re-exports all constructors — so `Installed`, `Native`, `Verified` etc. are -/// usable without qualification throughout the codebase. In TypeScript, this -/// would require either string literal unions (no exhaustiveness checking beyond -/// what the IDE approximates) or a maze of discriminated unions with manual type -/// guards. Here, the compiler enforces exhaustive matching on every `switch` — -/// add a new variant to any model and the compiler tells you every place in -/// 26,000+ lines that needs updating. That's not "nice to have" type safety; -/// it's the difference between refactoring with confidence and refactoring with -/// prayer. See https://rescript-lang.org/docs/manual/latest/variant - -/// Re-export Pane-L, Pane-N, Pane-W state types and their supporting types -/// (symbolicConstraint, neuralToken, oodaPhase, agencyState, eventChain*). -include PaneModel - -/// Re-export ECHIDNA theorem prover types (trust levels, axiom danger, -/// portfolio confidence, provers, sessions, dispatch results, tactic suggestions). -include EchidnaModel - -/// Re-export VeriSimDB types (proof obligations, drift scores, telemetry, -/// database backend state). -include VeriSimModel - -/// Re-export cognitive governance types (violationType, antiCrashState, -/// vexometer, orbital, humidity, viewMode, sync, contractiles). -include GovernanceModel - -/// Re-export VAB (Verified Assembly Building) types (categories, components, -/// warnings, capabilities, assembly state) for the server composer panel. -include VabModel - -/// Re-export CloudGuard types (zones, settings, DNS records, audit findings, -/// plan tiers, policy constraints, diff entries) for the Cloudflare domain -/// security management panel. -include CloudGuardModel - -/// Re-export Farm types (farmRepo, farmPriority, farmCategory, farmSortBy, -/// farmState) for the Git-Private-Farm panel — repo inventory and health. -include FarmModel - -/// Re-export Plaza types (complianceLevel, complianceAudit, adoptionStats, -/// plazaCategory, plazaState) for the Palimpsest Plaza licensing panel. -include PlazaModel - -/// Re-export Reposystem types for RSR compliance auditing. -include ReposystemModel - -/// Re-export Aerie types for network diagnostics and BGP forensics. -include AerieModel - -/// Re-export Interfaces types for ABI/FFI inventory. -include InterfacesModel - -/// Re-export Playgrounds types for code sandbox and NQC console. -include PlaygroundsModel - -/// Re-export Hypatia types (neuralNetId, neuralNetStatus, neuralNetState, -/// scanResult, pipelineStage, learningCycle, hypatiaCategory, hypatiaState) -/// for the neurosymbolic CI/CD intelligence panel. -include HypatiaModel - -/// Re-export Fleet types (botId, botStatus, botState, safetyTier, fleetFinding, -/// fleetHealth, fleetCategory, fleetState) for the Gitbot-Fleet panel. -include FleetModel - -/// Re-export Minter types (panelBackendKind, accessibilityLevel, minterCapability, -/// nameValidation, minterForm, mintResult, minterState) for the Panel Minter -/// wizard that generates new panel modules with accessibility baked in. -include MinterModel - -/// Re-export Provisioner types (panelIsolation, panelInstallStatus, panelConfig, -/// portfolio, portfolioInstallProgress, provisionerCategory, provisionerState) -/// for the portfolio bundling, panel configuration, and installation system. -include ProvisionerModel - -/// Re-export VoiceTag types (mriTagType, mriInputMethod, mriAttribution, -/// mriCodeAuthor, mriTag, mriFileSummary, mriFile, voiceState, voiceTagState) -/// for the Code MRI Layer 0 annotation system. Tags are stored as portable -/// `.mri.json` sidecars — standalone-first, no PanLL dependency required. -include VoiceTagModel - -/// Re-export Provenance types (trustLevel, provenanceRegion, provenanceSummary, -/// fileProvenance, accessibilityPalette, provenanceState) for the Qubes-style -/// code trust surface that is always visible as an ambient layer. -include ProvenanceModel - -/// Re-export Pattern Diagnostics types (Layer 3) — pattern detection, gamification. -include PatternDiagModel - -/// Re-export Attribution-to-Licensing types (Layer 4) — SPDX, trust→license link. -include AttributionLicenseModel - -/// Re-export 007 Toolchain types for agentic compiler and high-rigor execution. -include Oo7ToolchainModel -include VideoCoordinationModel - -/// Re-export AI types (aiProviderId, aiProviderConfig, aiProviderStatus, -/// aiMessage, aiCategory, aiState) for the multi-provider AI neural interface -/// panel that speaks to Anthropic, Google, Mistral, OpenAI, and local models. -include AiModel - -/// Re-export Repo Loader types (repoInfo, panelSuggestion, repoLoaderCategory, -/// repoLoaderState) for the repository scanning and panel configuration panel. -include RepoLoaderModel - -/// Re-export Watcher types (watchEventKind, watchEvent, watcherState) for -/// the filesystem observation infrastructure that feeds events into the TEA -/// loop. Every panel can react to relevant file changes. -include WatcherModel - -/// Re-export Panel Switcher types (panelId, connectionStatus, panelMeta, -/// panelSwitcherState) for the unified panel navigation system that replaces -/// ad-hoc `visible: bool` toggles on individual overlays. -include PanelSwitcherModel - -/// Re-export Workspace types (workspaceMode, sessionProtection, executionMode, -/// panelGroup, arrangement, session, checkpoint, polyTool, configuratorTab, -/// workspaceState) for the workspace management layer (DD-022–DD-027). -include WorkspaceModel - -/// Re-export Keybindings types (modifier, keyChord, keybindingAction, keybinding, -/// keybindingsState) for the customisable keyboard shortcut system. -include KeybindingsModel - -/// Re-export Migration types (migrationVersionBracket, migrationConfigFormat, -/// migrationRepoSummary, migrationSession, migrationSubmission, migrationConstraint, -/// migrationObligation, mergeResolution, migrationCategory, migrationState) for the -/// ReScript Migration Observatory panel — health tracking, session observation, -/// submission queue, and merge conflict resolution timeline. -include MigrationModel - -/// Re-export PanicAttack types (weakPointSeverity, weakPointCategory, weakPoint, -/// scanSummary, scanReport, panicCategory, panicAttackState) for the stress -/// testing and logic-based bug signature detection panel. -include PanicAttackModel - -/// Re-export MassPanic types (repoScanStatus, repoResult, assemblylineSummary, -/// deltaEntry, repoSortMode, repoFilterMode, storageTarget, massPanicState) for -/// the organisation-scale batch scanning panel (assemblyline + BLAKE3 + verisim). -include MassPanicModel - -/// Re-export TSDM types (scopeTier, maintenanceTier, auditTier, cleanupStep, -/// dialogueTopic, axisId, tsdmWorkItem, auditTooling, tsdmState) for the -/// Triaxial Software Development Methodology directive panel. -include TsdmModel - -/// Re-export Capture types (captureFormat, captureEntry, recordingState, demoStep, -/// demoPackage, comparisonMode, panelClone, captureCategory, captureState) for -/// the panel capture, recording, and demo/teaching system (DD-022). -include CaptureModel - -/// Re-export Status Bar types (widgetPosition, widgetKind, statusWidget, systemInfo, -/// statusBarState) for the configurable status bar widget system (DD-025). -include StatusBarModel - -/// Re-export Security types (redactionMode, redactionPattern, detectedSecret, -/// vaultStatus, vaultKey, twoFactorStatus, securityLevel, trustfilePolicy, -/// securityCategory, securityState) for secrets, vault, 2FA, Trustfile (DD-026/027). -include SecurityModel - -/// Re-export Valence Shell types (shellBackend, recordingState, terminalRecording, -/// approvalGateMode, pendingCommand, valenceCheckpoint, valenceShellCategory, -/// terminalLine, valenceShellState) for the embedded terminal panel with Claude -/// Code integration, session recording, and collaborative approval gate. -include ValenceShellModel - -/// Re-export Game Preview types (gameOverlay, gameExecutionState, -/// gameRecordingState, deviceInteraction, gameplayClip, renderStats, -/// gamePreviewCategory, gamePreviewState) for the live IDApTIK game -/// preview panel with hot-reload, overlays, and gameplay recording. -include GamePreviewModel - -/// Re-export VM Inspector types (vmInstructionTier, vmInstruction, -/// vmMemoryCell, vmPortEntry, vmSnapshot, vmBreakpoint, vmConnectionMode, -/// vmInspectorCategory, vmInspectorState) for the reversible VM visual -/// debugger with step forward/backward and execution timeline. -include VmInspectorModel - -/// Re-export Network Topology types (networkZone, connectionProtocol, -/// networkDevice, networkConnection, dnsEntry, packetFlowEvent, -/// networkTopologyCategory, networkTopologyState) for the IDApTIK -/// in-game network topology viewer. -include NetworkTopologyModel - -/// Re-export Level Architect types (levelEntityKind, levelEntity, -/// guardPatrol, defenceFlag, validationIssue, levelAsset, editorTool, -/// levelArchitectCategory, levelArchitectState) for the visual level -/// design tool with grid editor and LevelConfig export. -include LevelArchitectModel - -/// Re-export Coprocessors types (coprocessorBackend, coprocHealth, -/// coprocCallEntry, coprocMetrics, heatmapCell, coprocessorsCategory, -/// coprocessorsState) for the IDApTIK coprocessor monitoring dashboard. -include CoprocessorsModel - -/// Re-export Multiplayer Monitor types (wsConnectionState, connectedPlayer, -/// channelSubscription, stateDiffEntry, deviceLock, latencySample, -/// etsCacheEntry, multiplayerCategory, multiplayerMonitorState) for the -/// IDApTIK Phoenix sync server monitoring panel. -include MultiplayerMonitorModel - -/// Re-export DLC Workshop types (puzzleDifficulty, testRunStatus, -/// puzzleInstruction, dlcPuzzle, puzzleChain, dlcAsset, dlcPackMeta, -/// dlcWorkshopCategory, dlcWorkshopState) for the DLC puzzle pack -/// creation, testing, and packaging panel. -include DlcWorkshopModel - -/// Re-export UMS types (umsCategory, modProject, modTemplate, modAsset, -/// abiValidationResult, distributionTarget, apiEntry, umsState) for the -/// Universal Modding Studio panel — unified IDApTIK game content creation hub. -include UmsModel - -/// Re-export Editor Bridge types (editorKind, editorConnectionState, -/// openFileEntry, lspDiagnostic, workspaceSymbol, bridgeActivity, -/// editorBridgeCategory, editorBridgeState) for the external code -/// editor federation panel (LSP diagnostics, symbols, jump-to-line). -include EditorBridgeModel - -/// Re-export Build Dashboard types (buildTarget, buildStatus, -/// buildMessage, testResult, buildHistoryEntry, buildDashboardCategory, -/// buildDashboardState) for the IDApTIK build monitoring panel. -include BuildDashboardModel - -/// Re-export Release Manager types (releaseChannel, platformTarget, -/// releaseStatus, releaseArtifact, changelogEntry, releaseRecord, -/// releaseManagerCategory, releaseManagerState) for the versioning, -/// changelog, and distribution panel. -include ReleaseManagerModel - -/// Re-export Automation Router types (triggerEvent, ruleCondition, ruleAction, -/// approvalMode, automationRule, pendingAction, executionLogEntry, -/// automationRouterCategory, automationRouterState) for the hybrid cross-panel -/// workflow orchestration panel with event-driven rules and approval gates. -include AutomationRouterModel - -/// Re-export Databases types (databasesCategory, queryHistoryEntry, -/// schemaEntity, databasesState) for the unified database management panel -/// covering VeriSimDB, QuandleDB, and LithoGlyph. -include DatabasesModel - -/// Opens BojModel into this scope, contributing BoJ types -/// (bojCartridge, bojCategory, bojState, etc.) for the Bundle of Joy panel. -include BojModel -include CladeBrowserModel - -/// Re-export Tentacles types (tentacleId, tentacleStage, oodaPhase, -/// tentacleConstraint, reasoningEntry, validatedResult, tentaclePersonality, -/// tentacleNames, agentBroadcastPayload, tentacleAgentState, tentaclesCategory, -/// tentaclesState) for the 7-Tentacles compiler agent panel — seven colour-coded -/// agents representing compiler subsystems with progressive cephalopod staging. -include TentaclesModel -include ProtocolSquisherModel -include MyLangModel -include TypeLLModel - -/// Re-export Help types (helpCategory, helpEntry, glossaryTerm, onboardingStep, -/// onboardingState, helpState) for the in-application help system with -/// context-sensitive guides, neurosymbolic glossary, and onboarding walkthrough. -include HelpModel - -/// Re-export Accessibility types (fontSizePreset, animationPreference, -/// focusIndicatorStyle, accessibilityState) for the centralised accessibility -/// toolbar controlling colour palettes, animation, font size, and focus indicators. -include AccessibilityModel - -/// Re-export Tiling types (snapZone, tilingPreset, detachedPanel, tilingState) -/// for multi-monitor panel detachment, Aero-style snap zones, and tiling presets. -include TilingModel - -/// Re-export Focus Dimming types (dimmingMode, panelFocusOverride, -/// focusDimmingState) for focus-aware panel dimming and Smart Memory Mode -/// that throttles unfocused panel processing. -include FocusDimmingModel -include MenuBarModel - -/// Re-export ScriptGist types (gistLanguage, gistParam, gistSchema, gistTarget, -/// gistVisibility, gistResult, scriptGist, gistTemplate, gistCategory, gistSortBy, -/// scriptGistState) for the portable computation gist system — saveable, shareable, -/// LLM-callable as MCP tools, user-runnable standalone. -include ScriptGistModel - -/// Re-export Stapeln container assembly types (constraints, pipeline status, -/// validation, artifact formats, component catalog, panel state). -include StapelnModel - -/// Re-export Evangeliser types (evangeliserCategory, evangeliserDifficulty, -/// evangeliserGlyph, evangeliserNarrative, evangeliserPattern, evangeliserMatch, -/// evangeliserAnalysis, evangeliserConstraints, evangeliserTab, evangeliserViewLayer, -/// evangeliserState) for the JS→ReScript transformation teaching panel. -include EvangeliserModel - -/// Re-export Observatory types for the integrative dashboard panel. -include ObservatoryModel - -/// Re-export AmbientOps types for the hospital-model sysadmin panel. -include AmbientOpsModel - -/// Re-export Language Forge types (languagePhase, componentStatus, languageEntry, -/// forgeCategory, forgeSortBy, languageForgeState) for the nextgen-languages panel. -include LanguageForgeModel - -/// Re-export TangleViz types (braidGenerator, tangleViewMode, knotInvariant, -/// parsedStatus, tangleVizState) for the topological programming visualizer panel. -include TangleVizModel - -/// Re-export SpecBrowser types (specFileKind, filePresence, verificationSummary, -/// specLanguageEntry, specBrowserCategory, comparisonSide, specBrowserState) for -/// the language specification browser panel. -include SpecBrowserModel - -/// Re-export VerificationDashboard types (proofSystem, conformanceLevel, -/// benchmarkEntry, fuzzingCoverage, languageVerificationStatus, -/// verificationDashboardCategory, verificationSortBy, verificationDashboardState) -/// for the verification status panel. -include VerificationDashboardModel - -/// Re-export game testing panel types. -include UnitTestRunnerModel -include FunctionalTesterModel -include RegressionGuardModel -include PerformanceProfilerModel -include LoadTesterModel -include SoakMonitorModel -include CompatibilityMatrixModel -include ExploratoryWorkbenchModel -include BetaFeedbackHubModel -include BalanceAnalyserModel - -/// Re-export bridge panel types. -include TypingBridgeModel -include NeurosymBridgeModel -include AgenticBridgeModel -include AutomationBridgeModel -include DatabaseBridgeModel -include ProtocolBridgeModel -include ProofsBridgeModel -include ScriptingBridgeModel - -/// Re-export game-specific panel types. -include GeneratorModeModel -include ArchitectModeModel -include GuardAiTunerModel -include DeviceNetworkDesignerModel -include AssetManagerModel -include PlaytestRecorderModel - -/// Re-export team/collaboration panel types. -include CodeReviewModel -include MergeCoordinatorModel -include TeamDashboardModel -include DebuggingWorkbenchModel - -/// Re-export Wiring Inspector types (obligationStatus, failureClass, -/// repairability, obligation, panelVerification, wiringInspectorState) -/// for the PCC constraint state UI panel. -include WiringInspectorModel - -/// Re-export Floor Raise campaign panel types. -include FloorRaiseModel -include ProvenAdoptionModel -include ContractileCompletenessModel -include ManifestCoverageModel -include VerisimdbFeedsModel -include FeedbackRoutingModel -include VexometerFrictionModel - -/// Re-export Service Registry types (serviceStatus, serviceEntry, -/// serviceRegistryState) for centralized backend service lifecycle management. -include ServiceModel - -/// Re-export Settings types (settingsState) for user configuration management. -include SettingsModel - -/// Re-export Identity types (identitySnapshot, identityState) for snapshots -/// and team replication. -include IdentityModel - -/// The complete Model — composes all domain slices into a single record. -/// This is the "Gravitational Centre" of the Binary Star system. -type model = { - // Core panes - paneL: paneLState, - paneN: paneNState, - paneW: paneWState, - paneA: paneAState, - // Cognitive governance - antiCrash: antiCrashState, - vexometer: vexometerState, - orbital: orbitalState, - syncState: syncState, - contractiles: array, - humidity: humidityLevel, - barycentreTour: tourState, - menuBar: menuBarState, - // View state - viewMode: viewMode, - paneLVisible: bool, - paneNVisible: bool, - paneWVisible: bool, - protocolAnalysisVisible: bool, - panelBarVisible: bool, - fullscreenActive: bool, - // Database backends - verisim: verisimdbState, - // Theorem prover backend - echidna: echidnaState, - // VAB (Verified Assembly Building) - vab: vabState, - // CloudGuard (Cloudflare domain security management) - cloudguard: cloudguardState, - // Git-Private-Farm — repo inventory and health dashboard - farm: farmState, - // Palimpsest Plaza — PMPL licensing adoption and governance - plaza: plazaState, - // Reposystem — RSR compliance across 265+ repos - reposystem: reposystemState, - // System Update — rpm-ostree, flatpak, asdf, cargo, fwupd component management - systemUpdate: SystemUpdateModel.systemUpdateState, - // Aerie — network diagnostics, speed tests, BGP forensics - aerie: aerieState, - // 007 Toolchain — agentic compiler and high-rigor execution (Groove) - oo7toolchain: oo7State, - // VideoCoordination — Drive-to-Photos batch transfer dashboard - videoCoordination: videoCoordinationState, - // Interfaces — Idris2 ABI + Zig FFI inventory + binding coverage - interfaces: interfacesState, - // Playgrounds — code sandbox + NQC console + tutorials - playgrounds: playgroundsState, - // Hypatia — neurosymbolic CI/CD intelligence (5 neural networks, 298+ repos) - hypatia: hypatiaState, - // Gitbot-Fleet — 6-bot orchestration and dispatch dashboard - fleet: fleetState, - // Panel Minter — create new panel modules with accessibility by default - minter: minterState, - // Provisioner — portfolio bundles, panel config, isolation tiers - provisioner: provisionerState, - // Code MRI VoiceTag — voice-activated annotation system (Layer 0) - voiceTag: voiceTagState, - // Provenance Map — Qubes-style code trust surface (always visible, ambient) - provenance: provenanceState, - // Code MRI Timeline — VeriSimDB-backed development time series (Layer 2) - codeMriTimeline: TimelineModel.timelineState, - // Code MRI Pattern Diagnostics — anti-pattern detection + gamification (Layer 3) - patternDiag: patternDiagState, - // Code MRI Attribution-to-Licensing — SPDX/provenance→license link (Layer 4) - attributionLicense: attributionLicenseState, - // Wizard — Plugin/panel creation wizard - wizard: WizardModel.wizardState, - // Watcher — filesystem observation infrastructure (feeds all panels) - watcher: watcherState, - // AI — multi-provider neural interface (Claude, Gemini, Mistral, GPT, local) - ai: aiState, - // Repo Loader — repository scanner and panel configuration wizard - repoLoader: repoLoaderState, - // Panel Switcher — unified panel navigation (replaces ad-hoc visible toggles) - panelSwitcher: panelSwitcherState, - // Workspace — panel arrangements, groups, sessions, modes (DD-022–DD-027) - workspace: workspaceState, - // Keybindings — customisable keyboard shortcuts - keybindings: keybindingsState, - // Capture — screenshots, recordings, demos, cloning (DD-022) - capture: captureState, - // Status Bar — configurable bottom bar with system info widgets (DD-025) - statusBar: statusBarState, - // Security — redaction, vault, 2FA, Trustfile enforcement (DD-026/027) - security: securityState, - // Migration Observatory — ReScript migration health, sessions, submissions - migration: migrationState, - // panic-attack — stress testing and weak point analysis - panicAttack: panicAttackState, - // mass-panic — organisation-scale batch scanning (assemblyline + BLAKE3 + verisim) - massPanic: massPanicState, - // TSDM — triaxial software development methodology directive - tsdm: tsdmState, - // Valence Shell — embedded terminal with Claude Code and reversible ops - valenceShell: valenceShellState, - // Game Preview — live IDApTIK game preview with hot-reload and overlays - gamePreview: gamePreviewState, - // VM Inspector — reversible VM visual debugger (step forward/backward) - vmInspector: vmInspectorState, - // Network Topology — IDApTIK in-game network graph viewer - networkTopology: networkTopologyState, - // Level Architect — visual level design tool - levelArchitect: levelArchitectState, - // Coprocessors — coprocessor backend monitoring dashboard - coprocessors: coprocessorsState, - // Multiplayer Monitor — Phoenix sync server inspector - multiplayerMonitor: multiplayerMonitorState, - // DLC Workshop — puzzle pack creation, testing, packaging - dlcWorkshop: dlcWorkshopState, - // Universal Modding Studio — unified IDApTIK content creation hub - ums: umsState, - // Editor Bridge — federate with external code editors (VSCodium, Zed, etc.) - editorBridge: editorBridgeState, - // Build Dashboard — build/test/error monitoring for IDApTIK sub-projects - buildDashboard: buildDashboardState, - // Release Manager — versioning, changelog, artifacts, distribution - releaseManager: releaseManagerState, - // Automation Router — hybrid cross-panel workflow orchestration - automationRouter: automationRouterState, - // Databases — unified VeriSimDB/QuandleDB/LithoGlyph management - databases: databasesState, - // BoJ — Bundle of Joy cartridge server - boj: bojState, - // Clade Browser — panel taxonomy explorer - cladeBrowser: cladeBrowserState, - // Tentacles — 7-Tentacles compiler agent panel (within/without ECHIDNA) - tentacles: tentaclesState, - // Protocol-Squisher — 13-format schema analysis and compatibility - protocolSquisher: protocolSquisherState, - // My-Lang — AI-native language workbench (Solo/Duet/Ensemble/Me dialects) - myLang: myLangState, - // TypeLL — Verification kernel (cross-panel type intelligence) - typell: typellState, - // Panel Bus — pub/sub subscriber registry and event history - busRegistry: PanelBus.subscriberRegistry, - // A2ML — last loaded/validated manifest state - lastA2mlManifest: option, - lastA2mlValidation: option, - a2mlManifestPaths: array, - // K9 — last loaded/validated contractile state - lastK9Contractile: option, - lastK9Layout: option, - k9KennelSchema: option, - k9YardContract: option, - // Compliance seams — exception register and audit state - seamRegister: SeamEngine.seamRegister, - lastSeamAudit: option, - // ENSAID_CONFIG — cross-panel config generation state - ensaidConfigPreview: option, - ensaidConfigError: option, - // Undo/Redo — ring buffer of model snapshots - undoStack: array, - redoStack: array, - // Feedback-O-Tron - feedbackPending: option, - feedbackError: option, - feedbackReportType: option, - // Help — in-app help system with context-sensitive guides and glossary - help: helpState, - // Accessibility — centralised a11y preferences (palette, animation, font, focus) - accessibility: accessibilityState, - // Tiling — multi-monitor panel detachment and snap zone management - tiling: tilingState, - // Focus Dimming — focus-aware panel dimming and Smart Memory Mode - focusDimming: focusDimmingState, - // Script Gist — portable computation gists (saveable, LLM-callable, user-runnable) - scriptGist: scriptGistState, - // Stapeln — container stack assembly pipeline (constraints, reasoning, artifacts) - stapeln: stapelnState, - // Evangeliser — JS→ReScript pattern teaching with celebrate/minimize/better narratives - evangeliser: evangeliserState, - // Language Forge — nextgen-languages portfolio monitoring and development - languageForge: languageForgeState, - // TangleViz — topological programming visualizer for braid/knot topology - tangleViz: tangleVizState, - // Spec Browser — browse all language specs, grammars, typing rules - specBrowser: specBrowserState, - // Verification Dashboard — proof/test/benchmark/fuzzing status - verificationDashboard: verificationDashboardState, - // Observatory — integrative dashboard aggregating health, resources, activity - observatory: observatoryState, - // AmbientOps — hospital-model sysadmin (clinician, network ambulance, hardware crash team) - ambientOps: ambientOpsState, - // Game Testing panels - unitTestRunner: unitTestRunnerState, - functionalTester: functionalTesterState, - regressionGuard: regressionGuardState, - performanceProfiler: performanceProfilerState, - loadTester: loadTesterState, - soakMonitor: soakMonitorState, - compatibilityMatrix: compatibilityMatrixState, - exploratoryWorkbench: exploratoryWorkbenchState, - betaFeedbackHub: betaFeedbackHubState, - balanceAnalyser: balanceAnalyserState, - // Bridge panels - typingBridge: typingBridgeState, - neurosymBridge: neurosymBridgeState, - agenticBridge: agenticBridgeState, - automationBridge: automationBridgeState, - databaseBridge: databaseBridgeState, - protocolBridge: protocolBridgeState, - proofsBridge: proofsBridgeState, - scriptingBridge: scriptingBridgeState, - // Game-specific panels - generatorMode: generatorModeState, - architectMode: architectModeState, - guardAiTuner: guardAiTunerState, - deviceNetworkDesigner: deviceNetworkDesignerState, - assetManager: assetManagerState, - playtestRecorder: playtestRecorderState, - // Team / collaboration panels - codeReview: codeReviewState, - mergeCoordinator: mergeCoordinatorState, - teamDashboard: teamDashboardState, - debuggingWorkbench: debuggingWorkbenchState, - // Infrastructure panels - wiringInspector: wiringInspectorState, - // K9 and Contractile management panels - k9Manager: K9Model.k9ManagerState, - // Floor Raise campaign panels - floorRaise: floorRaiseState, - provenAdoption: provenAdoptionState, - contractileCompleteness: contractileCompletenessState, - manifestCoverage: manifestCoverageState, - verisimdbFeeds: verisimdbFeedsState, - feedbackRouting: feedbackRoutingState, - vexometerFriction: vexometerFrictionState, - // LLM Coding — multi-session Claude/LLM coordinator - llmCoding: LlmCodingModel.llmCodingState, - // Agent Coordination View - agentCoordination: AgentCoordinationModel.agentCoordinationState, - // Burble — voice huddle integration (groove-aware, workspace profile) - burble: BurbleModel.burbleState, - // Service Registry — centralized backend service lifecycle (Connected Workbench v0.2.0) - serviceRegistry: serviceRegistryState, - // Settings — user configuration (Connected Workbench v0.2.0) - settings: settingsState, - // Identity — named snapshots and team replication (Connected Workbench v0.2.0) - identity: identityState, -} - -/// Initial model state - "Dark Start" mode -let init = (): model => { - paneL: { - constraints: [ - { - id: "orbital-stability-inv", - expression: "forall t : Time, stability(t) >= 0.3 -> co_orbit_maintained(t)", - active: true, - pinned: true, - }, - { - id: "divergence-bound", - expression: "divergence(symbolic, neural) <= 0.7 // Jaccard distance ceiling", - active: true, - pinned: true, - }, - { - id: "autonomy-ceiling", - expression: "agency.autonomyLevel <= 0.8 // Human-in-the-loop bound", - active: true, - pinned: false, - }, - { - id: "trust-propagation", - expression: "forall a b : Artifact, depends(a, b) -> trust(a) <= trust(b)", - active: true, - pinned: false, - }, - { - id: "vexation-anti-inflammatory", - expression: "vexometer.index > 0.6 -> enable_anti_inflammatory()", - active: true, - pinned: false, - }, - { - id: "type-safety-invariant", - expression: "forall expr : Expr, type_check(expr) = Ok(t) -> eval(expr) : t", - active: false, - pinned: false, - }, - { - id: "sync-latency-bound", - expression: "sync_latency(L, N, W) <= 2000ms // Cross-pane coherence", - active: true, - pinned: false, - }, - ], - activeConstraintId: None, - editorContent: "// Symbolic Mass — Tractatus Editor\n// Define constraints that govern the Binary Star co-orbit.\n//\n// Active constraints feed into the barycentre position\n// and inform ECHIDNA's proof obligations.\n\ntype orbital_invariant =\n | StabilityBound(float) // Minimum stability threshold\n | DivergenceLimit(float) // Maximum symbolic-neural drift\n | AutonomyCeiling(float) // Human-in-the-loop guarantee\n | TrustPropagation // Provenance chain integrity\n\nlet verify_co_orbit : orbital_invariant -> result =\n fun inv -> match inv with\n | StabilityBound(min) ->\n if orbital.stability >= min then Ok(QED)\n else Error(DriftDetected(orbital.stability, min))\n | DivergenceLimit(max) ->\n let d = jaccard_distance(paneL.tokens, paneN.tokens) in\n if d <= max then Ok(WithinBound(d))\n else Error(Diverged(d, max))\n | AutonomyCeiling(cap) ->\n assert(agency.autonomyLevel <= cap);\n Ok(HumanInLoop)\n | TrustPropagation ->\n forall_chain(provenance.artifacts, fun a b ->\n trust(a) <= trust(b))\n", - lastInferredType: None, - }, - paneN: { - tokens: [ - { - id: "t-0", - content: "Initialising formal verification context...", - timestamp: 0.0, - confidence: 0.95, - validated: true, - source: NeuralInference, - category: Observation, - emittedDuring: Observe, - causedBy: [], - proofHash: None, - }, - { - id: "t-1", - content: "Loading Coq prover backend", - timestamp: 0.1, - confidence: 0.88, - validated: true, - source: EchidnaProver, - category: Observation, - emittedDuring: Observe, - causedBy: ["t-0"], - proofHash: None, - }, - { - id: "t-2", - content: "forall n : nat, n + 0 = n", - timestamp: 0.2, - confidence: 0.92, - validated: true, - source: EchidnaProver, - category: Hypothesis, - emittedDuring: Orient, - causedBy: ["t-1"], - proofHash: None, - }, - { - id: "t-3", - content: "Tactic suggestion: induction on n", - timestamp: 0.3, - confidence: 0.78, - validated: false, - source: EchidnaProver, - category: Abduction, - emittedDuring: Orient, - causedBy: ["t-2"], - proofHash: None, - }, - { - id: "t-4", - content: "Proof obligation discharged", - timestamp: 0.4, - confidence: 0.97, - validated: true, - source: EchidnaProver, - category: ProofStep, - emittedDuring: Decide, - causedBy: ["t-2", "t-3"], - proofHash: Some("sha256:a1b2c3..."), - }, - { - id: "t-5", - content: "Checking orbital stability invariant...", - timestamp: 0.5, - confidence: 0.91, - validated: true, - source: TypeLLKernel, - category: Observation, - emittedDuring: Observe, - causedBy: [], - proofHash: None, - }, - { - id: "t-6", - content: "Divergence bound verified: 0.23 <= 0.7", - timestamp: 0.6, - confidence: 0.94, - validated: true, - source: TypeLLKernel, - category: ProofStep, - emittedDuring: Decide, - causedBy: ["t-5"], - proofHash: Some("sha256:d4e5f6..."), - }, - { - id: "t-7", - content: "Trust propagation: 4 artifacts in chain", - timestamp: 0.7, - confidence: 0.86, - validated: true, - source: NeuralInference, - category: Deduction, - emittedDuring: Orient, - causedBy: ["t-4", "t-6"], - proofHash: None, - }, - { - id: "t-8", - content: "Autonomy ceiling: 0.0 <= 0.8 (human in loop)", - timestamp: 0.8, - confidence: 0.99, - validated: true, - source: AntiCrashGate, - category: Observation, - emittedDuring: Act, - causedBy: [], - proofHash: None, - }, - { - id: "t-9", - content: "7 constraints active, 6 satisfied, 1 pending", - timestamp: 0.9, - confidence: 0.93, - validated: true, - source: NeuralInference, - category: Synthesis, - emittedDuring: Act, - causedBy: ["t-7", "t-8"], - proofHash: None, - }, - ], - inferenceActive: true, - nextTokenId: 10, - activeCausalChain: ["t-9"], - filters: { - sources: [], - categories: [], - phases: [], - confidenceThreshold: 0.0, - validatedOnly: false, - proofOnly: false, - }, - monologue: "ECHIDNA neural advisor active. Processing 7 symbolic constraints from Panel-L.\n\n[OBSERVE] Scanning constraint set: orbital-stability-inv, divergence-bound, autonomy-ceiling, trust-propagation, vexation-anti-inflammatory, type-safety-invariant (disabled), sync-latency-bound.\n\n[ORIENT] Symbolic mass density: moderate (editor content ~180 tokens). Barycentre currently balanced — both stars contributing mass. Divergence level low: symbolic and neural streams share vocabulary overlap.\n\n[DECIDE] Recommend verifying trust-propagation constraint against current provenance chain. The forall quantifier over artifact dependencies requires inductive proof — dispatching to Coq backend.\n\n[ACT] Dispatched proof obligation: trust_propagation_inductive to Coq. Estimated completion: <200ms. Monitoring sync latency for cross-pane coherence bound (2000ms ceiling).\n\nContractile status: orbital-stability STRICT (elasticity 0.2), vexation-ceiling ADAPTIVE (elasticity 0.5), divergence-limit WARN (elasticity 0.3), autonomy-bound STRICT (elasticity 0.4). All within elastic bounds.\n\nNext: Awaiting Coq discharge for trust propagation. Will update inference manifold on completion.", - agency: { - phase: Orient, - autonomyLevel: 0.15, - lastOperatorInput: 0.0, - }, - }, - paneW: { - content: "", - topologyView: true, // Start with Binary Star diagram - lastValidatedOutput: "", - eventChain: [], - eventChainSummary: None, - eventChainTimeline: None, - eventChainInput: "", - eventChainError: None, - panicAttackerMode: "unknown", - panicAttackerBinary: None, - panicAttackerStatusDetail: None, - securityTarget: "", - securityTimeline: "", - securityAxes: "cpu,memory,concurrency", - securityIntensity: "medium", - securityDuration: "30", - securityStatus: None, - securityError: None, - securityMenuExpanded: false, - securityDialogOpen: false, - securityDialogTool: None, - securityViewActive: false, - }, - paneA: { - vexationIndex: 0.0, - antiInflammatoryActive: false, - humidity: High, - recentCancellations: 0, - recentCorrections: 0, - expanded: false, - }, - antiCrash: { - enabled: true, - strictMode: true, - violations: [], - halted: false, - pendingReview: None, - }, - vexometer: { - index: 0.0, - recentCancellations: 0, - recentCorrections: 0, - antiInflammatoryActive: false, - inertiaDetected: false, - }, - orbital: { - stability: 1.0, - divergenceLevel: 0.0, - driftAuraColour: "indigo", - symbolicMass: 0.0, - neuralStream: 0.0, - barycentrePosition: 0.0, - syncHealth: 1.0, - }, - syncState: { - lastSymbolicHash: "", - lastNeuralHash: "", - lastWorldHash: "", - pendingSync: [], - syncLatency: 0.0, - }, - contractiles: [ - { - id: "orbital-stability", - name: "Orbital Stability Bound", - description: "Ensures the Binary Star co-orbit remains stable", - enforcement: Strict, - status: Pending, - elasticity: 0.2, - lastEvaluated: 0.0, - }, - { - id: "vexation-ceiling", - name: "Vexation Ceiling", - description: "Prevents operator friction from exceeding acceptable levels", - enforcement: Adaptive, - status: Pending, - elasticity: 0.5, - lastEvaluated: 0.0, - }, - { - id: "divergence-limit", - name: "Divergence Limit", - description: "Limits drift between symbolic and neural subsystems", - enforcement: Warn, - status: Pending, - elasticity: 0.3, - lastEvaluated: 0.0, - }, - { - id: "autonomy-bound", - name: "Autonomy Bound", - description: "Constrains the machine's autonomous action level", - enforcement: Strict, - status: Pending, - elasticity: 0.4, - lastEvaluated: 0.0, - }, - ], - verisim: { - connected: true, - endpoint: ServiceEndpoints.verisim, - lastQuery: "SELECT * FROM entities WHERE modality = 'graph' LIMIT 10", - queryResult: None, - queryError: None, - entities: [ - "orbital-stability-proof", - "trust-chain-artifact-001", - "divergence-metric-snapshot", - "provenance-graph-root", - "temporal-drift-log", - ], - selectedEntity: None, - driftStatus: Some("Nominal — all 8 modalities within tolerance"), - driftScores: Some({ - graph: 0.03, - vector: 0.07, - tensor: 0.02, - semantic: 0.11, - document: 0.04, - temporal: 0.06, - provenance: 0.01, - spatial: 0.05, - }), - proofObligations: [ - { - proofType: "invariant", - contractName: "orbital-stability", - status: "verified", - proofHash: "a1b2c3d4e5f6", - }, - { - proofType: "temporal", - contractName: "drift-bound", - status: "pending", - proofHash: "f6e5d4c3b2a1", - }, - ], - dbMenuExpanded: false, - normalisingEntity: None, - entityDetail: None, - telemetry: None, - telemetryVisible: false, - orchStatus: Some("Orchestrator online — 5 entities indexed"), - lastTypeCheck: None, - proofDisplayActive: false, - inferenceStream: [], - antiCrashValidation: true, - queryCount: 0, - bojRouting: false, - }, - echidna: { - connected: true, - endpoint: ServiceEndpoints.echidna, - version: Some("0.4.1-neurosym"), - provers: [ - {name: "Coq", tier: "ITP", complexity: "CoC"}, - {name: "Lean 4", tier: "ITP", complexity: "DTT"}, - {name: "Z3", tier: "SMT", complexity: "QF_LIA"}, - {name: "Isabelle/HOL", tier: "ITP", complexity: "HOL"}, - {name: "CVC5", tier: "SMT", complexity: "QF_UFLIA"}, - ], - lastProofResult: None, - proofError: None, - proofLoading: false, - session: None, - tacticSuggestions: [ - { - tactic: "induction", - args: ["n"], - confidence: 0.92, - aspectTags: ["structural", "recursive"], - description: "Structural induction on the natural number argument", - }, - { - tactic: "apply", - args: ["trust_transitive"], - confidence: 0.85, - aspectTags: ["rewriting", "chain"], - description: "Apply trust transitivity lemma to close provenance chain goal", - }, - { - tactic: "simpl", - args: [], - confidence: 0.78, - aspectTags: ["simplification"], - description: "Simplify the current goal using reduction rules", - }, - ], - selectedProver: Some("Coq"), - proofInput: "", - menuExpanded: false, - activeTab: EchidnaProofTab, - tacticInput: "", - sessionLoading: false, - lastProofObligations: None, - bojRouting: false, - enterpriseModel: { - elements: [], - constraints: [], - checkResults: [], - checking: false, - activeMetamodel: None, - activeLayer: None, - lastXmiImport: None, - }, - }, - vab: { - visible: false, - catalog: VabCatalog.allComponents, - selectedCategory: VabCore, - sortBy: SortByName, - filterText: "", - server: { - name: "Untitled Server", - components: [], - }, - warnings: [], - capabilities: VabEngine.computeCapabilities([], VabCatalog.allComponents, []), - hoveredComponent: None, - }, - cloudguard: { - connection: Disconnected, - loading: false, - error: None, - zones: [], - selectedZoneIds: [], - settings: [], - dnsRecords: [], - pagesProjects: [], - auditResult: None, - constraints: [], - exceptions: [], - configDiff: None, - bulkProgress: None, - visible: false, - activeCategory: SslTls, - filterText: "", - settingFilter: "", - showDiff: false, - showAudit: true, - dnsEditingId: None, - }, - farm: { - loaded: false, - loading: false, - error: None, - repos: [], - selectedRepoNames: [], - activeCategory: AllRepos, - filterText: "", - sortBy: SortByName, - totalRepos: 0, - unhealthyCount: 0, - }, - plaza: { - loaded: false, - loading: false, - error: None, - stats: None, - audits: [], - signatures: [], - compatibilityResults: [], - activeCategory: Dashboard, - filterText: "", - selectedRepo: None, - }, - reposystem: ReposystemEngine.defaultState, - systemUpdate: SystemUpdateModel.init, - aerie: { - loaded: true, - loading: false, - error: None, - probes: [ - { - endpoint: "1.1.1.1", - label: "Cloudflare DNS", - protocol: "ICMP", - active: true, - }, - { - endpoint: "8.8.8.8", - label: "Google DNS", - protocol: "ICMP", - active: true, - }, - { - endpoint: "api.github.com", - label: "GitHub API", - protocol: "HTTPS", - active: true, - }, - ], - latencyResults: [ - { - endpoint: "1.1.1.1", - rttMs: 4.2, - jitterMs: 0.8, - packetLoss: 0.0, - timestamp: "2026-03-09T10:30:00Z", - }, - { - endpoint: "8.8.8.8", - rttMs: 12.7, - jitterMs: 1.3, - packetLoss: 0.0, - timestamp: "2026-03-09T10:30:00Z", - }, - { - endpoint: "api.github.com", - rttMs: 28.4, - jitterMs: 3.1, - packetLoss: 0.0, - timestamp: "2026-03-09T10:30:00Z", - }, - ], - speedTests: [], - bgpRoutes: [], - activeCategory: AerieDashboard, - bgpAnomalyCount: 0, - mtuResult: None, - interfaces: [], - bojRouting: false, - }, - oo7toolchain: { - loaded: false, - loading: false, - error: None, - isConnected: false, - permissions: PermissionReadOnly, - stageOutputs: [], - sourceCode: "spawn agent_name {\n 40 + 2\n}", - activeCategory: Oo7Dashboard, - nesyStatus: "Waiting for analysis...", - }, - videoCoordination: VideoCoordinationEngine.defaultState, - interfaces: InterfacesEngine.defaultState, - playgrounds: PlaygroundsEngine.defaultState, - hypatia: HypatiaEngine.defaultState, - fleet: { - loaded: true, - loading: false, - error: None, - bots: [ - { - id: Rhodibot, - status: BotActive, - queuedFindings: 3, - processedFindings: 47, - confidenceThreshold: 0.85, - lastActivity: "2026-03-09T10:30:00Z", - }, - { - id: Echidnabot, - status: BotActive, - queuedFindings: 7, - processedFindings: 112, - confidenceThreshold: 0.90, - lastActivity: "2026-03-09T10:28:00Z", - }, - { - id: Sustainabot, - status: BotIdle, - queuedFindings: 0, - processedFindings: 23, - confidenceThreshold: 0.80, - lastActivity: "2026-03-09T09:45:00Z", - }, - { - id: Glambot, - status: BotActive, - queuedFindings: 2, - processedFindings: 31, - confidenceThreshold: 0.75, - lastActivity: "2026-03-09T10:25:00Z", - }, - { - id: Seambot, - status: BotIdle, - queuedFindings: 0, - processedFindings: 18, - confidenceThreshold: 0.82, - lastActivity: "2026-03-09T08:50:00Z", - }, - { - id: Finishbot, - status: BotActive, - queuedFindings: 1, - processedFindings: 9, - confidenceThreshold: 0.88, - lastActivity: "2026-03-09T10:15:00Z", - }, - ], - findings: [ - { - id: "HYP-2026-0142", - repoName: "proven", - summary: "4,566 believe_me instances — formal verification undermined", - tier: Eliminate, - confidence: 0.97, - assignedBot: Some(Echidnabot), - resolved: false, - }, - { - id: "HYP-2026-0143", - repoName: "boj-server", - summary: "Missing SPDX headers in 3 cartridge source files", - tier: Control, - confidence: 0.91, - assignedBot: Some(Rhodibot), - resolved: false, - }, - { - id: "HYP-2026-0144", - repoName: "panll", - summary: "Documentation coverage below 60% threshold", - tier: Substitute, - confidence: 0.84, - assignedBot: Some(Glambot), - resolved: false, - }, - ], - health: Some({ - activeBots: 4, - totalQueued: 13, - totalProcessed: 240, - avgConfidence: 0.88, - triangleCounts: (1, 1, 1), - }), - activeCategory: FleetDashboard, - filterText: "", - }, - minter: MinterEngine.defaultState, - provisioner: ProvisionerEngine.defaultState, - voiceTag: VoiceTagEngine.defaultState, - provenance: ProvenanceEngine.defaultState, - codeMriTimeline: TimelineModel.defaultTimelineState(), - patternDiag: PatternDiagModel.init, - attributionLicense: AttributionLicenseModel.init, - wizard: WizardModel.defaultWizardState, - watcher: { - running: false, - watchedPaths: [], - eventCount: 0, - recentEvents: [], - error: None, - }, - ai: AiEngine.defaultState, - repoLoader: RepoLoaderEngine.defaultState, - panelSwitcher: PanelRegistry.init, - workspace: WorkspaceEngine.defaultState, - keybindings: KeybindingsEngine.defaultState, - capture: CaptureEngine.defaultState, - statusBar: StatusBarEngine.defaultState, - security: SecurityEngine.defaultState, - migration: MigrationEngine.defaultState, - panicAttack: PanicAttackModel.init, - massPanic: MassPanicModel.init, - tsdm: TsdmModel.init, - valenceShell: ValenceShellEngine.defaultState, - gamePreview: GamePreviewEngine.defaultState, - vmInspector: VmInspectorEngine.defaultState, - networkTopology: NetworkTopologyEngine.defaultState, - levelArchitect: LevelArchitectEngine.defaultState, - coprocessors: CoprocessorsEngine.defaultState, - multiplayerMonitor: MultiplayerMonitorEngine.defaultState, - dlcWorkshop: DlcWorkshopEngine.defaultState, - ums: UmsEngine.defaultState, - editorBridge: EditorBridgeEngine.defaultState, - buildDashboard: BuildDashboardEngine.defaultState, - releaseManager: ReleaseManagerEngine.defaultState, - automationRouter: AutomationRouterEngine.defaultState, - databases: DatabasesEngine.defaultState, - boj: { - serverUrl: ServiceEndpoints.bojServer, - connected: true, - lastHealthCheck: 1709942400.0, - cartridges: [ - { - name: "database-mcp", - displayName: "Database MCP", - description: "VeriSimDB query routing and schema introspection", - grade: GradeC, - loaded: true, - protocols: [ProtoMCP, ProtoREST, ProtoGRPC, ProtoGraphQL], - layers: {abiReady: true, ffiReady: true, adapterReady: true, sharedLibReady: true}, - soHash: "sha256:a1b2c3d4", - restPort: 7701, - grpcPort: 7702, - graphqlPort: 7703, - }, - { - name: "proof-mcp", - displayName: "Proof MCP", - description: "ECHIDNA proof dispatch and tactic suggestions", - grade: GradeC, - loaded: true, - protocols: [ProtoMCP, ProtoNeSy], - layers: {abiReady: true, ffiReady: true, adapterReady: true, sharedLibReady: true}, - soHash: "sha256:e5f6a7b8", - restPort: 7711, - grpcPort: 7712, - graphqlPort: 7713, - }, - { - name: "observe-mcp", - displayName: "Observe MCP", - description: "Network telemetry and Aerie probe routing", - grade: GradeC, - loaded: true, - protocols: [ProtoMCP, ProtoREST], - layers: {abiReady: true, ffiReady: true, adapterReady: false, sharedLibReady: true}, - soHash: "sha256:c9d0e1f2", - restPort: 7721, - grpcPort: 0, - graphqlPort: 0, - }, - { - name: "fleet-mcp", - displayName: "Fleet MCP", - description: "Gitbot-fleet orchestration and dispatch", - grade: GradeC, - loaded: true, - protocols: [ProtoMCP, ProtoFleet, ProtoAgentic], - layers: {abiReady: true, ffiReady: true, adapterReady: true, sharedLibReady: true}, - soHash: "sha256:34567890", - restPort: 7731, - grpcPort: 7732, - graphqlPort: 7733, - }, - { - name: "security-mcp", - displayName: "Security MCP", - description: "Panic-attack fuzzing and vulnerability scanning", - grade: GradeC, - loaded: false, - protocols: [ProtoMCP, ProtoREST], - layers: {abiReady: true, ffiReady: true, adapterReady: false, sharedLibReady: true}, - soHash: "sha256:abcdef01", - restPort: 0, - grpcPort: 0, - graphqlPort: 0, - }, - ], - selectedCartridge: None, - umoja: { - active: true, - localNodeId: "panll-primary-001", - peers: [ - { - nodeId: "boj-worker-002", - address: "127.0.0.1:7750", - state: PeerVerified, - gossipRound: 12, - catalogueDigest: "sha256:fedcba98", - lastSeen: 1709942380.0, - }, - ], - currentRound: 12, - }, - activeCategory: Dashboard, - invokeCartridge: "", - invokeTool: "", - invokeArgs: [], - invokeResult: None, - loading: false, - error: None, - filterText: "", - lastTypeCheck: None, - latencyLog: [], - umojaAddPeerInput: "", - }, - cladeBrowser: { - ...CladeBrowserModel.defaultState, - clades: CladeBrowserEngine.builtinClades, - permissionRules: CladeBrowserEngine.defaultPermissionRules, - }, - tentacles: TentaclesEngine.init(), - protocolSquisher: ProtocolSquisherEngine.defaultState, - myLang: MyLangEngine.defaultState, - typell: TypeLLEngine.defaultState, - busRegistry: PanelBus.defaultRegistry, - lastA2mlManifest: None, - lastA2mlValidation: None, - a2mlManifestPaths: [], - lastK9Contractile: None, - lastK9Layout: None, - k9KennelSchema: None, - k9YardContract: None, - seamRegister: SeamEngine.defaultRegister, - lastSeamAudit: None, - ensaidConfigPreview: None, - ensaidConfigError: None, - undoStack: [], - redoStack: [], - humidity: Medium, - barycentreTour: { - active: false, - currentStep: TourIntro, - completed: false, - }, - menuBar: { - activeMenu: None, - }, - viewMode: Standard, - paneLVisible: true, - paneNVisible: true, - paneWVisible: true, - protocolAnalysisVisible: false, - panelBarVisible: true, - fullscreenActive: false, - feedbackPending: None, - feedbackError: None, - feedbackReportType: Some("FeatureRequest"), - help: HelpEngine.defaultState, - accessibility: AccessibilityEngine.defaultState, - tiling: TilingEngine.defaultState, - focusDimming: FocusDimmingEngine.defaultState, - scriptGist: ScriptGistEngine.defaultState, - stapeln: StapelnEngine.defaultState, - evangeliser: EvangeliserEngine.defaultState, - languageForge: LanguageForgeEngine.defaultState, - tangleViz: TangleVizEngine.defaultState, - specBrowser: SpecBrowserModel.initial, - verificationDashboard: VerificationDashboardModel.initial, - observatory: ObservatoryEngine.defaultState, - ambientOps: AmbientOpsEngine.defaultState, - // Game Dev panels — testing - unitTestRunner: UnitTestRunnerEngine.defaultState, - functionalTester: FunctionalTesterEngine.defaultState, - regressionGuard: RegressionGuardEngine.defaultState, - performanceProfiler: PerformanceProfilerEngine.defaultState, - loadTester: LoadTesterEngine.defaultState, - soakMonitor: SoakMonitorEngine.defaultState, - compatibilityMatrix: CompatibilityMatrixEngine.defaultState, - exploratoryWorkbench: ExploratoryWorkbenchEngine.defaultState, - betaFeedbackHub: BetaFeedbackHubEngine.defaultState, - balanceAnalyser: BalanceAnalyserEngine.defaultState, - // Game Dev panels — bridges - typingBridge: TypingBridgeEngine.defaultState, - neurosymBridge: NeurosymBridgeEngine.defaultState, - agenticBridge: AgenticBridgeEngine.defaultState, - automationBridge: AutomationBridgeEngine.defaultState, - databaseBridge: DatabaseBridgeEngine.defaultState, - protocolBridge: ProtocolBridgeEngine.defaultState, - proofsBridge: ProofsBridgeEngine.defaultState, - scriptingBridge: ScriptingBridgeEngine.defaultState, - // Game Dev panels — game-specific - generatorMode: GeneratorModeEngine.defaultState, - architectMode: ArchitectModeEngine.defaultState, - guardAiTuner: GuardAiTunerEngine.defaultState, - deviceNetworkDesigner: DeviceNetworkDesignerEngine.defaultState, - assetManager: AssetManagerEngine.defaultState, - playtestRecorder: PlaytestRecorderEngine.defaultState, - codeReview: CodeReviewEngine.defaultState, - mergeCoordinator: MergeCoordinatorEngine.defaultState, - teamDashboard: TeamDashboardEngine.defaultState, - debuggingWorkbench: DebuggingWorkbenchEngine.defaultState, - wiringInspector: WiringInspectorEngine.defaultState, - // K9 and Contractile management panels - k9Manager: K9Model.init, - // Floor Raise campaign panels - floorRaise: FloorRaiseEngine.defaultState, - provenAdoption: ProvenAdoptionEngine.defaultState, - contractileCompleteness: ContractileCompletenessEngine.defaultState, - manifestCoverage: ManifestCoverageEngine.defaultState, - verisimdbFeeds: VerisimdbFeedsEngine.defaultState, - feedbackRouting: FeedbackRoutingEngine.defaultState, - vexometerFriction: VexometerFrictionEngine.defaultState, - llmCoding: LlmCodingEngine.init, - agentCoordination: AgentCoordinationEngine.init, - burble: BurbleEngine.defaultState, - serviceRegistry: { - services: Dict.make(), - lastChecked: None, - isRefreshing: false, - }, - settings: { - verisimdbUrl: "http://localhost:8080", - echidnaUrl: "http://localhost:9000", - burbleUrl: "http://localhost:6473", - bojUrl: "http://localhost:7700", - typellUrl: "http://localhost:7800", - configDir: "~/.panll", - theme: "DarkStart", - autoSaveIntervalMs: 30000, - autoConnectServices: true, - isLoading: false, - error: None, - isDirty: false, - }, - identity: { - snapshots: [], - activeSnapshotId: None, - isCapturing: false, - isRestoring: false, - error: None, - }, -} diff --git a/src/Msg.affine b/src/Msg.affine new file mode 100644 index 00000000..b1a59b85 --- /dev/null +++ b/src/Msg.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Msg; + +// TODO: Complete semantic implementation diff --git a/src/Msg.res b/src/Msg.res deleted file mode 100644 index 3a0a5a7f..00000000 --- a/src/Msg.res +++ /dev/null @@ -1,544 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Messages - the communication protocol for TEA updates. -/// -/// Every user action, backend response, and timer pulse is encoded here so -/// the Elm-style update loop can deterministically evolve the Binary Star -/// state machine. Each domain message type lives in its own module under -/// src/msg/ and is re-exported here via `include`. The unified `type msg` -/// at the bottom references all sub-types. -/// -/// Dependency graph (no cycles): -/// src/msg/*Msg.res <- Model (leaf types) -/// Msg.res <- all msg modules (this file) - -// -- Core pane messages -------------------------------------------------- - -/// Re-export Pane-L (Symbolic) messages. -include PaneLMsg - -/// Re-export Pane-N (Neural) messages. -include PaneNMsg - -/// Re-export Pane-W (World/Barycentre) messages. -include PaneWMsg - -/// Re-export Pane-A (Ambient) messages. -include PaneAMsg - -// -- Cognitive governance messages --------------------------------------- - -/// Re-export Vexometer messages. -include VexometerMsg - -/// Re-export Orbital stability messages. -include OrbitalMsg - -/// Re-export View control messages. -include ViewMsg - -/// Re-export Feedback-O-Tron messages. -include FeedbackMsg - -/// Re-export Anti-Crash validation messages. -include AntiCrashMsg - -// -- Backend service messages -------------------------------------------- - -/// Re-export VeriSimDB database messages. -include VeriSimDBMsg - -/// Re-export ECHIDNA theorem prover messages. -include EchidnaMsg - -/// Re-export VAB (Verified Assembly Building) messages. -include VabMsg - -/// Re-export CloudGuard Cloudflare messages. -include CloudguardMsg - -/// Re-export BoJ cartridge server messages. -include BojMsg - -/// Re-export unified Databases panel messages. -include DatabasesMsg - -// -- Tool and panel messages --------------------------------------------- - -/// Re-export Git-Private-Farm messages. -include FarmMsg - -/// Re-export Palimpsest Plaza messages. -include PlazaMsg - -/// Re-export Hypatia neurosymbolic scanner messages. -include HypatiaMsg - -/// Re-export Gitbot-Fleet messages. -include FleetMsg - -/// Re-export Reposystem RSR compliance messages. -include ReposystemMsg - -/// Re-export Aerie network diagnostics messages. -include AerieMsg - -/// Re-export Interfaces ABI/FFI messages. -include InterfacesMsg - -/// Re-export Playgrounds sandbox messages. -include PlaygroundsMsg - -/// Re-export Panel Minter wizard messages. -include MinterMsg - -/// Re-export Provisioner messages. -include ProvisionerMsg - -/// Re-export Wizard messages. -include WizardMsg - -/// Re-export Code MRI VoiceTag messages. -include VoiceTagMsg - -/// Re-export Provenance Map messages. -include ProvenanceMsg - -/// Re-export Watcher filesystem observation messages. -include WatcherMsg - -/// Re-export AI panel messages. -include AiMsg - -/// Re-export Repo Loader messages. -include RepoLoaderMsg - -/// Re-export Panel Switcher messages. -include PanelSwitcherMsg - -/// Re-export Workspace management messages. -include WorkspaceMsg - -/// Re-export Capture messages. -include CaptureMsg - -/// Re-export Security messages. -include SecurityMsg - -/// Re-export Keybindings messages. -include KeybindingsMsg - -/// Re-export Migration Observatory messages. -include MigrationMsg - -/// Re-export panic-attack messages. -include PanicAttackMsg - -/// Re-export Mass-panic batch scanning messages. -include MassPanicMsg - -/// Re-export TSDM directive messages. -include TsdmMsg - -/// Re-export Valence Shell terminal messages. -include ValenceShellMsg - -/// Re-export Game Preview messages. -include GamePreviewMsg - -/// Re-export VM Inspector messages. -include VmInspectorMsg - -/// Re-export Network Topology messages. -include NetworkTopologyMsg - -/// Re-export Level Architect messages. -include LevelArchitectMsg - -/// Re-export Coprocessors messages. -include CoprocessorsMsg - -/// Re-export Multiplayer Monitor messages. -include MultiplayerMonitorMsg - -/// Re-export Universal Modding Studio messages. -include UmsMsg - -/// Re-export DLC Workshop messages. -include DlcWorkshopMsg - -/// Re-export Editor Bridge messages. -include EditorBridgeMsg - -/// Re-export Build Dashboard messages. -include BuildDashboardMsg - -/// Re-export Release Manager messages. -include ReleaseManagerMsg - -/// Re-export Automation Router messages. -include AutomationRouterMsg - -/// Re-export Script Gist messages. -include ScriptGistMsg - -/// Re-export ENSAID_CONFIG messages. -include EnsaidConfigMsg - -/// Re-export Code MRI Timeline messages. -include TimelineMsg - -/// Re-export Code MRI Pattern Diagnostics messages (Layer 3). -include PatternDiagMsg - -/// Re-export Code MRI Attribution-to-Licensing messages (Layer 4). -include AttributionLicenseMsg - -/// Re-export Clade Browser messages. -include CladeBrowserMsg - -/// Re-export Panel Bus messages. -include PanelBusMsg - -/// Re-export 7-Tentacles compiler agent messages. -include TentaclesMsg - -/// Re-export Protocol-Squisher messages. -include ProtocolSquisherMsg - -/// Re-export My-Lang AI-native language messages. -include MyLangMsg - -/// Re-export TypeLL verification kernel messages. -include TypellMsg - -/// Re-export Observability messages. -include ObservabilityMsg - -/// Re-export A2ML manifest messages. -include A2mlMsg - -/// Re-export K9 contractile messages. -include K9Msg - -/// Re-export Help system messages. -include HelpMsg - -/// Re-export Accessibility toolbar messages. -include AccessibilityMsg - -/// Re-export Tiling and multi-monitor messages. -include TilingMsg - -/// Re-export Menu bar messages. -include MenuBarMsg - -/// Re-export Focus dimming messages. -include FocusDimmingMsg - -/// Re-export Stapeln container assembly messages. -include StapelnMsg - -/// Re-export Evangeliser JS->ReScript messages. -include EvangeliserMsg - -/// Re-export Language Forge messages. -include LanguageForgeMsg - -/// Re-export TangleViz topological programming messages. -include TangleVizMsg - -/// Re-export SpecBrowser language specification messages. -include SpecBrowserMsg - -/// Re-export VerificationDashboard messages. -include VerificationDashboardMsg - -/// Re-export Observatory integrative dashboard messages. -include ObservatoryMsg - -/// Re-export AmbientOps hospital-model sysadmin messages. -include AmbientOpsMsg - -// -- Game Testing panel messages ----------------------------------------- - -/// Re-export Unit Test Runner messages. -include UnitTestRunnerMsg - -/// Re-export Functional Tester messages. -include FunctionalTesterMsg - -/// Re-export Regression Guard messages. -include RegressionGuardMsg - -/// Re-export Performance Profiler messages. -include PerformanceProfilerMsg - -/// Re-export Load Tester messages. -include LoadTesterMsg - -/// Re-export Soak Monitor messages. -include SoakMonitorMsg - -/// Re-export Compatibility Matrix messages. -include CompatibilityMatrixMsg - -/// Re-export Exploratory Workbench messages. -include ExploratoryWorkbenchMsg - -/// Re-export Beta Feedback Hub messages. -include BetaFeedbackHubMsg - -/// Re-export Balance Analyser messages. -include BalanceAnalyserMsg - -// -- Bridge panel messages ----------------------------------------------- - -/// Re-export Typing Bridge messages. -include TypingBridgeMsg - -/// Re-export Neurosymbolic Bridge messages. -include NeurosymBridgeMsg - -/// Re-export Agentic Bridge messages. -include AgenticBridgeMsg - -/// Re-export Automation Bridge messages. -include AutomationBridgeMsg - -/// Re-export Database Bridge messages. -include DatabaseBridgeMsg - -/// Re-export Protocol Bridge messages. -include ProtocolBridgeMsg - -/// Re-export Proofs Bridge messages. -include ProofsBridgeMsg - -/// Re-export Scripting Bridge messages. -include ScriptingBridgeMsg - -// -- Game-specific panel messages ---------------------------------------- - -/// Re-export Generator Mode messages. -include GeneratorModeMsg - -/// Re-export Architect Mode messages. -include ArchitectModeMsg - -/// Re-export Guard AI Tuner messages. -include GuardAiTunerMsg - -/// Re-export Device Network Designer messages. -include DeviceNetworkDesignerMsg - -/// Re-export Asset Manager messages. -include AssetManagerMsg - -/// Re-export Playtest Recorder messages. -include PlaytestRecorderMsg - -// -- Team / collaboration panel messages --------------------------------- - -/// Re-export Code Review messages. -include CodeReviewMsg - -/// Re-export Merge Coordinator messages. -include MergeCoordinatorMsg - -/// Re-export Team Dashboard messages. -include TeamDashboardMsg - -/// Re-export Debugging Workbench messages. -include DebuggingWorkbenchMsg - -// -- Infrastructure panel messages --------------------------------------- - -/// Re-export Wiring Inspector messages. -include WiringInspectorMsg - -// -- Floor Raise campaign messages --------------------------------------- - -/// Re-export Floor Raise campaign dashboard messages. -include FloorRaiseMsg - -/// Re-export Proven Adoption scanner messages. -include ProvenAdoptionMsg - -/// Re-export Contractile Completeness scanner messages. -include ContractileCompletenessMsg - -/// Re-export Manifest Coverage scanner messages. -include ManifestCoverageMsg - -/// Re-export VeriSimDB Feeds viewer messages. -include VerisimdbFeedsMsg - -/// Re-export Feedback Routing viewer messages. -include FeedbackRoutingMsg - -/// Re-export Vexometer Friction viewer messages. -include VexometerFrictionMsg - -// -- 007 Toolchain and VideoCoordination --------------------------------- - -/// Re-export 007 Toolchain messages. -include Oo7Msg - -/// Re-export VideoCoordination messages. -include VideoCoordinationMsg - -/// Re-export Service Registry messages (Connected Workbench v0.2.0). -include ServiceMsg - -/// Re-export Settings messages (Connected Workbench v0.2.0). -include SettingsMsg - -/// Re-export Identity messages (Connected Workbench v0.2.0). -include IdentityMsg - -// -- The unified message type -------------------------------------------- - -/// The unified message type -type msg = - | PaneL(paneLMsg) - | PaneN(paneNMsg) - | PaneW(paneWMsg) - | PaneA(paneAMsg) - | VeriSimDB(verisimdbMsg) - | Echidna(echidnaMsg) - | Vexometer(vexometerMsg) - | Orbital(orbitalMsg) - | View(viewMsg) - | Feedback(feedbackMsg) - | AntiCrash(antiCrashMsg) - | Vab(vabMsg) - | CloudGuard(cloudguardMsg) // Cloudflare domain security management - | Farm(farmMsg) // Git-Private-Farm repo inventory - | Plaza(plazaMsg) // Palimpsest Plaza PMPL licensing - | Hypatia(hypatiaMsg) // Hypatia neurosymbolic scanner - | Fleet(fleetMsg) // Gitbot-Fleet orchestration - | Reposystem(reposystemMsg) // RSR compliance auditing - | Aerie(aerieMsg) // Network diagnostics - | Oo7Toolchain(oo7Msg) // Agentic compiler and high-rigor execution - | VideoCoordination(videoCoordinationMsg) // Drive-to-Photos batch transfer dashboard - | Interfaces(interfacesMsg) // ABI/FFI inventory - | Playgrounds(playgroundsMsg) // Code sandbox - | Minter(minterMsg) // Panel Minter wizard - | Provisioner(provisionerMsg) // Portfolio bundles, config, isolation - | VoiceTag(voiceTagMsg) // Code MRI Layer 0 -- voice-activated annotation - | Provenance(provenanceMsg) // Code trust surface (core infrastructure) - | Watcher(watcherMsg) // Filesystem observation (core infrastructure) - | Ai(aiMsg) // Multi-provider AI neural interface - | RepoLoader(repoLoaderMsg) // Repository scanner and panel configuration - | PanelSwitcher(panelSwitcherMsg) // Panel navigation and health checks - | Workspace(workspaceMsg) // Workspace management layer (DD-022–DD-027) - | Capture(captureMsg) // Screenshots, recordings, demos (DD-022) - | Security(securityMsg) // Redaction, vault, 2FA, Trustfile (DD-026/027) - | Wizard(wizardMsg) // Plugin/panel creation wizard - | Keybindings(keybindingsMsg) // Keyboard shortcut management - | Migration(migrationMsg) // ReScript Migration Observatory - | PanicAttack(panicAttackMsg) // Stress testing and bug detection - | MassPanic(massPanicMsg) // Organisation-scale batch scanning - | Tsdm(tsdmMsg) // TSDM directive -- triaxial priority ordering - | ValenceShell(valenceShellMsg) // Embedded terminal with Claude Code - | GamePreview(gamePreviewMsg) // Live IDApTIK game preview - | VmInspector(vmInspectorMsg) // Reversible VM visual debugger - | NetworkTopology(networkTopologyMsg) // IDApTIK in-game network graph - | LevelArchitect(levelArchitectMsg) // Visual level design tool - | Coprocessors(coprocessorsMsg) // Coprocessor backend monitoring - | MultiplayerMonitor(multiplayerMonitorMsg) // Phoenix sync server inspector - | DlcWorkshop(dlcWorkshopMsg) // DLC puzzle pack creation and testing - | Ums(umsMsg) // Universal Modding Studio -- unified game content creation hub - | EditorBridge(editorBridgeMsg) // External code editor federation (LSP) - | BuildDashboard(buildDashboardMsg) // Build/test/error monitoring - | ReleaseManager(releaseManagerMsg) // Versioning, changelog, distribution - | AutomationRouter(automationRouterMsg) // Hybrid cross-panel workflow orchestration - | ScriptGist(scriptGistMsg) // Portable computation gists (Minskian cardfiles) - | Databases(databasesMsg) // Unified database management (VeriSimDB/QuandleDB/LithoGlyph) - | Boj(bojMsg) // Bundle of Joy cartridge server - | CladeBrowser(cladeBrowserMsg) // Clade taxonomy browser - | Tentacles(tentaclesMsg) // 7-Tentacles compiler agent orchestra - | ProtocolSquisher(protocolSquisherMsg) // Format analysis and compatibility - | MyLang(myLangMsg) // AI-native language workbench - | TypeLL(typellMsg) // Verification kernel (cross-panel type intelligence) - | EnsaidConfig(ensaidConfigMsg) // Cross-panel ENSAID_CONFIG generation and I/O - | Timeline(timelineMsg) // Code MRI Layer 2 -- VeriSimDB development timeline - | PatternDiag(patternDiagMsg) // Code MRI Layer 3 -- pattern diagnostics + gamification - | AttrLicense(attributionLicenseMsg) // Code MRI Layer 4 -- attribution-to-licensing - | Bus(panelBusMsg) // Panel Bus subscriber management - | RecordBojLatency(string, string, float) // cartridge, tool, elapsed ms - | GovernanceNesyResult(result) // nesy-mcp governance query response - | GovernanceNesyValidateResult(result) // nesy-mcp adjustment validation - | GovernanceNesyProbeResult(result) // nesy-mcp stability probe - | Observability(observabilityMsg) // SARIF export and OpenTelemetry via observe-mcp - | A2ml(a2mlMsg) // AI manifest parsing and validation - | K9(k9Msg) // K9 contractile configuration and layout - | AuditSeams // Run compliance seam audit against exception register - | SeamAuditResult(SeamEngine.seamAuditResult) // Result of seam audit - | Help(helpMsg) // In-app help, glossary, onboarding - | MenuBar(menuBarMsg) // Standard application menu bar - | AccessibilityCtrl(accessibilityMsg) // Accessibility toolbar preferences - | Tiling(tilingMsg) // Multi-monitor panel detachment and tiling - | FocusDimming(focusDimmingMsg) // Focus-aware dimming and Smart Memory Mode - | Stapeln(stapelnMsg) // Stapeln container assembly pipeline - | Evangeliser(evangeliserMsg) // ReScript Evangeliser -- JS->ReScript teaching - | LanguageForge(languageForgeMsg) // Language Forge -- nextgen-languages portfolio - | TangleViz(tangleVizMsg) // Topological programming visualizer (braids, knots, invariants) - | SpecBrowser(specBrowserMsg) // Language specification browser -- grammars, typing rules, taxonomy - | VerificationDashboard(verificationDashboardMsg) // Proof/test/benchmark/fuzzing status - | Observatory(observatoryMsg) // Integrative dashboard -- cross-panel health and resources - | AmbientOps(ambientOpsMsg) // Hospital-model sysadmin -- clinician, network, hardware - // Game Testing panels - | UnitTestRunner(unitTestRunnerMsg) // ReScript test execution, coverage heatmap - | FunctionalTester(functionalTesterMsg) // End-to-end game workflow simulation - | RegressionGuard(regressionGuardMsg) // Snapshot comparison and golden-file testing - | PerformanceProfiler(performanceProfilerMsg) // Frame budget, GC pressure, flamegraphs - | LoadTester(loadTesterMsg) // Phoenix channel stress testing - | SoakMonitor(soakMonitorMsg) // Long-running session memory trend - | CompatibilityMatrix(compatibilityMatrixMsg) // Browser/device/resolution test matrix - | ExploratoryWorkbench(exploratoryWorkbenchMsg) // Freeform play session recording - | BetaFeedbackHub(betaFeedbackHubMsg) // Feedback-o-tron integration, sentiment - | BalanceAnalyser(balanceAnalyserMsg) // Game balance stats, Monte Carlo - // Bridge panels - | TypingBridge(typingBridgeMsg) // TypeLL type constraints for game state - | NeurosymBridge(neurosymBridgeMsg) // Guard AI behaviour reasoning via ECHIDNA - | AgenticBridge(agenticBridgeMsg) // Automated playtesting agents with OODA - | AutomationBridge(automationBridgeMsg) // CI/CD pipeline orchestration - | DatabaseBridge(databaseBridgeMsg) // VeriSimDB game state persistence - | ProtocolBridge(protocolBridgeMsg) // Multiplayer sync protocol analysis - | ProofsBridge(proofsBridgeMsg) // Proven repo formal verification - | ScriptingBridge(scriptingBridgeMsg) // VM instruction scripting REPL - // Game-specific panels - | GeneratorMode(generatorModeMsg) // Parametric procedural world builder - | ArchitectMode(architectModeMsg) // PixiJS fine-grained level editor - | GuardAiTuner(guardAiTunerMsg) // Guard patrol, alert threshold tuning - | DeviceNetworkDesigner(deviceNetworkDesignerMsg) // Wire devices, security levels - | AssetManager(assetManagerMsg) // PixiJS sprites, sounds, templates - | PlaytestRecorder(playtestRecorderMsg) // Record + replay sessions - // Team / collaboration panels - | CodeReview(codeReviewMsg) // PR review, inline comments, approval gates - | MergeCoordinator(mergeCoordinatorMsg) // Branch management, conflict resolution - | TeamDashboard(teamDashboardMsg) // Team presence, activity feed, progress - | DebuggingWorkbench(debuggingWorkbenchMsg) // Time-travel debugging, state inspection - // Infrastructure panels - | WiringInspector(wiringInspectorMsg) // PCC constraint state and bottleneck analysis - // Floor Raise panels -- foundational tool adoption campaign - | FloorRaise(floorRaiseMsg) // Floor Raise campaign dashboard - | ProvenAdoption(provenAdoptionMsg) // Proven library adoption scanner - | ContractileCompleteness(contractileCompletenessMsg) // Contractile coverage scanner - | ManifestCoverage(manifestCoverageMsg) // AI manifest coverage scanner - | VerisimdbFeeds(verisimdbFeedsMsg) // VeriSimDB data feed viewer - | FeedbackRouting(feedbackRoutingMsg) // Feedback-o-Tron routing viewer - | VexometerFriction(vexometerFrictionMsg) // Vexometer friction viewer - | SystemUpdate(SystemUpdateMsg.systemUpdateMsg) // System component update management - | Burble(BurbleModel.burbleMsg) // Burble voice huddle (groove-aware) - | Service(serviceMsg) // Service registry lifecycle (Connected Workbench v0.2.0) - | Settings(settingsMsg) // User configuration (Connected Workbench v0.2.0) - | Identity(identityMsg) // Identity snapshots + team replication (Connected Workbench v0.2.0) - | Undo // Undo last significant action - | Redo // Redo last undone action - | SaveState // Persist current state to storage - | VeriSimDBStateLoaded(result) // Async state restored from VeriSimDB - | VeriSimDBStateSaved(result) // Confirmation/error from VeriSimDB save - | NoOp diff --git a/src/Storage.affine b/src/Storage.affine new file mode 100644 index 00000000..7cd202fd --- /dev/null +++ b/src/Storage.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Storage; + +// TODO: Complete semantic implementation diff --git a/src/Storage.res b/src/Storage.res deleted file mode 100644 index 19e6a1b3..00000000 --- a/src/Storage.res +++ /dev/null @@ -1,588 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Storage module for persisting PanLL state between sessions. -/// -/// Uses localStorage for now (Gossamer storage API can be added later). -/// Handles serialization/deserialization of Model types to/from JSON. - -open Model - -// Storage key for localStorage -let storageKey = "panll_state_v1" - -// Type for serializable state (subset of model that should persist) -type persistedState = { - // User work (Priority 1) - constraints: array, - editorContent: string, - neuralTokens: array, - worldContent: string, - // Event chain data (Priority 1 - imported analysis results) - eventChain: array, - eventChainSummary: option, - eventChainTimeline: option, - // User preferences (Priority 2) - viewMode: viewMode, - paneLVisible: bool, - paneNVisible: bool, - paneWVisible: bool, - humidity: humidityLevel, - // Session state (Priority 3) - vexometerIndex: float, - orbitalStability: float, -} - -// Convert OODA phase to string -let oodaPhaseToString = (phase: oodaPhase): string => { - switch phase { - | Observe => "Observe" - | Orient => "Orient" - | Decide => "Decide" - | Act => "Act" - } -} - -// Convert string to OODA phase -let stringToOodaPhase = (str: string): oodaPhase => { - switch str { - | "Orient" => Orient - | "Decide" => Decide - | "Act" => Act - | _ => Observe // Default - } -} - -// Convert viewMode to string -let viewModeToString = (mode: viewMode): string => { - switch mode { - | Standard => "Standard" - | LightMode => "LightMode" - | Ambient => "Ambient" - | Zen => "Zen" - | DarkStart => "DarkStart" - } -} - -// Convert string to viewMode -let stringToViewMode = (str: string): viewMode => { - switch str { - | "LightMode" => LightMode - | "Ambient" => Ambient - | "Zen" => Zen - | "DarkStart" => DarkStart - | _ => Standard // Default - } -} - -// Convert humidity level to string -let humidityToString = (humidity: humidityLevel): string => { - switch humidity { - | High => "High" - | Medium => "Medium" - | Low => "Low" - } -} - -// Convert string to humidity level -let stringToHumidity = (str: string): humidityLevel => { - switch str { - | "High" => High - | "Low" => Low - | _ => Medium // Default - } -} - -// Extract persisted state from model -let extractPersistedState = (model: model): persistedState => { - constraints: model.paneL.constraints, - editorContent: model.paneL.editorContent, - neuralTokens: model.paneN.tokens, - worldContent: model.paneW.content, - eventChain: model.paneW.eventChain, - eventChainSummary: model.paneW.eventChainSummary, - eventChainTimeline: model.paneW.eventChainTimeline, - viewMode: model.viewMode, - paneLVisible: model.paneLVisible, - paneNVisible: model.paneNVisible, - paneWVisible: model.paneWVisible, - humidity: model.humidity, - vexometerIndex: model.vexometer.index, - orbitalStability: model.orbital.stability, -} - -// Build a plain JS object from persisted state for JSON serialization -let toJsonObject = (state: persistedState): JSON.t => { - let constraints = state.constraints->Array.map(c => { - let d = Dict.make() - d->Dict.set("id", JSON.Encode.string(c.id)) - d->Dict.set("expression", JSON.Encode.string(c.expression)) - d->Dict.set("active", JSON.Encode.bool(c.active)) - d->Dict.set("pinned", JSON.Encode.bool(c.pinned)) - JSON.Encode.object(d) - }) - let sourceToString = (s: tokenSource): string => - switch s { - | NeuralInference => "neural" - | EchidnaProver => "echidna" - | TypeLLKernel => "typell" - | VeriSimInference => "verisim" - | AntiCrashGate => "anticrash" - | OperatorInput => "operator" - | OrbitalSync => "orbital" - } - let categoryToString = (c: tokenCategory): string => - switch c { - | Observation => "observation" - | Hypothesis => "hypothesis" - | Deduction => "deduction" - | Abduction => "abduction" - | ProofStep => "proof" - | Violation => "violation" - | Correction => "correction" - | Synthesis => "synthesis" - } - let phaseToString = (p: oodaPhase): string => - switch p { - | Observe => "observe" - | Orient => "orient" - | Decide => "decide" - | Act => "act" - } - let tokens = state.neuralTokens->Array.map(t => { - let d = Dict.make() - d->Dict.set("id", JSON.Encode.string(t.id)) - d->Dict.set("content", JSON.Encode.string(t.content)) - d->Dict.set("timestamp", JSON.Encode.float(t.timestamp)) - d->Dict.set("confidence", JSON.Encode.float(t.confidence)) - d->Dict.set("validated", JSON.Encode.bool(t.validated)) - d->Dict.set("source", JSON.Encode.string(sourceToString(t.source))) - d->Dict.set("category", JSON.Encode.string(categoryToString(t.category))) - d->Dict.set("emittedDuring", JSON.Encode.string(phaseToString(t.emittedDuring))) - d->Dict.set("causedBy", JSON.Encode.array(t.causedBy->Array.map(JSON.Encode.string))) - switch t.proofHash { - | Some(h) => d->Dict.set("proofHash", JSON.Encode.string(h)) - | None => () - } - JSON.Encode.object(d) - }) - let eventChainEvents = state.eventChain->Array.map(e => { - let d = Dict.make() - d->Dict.set("id", JSON.Encode.string(e.id)) - d->Dict.set("axis", JSON.Encode.string(e.axis)) - switch e.startMs { - | Some(ms) => d->Dict.set("startMs", JSON.Encode.float(ms)) - | None => () - } - d->Dict.set("durationMs", JSON.Encode.float(e.durationMs)) - d->Dict.set("intensity", JSON.Encode.string(e.intensity)) - d->Dict.set("status", JSON.Encode.string(e.status)) - switch e.peakMemory { - | Some(mem) => d->Dict.set("peakMemory", JSON.Encode.float(mem)) - | None => () - } - switch e.notes { - | Some(n) => d->Dict.set("notes", JSON.Encode.string(n)) - | None => () - } - JSON.Encode.object(d) - }) - let root = Dict.make() - root->Dict.set("constraints", JSON.Encode.array(constraints)) - root->Dict.set("editorContent", JSON.Encode.string(state.editorContent)) - root->Dict.set("neuralTokens", JSON.Encode.array(tokens)) - root->Dict.set("worldContent", JSON.Encode.string(state.worldContent)) - root->Dict.set("eventChain", JSON.Encode.array(eventChainEvents)) - switch state.eventChainSummary { - | Some(summary) => { - let d = Dict.make() - d->Dict.set("program", JSON.Encode.string(summary.program)) - d->Dict.set("weakPoints", JSON.Encode.int(summary.weakPoints)) - d->Dict.set("criticalWeakPoints", JSON.Encode.int(summary.criticalWeakPoints)) - d->Dict.set("totalCrashes", JSON.Encode.int(summary.totalCrashes)) - d->Dict.set("robustnessScore", JSON.Encode.float(summary.robustnessScore)) - root->Dict.set("eventChainSummary", JSON.Encode.object(d)) - } - | None => () - } - switch state.eventChainTimeline { - | Some(timeline) => { - let d = Dict.make() - d->Dict.set("durationMs", JSON.Encode.float(timeline.durationMs)) - d->Dict.set("events", JSON.Encode.int(timeline.events)) - root->Dict.set("eventChainTimeline", JSON.Encode.object(d)) - } - | None => () - } - root->Dict.set("viewMode", JSON.Encode.string(viewModeToString(state.viewMode))) - root->Dict.set("paneLVisible", JSON.Encode.bool(state.paneLVisible)) - root->Dict.set("paneNVisible", JSON.Encode.bool(state.paneNVisible)) - root->Dict.set("paneWVisible", JSON.Encode.bool(state.paneWVisible)) - root->Dict.set("humidity", JSON.Encode.string(humidityToString(state.humidity))) - root->Dict.set("vexometerIndex", JSON.Encode.float(state.vexometerIndex)) - root->Dict.set("orbitalStability", JSON.Encode.float(state.orbitalStability)) - JSON.Encode.object(root) -} - -// Serialize persisted state to JSON string -let serialize = (state: persistedState): string => { - JSON.stringify(toJsonObject(state)) -} - -// Raw localStorage helpers that receive values as arguments -let setItem: ( - string, - string, -) => unit = %raw(`function(key, value) { localStorage.setItem(key, value) }`) - -/// Save model to localStorage (synchronous) and VeriSimDB (async fire-and-forget). -/// -/// localStorage is always written first as the fast synchronous fallback. -/// If a Gossamer runtime is available, the state is also persisted to -/// VeriSimDB for cross-session durability and identity snapshot support. -let save = (model: model): unit => { - try { - let state = extractPersistedState(model) - let json = serialize(state) - // Always write localStorage (fast, synchronous) - setItem(storageKey, json) - // Also persist to VeriSimDB (async, fire-and-forget) - if RuntimeBridge.isGossamerRuntime() { - RuntimeBridge.invoke("verisimdb_save_state", {"key": storageKey, "state": json}) - ->Promise.catch(_err => { - Console.warn("VeriSimDB state save failed — localStorage backup active") - Promise.resolve() - }) - ->ignore - } - } catch { - | exn => Console.error2("Failed to save state:", exn) - } -} - -// ── Token source/category/phase string parsers (used by decoders) ──── - -/// Parse a token source string into a tokenSource variant. -let parseSource = (s: string): tokenSource => - switch s { - | "echidna" => EchidnaProver - | "typell" => TypeLLKernel - | "verisim" => VeriSimInference - | "anticrash" => AntiCrashGate - | "operator" => OperatorInput - | "orbital" => OrbitalSync - | _ => NeuralInference - } - -/// Parse a token category string into a tokenCategory variant. -let parseCategory = (s: string): tokenCategory => - switch s { - | "hypothesis" => Hypothesis - | "deduction" => Deduction - | "abduction" => Abduction - | "proof" => ProofStep - | "violation" => Violation - | "correction" => Correction - | "synthesis" => Synthesis - | _ => Observation - } - -/// Parse an OODA phase string into an oodaPhase variant. -let parsePhase = (s: string): oodaPhase => - switch s { - | "orient" => Orient - | "decide" => Decide - | "act" => Act - | _ => Observe - } - -// ── Tea_Json decoders for persisted state ──────────────────────────── - -/// Tea_Json decoder for a symbolic constraint. -let constraintDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map4((id, expression, active, pinned): symbolicConstraint => { - id, - expression, - active, - pinned, - }, stringField( - "id", - ), stringField("expression"), fieldWithDefault("active", bool, true), boolField("pinned")) -} - -/// Tea_Json decoder for a neural token. -let neuralTokenDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map10( - ( - id, - content, - timestamp, - confidence, - validated, - sourceStr, - categoryStr, - phaseStr, - causedBy, - proofHash, - ): neuralToken => { - id, - content, - timestamp, - confidence, - validated, - source: parseSource(sourceStr), - category: parseCategory(categoryStr), - emittedDuring: parsePhase(phaseStr), - causedBy, - proofHash, - }, - stringField("id"), - stringField("content"), - floatField("timestamp"), - floatField("confidence"), - boolField("validated"), - fieldWithDefault("source", string, "neural"), - fieldWithDefault("category", string, "observation"), - fieldWithDefault("emittedDuring", string, "observe"), - stringArrayField("causedBy"), - optionalFieldDecoder("proofHash", string), - ) -} - -/// Tea_Json decoder for a persisted event chain event (camelCase field names). -let eventChainEventDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map8((id, axis, startMs, durationMs, intensity, status, peakMemory, notes): eventChainEvent => { - id, - axis, - startMs, - durationMs, - intensity, - status, - peakMemory, - notes, - }, stringField( - "id", - ), stringField( - "axis", - ), optionalFieldDecoder( - "startMs", - float, - ), floatField( - "durationMs", - ), stringField( - "intensity", - ), stringField( - "status", - ), optionalFieldDecoder("peakMemory", float), optionalFieldDecoder("notes", string)) -} - -/// Tea_Json decoder for a persisted event chain summary (camelCase field names). -let summaryDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map5( - (program, weakPoints, criticalWeakPoints, totalCrashes, robustnessScore): eventChainSummary => { - program, - weakPoints, - criticalWeakPoints, - totalCrashes, - robustnessScore, - }, - stringField("program"), - intField("weakPoints"), - intField("criticalWeakPoints"), - intField("totalCrashes"), - floatField("robustnessScore"), - ) -} - -/// Tea_Json decoder for a persisted event chain timeline (camelCase field names). -let timelineDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map2((durationMs, events): eventChainTimeline => { - durationMs, - events, - }, floatField("durationMs"), intField("events")) -} - -/// Tea_Json decoder for the full persisted state. -let persistedStateDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map13( - ( - constraints, - editorContent, - neuralTokens, - worldContent, - eventChain, - eventChainSummary, - eventChainTimeline, - viewModeStr, - paneLVisible, - paneNVisible, - paneWVisible, - humidityStr, - vexAndOrbital, - ) => { - let (vexometerIndex, orbitalStability) = vexAndOrbital - ( - { - constraints, - editorContent, - neuralTokens, - worldContent, - eventChain, - eventChainSummary, - eventChainTimeline, - viewMode: stringToViewMode(viewModeStr), - paneLVisible, - paneNVisible, - paneWVisible, - humidity: stringToHumidity(humidityStr), - vexometerIndex, - orbitalStability, - }: persistedState - ) - }, - fieldWithDefault("constraints", lenientArray(constraintDecoder), []), - stringField("editorContent"), - fieldWithDefault("neuralTokens", lenientArray(neuralTokenDecoder), []), - stringField("worldContent"), - fieldWithDefault("eventChain", lenientArray(eventChainEventDecoder), []), - optionalFieldDecoder("eventChainSummary", summaryDecoder), - optionalFieldDecoder("eventChainTimeline", timelineDecoder), - fieldWithDefault("viewMode", string, "DarkStart"), - fieldWithDefault("paneLVisible", bool, true), - fieldWithDefault("paneNVisible", bool, true), - fieldWithDefault("paneWVisible", bool, true), - fieldWithDefault("humidity", string, "Medium"), - // Pack the last two floats into a tuple to fit map13 - map2( - (vex, orb) => (vex, orb), - floatField("vexometerIndex"), - fieldWithDefault("orbitalStability", float, 1.0), - ), - ) -} - -/// Reconstruct a model from persisted state. -let modelFromPersisted = (state: persistedState): model => { - let baseModel = init() - { - ...baseModel, - paneL: { - ...baseModel.paneL, - constraints: state.constraints, - editorContent: state.editorContent, - }, - paneN: { - ...baseModel.paneN, - tokens: state.neuralTokens, - nextTokenId: Array.length(state.neuralTokens), - activeCausalChain: switch state.neuralTokens->Array.at(-1) { - | Some(last) => [last.id] - | None => [] - }, - }, - paneW: { - ...baseModel.paneW, - content: state.worldContent, - eventChain: state.eventChain, - eventChainSummary: state.eventChainSummary, - eventChainTimeline: state.eventChainTimeline, - }, - viewMode: state.viewMode, - paneLVisible: state.paneLVisible, - paneNVisible: state.paneNVisible, - paneWVisible: state.paneWVisible, - humidity: state.humidity, - vexometer: { - ...baseModel.vexometer, - index: state.vexometerIndex, - }, - orbital: { - ...baseModel.orbital, - stability: state.orbitalStability, - }, - } -} - -// Load persisted state from localStorage and merge with initial model -let load = (): option => { - try { - let getItem: string => option< - string, - > = %raw(`function(key) { var v = localStorage.getItem(key); return v === null ? undefined : v }`) - let json: option = getItem(storageKey) - - switch json { - | None => None - | Some(jsonStr) => - switch Decoders.decodeOption(persistedStateDecoder, jsonStr) { - | Some(state) => Some(modelFromPersisted(state)) - | None => None - } - } - } catch { - | exn => { - Console.error2("Failed to load state:", exn) - None - } - } -} - -// Raw localStorage remove helper -let removeItem: string => unit = %raw(`function(key) { localStorage.removeItem(key) }`) - -// Clear persisted state -let clear = (): unit => { - try { - removeItem(storageKey) - } catch { - | exn => Console.error2("Failed to clear state:", exn) - } -} - -// =========================================================================== -// VeriSimDB Async Persistence (Connected Workbench v0.2.0) -// =========================================================================== - -/// Load persisted state from VeriSimDB (async). -/// -/// Returns `Some(model)` if VeriSimDB holds valid state, `None` otherwise. -/// Used at startup after the synchronous localStorage load to upgrade to -/// the latest VeriSimDB-backed state if available. -let loadFromVeriSimDB = (): promise> => { - if RuntimeBridge.isGossamerRuntime() { - RuntimeBridge.invoke("verisimdb_load_state", {"key": storageKey}) - ->Promise.then(jsonStr => { - // VeriSimDB returns the state wrapper — extract the inner state JSON - let stateJson = switch JSON.parseExn(jsonStr)->JSON.Classify.classify { - | Object(d) => - switch d->Dict.get("state") { - | Some(v) => - switch JSON.Classify.classify(v) { - | String(s) => s - | _ => jsonStr - } - | None => jsonStr - } - | _ => jsonStr - } - switch Decoders.decodeOption(persistedStateDecoder, stateJson) { - | Some(state) => Promise.resolve(Some(modelFromPersisted(state))) - | None => Promise.resolve(None) - } - }) - ->Promise.catch(_err => Promise.resolve(None)) - } else { - Promise.resolve(None) - } -} diff --git a/src/SubscriptionsFixed.affine b/src/SubscriptionsFixed.affine new file mode 100644 index 00000000..3f8277e5 --- /dev/null +++ b/src/SubscriptionsFixed.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SubscriptionsFixed; + +// TODO: Complete semantic implementation diff --git a/src/SubscriptionsFixed.res b/src/SubscriptionsFixed.res deleted file mode 100644 index 16b87bf4..00000000 --- a/src/SubscriptionsFixed.res +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Subscriptions — External event handling (Custom TEA version). -/// -/// This module defines all subscriptions (external events) that the PanLL -/// environment listens to, following the TEA pattern. -/// -/// Keyboard shortcuts now use KeybindingsEngine for lookup instead of -/// hardcoded switch arms. This makes shortcuts customisable via the -/// Workspace panel's Keybindings configurator tab. - -open Model -open Msg - -/// Map a keybinding action to its corresponding msg. This is the bridge -/// between the configurable keybinding map and the TEA message dispatch. -let actionToMsg = (action: KeybindingsModel.keybindingAction): msg => { - switch action { - | ActionUndo => Undo - | ActionRedo => Redo - | ActionSave => SaveState - | ActionPrint => NoOp // Print needs the active panel ID — handled specially - | ActionResetPanel => Workspace(ResetPanel("active")) - | ActionResetAll => Workspace(ResetAllPanels) - | ActionTogglePaneL => View(TogglePaneL) - | ActionTogglePaneN => View(TogglePaneN) - | ActionTogglePaneW => View(TogglePaneW) - | ActionToggleVab => PanelSwitcher(TogglePanel(PanelVab)) - | ActionTogglePanelBar => View(TogglePanelBar) - | ActionFullscreen => View(ToggleFullscreen) - | ActionCloseOverlay => PanelSwitcher(ClosePanels) - | ActionToggleCapture => PanelSwitcher(TogglePanel(PanelCapture)) - | ActionToggleWorkspace => PanelSwitcher(TogglePanel(PanelWorkspace)) - | ActionToggleSecurity => PanelSwitcher(TogglePanel(PanelSecurity)) - | ActionCycleWorkspaceMode => Workspace(CycleWorkspaceMode) - | ActionToggleDryRun => Workspace(ToggleDryRun) - } -} - -/// Main subscriptions function — keyboard shortcuts + polling. -let subscriptions = (model: model): Tea_Sub.t => { - Tea_Sub.batch(list{ - // Keyboard shortcuts via configurable keybinding map. - KeyboardFixed.onKeyDown(evt => { - // If keybinding editor is in recording mode, capture the key for rebinding. - if model.keybindings.recording { - let modifiers = { - let mods = [] - let mods = if evt.ctrlKey { - Array.concat(mods, [KeybindingsModel.Ctrl]) - } else { - mods - } - let mods = if evt.shiftKey { - Array.concat(mods, [KeybindingsModel.Shift]) - } else { - mods - } - let mods = if evt.altKey { - Array.concat(mods, [KeybindingsModel.Alt]) - } else { - mods - } - let mods = if evt.metaKey { - Array.concat(mods, [KeybindingsModel.Meta]) - } else { - mods - } - mods - } - Keybindings(RecordKey({modifiers, key: evt.key})) - } else { - // Look up the key event in the keybinding map. - let action = KeybindingsEngine.lookup( - model.keybindings.bindings, - evt.ctrlKey, - evt.shiftKey, - evt.altKey, - evt.metaKey, - evt.key, - ) - switch action { - | Some(a) => actionToMsg(a) - | None => NoOp - } - } - }), - // Update vexation index periodically (every 2 seconds). - if model.paneN.inferenceActive { - Tea_Time.every(2000.0, _time => { - Vexometer(RequestVexationIndex) - }) - } else { - Tea_Sub.none - }, - }) -} - -/// Subscription for when inference is active — more frequent vexation polling. -let inferenceSubscriptions = (model: model): Tea_Sub.t => { - if model.paneN.inferenceActive { - Tea_Sub.batch(list{Tea_Time.every(500.0, _time => Vexometer(RequestVexationIndex))}) - } else { - Tea_Sub.none - } -} - -/// S2: Gossamer backend event subscriptions — real-time neurosymbolic streaming. -/// These fire when the Rust backend receives events from ECHIDNA, Tentacles, -/// VeriSimDB, or Hypatia. Only active when the relevant panel is connected. -let neurosymbolicSubscriptions = (_model: model): Tea_Sub.t => { - Tea_Sub.batch(list{ - // ECHIDNA proof progress → feed into proof session state. - GossamerEvents.onEchidnaProgress(payload => Echidna(ProofResult(Ok(payload)))), - // ECHIDNA tactic suggestions → populate suggestion ribbon. - GossamerEvents.onEchidnaTactics(payload => Echidna(TacticSuggestionsLoaded(Ok(payload)))), - // Tentacles agent phase changes → advance OODA indicators. - // Payload expected as "agentId:phaseId" string from FFI. - GossamerEvents.onTentaclesPhaseChange(payload => { - // Parse "0:2" as agent Red, phase Decide — graceful fallback. - let parts = String.split(payload, ":") - let agentIdx = parts[0]->Option.getOr("0")->Int.fromString->Option.getOr(0) - let agentId = switch agentIdx { - | 0 => TentaclesModel.Red - | 1 => TentaclesModel.Orange - | 2 => TentaclesModel.Yellow - | 3 => TentaclesModel.Green - | 4 => TentaclesModel.Blue - | 5 => TentaclesModel.Indigo - | _ => TentaclesModel.Violet - } - let phaseIdx = parts[1]->Option.getOr("0")->Int.fromString->Option.getOr(0) - let phase = switch phaseIdx { - | 0 => PaneModel.Observe - | 1 => PaneModel.Orient - | 2 => PaneModel.Decide - | _ => PaneModel.Act - } - Tentacles(AgentPhaseAdvanced(agentId, phase)) - }), - // Tentacles agent broadcasts → deliver as reasoning share. - GossamerEvents.onTentaclesBroadcast(payload => Tentacles( - BroadcastFromAgent( - Red, - ReasoningShare({ - agent: Red, - phase: Observe, - summary: payload, - detail: None, - timestamp: 0.0, - }), - ), - )), - // VeriSimDB drift alerts → refresh drift display. - GossamerEvents.onVeriSimDBDrift(payload => VeriSimDB(DriftLoaded(Ok(payload)))), - // Hypatia neural network status → refresh network grid. - GossamerEvents.onHypatiaStatus(payload => Hypatia(ScansLoaded(Ok(payload)))), - // Governance signals → Anti-Crash intervention request. - GossamerEvents.onGovernanceSignal(payload => AntiCrash(RequestOperatorIntervention(payload))), - // AI streaming chunks → feed into AI panel streaming state machine. - GossamerEvents.onAiStreamChunk(payload => Ai(AiStreamChunkReceived(payload))), - }) -} - -/// S3: Token drip-feed — synthetic token emission for neural stream animation. -/// When inference is active, emits a lightweight "heartbeat" token every 3 seconds -/// so the neural stream shows visible activity even between real inference bursts. -/// The token content cycles through OODA status lines to give contextual feedback. -let tokenDripFeed = (model: model): Tea_Sub.t => { - if model.paneN.inferenceActive { - Tea_Sub.batch(list{ - Tea_Time.every(3000.0, time => { - let phaseLabel = switch model.paneN.agency.phase { - | Observe => "OBSERVE" - | Orient => "ORIENT" - | Decide => "DECIDE" - | Act => "ACT" - } - let tokenContent = "[" ++ phaseLabel ++ "] Heartbeat @ " ++ Float.toString(time) - let tokenId = "t-" ++ Int.toString(model.paneN.nextTokenId) - PaneN( - ReceiveToken({ - id: tokenId, - content: tokenContent, - timestamp: time, - confidence: 0.5, - validated: false, - source: NeuralInference, - category: Observation, - emittedDuring: model.paneN.agency.phase, - causedBy: model.paneN.activeCausalChain, - proofHash: None, - }), - ) - }), - }) - } else { - Tea_Sub.none - } -} - -/// S4: OODA phase cycling — automatic phase progression for the neural agent. -/// Cycles Observe → Orient → Decide → Act → Observe every 8 seconds when -/// inference is active. This gives the appearance of an active deliberation -/// loop and keeps the agency monitor visually responsive. -let oodaPhaseCycling = (model: model): Tea_Sub.t => { - if model.paneN.inferenceActive { - Tea_Sub.batch(list{ - Tea_Time.every(8000.0, _time => { - let nextPhase = switch model.paneN.agency.phase { - | Observe => Orient - | Orient => Decide - | Decide => Act - | Act => Observe - } - PaneN( - UpdateAgency({ - ...model.paneN.agency, - phase: nextPhase, - }), - ) - }), - }) - } else { - Tea_Sub.none - } -} - -/// S5: Filesystem watcher subscriptions — relay watcher events into TEA loop. -/// The Rust watcher emits on `watcher://event` when files change on disk. -/// This parses the JSON payload into a typed watchEvent and dispatches it. -let watcherSubscriptions = (_model: model): Tea_Sub.t => { - Tea_Sub.batch(list{ - // Filesystem events → Watcher panel + consuming panels (Farm, Hypatia, etc.) - GossamerEvents.onWatcherEvent(payload => { - switch WatcherCmd.parseEvent(payload) { - | Some(evt) => Watcher(FileEvent(evt)) - | None => NoOp - } - }), - // Watcher errors → Observatory activity log - GossamerEvents.onWatcherError(payload => Watcher(WatcherResult(Error(payload)))), - }) -} - -/// Combined subscriptions — all subscription layers merged. -let all = (model: model): Tea_Sub.t => { - Tea_Sub.batch(list{ - subscriptions(model), - inferenceSubscriptions(model), - neurosymbolicSubscriptions(model), - tokenDripFeed(model), - oodaPhaseCycling(model), - watcherSubscriptions(model), - }) -} diff --git a/src/Update.affine b/src/Update.affine new file mode 100644 index 00000000..0ebdce06 --- /dev/null +++ b/src/Update.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Update; + +// TODO: Complete semantic implementation diff --git a/src/Update.res b/src/Update.res deleted file mode 100644 index 0ce18386..00000000 --- a/src/Update.res +++ /dev/null @@ -1,491 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Update Engine — The state transition kernel. -/// -/// This module implements the "Update" function of The Elm Architecture (TEA). -/// It is responsible for taking the current `model` and an incoming `msg`, -/// and producing a new version of the model along with any required -/// side-effect commands (`Tea_Cmd`). -/// -/// Architecture: -/// 1. Domain sub-updaters live in `src/update/Update*.res` modules -/// 2. The main `update()` orchestrator routes messages to sub-updaters -/// 3. Contractiles post-processing evaluates cognitive governance contracts -/// after every state-modifying update -/// -/// DESIGN: Sub-updaters are pure — commands represent deferred side effects. -/// The only imperative call is `Storage.save()` in the SaveState handler. - -open Model -open Msg - -// =========================================================================== -// Import all domain sub-updaters from src/update/ -// =========================================================================== - -// Shared helpers (undo/redo, logging) -open UpdateHelpers - -// Core panes -open UpdatePaneL -open UpdatePaneN -open UpdatePaneW -open UpdatePaneA - -// Major subsystems -open UpdateVeriSimDB -open UpdateEchidna -open UpdateGovernance -open UpdateVab -open UpdateCloudGuard -open UpdateFarm -open UpdatePlaza -open UpdatePanelSwitcher -open UpdateReposystem -open UpdateAerie -open UpdateInterfaces -open UpdatePlaygrounds -open UpdateHypatia -open UpdateFleet -open UpdateMinter -open UpdateProvisioner -open UpdateVoiceTag -open UpdateProvenance -open UpdateWatcher -open UpdateAi -open UpdateRepoLoader -open UpdateWorkspace -open UpdateCapture -open UpdateSecurity -open UpdateKeybindings -open UpdateMigration -open UpdatePanicAttack -open UpdateMassPanic -open UpdateTsdm -open UpdateValenceShell -open UpdateGamePreview -open UpdateVmInspector -open UpdateNetworkTopology -open UpdateLevelArchitect -open UpdateCoprocessors -open UpdateMultiplayerMonitor -open UpdateDlcWorkshop -open UpdateUms -open UpdateEditorBridge -open UpdateBuildDashboard -open UpdateReleaseManager -open UpdateAutomationRouter -open UpdateScriptGist -open UpdateDatabases -open UpdateBoj -open UpdateCladeBrowser -open UpdateTentacles -open UpdateProtocolSquisher -open UpdateMyLang -open UpdateTypeLL -open UpdateHelp -open UpdateMenuBar -open UpdateAccessibility -open UpdateTiling -open UpdateFocusDimming -open UpdateEnsaidConfig -open UpdateTimeline -open UpdatePatternDiag -open UpdateAttributionLicense -open UpdateStapeln -open UpdateEvangeliser -open UpdateLanguageForge -open UpdateTangleViz -open UpdateWizard - -// Extracted sub-updaters (formerly inline in this file) -open UpdateSpecBrowser -open UpdateVerificationDashboard -open UpdateBus -open UpdateObservatory -open UpdateAmbientOps -open UpdateObservability -open UpdateA2ml -open UpdateK9 -open UpdateSystemUpdate - -// Grouped small updaters -open UpdateGameDevTesting -open UpdateGameDevBridges -open UpdateGameDevSpecific -open UpdateTeamCollab -open UpdateWiringInspector -open UpdateFloorRaise - -// =========================================================================== -// Main Dispatcher -// =========================================================================== - -/// Determines whether a message should trigger auto-save to localStorage. -/// NoOp and SaveState itself are excluded to avoid infinite loops. -let shouldAutoSave = (msg: msg): bool => { - switch msg { - | NoOp => false - | SaveState => false - | VeriSimDBStateLoaded(_) => false - | VeriSimDBStateSaved(_) => false - | Service(_) => false // Service health checks should not trigger state save - | Settings(_) => false // Settings changes persist via their own mechanism - | Identity(_) => false // Identity operations have their own persistence - | _ => true - } -} - -/// The main TEA update function. Routes each message variant to its domain -/// sub-updater, then applies contractile post-processing. -let update = (model: model, msg: msg): (model, Tea_Cmd.t) => { - let (newModel, cmd) = switch msg { - // Core panes - | PaneL(subMsg) => updatePaneL(model, subMsg) - | PaneN(subMsg) => (updatePaneN(model, subMsg), Tea_Cmd.none) - | PaneW(subMsg) => updatePaneW(model, subMsg) - | PaneA(subMsg) => UpdatePaneA.update(model, subMsg) - // Major subsystems - | VeriSimDB(subMsg) => updateVeriSimDB(model, subMsg) - | Echidna(subMsg) => updateEchidna(model, subMsg) - | Vexometer(subMsg) => updateVexometer(model, subMsg) - | Orbital(subMsg) => updateOrbital(model, subMsg) - | View(subMsg) => updateView(model, subMsg) - | Feedback(subMsg) => updateFeedback(model, subMsg) - | AntiCrash(subMsg) => updateAntiCrash(model, subMsg) - | Vab(subMsg) => updateVab(model, subMsg) - | CloudGuard(subMsg) => updateCloudGuard(model, subMsg) - | Farm(subMsg) => updateFarm(model, subMsg) - | Plaza(subMsg) => updatePlaza(model, subMsg) - | Hypatia(subMsg) => updateHypatia(model, subMsg) - | Fleet(subMsg) => updateFleet(model, subMsg) - | Reposystem(subMsg) => updateReposystem(model, subMsg) - | Aerie(subMsg) => updateAerie(model, subMsg) - | Oo7Toolchain(subMsg) => updateOo7Toolchain(model, subMsg) - | VideoCoordination(subMsg) => UpdateVideoCoordination.updateVideoCoordination(model, subMsg) - | Interfaces(subMsg) => updateInterfaces(model, subMsg) - | Playgrounds(subMsg) => updatePlaygrounds(model, subMsg) - | Minter(subMsg) => updateMinter(model, subMsg) - | Provisioner(subMsg) => updateProvisioner(model, subMsg) - | VoiceTag(subMsg) => updateVoiceTag(model, subMsg) - | Provenance(subMsg) => updateProvenance(model, subMsg) - | Watcher(subMsg) => updateWatcher(model, subMsg) - | Ai(subMsg) => updateAi(model, subMsg) - | RepoLoader(subMsg) => updateRepoLoader(model, subMsg) - | PanelSwitcher(subMsg) => updatePanelSwitcher(model, subMsg) - | Workspace(subMsg) => updateWorkspace(model, subMsg) - | Capture(subMsg) => updateCapture(model, subMsg) - | Security(subMsg) => updateSecurity(model, subMsg) - | Keybindings(subMsg) => (updateKeybindings(model, subMsg), Tea_Cmd.none) - | Migration(subMsg) => updateMigration(model, subMsg) - | PanicAttack(subMsg) => updatePanicAttack(model, subMsg) - | MassPanic(subMsg) => updateMassPanic(model, subMsg) - | Tsdm(subMsg) => updateTsdm(model, subMsg) - | ValenceShell(subMsg) => updateValenceShell(model, subMsg) - | GamePreview(subMsg) => updateGamePreview(model, subMsg) - | VmInspector(subMsg) => updateVmInspector(model, subMsg) - | NetworkTopology(subMsg) => updateNetworkTopology(model, subMsg) - | LevelArchitect(subMsg) => updateLevelArchitect(model, subMsg) - | Coprocessors(subMsg) => updateCoprocessors(model, subMsg) - | MultiplayerMonitor(subMsg) => updateMultiplayerMonitor(model, subMsg) - | DlcWorkshop(subMsg) => updateDlcWorkshop(model, subMsg) - | Ums(subMsg) => updateUms(model, subMsg) - | EditorBridge(subMsg) => updateEditorBridge(model, subMsg) - | BuildDashboard(subMsg) => updateBuildDashboard(model, subMsg) - | ReleaseManager(subMsg) => updateReleaseManager(model, subMsg) - | AutomationRouter(subMsg) => updateAutomationRouter(model, subMsg) - | ScriptGist(subMsg) => updateScriptGist(model, subMsg) - | Databases(subMsg) => updateDatabases(model, subMsg) - | Boj(subMsg) => updateBoj(model, subMsg) - | CladeBrowser(subMsg) => updateCladeBrowser(model, subMsg) - | Tentacles(subMsg) => updateTentacles(model, subMsg) - | ProtocolSquisher(subMsg) => updateProtocolSquisher(model, subMsg) - | MyLang(subMsg) => updateMyLang(model, subMsg) - | TypeLL(subMsg) => updateTypeLL(model, subMsg) - | Help(subMsg) => updateHelp(model, subMsg) - | MenuBar(subMsg) => updateMenuBar(model, subMsg) - | AccessibilityCtrl(subMsg) => updateAccessibility(model, subMsg) - | Tiling(subMsg) => updateTiling(model, subMsg) - | FocusDimming(subMsg) => updateFocusDimming(model, subMsg) - | Stapeln(subMsg) => updateStapeln(model, subMsg) - | Evangeliser(subMsg) => updateEvangeliser(model, subMsg) - | LanguageForge(subMsg) => updateLanguageForge(model, subMsg) - | TangleViz(subMsg) => updateTangleViz(model, subMsg) - | EnsaidConfig(subMsg) => updateEnsaidConfig(model, subMsg) - | Timeline(subMsg) => updateTimeline(model, subMsg) - | PatternDiag(subMsg) => updatePatternDiag(model, subMsg) - | AttrLicense(subMsg) => updateAttributionLicense(model, subMsg) - // Game Dev panels — testing - | UnitTestRunner(subMsg) => updateUnitTestRunner(model, subMsg) - | FunctionalTester(subMsg) => updateFunctionalTester(model, subMsg) - | RegressionGuard(subMsg) => updateRegressionGuard(model, subMsg) - | PerformanceProfiler(subMsg) => updatePerformanceProfiler(model, subMsg) - | LoadTester(subMsg) => updateLoadTester(model, subMsg) - | SoakMonitor(subMsg) => updateSoakMonitor(model, subMsg) - | CompatibilityMatrix(subMsg) => updateCompatibilityMatrix(model, subMsg) - | ExploratoryWorkbench(subMsg) => updateExploratoryWorkbench(model, subMsg) - | BetaFeedbackHub(subMsg) => updateBetaFeedbackHub(model, subMsg) - | BalanceAnalyser(subMsg) => updateBalanceAnalyser(model, subMsg) - // Game Dev panels — bridges - | TypingBridge(subMsg) => updateTypingBridge(model, subMsg) - | NeurosymBridge(subMsg) => updateNeurosymBridge(model, subMsg) - | AgenticBridge(subMsg) => updateAgenticBridge(model, subMsg) - | AutomationBridge(subMsg) => updateAutomationBridge(model, subMsg) - | DatabaseBridge(subMsg) => updateDatabaseBridge(model, subMsg) - | ProtocolBridge(subMsg) => updateProtocolBridge(model, subMsg) - | ProofsBridge(subMsg) => updateProofsBridge(model, subMsg) - | ScriptingBridge(subMsg) => updateScriptingBridge(model, subMsg) - // Game Dev panels — game-specific - | GeneratorMode(subMsg) => updateGeneratorMode(model, subMsg) - | ArchitectMode(subMsg) => updateArchitectMode(model, subMsg) - | GuardAiTuner(subMsg) => updateGuardAiTuner(model, subMsg) - | DeviceNetworkDesigner(subMsg) => updateDeviceNetworkDesigner(model, subMsg) - | AssetManager(subMsg) => updateAssetManager(model, subMsg) - | PlaytestRecorder(subMsg) => updatePlaytestRecorder(model, subMsg) - // Team / collaboration panels - | CodeReview(subMsg) => updateCodeReview(model, subMsg) - | MergeCoordinator(subMsg) => updateMergeCoordinator(model, subMsg) - | TeamDashboard(subMsg) => updateTeamDashboard(model, subMsg) - | DebuggingWorkbench(subMsg) => updateDebuggingWorkbench(model, subMsg) - // Infrastructure panels - | WiringInspector(subMsg) => updateWiringInspector(model, subMsg) - // Floor Raise campaign panels - | FloorRaise(subMsg) => updateFloorRaise(model, subMsg) - | ProvenAdoption(subMsg) => updateProvenAdoption(model, subMsg) - | ContractileCompleteness(subMsg) => updateContractileCompleteness(model, subMsg) - | ManifestCoverage(subMsg) => updateManifestCoverage(model, subMsg) - | VerisimdbFeeds(subMsg) => updateVerisimdbFeeds(model, subMsg) - | FeedbackRouting(subMsg) => updateFeedbackRouting(model, subMsg) - | VexometerFriction(subMsg) => updateVexometerFriction(model, subMsg) - | Wizard(subMsg) => UpdateWizard.update(model, subMsg) - // SpecBrowser — language specification browsing - | SpecBrowser(subMsg) => updateSpecBrowser(model, subMsg) - // VerificationDashboard — proof/test/benchmark status - | VerificationDashboard(subMsg) => updateVerificationDashboard(model, subMsg) - // Panel Bus — cross-panel messaging - | Bus(busMsg) => updateBus(model, busMsg) - // Undo/Redo - | Undo => { - let len = Array.length(model.undoStack) - if len === 0 { - (model, Tea_Cmd.none) - } else { - let snapshot = model.undoStack[len - 1] - let remainingUndo = Array.slice(model.undoStack, ~start=0, ~end=len - 1) - let currentSnapshot = snapshotToJson(model) - let newRedo = Array.concat(model.redoStack, [currentSnapshot]) - let trimmedRedo = if Array.length(newRedo) > undoStackLimit { - Array.slice( - newRedo, - ~start=Array.length(newRedo) - undoStackLimit, - ~end=Array.length(newRedo), - ) - } else { - newRedo - } - switch snapshot { - | Some(s) => { - let restored = restoreSnapshot(model, s) - ({...restored, undoStack: remainingUndo, redoStack: trimmedRedo}, Tea_Cmd.none) - } - | None => (model, Tea_Cmd.none) - } - } - } - | Redo => { - let len = Array.length(model.redoStack) - if len === 0 { - (model, Tea_Cmd.none) - } else { - let snapshot = model.redoStack[len - 1] - let remainingRedo = Array.slice(model.redoStack, ~start=0, ~end=len - 1) - let currentSnapshot = snapshotToJson(model) - let newUndo = Array.concat(model.undoStack, [currentSnapshot]) - let trimmedUndo = if Array.length(newUndo) > undoStackLimit { - Array.slice( - newUndo, - ~start=Array.length(newUndo) - undoStackLimit, - ~end=Array.length(newUndo), - ) - } else { - newUndo - } - switch snapshot { - | Some(s) => { - let restored = restoreSnapshot(model, s) - ({...restored, undoStack: trimmedUndo, redoStack: remainingRedo}, Tea_Cmd.none) - } - | None => (model, Tea_Cmd.none) - } - } - } - // State persistence - | SaveState => { - Storage.save(model) - (model, Tea_Cmd.none) - } - // VeriSimDB async state restoration (Connected Workbench v0.2.0) - | VeriSimDBStateLoaded(result) => - switch result { - | Ok(jsonStr) => { - // Parse the VeriSimDB response — extract the inner "state" field - let stateJson = switch JSON.parseExn(jsonStr)->JSON.Classify.classify { - | Object(d) => - switch d->Dict.get("state") { - | Some(v) => - switch JSON.Classify.classify(v) { - | String(s) => s - | _ => jsonStr - } - | None => jsonStr - } - | _ => jsonStr - } - switch Decoders.decodeOption(Storage.persistedStateDecoder, stateJson) { - | Some(state) => { - let restoredModel = Storage.modelFromPersisted(state) - // Merge restored state with current model to preserve runtime-only fields - ( - { - ...model, - paneL: restoredModel.paneL, - paneN: restoredModel.paneN, - paneW: restoredModel.paneW, - viewMode: restoredModel.viewMode, - paneLVisible: restoredModel.paneLVisible, - paneNVisible: restoredModel.paneNVisible, - paneWVisible: restoredModel.paneWVisible, - humidity: restoredModel.humidity, - vexometer: {...model.vexometer, index: restoredModel.vexometer.index}, - orbital: {...model.orbital, stability: restoredModel.orbital.stability}, - }, - Tea_Cmd.none, - ) - } - | None => (model, Tea_Cmd.none) - } - } - | Error(_) => (model, Tea_Cmd.none) // VeriSimDB unavailable — localStorage already loaded - } - // VeriSimDB save confirmation (log-only, no state change) - | VeriSimDBStateSaved(_result) => (model, Tea_Cmd.none) - // BoJ latency recording - | RecordBojLatency(cartridge, tool, elapsed) => { - let entry: BojModel.bojLatencyEntry = { - cartridge, - tool, - durationMs: elapsed, - timestamp: Date.now(), - } - let log = Array.concat([entry], model.boj.latencyLog)->Array.slice(~start=0, ~end=100) - ({...model, boj: {...model.boj, latencyLog: log}}, Tea_Cmd.none) - } - // Governance NeSy results - | GovernanceNesyResult(result) => switch result { - | Ok(jsonStr) => { - let newModel = switch Decoders.decodeOption(Tea_Json.value, jsonStr) { - | Some(json) => - let o = json->JSON.Decode.object->Option.getOr(Dict.make()) - let confidence = - o->Dict.get("confidence")->Option.flatMap(JSON.Decode.float)->Option.getOr(0.5) - let approved = - o->Dict.get("approved")->Option.flatMap(JSON.Decode.bool)->Option.getOr(true) - if !approved { - {...model, antiCrash: {...model.antiCrash, strictMode: false}} - } else if confidence > 0.8 { - {...model, antiCrash: {...model.antiCrash, strictMode: true}} - } else if confidence < 0.3 { - {...model, antiCrash: {...model.antiCrash, strictMode: false}} - } else { - model - } - - | None => model - } - (newModel, Tea_Cmd.none) - } - | Error(_) => (model, Tea_Cmd.none) - } - | GovernanceNesyValidateResult(result) => switch result { - | Ok(jsonStr) => { - let newModel = switch Decoders.decodeOption(Tea_Json.value, jsonStr) { - | Some(json) => - let o = json->JSON.Decode.object->Option.getOr(Dict.make()) - let approved = - o->Dict.get("approved")->Option.flatMap(JSON.Decode.bool)->Option.getOr(true) - let reasoning = - o->Dict.get("reasoning")->Option.flatMap(JSON.Decode.string)->Option.getOr("") - if !approved { - ignore(reasoning) - {...model, antiCrash: {...model.antiCrash, strictMode: false}} - } else { - model - } - - | None => model - } - (newModel, Tea_Cmd.none) - } - | Error(_) => (model, Tea_Cmd.none) - } - | GovernanceNesyProbeResult(result) => switch result { - | Ok(jsonStr) => { - let newModel = switch Decoders.decodeOption(Tea_Json.value, jsonStr) { - | Some(json) => - let o = json->JSON.Decode.object->Option.getOr(Dict.make()) - let neuralCoherence = - o->Dict.get("neural_coherence")->Option.flatMap(JSON.Decode.float)->Option.getOr(0.5) - let driftMagnitude = - o->Dict.get("drift_magnitude")->Option.flatMap(JSON.Decode.float)->Option.getOr(0.0) - { - ...model, - orbital: { - ...model.orbital, - stability: neuralCoherence, - divergenceLevel: driftMagnitude, - }, - } - - | None => model - } - (newModel, Tea_Cmd.none) - } - | Error(_) => (model, Tea_Cmd.none) - } - // Observability - | Observability(obsMsg) => updateObservability(model, obsMsg) - // A2ML manifest management - | A2ml(a2mlMsg) => updateA2ml(model, a2mlMsg) - // K9 contractile management - | K9(k9Msg) => updateK9(model, k9Msg) - // Seam auditing - | AuditSeams => { - let register = SeamEngine.buildRegister("2026-03-09") - let audit = SeamEngine.auditRegister(register, "2026-03-09") - ({...model, seamRegister: register, lastSeamAudit: Some(audit)}, Tea_Cmd.none) - } - | SeamAuditResult(audit) => ({...model, lastSeamAudit: Some(audit)}, Tea_Cmd.none) - // Observatory — integrative dashboard - | Observatory(subMsg) => updateObservatory(model, subMsg) - // AmbientOps — hospital-model sysadmin - | AmbientOps(subMsg) => updateAmbientOps(model, subMsg) - // Burble — groove-aware voice huddle integration - | Burble(subMsg) => ({...model, burble: BurbleEngine.update(model.burble, subMsg)}, Tea_Cmd.none) - // Service Registry — centralized backend service lifecycle (Connected Workbench v0.2.0) - | Service(subMsg) => UpdateService.updateService(model, subMsg) - // Settings — user configuration (Connected Workbench v0.2.0) - | Settings(subMsg) => UpdateSettings.updateSettings(model, subMsg) - // Identity — snapshots and team replication (Connected Workbench v0.2.0) - | Identity(subMsg) => UpdateIdentity.updateIdentity(model, subMsg) - // System Update — component update management - | SystemUpdate(subMsg) => updateSystemUpdate(model, subMsg) - | NoOp => (model, Tea_Cmd.none) - } - - // Post-processing: evaluate contractiles after every state-modifying update. - // Skip for NoOp to avoid unnecessary computation. - switch msg { - | NoOp => (newModel, cmd) - | _ => applyContractiles(newModel, cmd) - } -} diff --git a/src/View.affine b/src/View.affine new file mode 100644 index 00000000..b69df0bc --- /dev/null +++ b/src/View.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module View; + +// TODO: Complete semantic implementation diff --git a/src/View.res b/src/View.res deleted file mode 100644 index 81e40210..00000000 --- a/src/View.res +++ /dev/null @@ -1,937 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL View - The render layer for the eNSAID environment. -/// -/// This module implements the TEA view function, rendering the -/// three-pane parallel architecture with ambient substrate. - -open Model -open Msg -open Tea.Html - -/// Render the Orbital Drift Aura background -let renderDriftAura = (orbital: orbitalState, humidity: humidityLevel): Tea_Vdom.t => { - let opacityClass = switch humidity { - | High => "opacity-30" - | Medium => "opacity-20" - | Low => "opacity-10" - } - let colourClass = orbital.driftAuraColour === "indigo" ? "bg-indigo-900" : "bg-amber-900" - - div( - list{ - Attrs.class_( - `fixed inset-0 ${colourClass} ${opacityClass} transition-all duration-1000 pointer-events-none`, - ), - }, - list{}, - ) -} - -/// Render Pane-L (Symbolic Mass) - using full component. -/// Receives proof obligations from VeriSimDB VCL-total queries to display -/// as symbolic constraints alongside the constraint editor. -let renderPaneL = (paneL: paneLState, proofs: array, visible: bool): Tea_Vdom.t< - msg, -> => { - if !visible { - noNode - } else { - div(list{Attrs.class_("flex-1 overflow-auto")}, list{PaneL.view(paneL, proofs)}) - } -} - -/// Render Pane-N (Neural Stream) with ECHIDNA theorem prover panel - using full component -let renderPaneN = ( - paneN: paneNState, - echidna: echidnaState, - ~inferenceStream: array=[], - visible: bool, -): Tea_Vdom.t => { - if !visible { - noNode - } else { - div( - list{Attrs.class_("flex-1 overflow-auto")}, - list{PaneN.view(paneN, echidna, ~inferenceStream)}, - ) - } -} - -/// Render Pane-W (World/Barycentre) - using full component -/// This pane draws the central security panel, event chain importer, and -/// panic-attacker toolset, ensuring the time/space study is visible when dialogs open. -/// Render Pane-W (World/Barycentre) with VeriSimDB database tools. -let renderPaneW = ( - paneW: paneWState, - orbital: orbitalState, - db: verisimdbState, - contractiles: array, - tour: tourState, - visible: bool, -): Tea_Vdom.t => { - if !visible { - noNode - } else { - div( - list{Attrs.class_("flex-1 overflow-auto")}, - list{PaneW.view(paneW, orbital, db, ~contractiles, ~tour)}, - ) - } -} - -/// Render Pane-A (Ambient Substrate) - the ergonomic control layer. -/// This pane is always visible when active, providing high-level sensory -/// feedback and cognitive relief adjustments for the co-orbit. -let renderPaneA = (paneA: paneAState, visible: bool): Tea_Vdom.t => { - if !visible { - noNode - } else { - div(list{Attrs.class_("w-72 border-r border-gray-900")}, list{PaneA.view(paneA)}) - } -} - -/// Render the Dark Start architecture manifold -/// Render a quick-access panel card for the DarkStart grid. -let renderQuickPanel = (panel: panelMeta): Tea_Vdom.t => { - let statusDot = switch panel.connectionStatus { - | ServiceConnected => "bg-green-400" - | ServiceDisconnected => "bg-gray-600" - | ServiceChecking => "bg-amber-400 animate-pulse" - | ServiceError(_) => "bg-red-400" - } - button( - list{ - Attrs.class_( - "bg-gray-900/60 border border-gray-800 rounded-lg p-3 hover:bg-gray-800/80 hover:border-gray-700 transition-all text-left group", - ), - Events.onClick(PanelSwitcher(TogglePanel(panel.id))), - Attrs.title(panel.description), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - div(list{Attrs.class_(`w-1.5 h-1.5 rounded-full ${statusDot}`)}, list{}), - span( - list{Attrs.class_("text-sm text-gray-300 group-hover:text-white transition-colors")}, - list{text(panel.name)}, - ), - }, - ), - div(list{Attrs.class_("text-[10px] text-gray-600 truncate")}, list{text(panel.description)}), - }, - ) -} - -/// Render the DarkStart front page — system overview, quick access, health. -let renderDarkStart = (model: model): Tea_Vdom.t => { - // Gather health metrics - let totalPanels = Array.length(model.panelSwitcher.panels) - let connectedPanels = - model.panelSwitcher.panels - ->Array.filter(p => p.connectionStatus === ServiceConnected) - ->Array.length - let hypatiaConfidence = HypatiaEngine.avgConfidence(model.hypatia.networks) - let sessionCount = Array.length(model.workspace.sessions) - let bojCartridges = Array.length(model.boj.cartridges) - let bojLoaded = model.boj.cartridges->Array.filter(c => c.loaded)->Array.length - - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950 overflow-auto"), - Attrs.role("main"), - Attrs.ariaLabel("PanLL DarkStart — system overview"), - }, - list{ - // Top section: branding + enter button - div( - list{Attrs.class_("max-w-5xl mx-auto px-8 pt-12 pb-6")}, - list{ - div( - list{Attrs.class_("flex items-end justify-between mb-8")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-3xl font-light text-gray-400")}, list{text("PanLL")}), - div( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{text("eNSAID Neurosymbolic Development Environment")}, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-6 py-2.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 transition-colors text-sm font-medium", - ), - Events.onClick(View(SetViewMode(Standard))), - KeyboardUtil.onEnterOrSpace(View(SetViewMode(Standard))), - }, - list{text("Enter Environment")}, - ), - }, - ), - // Binary Star diagram (compact) - div( - list{Attrs.class_("flex items-center justify-center gap-8 mb-10")}, - list{ - div( - list{ - Attrs.class_( - "w-16 h-16 rounded-full bg-indigo-600/30 border border-indigo-500/50 flex items-center justify-center", - ), - }, - list{ - div( - list{Attrs.class_("text-indigo-400 text-[10px] font-medium")}, - list{text("L")}, - ), - }, - ), - div(list{Attrs.class_("w-8 border-t border-dashed border-gray-700")}, list{}), - div( - list{ - Attrs.class_( - "w-16 h-16 rounded-full bg-emerald-600/30 border border-emerald-500/50 flex items-center justify-center", - ), - }, - list{ - div( - list{Attrs.class_("text-emerald-400 text-[10px] font-medium")}, - list{text("N")}, - ), - }, - ), - div(list{Attrs.class_("w-8 border-t border-dashed border-gray-700")}, list{}), - div( - list{ - Attrs.class_( - "w-16 h-16 rounded-full bg-amber-600/30 border border-amber-500/50 flex items-center justify-center", - ), - }, - list{ - div( - list{Attrs.class_("text-amber-400 text-[10px] font-medium")}, - list{text("W")}, - ), - }, - ), - }, - ), - // System health cards - div( - list{Attrs.class_("grid grid-cols-4 gap-3 mb-8")}, - list{ - // Panels - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Panels")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-gray-200")}, - list{text(Int.toString(totalPanels))}, - ), - div( - list{Attrs.class_("text-xs text-green-500 mt-1")}, - list{text(`${Int.toString(connectedPanels)} connected`)}, - ), - }, - ), - // Hypatia - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Hypatia")}, - ), - div( - list{ - Attrs.class_( - `text-2xl font-light ${if hypatiaConfidence > 0.8 { - "text-green-400" - } else if hypatiaConfidence > 0.5 { - "text-amber-400" - } else { - "text-gray-500" - }}`, - ), - }, - list{ - text( - if hypatiaConfidence > 0.0 { - `${Float.toFixed(hypatiaConfidence *. 100.0, ~digits=0)}%` - } else { - "---" - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{text("ensemble confidence")}, - ), - }, - ), - // BoJ - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text("BoJ Server")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-gray-200")}, - list{text(Int.toString(bojCartridges))}, - ), - div( - list{Attrs.class_("text-xs text-green-500 mt-1")}, - list{text(`${Int.toString(bojLoaded)} loaded`)}, - ), - }, - ), - // Sessions - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Sessions")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-gray-200")}, - list{text(Int.toString(sessionCount))}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{text("workspace sessions")}, - ), - }, - ), - }, - ), - // Quick-access panel grid (top 12 panels) - div( - list{Attrs.class_("mb-8")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-3")}, - list{text("Quick Access")}, - ), - div( - list{Attrs.class_("grid grid-cols-4 gap-2")}, - model.panelSwitcher.panels - ->Array.slice(~start=0, ~end=12) - ->Array.map(p => renderQuickPanel(p)) - ->List.fromArray, - ), - }, - ), - // Recent sessions - if Array.length(model.workspace.sessions) > 0 { - div( - list{Attrs.class_("mb-8")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-3")}, - list{text("Recent Sessions")}, - ), - div( - list{Attrs.class_("space-y-1")}, - model.workspace.sessions - ->Array.slice(~start=0, ~end=5) - ->Array.map(session => - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-900/40 rounded hover:bg-gray-800/60 transition-colors", - ), - }, - list{ - div(list{Attrs.class_("w-1.5 h-1.5 rounded-full bg-gray-600")}, list{}), - span( - list{Attrs.class_("text-sm text-gray-400 flex-1")}, - list{text(session.name)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Date.make()->Date.toLocaleDateString)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Footer - div( - list{Attrs.class_("text-center py-6 border-t border-gray-900")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-700")}, - list{text("Press Enter or click \"Enter Environment\" to begin")}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render cross-panel circuit lines — SVG overlay showing data flow connections -/// between Panel-L (Symbolic), Panel-N (Neural), and Panel-W (World). -/// Lines animate based on orbital sync health: green = healthy, amber = drifting, -/// red = diverged. The circuit only renders when all three panels are visible. -let renderCircuitLines = ( - orbital: orbitalState, - paneLVisible: bool, - paneNVisible: bool, - paneWVisible: bool, -): Tea_Vdom.t => { - if !(paneLVisible && paneNVisible && paneWVisible) { - noNode - } else { - let healthClass = if orbital.syncHealth > 0.7 { - "stroke-emerald-500/40" - } else if orbital.syncHealth > 0.4 { - "stroke-amber-500/40" - } else { - "stroke-red-500/40" - } - let pulseClass = if orbital.syncHealth > 0.7 { - "" - } else { - "animate-pulse" - } - - // SVG overlay positioned absolutely over the three-panel layout - Tea_Svg.svg( - list{ - Tea_Svg.Attrs.class_( - `absolute inset-0 w-full h-full pointer-events-none z-20 ${pulseClass}`, - ), - Tea_Svg.Attrs.viewBox("0 0 1200 800"), - Attrs.prop("preserveAspectRatio", "none"), - }, - list{ - // L→N connection (symbolic feeds neural) - Tea_Svg.line( - list{ - Tea_Svg.Attrs.class_(healthClass), - Tea_Svg.Attrs.x1("400"), - Tea_Svg.Attrs.y1("400"), - Tea_Svg.Attrs.x2("800"), - Tea_Svg.Attrs.y2("400"), - Tea_Svg.Attrs.strokeWidth("2"), - Tea_Svg.Attrs.strokeDasharray("8 4"), - }, - list{}, - ), - // N→W connection (neural feeds world) - Tea_Svg.line( - list{ - Tea_Svg.Attrs.class_(healthClass), - Tea_Svg.Attrs.x1("800"), - Tea_Svg.Attrs.y1("400"), - Tea_Svg.Attrs.x2("1100"), - Tea_Svg.Attrs.y2("200"), - Tea_Svg.Attrs.strokeWidth("2"), - Tea_Svg.Attrs.strokeDasharray("8 4"), - }, - list{}, - ), - // W→L feedback loop (world constrains symbolic) - Tea_Svg.line( - list{ - Tea_Svg.Attrs.class_(healthClass), - Tea_Svg.Attrs.x1("1100"), - Tea_Svg.Attrs.y1("600"), - Tea_Svg.Attrs.x2("100"), - Tea_Svg.Attrs.y2("600"), - Tea_Svg.Attrs.strokeWidth("1"), - Tea_Svg.Attrs.strokeDasharray("4 8"), - }, - list{}, - ), - // Barycentre indicator dot - Tea_Svg.circle( - list{ - Tea_Svg.Attrs.class_("fill-indigo-500/60"), - Tea_Svg.Attrs.cx(Float.toString(400.0 +. orbital.barycentrePosition *. 400.0)), - Tea_Svg.Attrs.cy("400"), - Tea_Svg.Attrs.r("6"), - }, - list{}, - ), - }, - ) - } -} - -/// Render the active panel overlay based on the panel switcher state. -/// Each panel module renders as a full-screen overlay on top of the core -/// three-panel layout. Panels not yet implemented show a placeholder. -let renderActivePanel = (model: model): Tea_Vdom.t => { - switch model.panelSwitcher.activePanel { - | None => noNode - | Some(PanelVab) => Vab.view(model.vab) - | Some(PanelCloudGuard) => CloudGuard.view(model.cloudguard) - | Some(PanelFarm) => Farm.view(model.farm) - | Some(PanelPlaza) => Plaza.view(model.plaza) - | Some(PanelHypatia) => Hypatia.view(model.hypatia) - | Some(PanelFleet) => Fleet.view(model.fleet) - | Some(PanelReposystem) => Reposystem.view(model.reposystem) - | Some(PanelDatabases) => Databases.view(model.databases) - | Some(PanelAerie) => Aerie.view(model.aerie) - | Some(PanelOo7Toolchain) => Oo7Toolchain.view(model.oo7toolchain) - | Some(PanelVideoCoordination) => VideoCoordination.view(model.videoCoordination) - | Some(PanelInterfaces) => Interfaces.view(model.interfaces) - | Some(PanelPlaygrounds) => Playgrounds.view(model.playgrounds) - | Some(PanelMinter) => Minter.view(model.minter) - | Some(PanelProvisioner) => Provisioner.view(model.provisioner) - | Some(PanelWizard) => Wizard.view(model) - | Some(PanelVoiceTag) => VoiceTag.view(model.voiceTag) - | Some(PanelAi) => Ai.view(model.ai) - | Some(PanelRepoLoader) => RepoLoader.view(model.repoLoader) - | Some(PanelWorkspace) => Workspace.view(model.workspace, model.keybindings) - | Some(PanelCapture) => Capture.view(model.capture) - | Some(PanelSecurity) => Security.view(model.security) - | Some(PanelMigration) => Migration.view(model.migration) - | Some(PanelPanicAttack) => PanicAttack.view(model.panicAttack) - | Some(PanelMassPanic) => MassPanic.view(model.massPanic) - | Some(PanelTsdm) => Tsdm.view(model.tsdm) - | Some(PanelValenceShell) => ValenceShell.view(model.valenceShell) - | Some(PanelGamePreview) => GamePreview.view(model.gamePreview) - | Some(PanelVmInspector) => VmInspector.view(model.vmInspector) - | Some(PanelNetworkTopology) => NetworkTopology.view(model.networkTopology) - | Some(PanelLevelArchitect) => LevelArchitect.view(model.levelArchitect) - | Some(PanelCoprocessors) => Coprocessors.view(model.coprocessors) - | Some(PanelMultiplayerMonitor) => MultiplayerMonitor.view(model.multiplayerMonitor) - | Some(PanelDlcWorkshop) => DlcWorkshop.view(model.dlcWorkshop) - | Some(PanelUms) => Ums.view(model.ums, ~levelArchitect=model.levelArchitect) - | Some(PanelEditorBridge) => EditorBridge.view(model.editorBridge) - | Some(PanelBuildDashboard) => BuildDashboard.view(model.buildDashboard) - | Some(PanelReleaseManager) => ReleaseManager.view(model.releaseManager) - | Some(PanelAutomationRouter) => AutomationRouter.view(model.automationRouter) - | Some(PanelScriptGist) => ScriptGist.view(model.scriptGist) - | Some(PanelBoj) => Boj.view(model.boj) - | Some(PanelCladeBrowser) => CladeBrowser.view(model.cladeBrowser) - | Some(PanelTentacles) => Tentacles.view(model.tentacles) - | Some(PanelProtocolSquisher) => ProtocolSquisher.view(model.protocolSquisher) - | Some(PanelMyLang) => MyLang.view(model.myLang) - | Some(PanelTypeLL) => TypeLL.view(model.typell) - | Some(PanelEvangeliser) => Evangeliser.view(model.evangeliser) - | Some(PanelHelp) => Help.view(model.help) - | Some(PanelLanguageForge) => LanguageForge.view(model.languageForge) - | Some(PanelTangleViz) => TangleViz.view(model.tangleViz) - | Some(PanelSpecBrowser) => SpecBrowser.view(model.specBrowser) - | Some(PanelVerificationDashboard) => VerificationDashboard.view(model.verificationDashboard) - | Some(PanelEchidna) => Echidna.view(model.echidna) - | Some(PanelObservatory) => Observatory.view(model.observatory) - | Some(PanelAmbientOps) => AmbientOps.view(model.ambientOps) - // Game Dev panels — testing - | Some(PanelUnitTestRunner) => UnitTestRunner.view(model.unitTestRunner) - | Some(PanelFunctionalTester) => FunctionalTester.view(model.functionalTester) - | Some(PanelRegressionGuard) => RegressionGuard.view(model.regressionGuard) - | Some(PanelPerformanceProfiler) => PerformanceProfiler.view(model.performanceProfiler) - | Some(PanelLoadTester) => LoadTester.view(model.loadTester) - | Some(PanelSoakMonitor) => SoakMonitor.view(model.soakMonitor) - | Some(PanelCompatibilityMatrix) => CompatibilityMatrix.view(model.compatibilityMatrix) - | Some(PanelExploratoryWorkbench) => ExploratoryWorkbench.view(model.exploratoryWorkbench) - | Some(PanelBetaFeedbackHub) => BetaFeedbackHub.view(model.betaFeedbackHub) - | Some(PanelBalanceAnalyser) => BalanceAnalyser.view(model.balanceAnalyser) - // Game Dev panels — bridges - | Some(PanelTypingBridge) => TypingBridge.view(model.typingBridge) - | Some(PanelNeurosymBridge) => NeurosymBridge.view(model.neurosymBridge) - | Some(PanelAgenticBridge) => AgenticBridge.view(model.agenticBridge) - | Some(PanelAutomationBridge) => AutomationBridge.view(model.automationBridge) - | Some(PanelDatabaseBridge) => DatabaseBridge.view(model.databaseBridge) - | Some(PanelProtocolBridge) => ProtocolBridge.view(model.protocolBridge) - | Some(PanelProofsBridge) => ProofsBridge.view(model.proofsBridge) - | Some(PanelScriptingBridge) => ScriptingBridge.view(model.scriptingBridge) - // Game Dev panels — game-specific - | Some(PanelGeneratorMode) => GeneratorMode.view(model.generatorMode) - | Some(PanelArchitectMode) => ArchitectMode.view(model.architectMode) - | Some(PanelGuardAiTuner) => GuardAiTuner.view(model.guardAiTuner) - | Some(PanelDeviceNetworkDesigner) => DeviceNetworkDesigner.view(model.deviceNetworkDesigner) - | Some(PanelAssetManager) => AssetManager.view(model.assetManager) - | Some(PanelPlaytestRecorder) => PlaytestRecorder.view(model.playtestRecorder) - // Team / collaboration panels - | Some(PanelCodeReview) => CodeReview.view(model.codeReview) - | Some(PanelMergeCoordinator) => MergeCoordinator.view(model.mergeCoordinator) - | Some(PanelTeamDashboard) => TeamDashboard.view(model.teamDashboard) - | Some(PanelDebuggingWorkbench) => DebuggingWorkbench.view(model.debuggingWorkbench) - // Infrastructure panels - | Some(PanelWiringInspector) => WiringInspector.view(model.wiringInspector) - // Floor Raise panels — foundational tool adoption campaign - | Some(PanelFloorRaise) => FloorRaise.view(model.floorRaise) - | Some(PanelProvenAdoption) => ProvenAdoption.view(model.provenAdoption) - | Some(PanelContractileCompleteness) => - ContractileCompleteness.view(model.contractileCompleteness) - | Some(PanelManifestCoverage) => ManifestCoverage.view(model.manifestCoverage) - | Some(PanelVerisimdbFeeds) => VerisimdbFeeds.view(model.verisimdbFeeds) - | Some(PanelFeedbackRouting) => FeedbackRouting.view(model.feedbackRouting) - | Some(PanelVexometerFriction) => VexometerFriction.view(model.vexometerFriction) - | Some(PanelK9Manager) => K9Manager.view(model.k9Manager) - | Some(PanelContractileManager) => ContractileManager.view(model.contractiles, model.vexometer) - // VCL-total panel (broken JSX — disabled pending Vcl.res fix) - | Some(PanelVcl) => - div(list{Attrs.class_("p-4 text-gray-400")}, list{text("VCL-total panel loading...")}) - // LLM Coding — multi-session Claude/LLM coordinator - | Some(PanelLlmCoding) => LlmCoding.view(model.llmCoding) - // Agent Coordination View - | Some(PanelAgentCoordination) => AgentCoordination.view(model.agentCoordination) - // GSA (Game Server Admin) panels — Clade-registered, views pending - | Some(PanelGsaServerBrowser) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("GSA Server Browser — panel loading...")}, - ) - | Some(PanelGsaConfigEditor) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("GSA Config Editor — panel loading...")}, - ) - | Some(PanelGsaServerActions) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("GSA Server Actions — panel loading...")}, - ) - | Some(PanelGsaLiveLogs) => - div(list{Attrs.class_("p-4 text-gray-400")}, list{text("GSA Live Logs — panel loading...")}) - | Some(PanelGsaHealthDashboard) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("GSA Health Dashboard — panel loading...")}, - ) - | Some(PanelGsaConfigHistory) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("GSA Config History — panel loading...")}, - ) - | Some(PanelGsaCrossSearch) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("GSA Cross-Search — panel loading...")}, - ) - // Burble panels — groove-aware voice integration - | Some(PanelBurbleServerStatus) => BurbleServerStatus.view(model.burble) - | Some(PanelBurbleVoiceQuality) => BurbleVoiceQuality.view(model.burble) - | Some(PanelBurbleRoomMonitor) => BurbleRoomMonitor.view(model.burble) - // IDApTIK panels — Clade-registered, views pending - | Some(PanelIdaptikServerStatus) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("IDApTIK Server Status — panel loading...")}, - ) - | Some(PanelIdaptikSessionMonitor) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("IDApTIK Session Monitor — panel loading...")}, - ) - | Some(PanelIdaptikPlayerOverview) => - div( - list{Attrs.class_("p-4 text-gray-400")}, - list{text("IDApTIK Player Overview — panel loading...")}, - ) - } -} - -/// Get the root colour classes based on view mode. Light mode uses lighter backgrounds -/// with dark text for better contrast in high-ambient-light environments. -let rootColourClasses = (viewMode: viewMode): string => { - switch viewMode { - | DarkStart | Standard => "bg-gray-950 text-gray-100" - | LightMode => "bg-gray-50 text-gray-900" - | Ambient | Zen => "bg-gray-950 text-gray-100" - } -} - -/// Main view function -let view = (model: model): Tea_Vdom.t => { - // Dark Start mode - show architecture manifold - if model.viewMode === DarkStart { - renderDarkStart(model) - } else { - div( - list{ - Attrs.class_( - `h-screen ${rootColourClasses( - model.viewMode, - )} flex flex-col ${AccessibilityEngine.rootClasses(model.accessibility)}`, - ), - }, - list{ - // ──────────────────────────────────────────────────────────────── - // Accessibility: Skip links (WCAG 2.1 AA §2.4.1) - // Visually hidden until focused via Tab key, allowing keyboard - // users to jump directly to main content areas. - // ──────────────────────────────────────────────────────────────── - nav( - list{ - Attrs.class_( - "sr-only focus-within:not-sr-only focus-within:fixed focus-within:top-0 focus-within:left-0 focus-within:z-50 focus-within:bg-gray-900 focus-within:p-2 focus-within:flex focus-within:gap-2", - ), - Attrs.ariaLabel("Skip navigation"), - }, - list{ - a( - list{ - Attrs.href("#pane-l"), - Attrs.class_( - "text-blue-400 underline focus:outline-2 focus:outline-blue-400 px-2 py-1 rounded", - ), - }, - list{text("Skip to Panel-L (Symbolic)")}, - ), - a( - list{ - Attrs.href("#pane-n"), - Attrs.class_( - "text-blue-400 underline focus:outline-2 focus:outline-blue-400 px-2 py-1 rounded", - ), - }, - list{text("Skip to Panel-N (Neural)")}, - ), - a( - list{ - Attrs.href("#pane-w"), - Attrs.class_( - "text-blue-400 underline focus:outline-2 focus:outline-blue-400 px-2 py-1 rounded", - ), - }, - list{text("Skip to Panel-W (World)")}, - ), - a( - list{ - Attrs.href("#panel-bar"), - Attrs.class_( - "text-blue-400 underline focus:outline-2 focus:outline-blue-400 px-2 py-1 rounded", - ), - }, - list{text("Skip to Panel Bar")}, - ), - }, - ), - // Ambient substrate - Orbital Drift Aura - renderDriftAura(model.orbital, model.humidity), - // Code Provenance Map — Qubes-style trust surface (hidden in fullscreen) - if !model.fullscreenActive { - Provenance.view(model.provenance) - } else { - noNode - }, - // Application menu bar — File / Edit / View / Panel / Tools / Help - if !model.fullscreenActive { - MenuBar.view(model.menuBar) - } else { - noNode - }, - // Main three-pane layout (padded right for the panel bar when visible) - div( - list{ - Attrs.class_( - `flex-1 flex overflow-hidden relative z-10 ${if model.panelBarVisible { - "pr-12" - } else { - "" - }}`, - ), - Attrs.role("main"), - Attrs.ariaLabel("PanLL workspace — three-pane parallel layout"), - }, - list{ - // Cross-panel circuit lines showing data flow - renderCircuitLines( - model.orbital, - model.paneLVisible, - model.paneNVisible, - model.paneWVisible, - ), - // Pane-A: Ambient Substrate (Ergonomics) - div( - list{ - Attrs.id("pane-a"), - Attrs.class_( - `w-72 overflow-auto relative border-r border-gray-900 transition-all duration-500 ${FocusDimmingEngine.panelOpacityClass( - model.focusDimming, - "paneA", - )}`, - ), - Attrs.role("region"), - Attrs.ariaLabel("Panel-A — Ambient substrate and cognitive ergonomics"), - Events.onClick(FocusDimming(RecordInteraction("paneA"))), - }, - list{ - renderPaneA(model.paneA, true), - CaptureBar.view( - "paneA", - false, - model.capture.captureBarVisible, - ), - }, - ), - // Pane-L with capture bar and focus dimming - div( - list{ - Attrs.id("pane-l"), - Attrs.class_( - `flex-1 overflow-auto relative transition-opacity duration-500 ${FocusDimmingEngine.panelOpacityClass( - model.focusDimming, - "paneL", - )}`, - ), - Attrs.role("region"), - Attrs.ariaLabel("Panel-L — Symbolic constraints and proof obligations"), - Events.onClick(FocusDimming(RecordInteraction("paneL"))), - }, - list{ - renderPaneL( - model.paneL, - if model.verisim.proofDisplayActive { - model.verisim.proofObligations - } else { - [] - }, - model.paneLVisible, - ), - CaptureBar.view( - "paneL", - false, - model.capture.captureBarVisible && model.paneLVisible, - ), - }, - ), - // Pane-N with capture bar and focus dimming - div( - list{ - Attrs.id("pane-n"), - Attrs.class_( - `flex-1 overflow-auto relative transition-opacity duration-500 ${FocusDimmingEngine.panelOpacityClass( - model.focusDimming, - "paneN", - )}`, - ), - Attrs.role("region"), - Attrs.ariaLabel("Panel-N — Neural inference stream and ECHIDNA prover"), - Events.onClick(FocusDimming(RecordInteraction("paneN"))), - }, - list{ - renderPaneN( - model.paneN, - model.echidna, - ~inferenceStream=model.verisim.inferenceStream, - model.paneNVisible, - ), - CaptureBar.view( - "paneN", - false, - model.capture.captureBarVisible && model.paneNVisible, - ), - }, - ), - // Pane-W with capture bar and focus dimming - div( - list{ - Attrs.id("pane-w"), - Attrs.class_( - `flex-1 overflow-auto relative transition-opacity duration-500 ${FocusDimmingEngine.panelOpacityClass( - model.focusDimming, - "paneW", - )}`, - ), - Attrs.role("region"), - Attrs.ariaLabel( - "Panel-W — World barycentre, validated output, and VeriSimDB tools", - ), - Events.onClick(FocusDimming(RecordInteraction("paneW"))), - }, - list{ - renderPaneW( - model.paneW, - model.orbital, - model.verisim, - model.contractiles, - model.barycentreTour, - model.paneWVisible, - ), - CaptureBar.view( - "paneW", - false, - model.capture.captureBarVisible && model.paneWVisible, - ), - }, - ), - }, - ), - // Vexometer - hidden in fullscreen - if !model.fullscreenActive { - Vexometer.view(model.vexometer, false) - } else { - noNode - }, - // Feedback-O-Tron - hidden in fullscreen - if !model.fullscreenActive { - FeedbackOTron.view( - model.feedbackPending, - model.feedbackError, - model.feedbackReportType, - model.boj, - ) - } else { - noNode - }, - // Active panel overlay — replaces ad-hoc visible checks on VAB/CloudGuard. - // The panel switcher routes to the correct module's view or a placeholder. - renderActivePanel(model), - // Panel switcher bar — controlled by panelBarVisible toggle - if model.panelBarVisible { - PanelSwitcher.view(model.panelSwitcher) - } else { - noNode - }, - // Status bar — hidden in fullscreen - if !model.fullscreenActive { - StatusBar.view(model) - } else { - noNode - }, - // Home button — fixed top-right, visible when any panel overlay is open. - // Provides a constant escape hatch back to the main three-panel view. - // Positioned top-right to avoid obscuring panel titles and left-side controls. - if model.panelSwitcher.activePanel !== None { - button( - list{ - Attrs.class_( - "fixed top-2 right-14 z-[9998] h-8 px-3 rounded-lg bg-gray-800 hover:bg-gray-700 text-gray-300 hover:text-white shadow-lg flex items-center gap-1.5 transition-all hover:scale-105 focus:outline-none focus:ring-2 focus:ring-indigo-400 border border-gray-700 text-xs font-medium", - ), - Attrs.title("Return to main view (Escape)"), - Attrs.ariaLabel("Return to main view"), - Events.onClick(PanelSwitcher(ClosePanels)), - }, - list{ - span(list{Attrs.class_("text-sm")}, list{text("\xe2\x86\x90")}), - span(list{}, list{text("Back")}), - }, - ) - } else { - noNode - }, - // Floating accessibility widget (FAB + panel) — always rendered last - // so it overlays all content via fixed positioning. - AccessibilityToolbar.view(model.accessibility), - }, - ) - } -} diff --git a/src/commands/A2mlCmd.affine b/src/commands/A2mlCmd.affine new file mode 100644 index 00000000..21dc5857 --- /dev/null +++ b/src/commands/A2mlCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module A2mlCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/A2mlCmd.res b/src/commands/A2mlCmd.res deleted file mode 100644 index 970d2d11..00000000 --- a/src/commands/A2mlCmd.res +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL A2ML Commands — backend invoke wrappers for A2ML manifest operations. -/// These call into the Rust backend at src-gossamer/src/a2ml/commands.rs which -/// handles filesystem access for loading, validating, and listing .a2ml files. -/// -/// The Rust backend reads files and returns content as JSON strings. The -/// ReScript A2mlEngine then handles the actual parsing and validation logic -/// on the client side for maximum testability. - -let invoke = RuntimeBridge.invoke - -/// Load an A2ML manifest file from disk. Returns the raw file content -/// as a JSON-wrapped string for client-side parsing by A2mlEngine. -let loadManifest = (path: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("a2ml_load_manifest", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to load A2ML manifest: ${path}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Validate an A2ML manifest file on the backend. The Rust side performs -/// basic structural checks (file exists, non-empty, valid encoding) and -/// returns a JSON validation result. Deeper semantic validation is done -/// client-side by A2mlEngine.validateManifest. -let validateManifestFile = (path: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("a2ml_validate", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to validate A2ML manifest: ${path}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all .a2ml files found in the current repository. Returns a JSON -/// array of file paths relative to the repo root. -let listManifests = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("a2ml_list", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list A2ML manifests"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/AerieCmd.affine b/src/commands/AerieCmd.affine new file mode 100644 index 00000000..26605b59 --- /dev/null +++ b/src/commands/AerieCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AerieCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/AerieCmd.res b/src/commands/AerieCmd.res deleted file mode 100644 index 34b80699..00000000 --- a/src/commands/AerieCmd.res +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Aerie Commands — Backend wrappers for network diagnostics. -/// -/// Backend is V-lang API gateway at :4000 (GraphQL + REST). - -let invoke = RuntimeBridge.invoke - -/// Fetch latest latency measurements. -let fetchLatency = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("aerie_get_latency", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Latency fetch failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run a speed test. -let runSpeedTest = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("aerie_speed_test", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Speed test failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/AgentCoordinationCmd.affine b/src/commands/AgentCoordinationCmd.affine new file mode 100644 index 00000000..af6cfb73 --- /dev/null +++ b/src/commands/AgentCoordinationCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentCoordinationCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/AgentCoordinationCmd.res b/src/commands/AgentCoordinationCmd.res deleted file mode 100644 index ddf5877d..00000000 --- a/src/commands/AgentCoordinationCmd.res +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent Coordination command wrappers — invoke bridge for the -/// coordination view panel. -/// -/// All commands invoke BoJ cartridge endpoints for multi-agent topology -/// and strategy management. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Fetch the current agent coordination topology. -/// Returns JSON with nodes, edges, and active strategy. -let topology = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_coord_topology", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch coordination topology"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Set the coordination strategy for the agent system. -/// Returns JSON confirming the strategy change. -let setStrategy = (strategyId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_coord_set_strategy", {"strategy_id": strategyId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to set coordination strategy"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/AgentOodaCmd.affine b/src/commands/AgentOodaCmd.affine new file mode 100644 index 00000000..48e28444 --- /dev/null +++ b/src/commands/AgentOodaCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentOodaCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/AgentOodaCmd.res b/src/commands/AgentOodaCmd.res deleted file mode 100644 index 4cf6e530..00000000 --- a/src/commands/AgentOodaCmd.res +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent OODA command wrappers — invoke bridge for the -/// OODA session monitor panel. -/// -/// All commands invoke BoJ cartridge endpoints for agent OODA session -/// management. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// List all active OODA sessions. -/// Returns JSON array of session summary objects. -let listSessions = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_ooda_list_sessions", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list OODA sessions"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get detailed information for a specific OODA session. -/// Returns JSON with full session detail including transition history. -let sessionDetail = (sessionId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("agent_ooda_session_detail", {"session_id": sessionId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch session detail"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Manually advance an agent session to the next OODA state. -/// Returns JSON with the updated session state. -let advance = (sessionId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_ooda_advance", {"session_id": sessionId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to advance session"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Halt an agent session immediately. -/// Returns JSON confirming the halt. -let halt = (sessionId: string, reason: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("agent_ooda_halt", {"session_id": sessionId, "reason": reason}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to halt session"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/AgentSafetyCmd.affine b/src/commands/AgentSafetyCmd.affine new file mode 100644 index 00000000..fe0162f3 --- /dev/null +++ b/src/commands/AgentSafetyCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentSafetyCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/AgentSafetyCmd.res b/src/commands/AgentSafetyCmd.res deleted file mode 100644 index ff5d5aa5..00000000 --- a/src/commands/AgentSafetyCmd.res +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent Safety command wrappers — invoke bridge for the -/// safety gate panel. -/// -/// All commands invoke BoJ cartridge endpoints for agent tool call -/// safety management. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Fetch all pending safety events awaiting human review. -/// Returns JSON array of safety event objects. -let pending = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_safety_pending", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch pending safety events"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Approve a pending safety event, allowing the tool call to proceed. -/// Returns JSON confirming the approval. -let approve = (eventId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_safety_approve", {"event_id": eventId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to approve safety event"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Deny a pending safety event, blocking the tool call. -/// Returns JSON confirming the denial. -let deny = (eventId: string, reason: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("agent_safety_deny", {"event_id": eventId, "reason": reason}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to deny safety event"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch historical safety events. -/// Returns JSON array of safety event objects sorted by timestamp. -let history = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("agent_safety_history", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch safety history"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/AiCmd.affine b/src/commands/AiCmd.affine new file mode 100644 index 00000000..5a661cc6 --- /dev/null +++ b/src/commands/AiCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AiCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/AiCmd.res b/src/commands/AiCmd.res deleted file mode 100644 index 4c641adb..00000000 --- a/src/commands/AiCmd.res +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AI Commands — Backend command wrappers for the multi-provider AI panel. -/// -/// Each function wraps a backend `invoke` call in a `Tea_Cmd.call`, converting -/// the Promise-based IPC into the TEA command model. Results arrive as -/// JSON strings; parsing happens in the Update layer, not here. -/// -/// Pattern: `commandName(args..., tagger) => Tea_Cmd.t<'msg>` -/// where `tagger: result => 'msg` wraps the result into -/// the panel's message type. - -let invoke = RuntimeBridge.invoke - -/// Send a message to the AI provider. The backend selects the highest-priority -/// enabled provider (or uses the specified one) and returns the response. -let sendMessage = ( - content: string, - history: array, - systemPrompt: string, - providerId: option, - broadcast: bool, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "ai_send_message", - { - "request": { - "content": content, - "history": history, - "system_prompt": systemPrompt, - "provider_id": providerId, - "broadcast": broadcast, - }, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to send AI message"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check if a provider is reachable and properly authenticated. -let checkProvider = (providerId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("ai_check_provider", {"providerId": providerId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to check provider: ${providerId}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Change the selected model for a provider. -let setModel = ( - providerId: string, - model: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ai_set_model", {"providerId": providerId, "model": model}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to set model for ${providerId}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Change a provider's precedence ranking. -let setPriority = ( - providerId: string, - priority: int, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ai_set_priority", {"providerId": providerId, "priority": priority}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to set priority for ${providerId}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Enable or disable a provider (mute/unmute without losing the API key). -let toggleProvider = (providerId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("ai_toggle_provider", {"providerId": providerId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to toggle provider: ${providerId}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Clear the conversation history. -let clearHistory = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ai_clear_history", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to clear history"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Build the system prompt context from a repository path. -let buildContext = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ai_build_context", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to build context for ${repoPath}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fire-and-forget streaming message. Results arrive via backend events on -/// `ai:stream-chunk`. This command returns immediately with "streaming_started". -/// -/// The streaming provider emits StreamChunk events that the frontend receives -/// via GossamerEvents.onAiStreamChunk and feeds into the TEA update loop. -/// -/// @param content — the user's message text -/// @param history — conversation history (JSON-serialised AiMessages) -/// @param systemPrompt — assembled system prompt -/// @param providerId — explicit provider or None for auto-select -/// @param tools — tool definitions for Claude's tool_use (or None) -/// @param toolResults — results from previously executed tool calls (or None) -/// @param tagger — TEA message tagger for the fire-and-forget acknowledgement -let sendMessageStreaming = ( - content: string, - history: array, - systemPrompt: string, - providerId: option, - tools: option, - toolResults: option, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "ai_send_message_streaming", - { - "request": { - "content": content, - "history": history, - "system_prompt": systemPrompt, - "provider_id": providerId, - "tools": tools, - "tool_results": toolResults, - }, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to start streaming"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load the current provider configuration state from disk. -let getState = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ai_get_state", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load AI provider state"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/AutomationRouterCmd.affine b/src/commands/AutomationRouterCmd.affine new file mode 100644 index 00000000..20386179 --- /dev/null +++ b/src/commands/AutomationRouterCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AutomationRouterCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/AutomationRouterCmd.res b/src/commands/AutomationRouterCmd.res deleted file mode 100644 index dd24d4c5..00000000 --- a/src/commands/AutomationRouterCmd.res +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Automation Router Commands — backend invoke wrappers for loading, -/// saving, and executing automation workflow rules. - -let invoke = RuntimeBridge.invoke - -/// Load automation rules from .machine_readable/ENSAID_CONFIG.a2ml or local storage. -let loadRules = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("automation_load_rules", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load automation rules"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save automation rules to local storage. -let saveRules = (rulesJson: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("automation_save_rules", {"rulesJson": rulesJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save automation rules"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Execute an automation rule's actions. -let executeRule = (ruleId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("automation_execute_rule", {"ruleId": ruleId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to execute automation rule"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load rules from .machine_readable/ENSAID_CONFIG.a2ml in the current repo. -let loadFromRepo = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("automation_load_from_repo", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("No ENSAID_CONFIG.a2ml found in repo"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read execution history. -let readHistory = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("automation_read_history", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read execution history"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/BojCmd.affine b/src/commands/BojCmd.affine new file mode 100644 index 00000000..c6bfe447 --- /dev/null +++ b/src/commands/BojCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BojCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/BojCmd.res b/src/commands/BojCmd.res deleted file mode 100644 index 2120e153..00000000 --- a/src/commands/BojCmd.res +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL BoJ Commands — backend invoke wrappers for the Bundle of Joy -/// cartridge server. These call into the Rust backend at src-gossamer/src/boj/commands.rs -/// which proxies to the BoJ server at BOJ_URL (default http://localhost:7700/api/v1). - -let invoke = RuntimeBridge.invoke - -/// Check BoJ server health. -let health = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_health", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("BoJ server unreachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all cartridges from the BoJ server. -let listCartridges = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_list_cartridges", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list cartridges"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get detailed info for a specific cartridge. -let getCartridge = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_get_cartridge", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to get cartridge: ${name}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load (mount) a cartridge into the BoJ runtime. -let loadCartridge = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_load_cartridge", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to load cartridge: ${name}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Unload (unmount) a cartridge from the BoJ runtime. -let unloadCartridge = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_unload_cartridge", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to unload cartridge: ${name}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get the full topology (architecture diagram data). -let topology = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_topology", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get topology"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Invoke a tool on a specific cartridge. -let invokeCartridge = ( - name: string, - tool: string, - args: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_invoke", {"name": name, "tool": tool, "args": args}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Invocation failed: ${name}/${tool}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Invoke a tool on a cartridge with latency measurement. -/// Fires both the result tagger and a latency tagger with (cartridge, tool, elapsed ms). -let invokeCartridgeWithLatency = ( - name: string, - tool: string, - args: string, - resultTagger: result => 'msg, - latencyTagger: (string, string, float) => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let startTime = Date.now() - invoke("boj_invoke", {"name": name, "tool": tool, "args": args}) - ->Promise.then(result => { - let elapsed = Date.now() -. startTime - callbacks.enqueue(resultTagger(Ok(result))) - callbacks.enqueue(latencyTagger(name, tool, elapsed)) - Promise.resolve() - }) - ->Promise.catch(_err => { - let elapsed = Date.now() -. startTime - callbacks.enqueue(resultTagger(Error(`Invocation failed: ${name}/${tool}`))) - callbacks.enqueue(latencyTagger(name, tool, elapsed)) - Promise.resolve() - }) - ->ignore - }) -} - -/// Type-safe cartridge invocation — compile-time validated via CartridgeAbi. -/// Prefer this over the raw string-based invokeCartridge/invokeCartridgeWithLatency. -/// Usage: BojCmd.invokeTyped(Database(ExecuteVcl), args, resultTagger, latencyTagger) -let invokeTyped = ( - inv: CartridgeAbi.invocation, - args: string, - resultTagger: result => 'msg, - latencyTagger: (string, string, float) => 'msg, -): Tea_Cmd.t<'msg> => { - let (name, tool) = CartridgeAbi.toWire(inv) - invokeCartridgeWithLatency(name, tool, args, resultTagger, latencyTagger) -} - -/// Type-safe cartridge invocation without latency tracking. -/// Usage: BojCmd.invokeTypedSimple(Nesy(Harmonize), args, resultTagger) -let invokeTypedSimple = ( - inv: CartridgeAbi.invocation, - args: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - let (name, tool) = CartridgeAbi.toWire(inv) - invokeCartridge(name, tool, args, tagger) -} - -/// Get Umoja federation status. -let umojaStatus = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_umoja_status", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get Umoja status"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/BojLiveCmd.affine b/src/commands/BojLiveCmd.affine new file mode 100644 index 00000000..c14646c0 --- /dev/null +++ b/src/commands/BojLiveCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BojLiveCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/BojLiveCmd.res b/src/commands/BojLiveCmd.res deleted file mode 100644 index 8c849c74..00000000 --- a/src/commands/BojLiveCmd.res +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// BoJ Live Commands — async BoJ-server connection via the shared HTTP client. -/// -/// These wrap the async backend commands from `boj_live.rs` and provide the same -/// TEA-compatible callback interface as `BojCmd.res`. Panels can switch between -/// mock (BojCmd) and live (BojLiveCmd) backends by routing through the panel -/// config's `bojRouting` flag. -/// -/// All commands talk to the BoJ server at BOJ_URL (default localhost:7700). - -let invoke = RuntimeBridge.invoke - -/// Check BoJ-server health (async endpoint). -/// Calls `boj_live_health` which hits `GET /health` on the BoJ server. -let checkHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_live_health", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("BoJ server unreachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List available cartridges (async endpoint). -/// Calls `boj_live_cartridges` which hits `GET /cartridges` on the BoJ server. -let listCartridges = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_live_cartridges", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list cartridges"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Invoke a cartridge tool (async endpoint). -/// Calls `boj_live_invoke` which hits `POST /cartridges/{cartridge}/invoke`. -/// -/// @param cartridge — name of the target cartridge (e.g. "database", "nesy") -/// @param tool — tool/function name within the cartridge -/// @param params — JSON string of tool arguments -/// @param tagger — TEA message tagger for the result -let invokeCartridge = ( - cartridge: string, - tool: string, - params: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_live_invoke", {"cartridge": cartridge, "tool": tool, "params": params}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Cartridge invoke failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get cartridge topology / dependency graph (async endpoint). -/// Calls `boj_live_topology` which hits `GET /topology` on the BoJ server. -let getTopology = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_live_topology", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get topology"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check if BoJ-server is reachable (async health probe). -/// Returns `{"reachable": bool, "endpoint": "..."}` — useful for panel bar -/// connection-dot indicators (green/red). -let checkReachable = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("boj_live_check", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Connection check failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/BuildDashboardCmd.affine b/src/commands/BuildDashboardCmd.affine new file mode 100644 index 00000000..1c1a19af --- /dev/null +++ b/src/commands/BuildDashboardCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BuildDashboardCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/BuildDashboardCmd.res b/src/commands/BuildDashboardCmd.res deleted file mode 100644 index e3c4e170..00000000 --- a/src/commands/BuildDashboardCmd.res +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Build Dashboard Commands — backend invoke wrappers for triggering -/// builds, reading build status, and running tests. - -let invoke = RuntimeBridge.invoke - -/// Trigger a build for a specific target. -let triggerBuild = (target: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("build_trigger", {"target": target}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Build trigger failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read the current build status for all targets. -let readBuildStatus = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("build_read_status", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read build status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run the test suite for a target. -let runTests = (target: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("build_run_tests", {"target": target}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Test run failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Cancel a running build. -let cancelBuild = (target: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("build_cancel", {"target": target}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to cancel build"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read build history. -let readHistory = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("build_read_history", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read build history"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/BurbleCmd.affine b/src/commands/BurbleCmd.affine new file mode 100644 index 00000000..b1d8ee39 --- /dev/null +++ b/src/commands/BurbleCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BurbleCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/BurbleCmd.res b/src/commands/BurbleCmd.res deleted file mode 100644 index 25259435..00000000 --- a/src/commands/BurbleCmd.res +++ /dev/null @@ -1,261 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -// -// BurbleCmd — TEA command wrappers for PanLL's Burble voice integration. -// -// All commands use the invoke bridge to communicate with the Rust -// backend, which manages the WebSocket connection to the Burble voice -// server at ws://localhost:6473/voice. -// -// Pattern: same as LlmCodingCmd.res — Tea_Cmd.call wrapping backend invokes -// with Promise-based async flow and result tagging. -// -// The Workspace profile is always used: -// - Always-on VAD (hands-free) -// - Noise suppression ON -// - Echo cancellation ON -// - No spatial audio -// - E2EE ON - -let invoke = RuntimeBridge.invoke - -// ============================================================================ -// Connection lifecycle -// ============================================================================ - -/// Connect to the Burble voice server. -/// The backend establishes a Phoenix WebSocket to ws://localhost:6473/voice -/// with the Workspace profile applied. -let connect = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "burble_connect", - { - "server_url": "ws://localhost:6473/voice", - "profile": "workspace", - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to connect to Burble"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Disconnect from the Burble voice server. -let disconnect = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_disconnect", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to disconnect from Burble"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Huddle lifecycle -// ============================================================================ - -/// Join a workspace huddle by huddle ID. -/// Creates/joins the voice room on the Burble server. -let joinHuddle = (huddleId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_join_huddle", {"huddle_id": huddleId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to join huddle"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Leave the current workspace huddle. -let leaveHuddle = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_leave_huddle", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to leave huddle"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Voice controls -// ============================================================================ - -/// Toggle the local user's mute state. -let toggleMute = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_toggle_mute", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to toggle mute"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Toggle the local user's deafen state. -/// Deafening also mutes the user. -let toggleDeafen = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_toggle_deafen", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to toggle deafen"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Participant queries -// ============================================================================ - -/// Fetch the current list of participants in the huddle. -/// Returns a JSON string that the update function can parse. -let getParticipants = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_get_participants", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get participants"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Groove discovery — probe Burble's /.well-known/groove endpoint -// ============================================================================ - -/// Probe the Burble groove endpoint to discover capabilities. -/// GET http://localhost:6473/.well-known/groove -/// Returns the groove manifest JSON (service_id, capabilities, endpoints). -let checkGroove = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "burble_check_groove", - { - "url": "http://localhost:6473/.well-known/groove", - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Groove probe failed — Burble not reachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Room management — list and query rooms via groove API -// ============================================================================ - -/// List active rooms on the Burble server. -/// GET http://localhost:6473/api/v1/servers/:id/rooms -/// The serverId "local" is used for the default local Burble instance. -let listRooms = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "burble_list_rooms", - { - "url": "http://localhost:6473/api/v1/servers/local/rooms", - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list rooms"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Health check — query Burble server health -// ============================================================================ - -/// Check the Burble server health endpoint. -/// GET http://localhost:6473/api/v1/health -/// Returns JSON with server status, uptime, room count, participant count. -let getHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "burble_get_health", - { - "url": "http://localhost:6473/api/v1/health", - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Health check failed — Burble not reachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Voice stats — WebRTC quality metrics -// ============================================================================ - -/// Query WebRTC voice statistics for the active session. -/// Returns latency, jitter, packet loss, bitrate, codec info as JSON. -let getVoiceStats = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("burble_get_voice_stats", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get voice stats"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/CaptureCmd.affine b/src/commands/CaptureCmd.affine new file mode 100644 index 00000000..5895fd87 --- /dev/null +++ b/src/commands/CaptureCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CaptureCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/CaptureCmd.res b/src/commands/CaptureCmd.res deleted file mode 100644 index 5a988f0a..00000000 --- a/src/commands/CaptureCmd.res +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Capture Commands — IPC wrappers for screenshot, recording, -/// and demo operations (DD-022). -/// -/// These functions bridge the ReScript TEA loop with the Rust backend -/// for capture persistence and demo package management. Each function -/// returns a Tea_Cmd that dispatches a result message back into the -/// update loop via `callbacks.enqueue`. - -open Msg - -/// Backend invoke binding via RuntimeBridge. -let invoke = RuntimeBridge.invoke - -/// Save a screenshot to disk. The base64 data is captured in the frontend -/// via html2canvas and sent to Rust for file I/O. -let saveScreenshot = ( - captureId: string, - panelId: string, - base64Data: string, - format: string, -): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke( - "save_screenshot", - { - "captureId": captureId, - "panelId": panelId, - "base64Data": base64Data, - "format": format, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(Capture(ScreenshotSaved(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Capture(ScreenshotSaved(Error("Failed to save screenshot")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Invoke the system print dialog for a panel. -let printPanel = (panelId: string): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("print_panel", {"panelId": panelId}) - ->Promise.then(result => { - callbacks.enqueue(Capture(PrintResult(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Capture(PrintResult(Error("Failed to print panel")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save a demo package to disk. -let saveDemo = (demoJson: string): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("save_demo", {"demoJson": demoJson}) - ->Promise.then(result => { - callbacks.enqueue(Capture(DemoSaved(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Capture(DemoSaved(Error("Failed to save demo")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load all demo packages from disk. -let loadDemos = (): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("load_demos", ()) - ->Promise.then(result => { - callbacks.enqueue(Capture(DemosLoaded(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Capture(DemosLoaded(Error("Failed to load demos")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete a demo package from disk. -let deleteDemo = (demoId: string): Tea_Cmd.t => { - Tea_Cmd.call(_callbacks => { - invoke("delete_demo", {"demoId": demoId})->ignore - }) -} diff --git a/src/commands/CladeCmd.affine b/src/commands/CladeCmd.affine new file mode 100644 index 00000000..6bb98e4c --- /dev/null +++ b/src/commands/CladeCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CladeCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/CladeCmd.res b/src/commands/CladeCmd.res deleted file mode 100644 index c2cacc0e..00000000 --- a/src/commands/CladeCmd.res +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Clade Commands — Backend command wrappers for scanning `.a2ml` clade files. -/// -/// Invokes `scan_clade_files` on the Rust backend to read all clade definitions -/// from `panel-clades/clades/`. Returns a JSON array of `{id, content}` objects. - -let invoke = RuntimeBridge.invoke - -/// Scan all `.a2ml` clade files from the panel-clades directory. -/// Returns a JSON string: `[{"id": "...", "content": "..."}]`. -let scanCladeFiles = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("scan_clade_files", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to scan clade files"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/CloudGuardCmd.affine b/src/commands/CloudGuardCmd.affine new file mode 100644 index 00000000..4242dfd7 --- /dev/null +++ b/src/commands/CloudGuardCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuardCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/CloudGuardCmd.res b/src/commands/CloudGuardCmd.res deleted file mode 100644 index 43e081c1..00000000 --- a/src/commands/CloudGuardCmd.res +++ /dev/null @@ -1,357 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// CloudGuard Backend Command Wrappers — TEA commands for Cloudflare API operations. -/// -/// Each function wraps a backend command handler from -/// `src-gossamer/src/cloudguard/commands.rs`, using the `Tea_Cmd.call` pattern -/// to bridge async backend invocations into the TEA update loop. -/// -/// Pattern: -/// 1. Call `invoke("cloudguard_*", params)` → returns Promise -/// 2. On success: `callbacks.enqueue(tagger(Ok(jsonString)))` -/// 3. On failure: `callbacks.enqueue(tagger(Error(errorMessage)))` -/// -/// The frontend parses JSON strings from `Ok(...)` results in the Update.res -/// sub-updater using `JSON.parseExn` and `JSON.Classify.classify`. - -/// Backend invoke binding via RuntimeBridge. -let invoke = RuntimeBridge.invoke - -// ============================================================================ -// Token verification (health check / connection) -// ============================================================================ - -/// Verify the Cloudflare API token. Returns connection status JSON. -/// Called when the CloudGuard panel opens and periodically to maintain state. -let verifyToken = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_verify_token", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Cloudflare token verification failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Zone operations -// ============================================================================ - -/// List all Cloudflare zones (domains) in the account. -/// Returns JSON array of zone objects. -let listZones = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_list_zones", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list Cloudflare zones"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get details for a single zone by ID. -/// Returns JSON zone object. -let getZone = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_get_zone", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get zone details"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Settings operations -// ============================================================================ - -/// Get all settings for a zone. -/// Returns JSON array of setting objects. -let getSettings = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_get_settings", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get zone settings"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Update a single zone setting. -/// `value` is a JSON-encoded string of the new value. -/// Returns JSON of the updated setting. -let updateSetting = ( - zoneId: string, - settingId: string, - value: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "cloudguard_update_setting", - {"zone_id": zoneId, "setting_id": settingId, "value": value}, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to update setting " ++ settingId))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Batch-update multiple settings for a zone. -/// `settingsJson` is a JSON array of `[{id, value}]` objects. -/// Returns JSON of the updated settings. -let updateSettingsBatch = ( - zoneId: string, - settingsJson: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_update_settings_batch", {"zone_id": zoneId, "settings_json": settingsJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to batch-update settings"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// DNS record operations -// ============================================================================ - -/// List all DNS records for a zone. -/// Returns JSON array of DNS record objects. -let listDnsRecords = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_list_dns_records", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list DNS records"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Create a new DNS record. -/// Payload fields: zone_id, record_type, name, content, ttl, proxied, priority, comment. -let createDnsRecord = ( - zoneId: string, - recordType: string, - name: string, - content: string, - ttl: int, - proxied: option, - priority: option, - comment: option, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let payload = Dict.fromArray([ - ("zone_id", JSON.Encode.string(zoneId)), - ("record_type", JSON.Encode.string(recordType)), - ("name", JSON.Encode.string(name)), - ("content", JSON.Encode.string(content)), - ("ttl", JSON.Encode.int(ttl)), - ]) - // Add optional fields - switch proxied { - | Some(p) => Dict.set(payload, "proxied", JSON.Encode.bool(p)) - | None => () - } - switch priority { - | Some(p) => Dict.set(payload, "priority", JSON.Encode.int(p)) - | None => () - } - switch comment { - | Some(c) => Dict.set(payload, "comment", JSON.Encode.string(c)) - | None => () - } - - invoke("cloudguard_create_dns_record", JSON.Encode.object(payload)) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to create DNS record"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete a DNS record from a zone. -/// Returns a success/failure result. -let deleteDnsRecord = ( - zoneId: string, - recordId: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_delete_dns_record", {"zone_id": zoneId, "record_id": recordId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to delete DNS record"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// DNSSEC operations -// ============================================================================ - -/// Get DNSSEC status for a zone. -/// Returns JSON with status, DS record info, algorithm, etc. -let getDnssec = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_get_dnssec", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get DNSSEC status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Enable DNSSEC for a zone. -/// Returns the updated DNSSEC status JSON. -let enableDnssec = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_enable_dnssec", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to enable DNSSEC"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Offline config — download/upload zone configurations -// ============================================================================ - -/// Download the offline configuration for a zone (settings + DNS records). -/// Saves to ~/.config/cloudguard/configs/{domain}.json and returns the path. -let downloadConfig = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_download_config", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to download config"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Hardening — one-click security defaults -// ============================================================================ - -/// Apply the standard hardening settings to a zone. -/// This is the "Harden" button — applies SSL/TLS, HSTS, headers, etc. -/// Returns JSON with status and number of settings updated. -/// Compute a diff between current live settings and a saved configuration. -/// Returns JSON with the list of changed, added, and removed settings. -let computeDiff = ( - zoneId: string, - savedConfigId: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_compute_diff", {"zone_id": zoneId, "config_id": savedConfigId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to compute config diff"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all saved configurations for a zone. -/// Returns JSON array of saved config metadata (id, name, timestamp). -let listSavedConfigs = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_list_saved_configs", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list saved configs"))) - Promise.resolve() - }) - ->ignore - }) -} - -let hardenZone = (zoneId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("cloudguard_harden_zone", {"zone_id": zoneId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Zone hardening failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/CoprocessorsCmd.affine b/src/commands/CoprocessorsCmd.affine new file mode 100644 index 00000000..8515fc86 --- /dev/null +++ b/src/commands/CoprocessorsCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CoprocessorsCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/CoprocessorsCmd.res b/src/commands/CoprocessorsCmd.res deleted file mode 100644 index e0a9e3ed..00000000 --- a/src/commands/CoprocessorsCmd.res +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Coprocessors Commands — backend invoke wrappers for reading -/// coprocessor metrics, call logs, and backend health from the -/// running IDApTIK game instance. - -let invoke = RuntimeBridge.invoke - -/// Read metrics for all coprocessor backends. -let readMetrics = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("read_coprocessor_metrics", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read coprocessor metrics"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read the coprocessor call log. -let readCallLog = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("read_coprocessor_call_log", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read call log"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read the heatmap data (call frequency over time). -let readHeatmap = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("read_coprocessor_heatmap", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read heatmap"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Toggle a coprocessor backend on/off. -let toggleBackend = ( - backendId: string, - enabled: bool, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("toggle_coprocessor_backend", {"backendId": backendId, "enabled": enabled}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to toggle backend"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Query an external compute engine (control plane). -let queryComputeEngine = ( - engineId: string, - operation: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("query_compute_engine", {"engineId": engineId, "operation": operation}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to query compute engine"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Discover available compute devices from all engines. -let discoverDevices = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("discover_compute_devices", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to discover compute devices"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Dispatch a compute operation to local Zig FFI (Phase 2). -let dispatchLocal = ( - operation: string, - input: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("coprocessor_dispatch_local", {"operation": operation, "input": input}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to dispatch local compute operation"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check local FFI availability — is the .so loaded? (Phase 2). -let checkFfiStatus = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("coprocessor_check_ffi", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to check FFI status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Benchmark local compute — run standard test suite (Phase 2). -let benchmarkLocal = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("coprocessor_benchmark", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to run local benchmark"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Phase 2: Load the Zig FFI shared library for local GPU/CPU dispatch. -/// Returns JSON with load status, available devices, and library version. -let loadLocalFfi = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("coprocessor_load_ffi", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load Zig FFI library"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Phase 2: Query local system resources (CPU utilisation, GPU memory). -let queryLocalResources = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("coprocessor_local_resources", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to query local resources"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Phase 3: Smart dispatch — auto-selects local vs remote based on load, -/// capability, and availability. The backend implements the routing logic. -let smartDispatch = ( - operation: string, - payload: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("coprocessor_smart_dispatch", {"operation": operation, "payload": payload}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Smart dispatch failed: ${operation}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/DlcWorkshopCmd.affine b/src/commands/DlcWorkshopCmd.affine new file mode 100644 index 00000000..6d49406f --- /dev/null +++ b/src/commands/DlcWorkshopCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DlcWorkshopCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/DlcWorkshopCmd.res b/src/commands/DlcWorkshopCmd.res deleted file mode 100644 index 503040a6..00000000 --- a/src/commands/DlcWorkshopCmd.res +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL DLC Workshop Commands — backend invoke wrappers for DLC puzzle -/// pack creation, testing, and packaging operations. - -let invoke = RuntimeBridge.invoke - -/// Load puzzles from the DLC directory. -let loadPuzzles = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_load_puzzles", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load puzzles"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save a puzzle to disk. -let savePuzzle = (data: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_save_puzzle", {"data": data}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save puzzle"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run the solution test suite for a puzzle. -let runTest = (puzzleId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_run_test", {"puzzleId": puzzleId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Test run failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run all tests in the DLC pack. -let runAllTests = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_run_all_tests", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Test suite failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Browse DLC assets. -let browseAssets = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_browse_assets", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to browse assets"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Package the DLC pack for distribution. -let packageDlc = (data: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_package", {"data": data}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to package DLC"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Import a puzzle from a file. -let importPuzzle = (path: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_import_puzzle", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to import puzzle"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export a puzzle to a file. -let exportPuzzle = (puzzleId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("dlc_export_puzzle", {"puzzleId": puzzleId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export puzzle"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/EchidnaLiveCmd.affine b/src/commands/EchidnaLiveCmd.affine new file mode 100644 index 00000000..89b5b29f --- /dev/null +++ b/src/commands/EchidnaLiveCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EchidnaLiveCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/EchidnaLiveCmd.res b/src/commands/EchidnaLiveCmd.res deleted file mode 100644 index 7aff6ecf..00000000 --- a/src/commands/EchidnaLiveCmd.res +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// ECHIDNA Live Commands — async ECHIDNA proof assistant connection via the shared HTTP client. -/// -/// These wrap the async backend commands from `echidna_live.rs` and provide the same -/// TEA-compatible callback interface used throughout PanLL. Panels can switch between -/// mock and live backends by routing through the panel config's routing flag. -/// -/// All commands talk to the ECHIDNA server at ServiceEndpoints.echidna -/// (default http://localhost:9000/api/v1). -/// -/// ECHIDNA is the multi-solver dispatch layer — it receives proof obligations, -/// farms them out to Idris2/Lean/Coq/Z3 backends, and returns tactics or results. -/// -/// In browser-only mode (no desktop runtime), commands fall back to direct -/// fetch() calls against the ECHIDNA server URL. - -let hasDesktopRuntime = RuntimeBridge.hasDesktopRuntime - -/// GET helper for ECHIDNA direct fetch (bypasses backend invoke). -/// panic-attack:allow insecure-protocol — localhost development endpoint. -let fetchGet: string => promise = %raw(` - function(path) { - return fetch("http://localhost:9000/api/v1" + path) - .then(function(r) { - if (!r.ok) throw new Error("ECHIDNA returned " + r.status); - return r.text(); - }); - } -`) - -/// POST helper for ECHIDNA direct fetch (bypasses backend invoke). -/// panic-attack:allow insecure-protocol — localhost development endpoint. -let fetchPost: (string, string) => promise = %raw(` - function(path, body) { - return fetch("http://localhost:9000/api/v1" + path, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: body - }).then(function(r) { - if (!r.ok) throw new Error("ECHIDNA returned " + r.status); - return r.text(); - }); - } -`) - -/// Check ECHIDNA server health (async endpoint). -/// Calls `echidna_live_health` which hits `GET /health` on the ECHIDNA server. -/// Returns a JSON string with server status, connected solvers, and queue depth. -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let checkHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("echidna_live_health", ()) - } else { - fetchGet("/health") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA server unreachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Recommend proof tactics for an obligation (async endpoint). -/// Calls `echidna_live_recommend_tactics` which hits `POST /tactics/recommend` -/// on the ECHIDNA server. The multi-solver dispatch analyses the obligation -/// and returns ranked tactic suggestions from available backends. -/// -/// @param obligation — the proof obligation expression (Idris2/Lean/Coq syntax) -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let recommendTactics = (obligation: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("echidna_live_recommend_tactics", {"obligation": obligation}) - } else { - fetchPost( - "/tactics/recommend", - `{"obligation":${JSON.stringifyAny(obligation)->Option.getOr("\"\"")}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Tactic recommendation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Submit a proof obligation for asynchronous solving (async endpoint). -/// Calls `echidna_live_submit_obligation` which hits `POST /obligations/submit` -/// on the ECHIDNA server. Returns an obligation ID that can be polled via getResult. -/// -/// This is the async counterpart to recommendTactics — use this for long-running -/// proofs that may take multiple solver passes. -/// -/// @param obligation — the proof obligation expression (Idris2/Lean/Coq syntax) -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let submitObligation = (obligation: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("echidna_live_submit_obligation", {"obligation": obligation}) - } else { - fetchPost( - "/obligations/submit", - `{"obligation":${JSON.stringifyAny(obligation)->Option.getOr("\"\"")}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Obligation submission failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get the result of a previously submitted obligation (async endpoint). -/// Calls `echidna_live_get_result` which hits `GET /obligations/{id}/result` -/// on the ECHIDNA server. Returns the current status (pending/solved/failed) -/// and proof term if solved. -/// -/// @param obligationId — the obligation ID returned by submitObligation -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let getResult = (obligationId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("echidna_live_get_result", {"obligation_id": obligationId}) - } else { - fetchGet("/obligations/" ++ obligationId ++ "/result") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get obligation result: " ++ obligationId))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get ECHIDNA solver statistics (async endpoint). -/// Calls `echidna_live_stats` which hits `GET /stats` on the ECHIDNA server. -/// Returns solver utilisation, queue depths, success rates, and LLM integration -/// metrics from the Prover Wars dodeca-API. -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let getStats = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("echidna_live_stats", ()) - } else { - fetchGet("/stats") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get ECHIDNA stats"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/EditorBridgeCmd.affine b/src/commands/EditorBridgeCmd.affine new file mode 100644 index 00000000..574799fd --- /dev/null +++ b/src/commands/EditorBridgeCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EditorBridgeCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/EditorBridgeCmd.res b/src/commands/EditorBridgeCmd.res deleted file mode 100644 index 32b497f4..00000000 --- a/src/commands/EditorBridgeCmd.res +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Editor Bridge Commands — backend invoke wrappers for connecting -/// to external editors via LSP, extension protocols, or file watchers. - -let invoke = RuntimeBridge.invoke - -/// Detect which editor is running and attempt to connect. -let detectEditor = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("editor_bridge_detect", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("No supported editor detected"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Connect to an editor via LSP on a given port. -let connectLsp = (port: int, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("editor_bridge_connect_lsp", {"port": port}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to connect to LSP"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read diagnostics from the connected editor. -let readDiagnostics = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("editor_bridge_diagnostics", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read diagnostics"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read open files from the connected editor. -let readOpenFiles = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("editor_bridge_open_files", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read open files"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read workspace symbols. -let readSymbols = (query: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("editor_bridge_symbols", {"query": query}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read symbols"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Tell the external editor to open a file at a specific line. -let openFileAtLine = ( - filePath: string, - line: int, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("editor_bridge_open_file", {"filePath": filePath, "line": line}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to open file in editor"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/EnsaidConfigCmd.affine b/src/commands/EnsaidConfigCmd.affine new file mode 100644 index 00000000..5339bb3d --- /dev/null +++ b/src/commands/EnsaidConfigCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EnsaidConfigCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/EnsaidConfigCmd.res b/src/commands/EnsaidConfigCmd.res deleted file mode 100644 index 7e1ac0f6..00000000 --- a/src/commands/EnsaidConfigCmd.res +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ENSAID_CONFIG Commands — backend invoke wrappers for reading and writing -/// .machine_readable/ENSAID_CONFIG.a2ml files. -/// -/// Used by Minter, Provisioner, Workspace, and Automation Router to export -/// the current PanLL configuration as a well-annotated, human-editable file. - -let invoke = RuntimeBridge.invoke - -/// Write ENSAID_CONFIG.a2ml to a repo's .machine_readable/ directory. -/// Creates the directory if it doesn't exist. -let writeConfig = ( - repoPath: string, - content: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ensaid_config_write", {"repoPath": repoPath, "content": content}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to write ENSAID_CONFIG.a2ml"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read ENSAID_CONFIG.a2ml from a repo's .machine_readable/ directory. -/// Returns the file content as a string, or an error if not found. -let readConfig = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ensaid_config_read", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("No ENSAID_CONFIG.a2ml found"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Preview what the generated ENSAID_CONFIG would look like (pure, no I/O). -/// This is used by the "Preview" button in the export UI to show the file -/// content before writing it. -let preview = ( - ~repoName: string, - ~workspace: option=?, - ~humidity: string="medium", - ~panelConfigs: array=[], - ~portfolios: array=[], - ~automationRules: array=[], - (), -): string => { - EnsaidConfigEngine.generate( - ~repoName, - ~workspace?, - ~humidity, - ~panelConfigs, - ~portfolios, - ~automationRules, - (), - ) -} diff --git a/src/commands/FarmCmd.affine b/src/commands/FarmCmd.affine new file mode 100644 index 00000000..49081101 --- /dev/null +++ b/src/commands/FarmCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FarmCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/FarmCmd.res b/src/commands/FarmCmd.res deleted file mode 100644 index b95ea7fb..00000000 --- a/src/commands/FarmCmd.res +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Farm Commands — Backend command wrappers for the Git-Private-Farm panel. -/// -/// Each function wraps a backend `invoke` call in a `Tea_Cmd.call`, converting -/// the Promise-based IPC into the TEA command model. Results arrive as -/// JSON strings; parsing happens in the Update layer, not here. -/// -/// Pattern: `commandName(args..., tagger) => Tea_Cmd.t<'msg>` -/// where `tagger: result => 'msg` wraps the result into -/// the panel's message type. -/// -/// ON THE COMMAND PATTERN: Every backend call goes through `Tea_Cmd.call` which -/// hands us a `callbacks` object with `.enqueue`. This is TEA's answer to the -/// "where do side effects go?" question. In React, you'd `useEffect` with an -/// async function, manage loading/error states manually, worry about cleanup -/// on unmount, and pray the dependency array is correct. Here: the command -/// returns a tagged Result, the Update switch arm handles both cases, done. -/// No effect cleanup, no stale closures, no forgotten error boundaries. - -let invoke = RuntimeBridge.invoke - -/// Load the full repo inventory from farm-manifest.json. -/// Returns a JSON string containing the FarmInventory structure -/// (total, repos array, groups, languages, forge_names). -let listRepos = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("farm_list_repos", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load farm manifest"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get details for a single repo by name. -/// Returns a JSON string containing the FarmRepoEntry. -let getRepo = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("farm_get_repo", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to load repo: ${name}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get aggregate statistics from the manifest (counts by language, -/// forge, priority, group). -let getStats = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("farm_get_stats", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load farm statistics"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/FeedbackCmd.affine b/src/commands/FeedbackCmd.affine new file mode 100644 index 00000000..f4b19260 --- /dev/null +++ b/src/commands/FeedbackCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FeedbackCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/FeedbackCmd.res b/src/commands/FeedbackCmd.res deleted file mode 100644 index 80a5accd..00000000 --- a/src/commands/FeedbackCmd.res +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Feedback Commands — Backend command wrapper for saving Feedback-O-Tron reports. -/// -/// Persists feedback reports to `~/.panll/feedback/.json` via the -/// Rust backend. The report includes the feedback text, report type, and -/// optional BoJ context snapshot. - -let invoke = RuntimeBridge.invoke - -/// Save a feedback report to disk via the Rust backend. -let saveReport = (reportJson: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("feedback_save_report", {"reportJson": reportJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save feedback report"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/FleetCmd.affine b/src/commands/FleetCmd.affine new file mode 100644 index 00000000..e64d9769 --- /dev/null +++ b/src/commands/FleetCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FleetCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/FleetCmd.res b/src/commands/FleetCmd.res deleted file mode 100644 index 423c3b18..00000000 --- a/src/commands/FleetCmd.res +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Fleet Commands — Backend command wrappers for Gitbot-Fleet. -/// -/// The fleet backend connects to the gitbot-fleet Axum dashboard API -/// at :8080 for bot status, findings, and dispatch operations. - -let invoke = RuntimeBridge.invoke - -/// Fetch the current status of all 6 bots. -let fetchBots = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("fleet_get_bots", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch bot status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch the findings queue. -let fetchFindings = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("fleet_get_findings", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch findings"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Dispatch a finding to a specific bot for processing. -let dispatchFinding = ( - findingId: string, - botId: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("fleet_dispatch", {"findingId": findingId, "botId": botId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Dispatch failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/FleetLiveCmd.affine b/src/commands/FleetLiveCmd.affine new file mode 100644 index 00000000..5f307ace --- /dev/null +++ b/src/commands/FleetLiveCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FleetLiveCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/FleetLiveCmd.res b/src/commands/FleetLiveCmd.res deleted file mode 100644 index 5216b4c2..00000000 --- a/src/commands/FleetLiveCmd.res +++ /dev/null @@ -1,179 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// Gitbot-Fleet Live Commands — async fleet connection via direct HTTP fetch. -/// -/// These provide live connections to the gitbot-fleet Axum dashboard API -/// for bot status, findings, dispatch operations, and fleet health monitoring. -/// Falls back to direct fetch() in browser-only mode (no desktop runtime). -/// -/// All commands talk to the gitbot-fleet API at ServiceEndpoints.fleet -/// (default http://localhost:8090/api/v1). -/// -/// In browser-only mode (no desktop runtime), commands fall back to direct -/// fetch() calls against the fleet server URL. - -let hasDesktopRuntime = RuntimeBridge.hasDesktopRuntime - -/// GET helper for fleet direct fetch (bypasses backend invoke). -/// panic-attack:allow insecure-protocol — localhost development endpoint. -let fetchGet: string => promise = %raw(` - function(path) { - return fetch("http://localhost:8090/api/v1" + path) - .then(function(r) { - if (!r.ok) throw new Error("Fleet returned " + r.status); - return r.text(); - }); - } -`) - -/// POST helper for fleet direct fetch (bypasses backend invoke). -/// panic-attack:allow insecure-protocol — localhost development endpoint. -let fetchPost: (string, string) => promise = %raw(` - function(path, body) { - return fetch("http://localhost:8090/api/v1" + path, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: body - }).then(function(r) { - if (!r.ok) throw new Error("Fleet returned " + r.status); - return r.text(); - }); - } -`) - -/// Check gitbot-fleet server health (async endpoint). -/// Calls `fleet_live_health` which hits `GET /health` on the fleet server. -/// Returns a JSON string with server status, uptime, and bot count. -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let checkHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("fleet_live_health", ()) - } else { - fetchGet("/health") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Fleet server unreachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch the current status of all 6 bots (async endpoint). -/// Calls `fleet_live_bots` which hits `GET /bots` on the fleet server. -/// Returns a JSON array of bot objects with status, last-run, and findings count. -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let fetchBots = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("fleet_live_bots", ()) - } else { - fetchGet("/bots") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch bot status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch the findings queue (async endpoint). -/// Calls `fleet_live_findings` which hits `GET /findings` on the fleet server. -/// Returns a JSON array of finding objects with severity, target repo, and -/// assigned bot. -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let fetchFindings = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("fleet_live_findings", ()) - } else { - fetchGet("/findings") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch findings"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Dispatch a finding to a specific bot for processing (async endpoint). -/// Calls `fleet_live_dispatch` which hits `POST /dispatch` on the fleet server. -/// -/// @param findingId — the finding to dispatch -/// @param botId — the target bot (e.g., "rhodibot", "echidnabot") -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let dispatchFinding = ( - findingId: string, - botId: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("fleet_live_dispatch", {"finding_id": findingId, "bot_id": botId}) - } else { - fetchPost( - "/dispatch", - `{"finding_id":${JSON.stringifyAny(findingId)->Option.getOr("\"\"")},` ++ - `"bot_id":${JSON.stringifyAny(botId)->Option.getOr("\"\"")}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Dispatch failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check if gitbot-fleet is reachable (async health probe). -/// Wraps checkHealth but normalises the result into a reachability flag. -/// Returns `{"reachable": true/false, "endpoint": "..."}` — useful for panel bar -/// connection-dot indicators (green/red). -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let checkReachable = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("fleet_live_health", ()) - } else { - fetchGet("/health") - } - p - ->Promise.then(_result => { - callbacks.enqueue(tagger(Ok(`{"reachable":true,"endpoint":"${ServiceEndpoints.fleet}"}`))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Ok(`{"reachable":false,"endpoint":"${ServiceEndpoints.fleet}"}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/GamePreviewCmd.affine b/src/commands/GamePreviewCmd.affine new file mode 100644 index 00000000..9df458aa --- /dev/null +++ b/src/commands/GamePreviewCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GamePreviewCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/GamePreviewCmd.res b/src/commands/GamePreviewCmd.res deleted file mode 100644 index c6f1fc0b..00000000 --- a/src/commands/GamePreviewCmd.res +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Game Preview Commands — Backend async bindings for the live game -/// preview panel. Handles dev server health checks, game loop control, -/// gameplay recording, and render stats polling. - -let invoke = RuntimeBridge.invoke - -/// Check whether the Vite dev server is running and responding. -let checkDevServer = (url: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_check_server", {"url": url}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Dev server not responding"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Send a game loop control command (pause, resume, step). -let controlGameLoop = (command: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_control", {"command": command}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to control game loop"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Start recording gameplay to WebM. -let startGameRecording = (name: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_record_start", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to start gameplay recording"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Stop gameplay recording. -let stopGameRecording = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_record_stop", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to stop gameplay recording"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Take a screenshot of the current game frame. -let screenshotGameFrame = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_screenshot", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to capture game screenshot"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch current render statistics from the game engine. -let fetchRenderStats = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_stats", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch render stats"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List saved gameplay clips. -let listClips = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_clips_list", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list gameplay clips"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete a gameplay clip by ID. -let deleteClip = (id: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("game_preview_clip_delete", {"id": id}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to delete clip"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/GossamerCmd.affine b/src/commands/GossamerCmd.affine new file mode 100644 index 00000000..897bde9a --- /dev/null +++ b/src/commands/GossamerCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GossamerCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/GossamerCmd.res b/src/commands/GossamerCmd.res deleted file mode 100644 index 16177937..00000000 --- a/src/commands/GossamerCmd.res +++ /dev/null @@ -1,792 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Gossamer Command Integration for TEA -/// -/// Gossamer command integration for TEA. Provides TEA commands for invoking -/// backend functions through the RuntimeBridge, which dispatches to Gossamer -/// or rejects with a descriptive error in browser-only mode. -/// -/// All panel command modules should use RuntimeBridge.invoke directly. -/// This module provides higher-level TEA command wrappers for the core -/// PanLL operations (dialogs, filesystem, service health, VeriSimDB, -/// ECHIDNA, panic-attacker, vexation tracking). - -let invoke = RuntimeBridge.invoke -let isGossamerRuntime = RuntimeBridge.isGossamerRuntime - -// =========================================================================== -// Direct HTTP helpers for browser-only mode -// =========================================================================== - -/// ECHIDNA base URL for direct browser fetch. -let echidnaUrl = "http://localhost:9000/api/v1" - -/// GET helper for ECHIDNA direct fetch (bypasses desktop runtime). -let echidnaGet: string => promise = %raw(` - function(path) { - return fetch("http://localhost:9000/api/v1" + path) - .then(function(r) { - if (!r.ok) throw new Error("ECHIDNA returned " + r.status); - return r.text(); - }); - } -`) - -/// POST helper for ECHIDNA direct fetch (bypasses desktop runtime). -let echidnaPost: (string, string) => promise = %raw(` - function(path, body) { - return fetch("http://localhost:9000/api/v1" + path, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: body - }).then(function(r) { - if (!r.ok) throw new Error("ECHIDNA returned " + r.status); - return r.text(); - }); - } -`) - -module Dialog = { - /// Open a file picker dialog through the RuntimeBridge. - let openDialog = (opts: JSON.t): promise> => { - RuntimeBridge.Dialog.openDialog(opts) - } -} - -module Fs = { - /// Read a text file through the RuntimeBridge. - let readTextFile = (path: string): promise => { - RuntimeBridge.Fs.readTextFile(path) - } -} - -let decodeDialogPath = RuntimeBridge.decodeDialogPath - -/// Validate a neural inference token against symbolic constraints -let validateInference = ( - token: string, - constraints: array, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("validate_inference", {"token": token, "constraints": constraints}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Validation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Open a timeline specification file for Security Ambush runs -let openSecurityTimelineFile = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let options: JSON.t = %raw(`({ - multiple: false, - filters: [ - { name: "PanLL Timeline (JSON/YAML)", extensions: ["json", "yaml", "yml"] } - ] - })`) - Dialog.openDialog(options) - ->Promise.then(result => { - switch Nullable.toOption(result) { - | None => { - callbacks.enqueue(tagger(Error("No timeline selected"))) - Promise.resolve() - } - | Some(value) => - switch decodeDialogPath(value) { - | Some(path) => { - callbacks.enqueue(tagger(Ok(path))) - Promise.resolve() - } - | None => { - callbacks.enqueue(tagger(Error("Unsupported dialog response"))) - Promise.resolve() - } - } - } - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Timeline selection failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get the current vexation index from the backend -let getVexationIndex = (tagger: float => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("get_vexation_index", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(result)) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(0.0)) - Promise.resolve() - }) - ->ignore - }) -} - -/// Submit feedback to the Feedback-O-Tron -let submitFeedback = ( - paneLState: string, - paneNState: string, - paneWState: string, - reportType: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "submit_feedback", - { - "pane_l_state": paneLState, - "pane_n_state": paneNState, - "pane_w_state": paneWState, - "report_type": reportType, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Feedback submission failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Open and read a PanLL event-chain JSON file -let openEventChainFile = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let options: JSON.t = %raw(`({ - multiple: false, - filters: [{ name: "PanLL Event Chain", extensions: ["json"] }] - })`) - Dialog.openDialog(options) - ->Promise.then(result => { - switch Nullable.toOption(result) { - | None => { - callbacks.enqueue(tagger(Error("No file selected"))) - Promise.resolve() - } - | Some(value) => - switch decodeDialogPath(value) { - | Some(path) => - Fs.readTextFile(path) - ->Promise.then( - contents => { - callbacks.enqueue(tagger(Ok(contents))) - Promise.resolve() - }, - ) - ->Promise.catch( - _err => { - callbacks.enqueue(tagger(Error("Failed to read file"))) - Promise.resolve() - }, - ) - | None => { - callbacks.enqueue(tagger(Error("Unsupported dialog response"))) - Promise.resolve() - } - } - } - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("File selection failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Open a panic-attacker assault report JSON file and return the chosen path. -let openPanicAttackerReportFile = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let options: JSON.t = %raw(`({ - multiple: false, - filters: [{ name: "panic-attacker Assault Report", extensions: ["json"] }] - })`) - Dialog.openDialog(options) - ->Promise.then(result => { - switch Nullable.toOption(result) { - | None => { - callbacks.enqueue(tagger(Error("No panic-attacker report selected"))) - Promise.resolve() - } - | Some(value) => - switch decodeDialogPath(value) { - | Some(path) => { - callbacks.enqueue(tagger(Ok(path))) - Promise.resolve() - } - | None => { - callbacks.enqueue(tagger(Error("Unsupported dialog response"))) - Promise.resolve() - } - } - } - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("panic-attacker report selection failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Convert a panic-attacker assault report into PanLL event-chain JSON. -let importPanicAttackerReport = ( - reportPath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("import_panic_attacker_report", {"report_path": reportPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("panic-attacker import failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -let optionToJson = (value: option): JSON.t => - switch value { - | Some(v) => JSON.Encode.string(v) - | None => JSON.Encode.null - } - -/// Invokes the backend `run_panic_attack_ambush` command. -let runPanicAttackAmbush = ( - program: string, - timeline: option, - axes: option, - intensity: string, - durationSecs: int, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let payload = Dict.fromArray([ - ("program", JSON.Encode.string(program)), - ("timeline", optionToJson(timeline)), - ("axes", optionToJson(axes)), - ("intensity", JSON.Encode.string(intensity)), - ("duration_secs", JSON.Encode.int(durationSecs)), - ]) - - invoke("run_panic_attack_ambush", JSON.Encode.object(payload)) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("panic-attacker ambush failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Import the latest panic-attacker report from its reports directory. -let importLatestPanicAttackerReport = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("import_latest_panic_attacker_report", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("No latest panic-attacker report could be imported"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Probe panic-attacker capabilities. -let getPanicAttackerCapability = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("get_panic_attacker_capability", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("panic-attacker capability probe failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Record a vexation event in the backend. -let recordVexationEvent = (eventType: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("record_vexation_event", {"event_type": eventType}) - ->Promise.then(_result => { - callbacks.enqueue(tagger(Ok())) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to record vexation event"))) - Promise.resolve() - }) - ->ignore - }) -} - -// =========================================================================== -// VeriSimDB Database Backend Commands -// =========================================================================== - -/// Check VeriSimDB server health status. -let checkVeriSimDBHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_health", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("VeriSimDB health check failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Execute a VCL query against VeriSimDB. -let queryVeriSimDB = (query: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_query", {"query": query}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("VCL query execution failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List octad entities from VeriSimDB with pagination. -let listOctads = (limit: int, offset: int, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_list_octads", {"limit": limit, "offset": offset}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list octad entities"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get drift detection status for a specific entity. -let getDrift = (entityId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_get_drift", {"entity_id": entityId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Drift status retrieval failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Trigger normalisation for a drifted entity. -let triggerNormalise = (entityId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_normalise", {"entity_id": entityId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Normalisation failed for " ++ entityId))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load full entity detail for a specific octad. -let getEntityDetail = (entityId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_get_entity", {"entity_id": entityId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Entity detail retrieval failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch product telemetry from VeriSimDB. -let getTelemetry = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_telemetry", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Telemetry fetch failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch orchestration status from VeriSimDB. -let getOrchStatus = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_orch_status", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Orchestration status fetch failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -// =========================================================================== -// VeriSimDB State Persistence Commands (Connected Workbench v0.2.0) -// =========================================================================== - -/// Load persisted PanLL state from VeriSimDB. -/// -/// Sends a load request for the canonical state key. On success, the tagger -/// receives `Ok(stateJson)` which can be decoded via `Storage.persistedStateDecoder`. -/// On failure, receives `Error(reason)` — the caller should fall back to localStorage. -let loadStateFromVeriSimDB = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_load_state", {"key": "panll_state_v1"}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("VeriSimDB state load failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save persisted PanLL state to VeriSimDB. -/// -/// Accepts the pre-serialized JSON string from `Storage.serialize()`. -/// Returns confirmation or error via the tagger. -let saveStateToVeriSimDB = (stateJson: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("verisim_save_state", {"key": "panll_state_v1", "state": stateJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("VeriSimDB state save failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -// =========================================================================== -// ECHIDNA Theorem Prover Backend Commands -// =========================================================================== - -/// Check ECHIDNA prover health status. -/// In browser mode, calls ECHIDNA directly via fetch. -let checkEchidnaHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - invoke("echidna_health", ()) - } else { - echidnaGet("/health") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue( - tagger(Error("ECHIDNA health check failed — server not running on localhost:9000")), - ) - Promise.resolve() - }) - ->ignore - }) -} - -/// List available provers from the ECHIDNA catalog. -let listEchidnaProvers = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - invoke("echidna_list_provers", ()) - } else { - echidnaGet("/provers") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue( - tagger(Error("ECHIDNA prover listing failed — server not running on localhost:9000")), - ) - Promise.resolve() - }) - ->ignore - }) -} - -/// Submit proof content to ECHIDNA. -let echidnaProve = ( - content: string, - prover: option, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - let payload = Dict.fromArray([ - ("content", JSON.Encode.string(content)), - ("prover", optionToJson(prover)), - ]) - invoke("echidna_prove", JSON.Encode.object(payload)) - } else { - let proverJson = switch prover { - | Some(pv) => JSON.stringifyAny(pv)->Option.getOr("null") - | None => "null" - } - echidnaPost( - "/prove", - `{"content":${JSON.stringifyAny(content)->Option.getOr("\"\"")}, "prover":${proverJson}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA proof submission failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Submit content for verification to ECHIDNA. -let echidnaVerify = (content: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - invoke("echidna_verify", {"content": content}) - } else { - echidnaPost("/verify", `{"content":${JSON.stringifyAny(content)->Option.getOr("\"\"")}}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA verification failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Search the ECHIDNA theorem library. -let echidnaSearchTheorems = (query: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - let encoded = query->String.replaceAll(" ", "%20") - let p = if isGossamerRuntime() { - invoke("echidna_search_theorems", {"query": query}) - } else { - echidnaGet(`/search?q=${encoded}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA theorem search failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -// =========================================================================== -// ECHIDNA Interactive Session Commands -// =========================================================================== - -/// Create a new proof session. -let createEchidnaSession = ( - goal: string, - prover: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - invoke("echidna_create_session", {"goal": goal, "prover": prover}) - } else { - echidnaPost( - "/proofs", - `{"goal":${JSON.stringifyAny(goal)->Option.getOr("\"\"")}, "prover":${JSON.stringifyAny( - prover, - )->Option.getOr("\"\"")}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA session creation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Retrieve the current state of a proof session. -let getEchidnaSession = (sessionId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - invoke("echidna_get_session", {"session_id": sessionId}) - } else { - echidnaGet(`/proofs/${sessionId}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA get session failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Apply a tactic to an active proof session. -let applyEchidnaTactic = ( - sessionId: string, - name: string, - args: array, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - let payload = Dict.fromArray([ - ("session_id", JSON.Encode.string(sessionId)), - ("name", JSON.Encode.string(name)), - ("args", JSON.Encode.array(Array.map(args, JSON.Encode.string))), - ]) - invoke("echidna_apply_tactic", JSON.Encode.object(payload)) - } else { - let argsJson = JSON.stringifyAny(args)->Option.getOr("[]") - echidnaPost( - `/proofs/${sessionId}/tactics`, - `{"name":${JSON.stringifyAny(name)->Option.getOr("\"\"")}, "args":${argsJson}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA apply tactic failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Request ML-powered tactic suggestions. -let suggestEchidnaTactics = ( - sessionId: string, - limit: int, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if isGossamerRuntime() { - invoke("echidna_suggest_tactics", {"session_id": sessionId, "limit": limit}) - } else { - echidnaGet(`/proofs/${sessionId}/tactics/suggest?limit=${Int.toString(limit)}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA tactic suggestions failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Batch multiple commands together -let batch = (commands: list>): Tea_Cmd.t<'msg> => { - Tea_Cmd.batch(commands) -} - -/// No-op command -let none: Tea_Cmd.t<'msg> = Tea_Cmd.none diff --git a/src/commands/GovernanceCmd.affine b/src/commands/GovernanceCmd.affine new file mode 100644 index 00000000..d5b8cfd9 --- /dev/null +++ b/src/commands/GovernanceCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GovernanceCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/GovernanceCmd.res b/src/commands/GovernanceCmd.res deleted file mode 100644 index ec0e1823..00000000 --- a/src/commands/GovernanceCmd.res +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Governance Commands — backend invoke wrappers for nesy-MCP governance -/// queries. These call into the Rust backend at src-gossamer/src/governance/commands.rs -/// which routes governance decisions through the BoJ nesy-mcp cartridge for -/// real-time neural validation. -/// -/// Used by the GovernanceEngine's `evaluateWithCmd` path when the engine cannot -/// make a confident pure decision and needs async neural consultation. - -let invoke = RuntimeBridge.invoke - -/// Query nesy-mcp for a confidence assessment on a borderline governance -/// decision. Returns JSON with confidence score, recommended action, and -/// reasoning from the neural subsystem. -let queryNesyConfidence = (query: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("governance_nesy_query", {"query": query}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Nesy confidence query failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Ask nesy-mcp to validate a governance adjustment before it is applied. -/// Used primarily for HaltInference decisions — the neural subsystem can -/// approve or reject the halt with reasoning. -let validateAdjustment = (adj: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("governance_nesy_validate", {"adjustment": adj}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Nesy adjustment validation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Probe nesy-mcp for overall stability metrics from the neural subsystem. -/// Returns JSON with neural coherence, drift magnitude, and recommendation. -/// Emitted automatically when any governance query is generated. -let probeStability = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("governance_nesy_probe", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Nesy stability probe failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/HealthCheckCmd.affine b/src/commands/HealthCheckCmd.affine new file mode 100644 index 00000000..3b426b49 --- /dev/null +++ b/src/commands/HealthCheckCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module HealthCheckCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/HealthCheckCmd.res b/src/commands/HealthCheckCmd.res deleted file mode 100644 index 431fdbc2..00000000 --- a/src/commands/HealthCheckCmd.res +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Health Check Command — generic backend command for checking if an -/// HTTP service is reachable. Used by all panels with backend services. -/// -/// Each panel calls `checkEndpoint` with its service URL and a tagger -/// function that wraps the result into its own message type. - -let invoke = RuntimeBridge.invoke - -/// Check if an HTTP endpoint responds with a 2xx status. -/// The backend makes a GET request and returns the response body -/// (or an error message). -/// -/// `endpoint` — full URL to health check (e.g. "http://localhost:8080/health") -/// `tagger` — function to wrap result into the panel's msg type -let checkEndpoint = (endpoint: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("health_check", {"endpoint": endpoint}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Service unreachable: ${endpoint}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/HypatiaCmd.affine b/src/commands/HypatiaCmd.affine new file mode 100644 index 00000000..0fda4d90 --- /dev/null +++ b/src/commands/HypatiaCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module HypatiaCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/HypatiaCmd.res b/src/commands/HypatiaCmd.res deleted file mode 100644 index b3ebd758..00000000 --- a/src/commands/HypatiaCmd.res +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Hypatia Commands — Backend command wrappers for the Hypatia scanner. -/// -/// The Hypatia backend is an Elixir Phoenix API at /api/v1/. - -let invoke = RuntimeBridge.invoke - -/// Fetch the status of all 5 neural networks. -let fetchNetworks = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("hypatia_get_networks", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch network status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch scan results across all repos. -let fetchScans = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("hypatia_get_scans", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch scan results"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Trigger a scan on a specific repo. -let scanRepo = (repoName: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("hypatia_scan_repo", {"repoName": repoName}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Scan failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/IdentityCmd.affine b/src/commands/IdentityCmd.affine new file mode 100644 index 00000000..85fed154 --- /dev/null +++ b/src/commands/IdentityCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module IdentityCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/IdentityCmd.res b/src/commands/IdentityCmd.res deleted file mode 100644 index 7e34235c..00000000 --- a/src/commands/IdentityCmd.res +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// IdentityCmd — TEA command wrappers for identity snapshot operations. - -let invoke = RuntimeBridge.invoke - -/// Capture a new identity snapshot with the given name and state payloads. -let captureSnapshot = ( - name: string, - panllState: string, - settingsJson: string, - serviceUrls: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "identity_save", - { - "name": name, - "panll_state": panllState, - "settings": settingsJson, - "service_urls": serviceUrls, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Identity capture failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load a full identity snapshot by ID. -let loadSnapshot = (id: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("identity_load", {"id": id}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Identity load failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all available identity snapshots (metadata only). -let listSnapshots = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("identity_list", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Identity list failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete an identity snapshot by ID. -let deleteSnapshot = (id: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("identity_delete", {"id": id}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Identity delete failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Broadcast an identity snapshot to team members via Burble. -let broadcastSnapshot = (snapshotJson: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("team_broadcast_state", {"snapshot": snapshotJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Team broadcast failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/InterfacesCmd.affine b/src/commands/InterfacesCmd.affine new file mode 100644 index 00000000..5b8eaee0 --- /dev/null +++ b/src/commands/InterfacesCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module InterfacesCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/InterfacesCmd.res b/src/commands/InterfacesCmd.res deleted file mode 100644 index 23469675..00000000 --- a/src/commands/InterfacesCmd.res +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Interfaces Commands — Backend wrappers for ABI/FFI scanning. -/// -/// Scans src/abi/ for Idris2 definitions and ffi/zig/ for implementations. - -let invoke = RuntimeBridge.invoke - -/// Scan ABI/FFI definitions and binding coverage. -let scanInterfaces = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("interfaces_scan", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Interface scan failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/K9Cmd.affine b/src/commands/K9Cmd.affine new file mode 100644 index 00000000..c082565b --- /dev/null +++ b/src/commands/K9Cmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module K9Cmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/K9Cmd.res b/src/commands/K9Cmd.res deleted file mode 100644 index aa7777ab..00000000 --- a/src/commands/K9Cmd.res +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL K9 Commands — backend invoke wrappers for K9 contractile operations. -/// These call into the Rust backend at src-gossamer/src/k9/commands.rs which -/// handles filesystem access for loading, validating, and applying K9 -/// contractile files (.k9.ncl). -/// -/// The Rust backend reads files and returns content as JSON strings. The -/// ReScript K9Engine then handles the actual parsing and validation logic -/// on the client side for maximum testability. - -let invoke = RuntimeBridge.invoke - -/// Load a K9 contractile file from disk. Returns the raw file content -/// as a JSON-wrapped string for client-side parsing by K9Engine. -let loadContractile = (path: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("k9_load_contractile", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to load K9 contractile: ${path}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Validate a K9 contractile file on the backend. The Rust side performs -/// basic structural checks (file exists, non-empty, valid encoding, K9 -/// magic header presence) and returns a JSON validation result. Deeper -/// semantic validation (security level, pedigree checks) is done -/// client-side by K9Engine.validateContractile. -let validateContractileFile = (path: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("k9_validate", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to validate K9 contractile: ${path}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Apply a K9 layout preset by name. The Rust backend locates the layout -/// file in the `layouts/` directory, reads it, and returns the parsed -/// layout configuration as JSON. The client side then applies the panel -/// arrangement to the PanLL workspace. -let applyLayout = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("k9_apply_layout", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to apply K9 layout: ${name}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/LevelArchitectCmd.affine b/src/commands/LevelArchitectCmd.affine new file mode 100644 index 00000000..26f8b968 --- /dev/null +++ b/src/commands/LevelArchitectCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LevelArchitectCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/LevelArchitectCmd.res b/src/commands/LevelArchitectCmd.res deleted file mode 100644 index bdb0b012..00000000 --- a/src/commands/LevelArchitectCmd.res +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Level Architect Commands — backend invoke wrappers for level -/// file I/O, asset browsing, level validation, and LevelConfig export. - -let invoke = RuntimeBridge.invoke - -/// Load a level from a JSON file. -let loadLevel = (path: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("load_level", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load level"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save the current level to a JSON file. -let saveLevel = (path: string, data: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("save_level", {"path": path, "data": data}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save level"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export the level as a LevelConfig.res source file. -let exportLevelConfig = (data: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("export_level_config", {"data": data}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export LevelConfig"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Browse available game assets. -let browseAssets = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("browse_level_assets", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to browse assets"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Validate the current level design. -let validateLevel = (data: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("validate_level", {"data": data}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to validate level"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/LlmCodingCmd.affine b/src/commands/LlmCodingCmd.affine new file mode 100644 index 00000000..c7aaa654 --- /dev/null +++ b/src/commands/LlmCodingCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LlmCodingCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/LlmCodingCmd.res b/src/commands/LlmCodingCmd.res deleted file mode 100644 index dc8467f8..00000000 --- a/src/commands/LlmCodingCmd.res +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL LLM Coding command wrappers — invoke bridge for spawning -/// and managing LLM coding sessions. -/// -/// All commands invoke backend commands registered in -/// src-gossamer/src/llm_coding/commands.rs. - -let invoke = RuntimeBridge.invoke - -// ============================================================================ -// Session Management -// ============================================================================ - -/// Fetch all sessions with updated resource stats. -let listSessions = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_list_sessions", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list sessions"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Spawn a new Claude coding session. -let spawnSession = ( - name: string, - workDir: string, - taskList: string, - allowedRepos: array, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "llm_coding_spawn", - { - "name": name, - "work_dir": workDir, - "task_list": taskList, - "allowed_repos": allowedRepos, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to spawn session"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Freeze (SIGSTOP) a session. -let freezeSession = (sessionId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_freeze", {"session_id": sessionId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to freeze session"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Thaw (SIGCONT) a session. -let thawSession = (sessionId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_thaw", {"session_id": sessionId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to thaw session"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Kill (terminate) a session. -let killSession = (sessionId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_kill", {"session_id": sessionId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to kill session"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Resource & Lock Queries -// ============================================================================ - -/// Fetch system resource snapshot. -let systemResources = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_system_resources", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch system resources"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch workspace locks. -let listLocks = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_list_locks", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list locks"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch cross-session messages. -let listMessages = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("llm_coding_list_messages", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list messages"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/MassPanicCmd.affine b/src/commands/MassPanicCmd.affine new file mode 100644 index 00000000..2661feac --- /dev/null +++ b/src/commands/MassPanicCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MassPanicCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/MassPanicCmd.res b/src/commands/MassPanicCmd.res deleted file mode 100644 index 0b5f6022..00000000 --- a/src/commands/MassPanicCmd.res +++ /dev/null @@ -1,246 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Mass Panic command wrappers — invoke bridge for the -/// organisation-scale batch scanning panel (assemblyline + incremental -/// BLAKE3 + verisim + delta reporting + notifications). -/// -/// All commands invoke panic-attack assemblyline via the backend. -/// Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Discover git repos in a directory. -/// Returns JSON array of { path, name, has_git } objects. -let discoverRepos = (directory: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("mass_panic_discover_repos", {"directory": directory}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to discover repos — check directory path"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run assemblyline scan on selected repos. -/// Accepts configuration for incremental scanning, cache, storage, and filtering. -/// Returns JSON with per-repo results and aggregate summary. -let runAssemblyline = ( - directory: string, - incremental: bool, - cachePath: option, - storePath: option, - minFindings: int, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "mass_panic_run_assemblyline", - { - "directory": directory, - "incremental": incremental, - "cache_path": cachePath, - "store_path": storePath, - "min_findings": minFindings, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Assemblyline scan failed — is panic-attack installed?"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get scan progress (polled during long-running assemblyline scans). -/// Returns JSON with { repos_done, repos_total, current_repo, elapsed_seconds }. -let getProgress = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mass_panic_get_progress", Dict.make()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch scan progress"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Diff two assemblyline reports (delta reporting). -/// Returns JSON array of { repo, new_findings, fixed_findings, direction }. -let diffReports = ( - leftPath: string, - rightPath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mass_panic_diff_reports", {"left_path": leftPath, "right_path": rightPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to compare reports"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Generate notification summary (markdown + optional GitHub issues). -/// Returns the markdown content as a string. -let generateNotification = ( - reportPath: string, - criticalOnly: bool, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "mass_panic_generate_notification", - {"report_path": reportPath, "critical_only": criticalOnly}, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to generate notification"))) - Promise.resolve() - }) - ->ignore - }) -} - -// --------------------------------------------------------------------------- -// Imaging — fNIRS-style spatial health map -// --------------------------------------------------------------------------- - -/// Build a system image from an assemblyline scan. -/// Runs assemblyline internally, then builds the fNIRS-style image. -/// Returns panll.system-image.v0 JSON. -let buildImage = ( - directory: string, - incremental: bool, - cachePath: option, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "mass_panic_build_image", - { - "directory": directory, - "incremental": incremental, - "cache_path": cachePath, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to build system image"))) - Promise.resolve() - }) - ->ignore - }) -} - -// --------------------------------------------------------------------------- -// Temporal — time-series navigation -// --------------------------------------------------------------------------- - -/// List temporal snapshots in VeriSimDB. -/// Returns JSON array of snapshot entries. -let listSnapshots = (verisimdbDir: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("mass_panic_list_snapshots", {"verisim_dir": verisimdbDir}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list temporal snapshots"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Diff two temporal snapshots. -/// Returns panll.temporal-diff.v0 JSON. -let diffSnapshots = ( - verisimdbDir: string, - fromSeq: int, - toSeq: int, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "mass_panic_diff_snapshots", - {"verisim_dir": verisimdbDir, "from_seq": fromSeq, "to_seq": toSeq}, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to diff temporal snapshots"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Take a temporal snapshot of the current image. -/// Returns snapshot entry JSON. -let takeSnapshot = ( - verisimdbDir: string, - label: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mass_panic_take_snapshot", {"verisim_dir": verisimdbDir, "label": label}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to take temporal snapshot"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load the BLAKE3 fingerprint cache (shows which repos have changed). -/// Returns JSON with { cached_repos, total_entries }. -let loadCache = (cachePath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mass_panic_load_cache", {"cache_path": cachePath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load fingerprint cache"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/MinterCmd.affine b/src/commands/MinterCmd.affine new file mode 100644 index 00000000..f3496c6b --- /dev/null +++ b/src/commands/MinterCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MinterCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/MinterCmd.res b/src/commands/MinterCmd.res deleted file mode 100644 index 722411c3..00000000 --- a/src/commands/MinterCmd.res +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Minter Commands — Backend command wrappers for panel scaffolding. -/// -/// The minter backend generates ReScript source files and Rust backend -/// stubs, then patches the global wiring files to register the new panel. - -let invoke = RuntimeBridge.invoke - -/// Mint a new panel from the given form data. -/// The backend generates all files and patches wiring. -/// Returns a JSON-serialised MintResult. -let mintPanel = ( - panelName: string, - shortName: string, - description: string, - icon: string, - backendKind: string, - accessibility: string, - capabilities: string, - endpoint: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "minter_mint_panel", - { - "panelName": panelName, - "shortName": shortName, - "description": description, - "icon": icon, - "backendKind": backendKind, - "accessibility": accessibility, - "capabilities": capabilities, - "endpoint": endpoint, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to mint panel"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Validate a panel name against the existing registry. -/// Returns "valid" or an error description. -let validateName = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("minter_validate_name", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Validation failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/MultiplayerMonitorCmd.affine b/src/commands/MultiplayerMonitorCmd.affine new file mode 100644 index 00000000..0373cb05 --- /dev/null +++ b/src/commands/MultiplayerMonitorCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MultiplayerMonitorCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/MultiplayerMonitorCmd.res b/src/commands/MultiplayerMonitorCmd.res deleted file mode 100644 index 8acb9afe..00000000 --- a/src/commands/MultiplayerMonitorCmd.res +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Multiplayer Monitor Commands — backend invoke wrappers for -/// connecting to the IDApTIK Phoenix sync server and reading -/// multiplayer state. - -let invoke = RuntimeBridge.invoke - -/// Connect to the Phoenix WebSocket sync server. -let connectToServer = (url: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("multiplayer_connect", {"url": url}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to connect to sync server"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Disconnect from the sync server. -let disconnectFromServer = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("multiplayer_disconnect", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to disconnect"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read the current multiplayer state (players, channels, locks). -let readMultiplayerState = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("multiplayer_read_state", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read multiplayer state"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read state diffs between local and remote. -let readStateDiffs = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("multiplayer_read_diffs", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read state diffs"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read ETS cache entries for inspection. -let readEtsCache = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("multiplayer_read_ets", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read ETS cache"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Trigger a reconnection test. -let reconnectionTest = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("multiplayer_reconnection_test", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Reconnection test failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/MyLangCmd.affine b/src/commands/MyLangCmd.affine new file mode 100644 index 00000000..986ea331 --- /dev/null +++ b/src/commands/MyLangCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MyLangCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/MyLangCmd.res b/src/commands/MyLangCmd.res deleted file mode 100644 index 068e6c13..00000000 --- a/src/commands/MyLangCmd.res +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL My-Lang Commands — Backend wrappers for the AI-native language tools. -/// -/// Invokes the my-lang CLI through backend commands. -/// The Rust backend shells out to `my compile`, `my repl`, etc. - -let invoke = RuntimeBridge.invoke - -/// Check whether the my-lang CLI binary is available. -let checkCli = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mylang_check", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("my-lang CLI not found"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Compile source code in a given dialect. Returns JSON compilation result. -let compile = (source: string, dialect: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("mylang_compile", {"source": source, "dialect": dialect}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Compilation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Connect to the my-lang LSP server. Returns connection status JSON. -let connectLsp = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mylang_lsp_connect", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("LSP connection failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Request diagnostics from the my-lang LSP for a file. -/// Sends content to the LSP and returns diagnostic JSON. -let requestDiagnostics = ( - filePath: string, - content: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("mylang_lsp_diagnostics", {"file_path": filePath, "content": content}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("LSP diagnostics request failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Send a line to the REPL. Returns the REPL output. -let replEval = (input: string, dialect: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("mylang_repl", {"input": input, "dialect": dialect}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("REPL evaluation failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/NesyDriftCmd.affine b/src/commands/NesyDriftCmd.affine new file mode 100644 index 00000000..0d0b993b --- /dev/null +++ b/src/commands/NesyDriftCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NesyDriftCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/NesyDriftCmd.res b/src/commands/NesyDriftCmd.res deleted file mode 100644 index 71472b08..00000000 --- a/src/commands/NesyDriftCmd.res +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL NeSy Drift command wrappers — invoke bridge for the -/// drift dashboard panel. -/// -/// All commands invoke BoJ cartridge endpoints for neural model drift -/// detection. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Run a drift check on all monitored models. -/// Returns JSON with per-model drift status and any new alerts. -let check = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("nesy_drift_check", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to run drift check"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch historical drift alerts. -/// Returns JSON array of drift alert objects sorted by timestamp. -let history = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("nesy_drift_history", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch drift history"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/NesyHarmonizeCmd.affine b/src/commands/NesyHarmonizeCmd.affine new file mode 100644 index 00000000..ae8ac26b --- /dev/null +++ b/src/commands/NesyHarmonizeCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NesyHarmonizeCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/NesyHarmonizeCmd.res b/src/commands/NesyHarmonizeCmd.res deleted file mode 100644 index a2419755..00000000 --- a/src/commands/NesyHarmonizeCmd.res +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL NeSy Harmonization command wrappers — invoke bridge for the -/// harmonization monitor panel. -/// -/// All commands invoke BoJ cartridge endpoints for neural-symbolic -/// harmonization data. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Fetch current harmonization entries from the BoJ NeSy cartridge. -/// Returns JSON array of harmonization entry objects. -let fetchEntries = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("nesy_harmonize_fetch_entries", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch harmonization entries"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Submit a new harmonization request to the BoJ NeSy cartridge. -/// Triggers neural-symbolic verdict fusion for the given input. -/// Returns JSON with the resulting harmonization entry. -let submit = (source: string, inputData: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("nesy_harmonize_submit", {"source": source, "input_data": inputData}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to submit harmonization request"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/NesyModesCmd.affine b/src/commands/NesyModesCmd.affine new file mode 100644 index 00000000..94523a9f --- /dev/null +++ b/src/commands/NesyModesCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NesyModesCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/NesyModesCmd.res b/src/commands/NesyModesCmd.res deleted file mode 100644 index 4de67b3d..00000000 --- a/src/commands/NesyModesCmd.res +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL NeSy Modes command wrappers — invoke bridge for the -/// reasoning mode selector panel. -/// -/// All commands invoke BoJ cartridge endpoints for reasoning mode -/// management. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Get the currently active reasoning mode. -/// Returns JSON with the mode identifier and metadata. -let getMode = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("nesy_mode_get", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get current reasoning mode"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Set the active reasoning mode. -/// Returns JSON confirming the mode switch. -let setMode = (modeId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("nesy_mode_set", {"mode_id": modeId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to set reasoning mode"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/NetworkTopologyCmd.affine b/src/commands/NetworkTopologyCmd.affine new file mode 100644 index 00000000..2bccda17 --- /dev/null +++ b/src/commands/NetworkTopologyCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NetworkTopologyCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/NetworkTopologyCmd.res b/src/commands/NetworkTopologyCmd.res deleted file mode 100644 index c466d56d..00000000 --- a/src/commands/NetworkTopologyCmd.res +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Network Topology Commands — backend invoke wrappers for reading -/// the in-game network topology from the running IDApTIK instance. - -let invoke = RuntimeBridge.invoke - -/// Read the current network topology from the running game. -let readTopology = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("read_network_topology", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read network topology"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read DNS resolution table from the game. -let readDnsTable = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("read_dns_table", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read DNS table"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export the topology as SVG. -let exportSvg = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("export_topology_svg", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export SVG"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read packet flow events from the game. -let readPacketFlow = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("read_packet_flow", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read packet flow"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ObservabilityCmd.affine b/src/commands/ObservabilityCmd.affine new file mode 100644 index 00000000..d80a3ddf --- /dev/null +++ b/src/commands/ObservabilityCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ObservabilityCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ObservabilityCmd.res b/src/commands/ObservabilityCmd.res deleted file mode 100644 index b35f7921..00000000 --- a/src/commands/ObservabilityCmd.res +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// ObservabilityCmd — backend invoke wrappers for the observe-mcp BoJ cartridge. -/// -/// Routes SARIF export and OpenTelemetry trace collection through the -/// observe-mcp cartridge backend (src-gossamer/src/observability/commands.rs), -/// enabling BoJ-routed observability when bojRouting is on. -/// -/// All commands use `Tea_Cmd.call` for async backend invocations, matching -/// the pattern established in BojCmd.res and PanicAttackCmd.res. - -let invoke = RuntimeBridge.invoke - -/// Export a panic-attack report as SARIF via the observe-mcp cartridge. -/// -/// This routes through the BoJ observe-mcp cartridge rather than directly -/// to the panic-attack backend, enabling centralised observability tracking. -/// -/// The backend command `observe_export_sarif` accepts a report ID and -/// returns the SARIF JSON or an error. -let exportSarifViaObserveMcp = ( - reportId: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("observe_export_sarif", {"report_id": reportId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export SARIF via observe-mcp"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export OpenTelemetry trace spans via the observe-mcp cartridge. -/// -/// The `batch` parameter should be an OTLP JSON string produced by -/// `ObservabilityEngine.exportTraceBatch`. The backend forwards this -/// to the configured collector endpoint and returns an acceptance count. -let exportOtelTraces = (batch: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("observe_export_traces", {"batch": batch}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export OTLP traces via observe-mcp"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fetch an observability summary from the observe-mcp cartridge. -/// -/// Returns JSON with trace count, span count, active exporters, and -/// collector health — used to populate the BoJ observability dashboard. -let fetchObservabilitySummary = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("observe_summary", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to fetch observability summary"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/PanicAttackCmd.affine b/src/commands/PanicAttackCmd.affine new file mode 100644 index 00000000..4fe1d75e --- /dev/null +++ b/src/commands/PanicAttackCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PanicAttackCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/PanicAttackCmd.res b/src/commands/PanicAttackCmd.res deleted file mode 100644 index 8089da7c..00000000 --- a/src/commands/PanicAttackCmd.res +++ /dev/null @@ -1,146 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL panic-attack command wrappers — invoke bridge for -/// the panic-attack stress testing and weak point analysis panel. -/// -/// All commands invoke the panic-attack binary via the backend, -/// returning JSON results. Uses `Tea_Cmd.call` for async operations. - -let invoke = RuntimeBridge.invoke - -/// Check panic-attack capability (is the binary available? what mode?). -let checkCapability = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("check_panic_attacker_capability", Dict.make()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to probe panic-attack capability"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run a static analysis scan (assail) on a target directory. -/// Returns JSON with weak points, statistics, and recommendations. -let assail = (targetPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_assail", {"target_path": targetPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Scan failed — is panic-attack installed?"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run a full assault (static analysis + stress testing) on a target. -/// Returns JSON with combined assail + attack results. -let assault = (targetPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_assault", {"target_path": targetPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Assault scan failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// View a saved report by path. -let viewReport = (reportPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_view_report", {"report_path": reportPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load report"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Compare two reports (diff). -let diffReports = ( - leftPath: string, - rightPath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_diff", {"left_path": leftPath, "right_path": rightPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to compare reports"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List saved scan reports. -let listReports = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_list_reports", Dict.make()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list reports"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export a report as SARIF format for GitHub Security tab. -let exportSarif = (reportPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_export_sarif", {"report_path": reportPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export SARIF"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export a report as PanLL event-chain model. -let exportEventChain = (reportPath: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("panic_attack_export_panll", {"report_path": reportPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export event chain"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/PlaygroundsCmd.affine b/src/commands/PlaygroundsCmd.affine new file mode 100644 index 00000000..ed528917 --- /dev/null +++ b/src/commands/PlaygroundsCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PlaygroundsCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/PlaygroundsCmd.res b/src/commands/PlaygroundsCmd.res deleted file mode 100644 index f2ea1f30..00000000 --- a/src/commands/PlaygroundsCmd.res +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Playgrounds Commands — Backend wrappers for code execution. -/// -/// Connects to the NQC proxy at :4000 for VCL/KQL/GQL queries. - -let invoke = RuntimeBridge.invoke - -/// Execute a query through the NQC proxy. -let executeQuery = ( - language: string, - code: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("playgrounds_execute", {"language": language, "code": code}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Execution failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/PlazaCmd.affine b/src/commands/PlazaCmd.affine new file mode 100644 index 00000000..4d7d650b --- /dev/null +++ b/src/commands/PlazaCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PlazaCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/PlazaCmd.res b/src/commands/PlazaCmd.res deleted file mode 100644 index 33e22e69..00000000 --- a/src/commands/PlazaCmd.res +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Palimpsest Plaza Commands — Backend command wrappers for the -/// PMPL licensing panel. -/// -/// Each function wraps a backend `invoke` call for license compliance -/// scanning, adoption statistics, and compatibility checking. - -let invoke = RuntimeBridge.invoke - -/// Scan a single repository for PMPL compliance indicators. -/// Returns JSON with license detection, SPDX header counts, exhibit status. -let scanRepo = (repoName: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("plaza_scan_repo", {"repo_name": repoName}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to scan repo: ${repoName}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Compute adoption statistics across the entire ecosystem. -/// Scans all repos under the canonical path and returns aggregate counts. -let adoptionStats = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("plaza_adoption_stats", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to compute adoption statistics"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check PMPL compatibility with another license. -/// Returns JSON with compatible (bool) and notes. -let checkCompatibility = (license: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("plaza_check_compatibility", {"license": license}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Compatibility check failed for: ${license}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ProtocolSquisherCmd.affine b/src/commands/ProtocolSquisherCmd.affine new file mode 100644 index 00000000..60c5b423 --- /dev/null +++ b/src/commands/ProtocolSquisherCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProtocolSquisherCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ProtocolSquisherCmd.res b/src/commands/ProtocolSquisherCmd.res deleted file mode 100644 index b2d54343..00000000 --- a/src/commands/ProtocolSquisherCmd.res +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Protocol-Squisher Commands — Backend wrappers for format analysis. -/// -/// Invokes the protocol-squisher CLI through backend commands. -/// The Rust backend shells out to `protocol-squisher analyze`, `compare`, etc. - -let invoke = RuntimeBridge.invoke - -/// Check whether the protocol-squisher CLI binary is available. -let checkCli = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("protocol_squisher_check", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("protocol-squisher CLI not found"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Analyse a schema file. Returns JSON analysis result. -let analyse = (filePath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("protocol_squisher_analyze", {"file_path": filePath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Schema analysis failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Compare two schema files for compatibility. Returns JSON comparison result. -let compare = ( - leftPath: string, - rightPath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("protocol_squisher_compare", {"left_path": leftPath, "right_path": rightPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Schema comparison failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ProvenanceCmd.affine b/src/commands/ProvenanceCmd.affine new file mode 100644 index 00000000..d3ef40fe --- /dev/null +++ b/src/commands/ProvenanceCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProvenanceCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ProvenanceCmd.res b/src/commands/ProvenanceCmd.res deleted file mode 100644 index 590483df..00000000 --- a/src/commands/ProvenanceCmd.res +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ProvenanceCmd — Backend command wrappers for git blame provenance analysis. -/// -/// The Rust backend runs `git blame --porcelain` on a file and enriches each -/// region with Co-Authored-By trailer parsing. Results come back as JSON that -/// the Update layer parses into `provenanceRegion` arrays. -/// -/// Pattern: `commandName(args..., tagger) => Tea_Cmd.t<'msg>` - -let invoke = RuntimeBridge.invoke - -/// Analyse a file's provenance via git blame + Co-Authored-By parsing. -/// -/// The Rust backend runs `git blame --porcelain ` in the repo root, -/// parses the output, and returns a JSON array of blame regions with -/// author, co-author, and commit metadata. -let analyseFile = ( - repoPath: string, - filePath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("provenance_analyse_file", {"repoPath": repoPath, "filePath": filePath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to analyse provenance: ${filePath}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Scan a file for unsound markers (believe_me, sorry, Admitted, assert_total). -/// -/// Returns a JSON object with counts per marker type. Used to validate that -/// regions marked as Verified actually contain no proof-undermining patterns. -let scanUnsoundMarkers = (filePath: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("provenance_scan_unsound", {"filePath": filePath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to scan for unsound markers: ${filePath}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ProvisionerCmd.affine b/src/commands/ProvisionerCmd.affine new file mode 100644 index 00000000..85f9d2e5 --- /dev/null +++ b/src/commands/ProvisionerCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProvisionerCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ProvisionerCmd.res b/src/commands/ProvisionerCmd.res deleted file mode 100644 index cc2ccb50..00000000 --- a/src/commands/ProvisionerCmd.res +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ProvisionerCmd — Backend command wrappers for portfolio provisioning. -/// -/// Handles panel installation (native or containerised), configuration -/// persistence, and portfolio management. Container operations route through -/// Stapeln when available, falling back to direct Podman commands. - -let invoke = RuntimeBridge.invoke - -/// Install a panel. For native panels this is a no-op (they're built in). -/// For podded panels, this pulls/builds the container image. -let installPanel = ( - panelName: string, - isolation: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("provisioner_install_panel", {"panelName": panelName, "isolation": isolation}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to install panel: ${panelName}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Remove a panel. For native panels, just disables it. For podded panels, -/// deletes the container and all its data — clean uninstall, everything gone. -let removePanel = (panelName: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("provisioner_remove_panel", {"panelName": panelName}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to remove panel: ${panelName}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save panel configuration to persistent storage. -let saveConfig = (configJson: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("provisioner_save_config", {"config": configJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save panel configuration"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load panel configuration from persistent storage. -let loadConfig = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("provisioner_load_config", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load panel configuration"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ReleaseManagerCmd.affine b/src/commands/ReleaseManagerCmd.affine new file mode 100644 index 00000000..50295297 --- /dev/null +++ b/src/commands/ReleaseManagerCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ReleaseManagerCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ReleaseManagerCmd.res b/src/commands/ReleaseManagerCmd.res deleted file mode 100644 index 9fe5bd64..00000000 --- a/src/commands/ReleaseManagerCmd.res +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Release Manager Commands — backend invoke wrappers for versioning, -/// changelog generation, artifact building, and distribution. - -let invoke = RuntimeBridge.invoke - -/// Generate a changelog from git history. -let generateChangelog = (fromVersion: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("release_generate_changelog", {"fromVersion": fromVersion}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to generate changelog"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Build artifacts for the specified platforms. -let buildArtifacts = ( - version: string, - platforms: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("release_build_artifacts", {"version": version, "platforms": platforms}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Artifact build failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Publish a release. -let publishRelease = ( - version: string, - channel: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("release_publish", {"version": version, "channel": channel}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to publish release"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read release history. -let readReleases = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("release_read_history", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read releases"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Bump the version number. -let bumpVersion = (bumpType: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("release_bump_version", {"bumpType": bumpType}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to bump version"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/RepoLoaderCmd.affine b/src/commands/RepoLoaderCmd.affine new file mode 100644 index 00000000..d115295d --- /dev/null +++ b/src/commands/RepoLoaderCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RepoLoaderCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/RepoLoaderCmd.res b/src/commands/RepoLoaderCmd.res deleted file mode 100644 index 90c6af7a..00000000 --- a/src/commands/RepoLoaderCmd.res +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Repo Loader Commands — Backend command wrappers for the repo loading panel. -/// -/// Each function wraps a backend `invoke` call in a `Tea_Cmd.call`, converting -/// the Promise-based IPC into the TEA command model. - -let invoke = RuntimeBridge.invoke - -let openDialog = RuntimeBridge.Dialog.openDialog - -/// Scan a repository directory and return info + panel suggestions. -let scan = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("repoloader_scan", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to scan repo: ${repoPath}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save panel configuration to PANELS.a2ml in the repo. -let savePanels = ( - repoPath: string, - panelsJson: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("repoloader_save_panels", {"repoPath": repoPath, "panelsJson": panelsJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save panel configuration"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List recently loaded repositories. -let listRecent = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("repoloader_list_recent", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load recent repos"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Search the git-private-farm for repos matching a query. -let searchFarm = (query: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("repoloader_search_farm", {"query": query}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to search farm for: ${query}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Open a directory picker dialog for the user to select a repo. -let pickDirectory = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let options: JSON.t = %raw(`({ directory: true, multiple: false, title: "Select Repository" })`) - openDialog(options) - ->Promise.then(result => { - switch Nullable.toOption(result) { - | Some(value) => switch JSON.Classify.classify(value) { - | String(path) => callbacks.enqueue(tagger(Ok(path))) - | _ => callbacks.enqueue(tagger(Error("Unexpected dialog result type"))) - } - | None => callbacks.enqueue(tagger(Error("No directory selected"))) - } - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Directory selection failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ReposystemCmd.affine b/src/commands/ReposystemCmd.affine new file mode 100644 index 00000000..8b4b74de --- /dev/null +++ b/src/commands/ReposystemCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ReposystemCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ReposystemCmd.res b/src/commands/ReposystemCmd.res deleted file mode 100644 index 840a2aa9..00000000 --- a/src/commands/ReposystemCmd.res +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Reposystem Commands — Backend wrappers for RSR compliance scanning. -/// -/// The reposystem backend scans local repo directories for required files. - -let invoke = RuntimeBridge.invoke - -/// Scan all repos for RSR compliance. -let scanAll = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("reposystem_scan_all", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("RSR scan failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ScriptGistCmd.affine b/src/commands/ScriptGistCmd.affine new file mode 100644 index 00000000..e3ab63a4 --- /dev/null +++ b/src/commands/ScriptGistCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ScriptGistCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ScriptGistCmd.res b/src/commands/ScriptGistCmd.res deleted file mode 100644 index 8b025ffc..00000000 --- a/src/commands/ScriptGistCmd.res +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ScriptGist Commands — backend invoke wrappers for gist persistence, -/// execution dispatch, and diachronic snapshot restoration. -/// -/// Routes to Rust backend at src-gossamer/src/script_gist/commands.rs which -/// handles filesystem I/O and target dispatch. - -let invoke = RuntimeBridge.invoke - -/// Save a gist to persistent storage (`~/.panll/gists/.json`). -/// The gist is serialised as a JSON string on the frontend side. -let saveGist = (gistJson: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("script_gist_save", {"gistJson": gistJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save gist"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Execute a gist by dispatching to its target backend. -/// Returns a JSON string representing a gistResult. -let executeGist = (gistJson: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("script_gist_execute", {"gistJson": gistJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Gist execution failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Restore a diachronic checkpoint by deserialising the snapshot. -/// Returns the validated scriptGistState JSON for the frontend to parse. -let restoreSnapshot = (snapshotJson: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("script_gist_restore_snapshot", {"snapshotJson": snapshotJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Snapshot restoration failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all saved gist files from persistent storage. -let listGists = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("script_gist_list", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list gists"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/SecurityCmd.affine b/src/commands/SecurityCmd.affine new file mode 100644 index 00000000..876bb270 --- /dev/null +++ b/src/commands/SecurityCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SecurityCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/SecurityCmd.res b/src/commands/SecurityCmd.res deleted file mode 100644 index f3e5452a..00000000 --- a/src/commands/SecurityCmd.res +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Security Commands — IPC wrappers for redaction, vault, -/// 2FA, and Trustfile operations (DD-026, DD-027). -/// -/// Each function returns a Tea_Cmd that dispatches a result message -/// back into the update loop via `callbacks.enqueue`. - -open Msg - -/// Backend invoke binding via RuntimeBridge. -let invoke = RuntimeBridge.invoke - -/// Redact secrets from text using backend regex patterns. -let redactText = (text: string, panelId: string, patternsJson: string): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke( - "redact_text", - { - "text": text, - "panelId": panelId, - "patternsJson": patternsJson, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(Security(RedactionResult(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Security(RedactionResult(Error("Failed to redact text")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Store a secret in the vault. -let vaultStore = (key: string, value: string): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke( - "vault_store", - { - "key": key, - "value": value, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(Security(VaultStoreResult(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Security(VaultStoreResult(Error("Failed to store in vault")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Retrieve a secret from the vault. -let vaultRetrieve = (key: string): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("vault_retrieve", {"key": key}) - ->Promise.then(result => { - callbacks.enqueue(Security(VaultRetrieveResult(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Security(VaultRetrieveResult(Error("Failed to retrieve from vault")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all keys in the vault. -let vaultList = (): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("vault_list", ()) - ->Promise.then(result => { - callbacks.enqueue(Security(VaultListResult(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Security(VaultListResult(Error("Failed to list vault keys")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load and parse a Trustfile from the given repo path. -let loadTrustfile = (repoPath: string): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("load_trustfile", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(Security(TrustfileLoaded(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Security(TrustfileLoaded(Error("Failed to load Trustfile")))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ServiceCmd.affine b/src/commands/ServiceCmd.affine new file mode 100644 index 00000000..5f4c01fa --- /dev/null +++ b/src/commands/ServiceCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ServiceCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ServiceCmd.res b/src/commands/ServiceCmd.res deleted file mode 100644 index 84252f73..00000000 --- a/src/commands/ServiceCmd.res +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// ServiceCmd — TEA command wrappers for the PanLL service registry. -/// -/// Provides side-effectful commands that invoke service registry operations -/// through the Gossamer backend. Each command follows the standard pattern: -/// `Tea_Cmd.call` + `RuntimeBridge.invoke` + `Promise.then/catch`. - -let invoke = RuntimeBridge.invoke - -/// Refresh health status of all registered services. -/// -/// Returns the full registry as JSON on success. -let refreshAll = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("service_status_all", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Service registry refresh failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check health of a single service by key. -/// -/// Returns the updated service entry as JSON on success. -let checkService = (serviceKey: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("service_status", {"service_key": serviceKey}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Health check failed for " ++ serviceKey))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Update the URL of a registered service. -/// -/// Resets the service status to Stopped after URL change. -let updateServiceUrl = ( - serviceKey: string, - url: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("service_update_url", {"service_key": serviceKey, "url": url}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("URL update failed for " ++ serviceKey))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load the current registry snapshot from the backend. -let getRegistry = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("service_registry_get", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Registry load failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/SettingsCmd.affine b/src/commands/SettingsCmd.affine new file mode 100644 index 00000000..fb129cee --- /dev/null +++ b/src/commands/SettingsCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SettingsCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/SettingsCmd.res b/src/commands/SettingsCmd.res deleted file mode 100644 index f22a6451..00000000 --- a/src/commands/SettingsCmd.res +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// SettingsCmd — TEA command wrappers for PanLL settings operations. - -let invoke = RuntimeBridge.invoke - -/// Load all settings from the backend. -let getSettings = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("settings_get", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Settings load failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Set a single setting by key. -let setSetting = ( - key: string, - value: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("settings_set", {"key": key, "value": value}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Setting update failed for " ++ key))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Save all settings as a complete JSON blob. -let saveAllSettings = (settingsJson: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("settings_save", {"settings": settingsJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Settings save failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/StapelnCmd.affine b/src/commands/StapelnCmd.affine new file mode 100644 index 00000000..a4132108 --- /dev/null +++ b/src/commands/StapelnCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module StapelnCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/StapelnCmd.res b/src/commands/StapelnCmd.res deleted file mode 100644 index c941b37e..00000000 --- a/src/commands/StapelnCmd.res +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Stapeln Commands — backend invoke wrappers for the container -/// assembly pipeline. These call into the Rust backend which proxies -/// to the Stapeln server API (default http://localhost:8420/api/v1). - -let invoke = RuntimeBridge.invoke - -/// Connect to the stapeln backend and check availability. -let connect = (url: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("stapeln_health", {"url": url}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Cannot reach stapeln backend"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Request validation of the current assembly from the backend. -let requestValidation = (url: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("stapeln_validate", {"url": url}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Validation request failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Request artifact generation from the backend. -let requestGenerate = ( - url: string, - format: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("stapeln_generate", {"url": url, "format": format}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Artifact generation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Refresh pipeline status from the backend. -let refreshStatus = (url: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("stapeln_status", {"url": url}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Status refresh failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/SystemUpdateCmd.affine b/src/commands/SystemUpdateCmd.affine new file mode 100644 index 00000000..7da6120f --- /dev/null +++ b/src/commands/SystemUpdateCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SystemUpdateCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/SystemUpdateCmd.res b/src/commands/SystemUpdateCmd.res deleted file mode 100644 index 880916b1..00000000 --- a/src/commands/SystemUpdateCmd.res +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// SystemUpdateCmd — TEA command wrappers for system update backend operations. -/// -/// Each function wraps a backend command handler from -/// `src-gossamer/src/system_update/commands.rs`, using the `Tea_Cmd.call` pattern -/// to bridge async backend invocations into the TEA update loop. -/// -/// Pattern: -/// 1. Call `invoke("system_update_*", params)` → returns Promise -/// 2. On success: `callbacks.enqueue(tagger(Ok(jsonString)))` -/// 3. On failure: `callbacks.enqueue(tagger(Error(errorMessage)))` - -/// Backend invoke binding via RuntimeBridge. -let invoke = RuntimeBridge.invoke - -// ============================================================================ -// Component listing -// ============================================================================ - -/// List all updatable components with current/latest versions. -/// Returns: JSON array of component objects. -let listComponents = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_list_components", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list system components"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Update checking -// ============================================================================ - -/// Check all components for available updates. -/// Returns: JSON with summary and components array. -let checkAll = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_check_all", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to check for updates"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check a single component for updates by ID. -/// Returns: JSON component object. -let checkComponent = (componentId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_check_component", {"component_id": componentId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to check component: " ++ componentId))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Update application -// ============================================================================ - -/// Apply update to a single component by ID. -/// Returns: JSON with success/output fields. -let applyComponent = (componentId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_apply_component", {"component_id": componentId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to apply update: " ++ componentId))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Apply all available updates in sequence. -/// Returns: JSON with success/applied/failed/summary fields. -let applyAll = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_apply_all", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to apply all updates"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// asdf details -// ============================================================================ - -/// Get detailed asdf plugin status (all 33+ plugins). -/// Returns: JSON array of {plugin, installed, latest}. -let asdfStatus = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_asdf_status", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get asdf status"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// Logs and summary -// ============================================================================ - -/// Get update log history. -/// Returns: JSON array of {timestamp, summary}. -let logs = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_logs", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get update logs"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get last update run summary. -/// Returns: JSON with summary text. -let lastSummary = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("system_update_last_summary", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get last summary"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/TentaclesCmd.affine b/src/commands/TentaclesCmd.affine new file mode 100644 index 00000000..5ebfd3c6 --- /dev/null +++ b/src/commands/TentaclesCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TentaclesCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/TentaclesCmd.res b/src/commands/TentaclesCmd.res deleted file mode 100644 index 2964f744..00000000 --- a/src/commands/TentaclesCmd.res +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Tentacles Commands — backend invoke wrappers for the ECHIDNA FFI bridge. -/// -/// These call into the Rust backend which proxies to the ECHIDNA V-lang REST -/// adapters. Used for "without" mode — agents operating through the FFI/ABI -/// layer rather than embedded TEA state. - -let invoke = RuntimeBridge.invoke - -/// Check ECHIDNA FFI bridge health. -let checkFfiBridge = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("tentacles_ffi_health", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ECHIDNA FFI bridge unreachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Send a task to a specific agent via the FFI bridge. -let sendAgentTask = ( - agentId: string, - task: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("tentacles_agent_task", {"agent": agentId, "task": task}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to dispatch agent task"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Poll for events from the ECHIDNA FFI event stream. -let pollEvents = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("tentacles_poll_events", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to poll FFI events"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/TimelineCmd.affine b/src/commands/TimelineCmd.affine new file mode 100644 index 00000000..afacba9d --- /dev/null +++ b/src/commands/TimelineCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TimelineCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/TimelineCmd.res b/src/commands/TimelineCmd.res deleted file mode 100644 index 217badbd..00000000 --- a/src/commands/TimelineCmd.res +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Code MRI — Timeline Commands (Layer 2) -/// -/// backend invoke wrappers for VeriSimDB-backed development timeline persistence. -/// The Rust backend manages the VeriSimDB connection, stores timeline snapshots, -/// and retrieves historical data for the "time machine" scrubber. -/// -/// Command pattern follows PanLL convention: -/// `commandName(args..., tagger) => Tea_Cmd.t<'msg>` -/// -/// DESIGN NOTE: Snapshots are captured on commit hooks (via the Watcher panel) -/// and on-demand via the Code MRI dashboard. The backend aggregates metrics -/// from git, panic-attack findings, Vexometer readings, and .mri.json tag -/// counts into a single TimelineEngine.timelineSnapshot struct. - -let invoke = RuntimeBridge.invoke - -/// Connect to the VeriSimDB timeline database for the current repo. -/// -/// Creates the database file if it doesn't exist. The path is derived from -/// the repo root: `/.panll/timeline.verisim`. -/// -/// @param repoPath Root path of the repository -/// @param tagger Callback receiving Ok(dbPath) or Error(message) -/// @returns TEA command that initiates the connection -let connect = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("timeline_connect", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to connect to timeline database"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Capture a new timeline snapshot for the current repo state. -/// -/// The Rust backend gathers metrics from: -/// - git (lines of code, commit hash) -/// - panic-attack (finding count) -/// - .mri.json sidecars (tag count) -/// - Vexometer state (friction reading) -/// - AI attribution from provenance data -/// -/// The snapshot is stored in VeriSimDB and returned as JSON. -/// -/// @param repoPath Root path of the repository -/// @param tagger Callback receiving Ok(snapshotJson) or Error(message) -/// @returns TEA command that triggers snapshot capture -let captureSnapshot = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("timeline_capture_snapshot", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to capture timeline snapshot"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load all timeline snapshots from VeriSimDB for the current repo. -/// -/// Returns a JSON array of snapshot objects, ordered oldest-first. -/// The caller should parse these into TimelineEngine.timelineSnapshot values. -/// -/// @param repoPath Root path of the repository -/// @param tagger Callback receiving Ok(snapshotsJson) or Error(message) -/// @returns TEA command that loads the timeline history -let loadHistory = (repoPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("timeline_load_history", {"repoPath": repoPath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load timeline history"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Query timeline snapshots within a date range. -/// -/// Returns snapshots where timestamp is between startDate and endDate -/// (inclusive, ISO 8601 format). Useful for zoomed timeline views. -/// -/// @param repoPath Root path of the repository -/// @param startDate ISO 8601 start date (inclusive) -/// @param endDate ISO 8601 end date (inclusive) -/// @param tagger Callback receiving Ok(snapshotsJson) or Error(message) -/// @returns TEA command that queries the range -let queryRange = ( - repoPath: string, - startDate: string, - endDate: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "timeline_query_range", - { - "repoPath": repoPath, - "startDate": startDate, - "endDate": endDate, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to query timeline range"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export the full timeline as a JSON file for external analysis. -/// -/// Writes a standalone JSON file to the specified path, containing all -/// snapshots and computed metrics. This file can be consumed by Hypatia -/// for pattern analysis or by external tools. -/// -/// @param repoPath Root path of the repository -/// @param outputPath Path where the exported JSON file will be written -/// @param tagger Callback receiving Ok(outputPath) or Error(message) -/// @returns TEA command that triggers the export -let exportTimeline = ( - repoPath: string, - outputPath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "timeline_export", - { - "repoPath": repoPath, - "outputPath": outputPath, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export timeline"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/TsdmCmd.affine b/src/commands/TsdmCmd.affine new file mode 100644 index 00000000..052cd755 --- /dev/null +++ b/src/commands/TsdmCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TsdmCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/TsdmCmd.res b/src/commands/TsdmCmd.res deleted file mode 100644 index b17a1f32..00000000 --- a/src/commands/TsdmCmd.res +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL TSDM command wrappers — persistence for the Triaxial Software -/// Development Methodology directive panel. -/// -/// The TSDM panel is a directive panel — it stores user preferences for -/// axis ordering, tier priorities, and cleanup steps. These are persisted -/// to localStorage (fast) and optionally to verisim (durable). -/// -/// No heavy backend operations — this is mostly client-side state management -/// with optional persistence calls. - -let invoke = RuntimeBridge.invoke - -/// Save TSDM directive preferences to persistent storage. -/// Accepts JSON-serialised TsdmState. -let saveDirective = (directiveJson: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("tsdm_save_directive", {"directive": directiveJson}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save TSDM directive"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load TSDM directive preferences from persistent storage. -/// Returns JSON-serialised TsdmState or empty string if none saved. -let loadDirective = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("tsdm_load_directive", Dict.make()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load TSDM directive"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Collect work items from all consumer panels. -/// Returns JSON array of classified work items. -let collectWorkItems = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("tsdm_collect_work_items", Dict.make()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to collect work items"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/TypeLLCmd.affine b/src/commands/TypeLLCmd.affine new file mode 100644 index 00000000..00c3e232 --- /dev/null +++ b/src/commands/TypeLLCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TypeLLCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/TypeLLCmd.res b/src/commands/TypeLLCmd.res deleted file mode 100644 index 20705eb7..00000000 --- a/src/commands/TypeLLCmd.res +++ /dev/null @@ -1,339 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL TypeLL Commands — Backend wrappers for the verification kernel. -/// -/// TypeLL exposes a JSON-RPC-style API at TYPELL_URL (default http://localhost:7800/api/v1). -/// These bindings wrap the 7 backend commands defined in src-gossamer/src/typell/commands.rs. -/// -/// In browser-only mode (no desktop runtime), commands fall back to direct -/// fetch() calls against the TypeLL server URL. -/// -/// Unlike most panels which are self-contained, TypeLL commands are also called -/// by other panels through TypeLLService — making TypeLL a cross-cutting concern. - -let hasDesktopRuntime = RuntimeBridge.hasDesktopRuntime - -/// GET helper for TypeLL direct fetch (bypasses backend invoke). -let fetchGet: string => promise = %raw(` - function(path) { - return fetch("http://localhost:7800/api/v1" + path) - .then(function(r) { - if (!r.ok) throw new Error("TypeLL returned " + r.status); - return r.text(); - }); - } -`) - -/// POST helper for TypeLL direct fetch (bypasses backend invoke). -let fetchPost: (string, string) => promise = %raw(` - function(path, body) { - return fetch("http://localhost:7800/api/v1" + path, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: body - }).then(function(r) { - if (!r.ok) throw new Error("TypeLL returned " + r.status); - return r.text(); - }); - } -`) - -/// Check TypeLL server health. -let health = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_health", ()) - } else { - fetchGet("/health") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("TypeLL server not reachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Type-check an expression with optional context. -/// POST /check — bidirectional type checking with full feature detection. -let check = ( - expression: string, - context: option, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - let ctx = context->Option.getOr("") - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_check", {"expression": expression, "context": ctx}) - } else { - fetchPost( - "/check", - `{"expression":${JSON.stringifyAny(expression)->Option.getOr( - "\"\"", - )}, "context":${JSON.stringifyAny(ctx)->Option.getOr("{}")}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Type checking failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Infer the type of an expression. -/// POST /infer — returns the most general type. -let infer = (expression: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_infer", {"expression": expression}) - } else { - fetchPost("/infer", `{"expression":${JSON.stringifyAny(expression)->Option.getOr("\"\"")}}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Type inference failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Apply refinement types to a specification. -/// POST /refine — narrows a type with constraints. -let refine = ( - spec: string, - constraints: option, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - let cons = constraints->Option.getOr("") - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_refine", {"spec": spec, "constraints": cons}) - } else { - fetchPost( - "/refine", - `{"spec":${JSON.stringifyAny(spec)->Option.getOr( - "\"\"", - )}, "constraints":${JSON.stringifyAny(cons)->Option.getOr("[]")}}`, - ) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Refinement failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Evaluate a type-level computation. -/// POST /compute — evaluates normalisation, unification, etc. -let compute = (term: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_compute", {"term": term}) - } else { - fetchPost("/compute", `{"term":${JSON.stringifyAny(term)->Option.getOr("\"\"")}}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Type computation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List available type signatures from the server. -/// GET /signatures — returns the signature catalogue. -let listSignatures = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_list_signatures", ()) - } else { - fetchGet("/signatures") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list signatures"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get the type universe hierarchy. -/// GET /universes — returns the hierarchy of type universes. -let universes = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_universes", ()) - } else { - fetchGet("/universes") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get universes"))) - Promise.resolve() - }) - ->ignore - }) -} - -// ============================================================================ -// New Kernel Integration — localhost:7800 routed operations -// ============================================================================ - -/// Route a type check to the kernel with a specific language target. -/// POST /check — sends source + language to the kernel for type checking. -let checkType = ( - source: string, - language: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let body = TypeLLEngine.buildCheckBody(source, language) - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_check", {"expression": source, "context": language}) - } else { - fetchPost("/check", body) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Kernel type check failed for " ++ language))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Infer usage quantifiers (linear/affine/unrestricted) for an expression. -/// POST /infer-usage — returns QTT quantifier annotations. -let inferUsage = (source: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let body = TypeLLEngine.buildInferUsageBody(source) - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_infer", {"expression": source}) - } else { - fetchPost("/infer-usage", body) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Usage inference failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Infer effects for an expression. -/// POST /check-effects — returns effect list and purity status. -let checkEffects = (source: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let body = TypeLLEngine.buildCheckEffectsBody(source) - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_compute", {"term": source}) - } else { - fetchPost("/check-effects", body) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Effect checking failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check dimensional consistency (for Eclexia's dimensional type system). -/// POST /check-dimensional — validates unit/dimension annotations. -let checkDimensional = (source: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - let body = TypeLLEngine.buildCheckDimensionalBody(source) - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_compute", {"term": source}) - } else { - fetchPost("/check-dimensional", body) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Dimensional check failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Generate proof obligations from dependent types in the source. -/// POST /generate-obligations — extracts propositions that need proving. -let generateProofObligation = (source: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - let body = TypeLLEngine.buildGenerateProofObligationBody(source) - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("typell_compute", {"term": source}) - } else { - fetchPost("/generate-obligations", body) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Proof obligation generation failed"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/UmojaCmd.affine b/src/commands/UmojaCmd.affine new file mode 100644 index 00000000..f21798f3 --- /dev/null +++ b/src/commands/UmojaCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module UmojaCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/UmojaCmd.res b/src/commands/UmojaCmd.res deleted file mode 100644 index 69cfce96..00000000 --- a/src/commands/UmojaCmd.res +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Umoja Commands — backend invoke wrappers for Umoja peer management. -/// -/// These call into the Rust backend at src-gossamer/src/umoja/commands.rs -/// for federation peer lifecycle operations: add, disconnect, gossip, -/// catalogue sync, and metrics retrieval. - -let invoke = RuntimeBridge.invoke - -/// Add a new peer to the Umoja federation by address. -let addPeer = (address: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("umoja_add_peer", {"address": address}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to add peer: ${address}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Disconnect a peer from the Umoja federation by node ID. -let disconnectPeer = (nodeId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("umoja_disconnect_peer", {"nodeId": nodeId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to disconnect peer: ${nodeId}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Trigger a manual gossip round across the Umoja federation. -let triggerGossipRound = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("umoja_trigger_gossip", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to trigger gossip round"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Request a catalogue sync with a specific peer by node ID. -let syncCatalogue = (nodeId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("umoja_sync_catalogue", {"nodeId": nodeId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to sync catalogue with peer: ${nodeId}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Retrieve metrics for a specific peer by node ID. -let getPeerMetrics = (nodeId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("umoja_peer_metrics", {"nodeId": nodeId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to get metrics for peer: ${nodeId}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/UmsCmd.affine b/src/commands/UmsCmd.affine new file mode 100644 index 00000000..086ef09a --- /dev/null +++ b/src/commands/UmsCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module UmsCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/UmsCmd.res b/src/commands/UmsCmd.res deleted file mode 100644 index 0a4d1689..00000000 --- a/src/commands/UmsCmd.res +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Universal Modding Studio Commands — backend invoke wrappers for -/// mod project management, ABI validation, template instantiation, -/// asset pipeline, distribution, and API reference loading. - -let invoke = RuntimeBridge.invoke - -/// Load all mod projects from the UMS projects directory. -let loadProjects = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_load_projects", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load projects"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Create a new mod project with the given name and description. -let createProject = ( - name: string, - description: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_create_project", {"name": name, "description": description}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to create project"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Open an existing mod project by its ID. -let openProject = (projectId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_open_project", {"projectId": projectId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to open project"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete a mod project by its ID. -let deleteProject = (projectId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("ums_delete_project", {"projectId": projectId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to delete project"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run ABI validation on a level within the current project. -let validateLevel = (levelId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_validate_level", {"levelId": levelId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("ABI validation failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load available mod templates. -let loadTemplates = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_load_templates", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load templates"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Instantiate a mod template to create a new project. -let instantiateTemplate = ( - templateId: string, - projectName: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_instantiate_template", {"templateId": templateId, "projectName": projectName}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to instantiate template"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load assets for the currently selected project. -let loadAssets = (projectId: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_load_assets", {"projectId": projectId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load assets"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Import an asset file into the current project. -let importAsset = ( - projectId: string, - filePath: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_import_asset", {"projectId": projectId, "filePath": filePath}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to import asset"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Publish the current mod to a distribution target. -let publishMod = ( - projectId: string, - platform: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_publish_mod", {"projectId": projectId, "platform": platform}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to publish mod"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load the modding API reference documentation. -let loadApiReference = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("ums_load_api_reference", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load API reference"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/ValenceShellCmd.affine b/src/commands/ValenceShellCmd.affine new file mode 100644 index 00000000..77597cdf --- /dev/null +++ b/src/commands/ValenceShellCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ValenceShellCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/ValenceShellCmd.res b/src/commands/ValenceShellCmd.res deleted file mode 100644 index 5cc6262c..00000000 --- a/src/commands/ValenceShellCmd.res +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Valence Shell Commands — Backend async bindings for PTY, Valence -/// shell binary, session recording, and checkpoint operations. -/// -/// Each function returns a `Tea_Cmd.t<'msg>` that wraps a backend invoke -/// call in a Promise, tagging the result back into the TEA message loop. -/// -/// The terminal PTY is managed by the shell plugin. The Valence -/// shell binary handles reversible filesystem ops. Recordings use the -/// asciinema .cast format for portability. - -let invoke = RuntimeBridge.invoke - -/// Check whether the Valence shell binary is available on PATH. -/// Returns the version string on success, or an error if not found. -let checkValenceAvailability = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_check", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Valence shell binary not found on PATH"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Spawn a PTY process with the given shell command. -/// Returns a session ID on success that can be used for subsequent I/O. -let spawnPty = ( - shellCommand: string, - cwd: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_spawn", {"shell": shellCommand, "cwd": cwd}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to spawn PTY"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Send input to the running PTY session. -let sendInput = (input: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_input", {"input": input}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to send input to PTY"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Start recording the terminal session to an asciinema .cast file. -let startRecording = (name: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_record_start", {"name": name}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to start recording"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Stop the current recording and save the .cast file. -let stopRecording = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_record_stop", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to stop recording"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all saved recordings. -let listRecordings = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_recordings_list", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list recordings"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete a recording by ID. -let deleteRecording = (id: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_recording_delete", {"id": id}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to delete recording"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Create a Valence filesystem checkpoint. -let createCheckpoint = (label: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_checkpoint_create", {"label": label}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to create checkpoint"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Restore a Valence filesystem checkpoint by ID. -let restoreCheckpoint = (id: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_checkpoint_restore", {"id": id}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to restore checkpoint"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all Valence filesystem checkpoints. -let listCheckpoints = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_checkpoints_list", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list checkpoints"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Take a screenshot of the terminal state and save to the Capture panel. -let screenshotTerminal = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_screenshot", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to capture terminal screenshot"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export a recording as HTML replay (self-contained, shareable). -let exportRecording = ( - id: string, - format: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("valence_shell_recording_export", {"id": id, "format": format}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export recording"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/VeriSimDbLiveCmd.affine b/src/commands/VeriSimDbLiveCmd.affine new file mode 100644 index 00000000..a5671166 --- /dev/null +++ b/src/commands/VeriSimDbLiveCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VeriSimDbLiveCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/VeriSimDbLiveCmd.res b/src/commands/VeriSimDbLiveCmd.res deleted file mode 100644 index ae506143..00000000 --- a/src/commands/VeriSimDbLiveCmd.res +++ /dev/null @@ -1,173 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// VeriSimDB Live Commands — async VeriSimDB connection via the shared HTTP client. -/// -/// These wrap the async backend commands from `verisim_live.rs` and provide the same -/// TEA-compatible callback interface used throughout PanLL. Panels can switch between -/// mock and live backends by routing through the panel config's routing flag. -/// -/// All commands talk to the VeriSimDB server at ServiceEndpoints.verisim -/// (default http://localhost:8080/api/v1). -/// -/// In browser-only mode (no desktop runtime), commands fall back to direct -/// fetch() calls against the VeriSimDB server URL. - -let hasDesktopRuntime = RuntimeBridge.hasDesktopRuntime - -/// GET helper for VeriSimDB direct fetch (bypasses backend invoke). -/// panic-attack:allow insecure-protocol — localhost development endpoint. -let fetchGet: string => promise = %raw(` - function(path) { - return fetch("http://localhost:8080/api/v1" + path) - .then(function(r) { - if (!r.ok) throw new Error("VeriSimDB returned " + r.status); - return r.text(); - }); - } -`) - -/// POST helper for VeriSimDB direct fetch (bypasses backend invoke). -/// panic-attack:allow insecure-protocol — localhost development endpoint. -let fetchPost: (string, string) => promise = %raw(` - function(path, body) { - return fetch("http://localhost:8080/api/v1" + path, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: body - }).then(function(r) { - if (!r.ok) throw new Error("VeriSimDB returned " + r.status); - return r.text(); - }); - } -`) - -/// Check VeriSimDB server health (async endpoint). -/// Calls `verisim_live_health` which hits `GET /health` on the VeriSimDB server. -/// Returns a JSON string with server status, uptime, and octad store metrics. -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let checkHealth = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("verisim_live_health", ()) - } else { - fetchGet("/health") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("VeriSimDB server unreachable"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// List all octads in the VeriSimDB store (async endpoint). -/// Calls `verisim_live_list_octads` which hits `GET /octads` on the VeriSimDB server. -/// Returns a JSON array of octad summaries (id, name, modality counts). -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let listOctads = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("verisim_live_list_octads", ()) - } else { - fetchGet("/octads") - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to list octads"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Execute a VCL query against VeriSimDB (async endpoint). -/// Calls `verisim_live_query` which hits `POST /query` on the VeriSimDB server. -/// The query string is a VCL-total expression that selects across octad modalities. -/// -/// @param query — VCL-total query string (e.g. "SELECT * FROM octad WHERE modality = 'text'") -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let executeQuery = (query: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("verisim_live_query", {"query": query}) - } else { - fetchPost("/query", `{"query":${JSON.stringifyAny(query)->Option.getOr("\"\"")}}`) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("VeriSimDB query failed"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Retrieve a specific octad by ID (async endpoint). -/// Calls `verisim_live_get_octad` which hits `GET /octads/{id}` on the VeriSimDB server. -/// Returns the full octad structure including all 8 modality slots. -/// -/// @param id — the octad identifier (UUID or slug) -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let getOctad = (id: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("verisim_live_get_octad", {"id": id}) - } else { - fetchGet("/octads/" ++ id) - } - p - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get octad: " ++ id))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Check if VeriSimDB is reachable (async health probe). -/// Wraps checkHealth but normalises the result into a reachability flag. -/// Returns `{"reachable": true/false, "endpoint": "..."}` — useful for panel bar -/// connection-dot indicators (green/red). -/// -/// @param tagger — TEA message tagger receiving Ok(json) or Error(reason) -let checkReachable = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let p = if hasDesktopRuntime() { - RuntimeBridge.invoke("verisim_live_health", ()) - } else { - fetchGet("/health") - } - p - ->Promise.then(_result => { - callbacks.enqueue(tagger(Ok(`{"reachable":true,"endpoint":"${ServiceEndpoints.verisim}"}`))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue( - tagger(Ok(`{"reachable":false,"endpoint":"${ServiceEndpoints.verisim}"}`)), - ) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/VideoCoordinationCmd.affine b/src/commands/VideoCoordinationCmd.affine new file mode 100644 index 00000000..3512b410 --- /dev/null +++ b/src/commands/VideoCoordinationCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VideoCoordinationCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/VideoCoordinationCmd.res b/src/commands/VideoCoordinationCmd.res deleted file mode 100644 index 805cb7cb..00000000 --- a/src/commands/VideoCoordinationCmd.res +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// VideoCoordination Cmd — side effects for video transfers. -/// -/// Calls backend commands for rclone and laminar orchestration. - -open RuntimeBridge - -/// Start a new rclone transfer batch. -let startTransfer = (source, destination, options) => { - invoke( - "video_start_transfer", - { - "source": source, - "destination": destination, - "options": options, - }, - ) -} - -/// Fetch the latest status of all active transfers. -let fetchStatus = () => { - invoke("video_fetch_status", ()) -} - -/// Pause an ongoing transfer batch. -let pauseTransfer = batchId => { - invoke("video_pause_transfer", {"id": batchId}) -} diff --git a/src/commands/VmInspectorCmd.affine b/src/commands/VmInspectorCmd.affine new file mode 100644 index 00000000..32e9c3ad --- /dev/null +++ b/src/commands/VmInspectorCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VmInspectorCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/VmInspectorCmd.res b/src/commands/VmInspectorCmd.res deleted file mode 100644 index 695a7bb7..00000000 --- a/src/commands/VmInspectorCmd.res +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL VM Inspector Commands — Backend async bindings for the reversible -/// VM debugger. Handles VM state reading, step execution, breakpoint -/// management, and state export. - -let invoke = RuntimeBridge.invoke - -/// Read the current VM state from the running game (inter-webview). -let readVmState = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_read_state", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read VM state"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Step the VM forward by one instruction. -let stepForward = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_step_forward", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to step VM forward"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Step the VM backward by one instruction (reverse execution). -let stepBackward = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_step_backward", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to step VM backward"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run the VM until the next breakpoint or program end. -let runToBreakpoint = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_run", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to run VM"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Load a VM program from assembly text. -let loadProgram = (assembly: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_load_program", {"assembly": assembly}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load VM program"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Export the current VM state as a JSON snapshot. -let exportSnapshot = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_export_snapshot", {"_": true}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to export VM snapshot"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Read VM state from a serialised JSON file (file-based connection mode). -let readVmStateFromFile = (path: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("vm_inspector_read_file", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to read VM state file"))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/VoiceTagCmd.affine b/src/commands/VoiceTagCmd.affine new file mode 100644 index 00000000..0378f613 --- /dev/null +++ b/src/commands/VoiceTagCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VoiceTagCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/VoiceTagCmd.res b/src/commands/VoiceTagCmd.res deleted file mode 100644 index 903a1e37..00000000 --- a/src/commands/VoiceTagCmd.res +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Code MRI — VoiceTag Commands (Layer 0) -/// -/// Backend command wrappers for .mri.json file I/O and Web Speech API voice input. -/// The file format is portable — any tool can read/write .mri.json files. PanLL -/// adds voice input and agentic integration on top. -/// -/// .mri.json sidecar convention: -/// Source file: `src/Model.res` -/// Sidecar: `src/Model.res.mri.json` -/// -/// The sidecar lives alongside the source file. It's a plain JSON file that -/// editors, CLI tools, and CI pipelines can all consume without PanLL installed. - -let invoke = RuntimeBridge.invoke - -/// Load tags from a .mri.json sidecar file. -/// Returns the raw JSON string; parsing happens in the Update layer. -let loadTags = (filePath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let sidecarPath = filePath ++ ".mri.json" - let _ = - invoke("voicetag_load", {"path": sidecarPath}) - ->Promise.thenResolve(result => { - callbacks.enqueue(tagger(Ok(result))) - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to load .mri.json"))) - Promise.resolve() - }) - }) -} - -/// Save tags to a .mri.json sidecar file. -/// Takes a JSON string (serialised in the Update layer). -let saveTags = ( - filePath: string, - jsonContent: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let sidecarPath = filePath ++ ".mri.json" - let _ = - invoke("voicetag_save", {"path": sidecarPath, "content": jsonContent}) - ->Promise.thenResolve(result => { - callbacks.enqueue(tagger(Ok(result))) - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to save .mri.json"))) - Promise.resolve() - }) - }) -} - -/// Delete a .mri.json sidecar file (when all tags are removed). -let deleteSidecar = (filePath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let sidecarPath = filePath ++ ".mri.json" - let _ = - invoke("voicetag_delete", {"path": sidecarPath}) - ->Promise.thenResolve(result => { - callbacks.enqueue(tagger(Ok(result))) - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to delete .mri.json"))) - Promise.resolve() - }) - }) -} - -/// Scan a directory for all .mri.json sidecar files (for project-wide tag summary). -let scanProject = (dirPath: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let _ = - invoke("voicetag_scan", {"path": dirPath}) - ->Promise.thenResolve(result => { - callbacks.enqueue(tagger(Ok(result))) - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to scan for .mri.json files"))) - Promise.resolve() - }) - }) -} diff --git a/src/commands/VqlCmd.affine b/src/commands/VqlCmd.affine new file mode 100644 index 00000000..fa6343ce --- /dev/null +++ b/src/commands/VqlCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VqlCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/VqlCmd.res b/src/commands/VqlCmd.res deleted file mode 100644 index 3ee728ff..00000000 --- a/src/commands/VqlCmd.res +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// VCL-total Commands — backend command bridge for VCL panel I/O. -// -// All side effects (HTTP calls, file I/O, clipboard) route through here. -// The VCL panel's Engine layer is pure; this module handles the real world. -// -// Endpoints: -// VeriSimDB — http://localhost:8200 (octad query execution) -// ECHIDNA — http://localhost:8000 (cross-prover dispatch) -// TypeLL — http://localhost:7800 (type checking) -// BoJ Server — http://localhost:7700 (cartridge routing) - -open VqlModel - -// ============================================================ -// SECTION 1: Backend Command Bindings -// ============================================================ - -let invoke = RuntimeBridge.invoke - -// ============================================================ -// SECTION 2: VeriSimDB Commands -// ============================================================ - -/// Execute a VCL-total query against VeriSimDB. -let executeQuery = ( - query: string, - target: executionTarget, - level: typeSafetyLevel, - _bojRouting: bool, -): promise> => { - let endpoint = switch target { - | TargetVeriSimDB => "http://localhost:8200/api/v1/query" - | TargetEchidna => "http://localhost:8000/api/v1/vcl" - | TargetBoJ => "http://localhost:7700/echidna-llm/vcl" - | TargetTypeLL => "http://localhost:7800/api/v1/vcl-total/check" - | TargetDryRun => "http://localhost:8200/api/v1/explain" - } - let levelInt = VqlEngine.levelToInt(level) - let body = `{"query": ${JSON.stringify(JSON.Encode.string(query))}, "level": ${Int.toString( - levelInt, - )}}` - invoke("http_post", {"url": endpoint, "body": body}) - ->Promise.then(response => Promise.resolve(Ok(response))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Unknown error" - } - | _ => "Unknown error" - } - Promise.resolve(Error(msg)) - }) -} - -/// Fetch the VeriSimDB schema (tables, columns, modalities). -let fetchSchema = (): promise> => { - invoke("http_get", {"url": "http://localhost:8200/api/v1/schema"}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Schema fetch failed" - } - | _ => "Schema fetch failed" - } - Promise.resolve(Error(msg)) - }) -} - -/// Check VeriSimDB connection health. -let checkConnection = (): promise> => { - invoke("http_get", {"url": "http://localhost:8200/api/v1/health"}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Connection failed" - } - | _ => "Connection failed" - } - Promise.resolve(Error(msg)) - }) -} - -// ============================================================ -// SECTION 3: TypeLL Type Checking Commands -// ============================================================ - -/// Send a VCL-total query to TypeLL for type checking. -let typeCheckQuery = (query: string, level: typeSafetyLevel): promise> => { - let levelInt = VqlEngine.levelToInt(level) - let body = `{"query": ${JSON.stringify(JSON.Encode.string(query))}, "level": ${Int.toString( - levelInt, - )}, "mode": "vcl-total"}` - invoke("http_post", {"url": "http://localhost:7800/api/v1/vcl-total/check", "body": body}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "TypeLL check failed" - } - | _ => "TypeLL check failed" - } - Promise.resolve(Error(msg)) - }) -} - -// ============================================================ -// SECTION 4: ECHIDNA Cross-Prover Commands -// ============================================================ - -/// Fetch available prover statuses from ECHIDNA. -let fetchProverStatus = (): promise> => { - invoke("http_get", {"url": "http://localhost:8000/api/v1/provers"}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Prover fetch failed" - } - | _ => "Prover fetch failed" - } - Promise.resolve(Error(msg)) - }) -} - -/// Get a query execution plan without executing. -let explainQuery = (query: string): promise> => { - let body = `{"query": ${JSON.stringify(JSON.Encode.string(query))}, "explain": true}` - invoke("http_post", {"url": "http://localhost:8000/api/v1/vcl/explain", "body": body}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Explain failed" - } - | _ => "Explain failed" - } - Promise.resolve(Error(msg)) - }) -} - -// ============================================================ -// SECTION 5: Export Commands -// ============================================================ - -/// Export query results to a file. -let exportResults = (data: string, format: string): promise> => { - invoke("save_file", {"content": data, "format": format, "defaultName": `vcl-results.${format}`}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Export failed" - } - | _ => "Export failed" - } - Promise.resolve(Error(msg)) - }) -} - -/// Copy text to clipboard. -let copyToClipboard = (text: string): promise> => { - invoke("clipboard_write", {"text": text}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Clipboard failed" - } - | _ => "Clipboard failed" - } - Promise.resolve(Error(msg)) - }) -} - -// ============================================================ -// SECTION 6: History Persistence -// ============================================================ - -/// Save query history to local storage. -let saveHistory = (history: array): promise> => { - let json = switch JSON.stringifyAny(history) { - | Some(s) => s - | None => "[]" - } - invoke("store_set", {"key": "vcl_history", "value": json}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Save failed" - } - | _ => "Save failed" - } - Promise.resolve(Error(msg)) - }) -} - -/// Load query history from local storage. -let loadHistory = (): promise> => { - invoke("store_get", {"key": "vcl_history"}) - ->Promise.then(r => Promise.resolve(Ok(r))) - ->Promise.catch(err => { - let msg = switch err { - | JsExn(e) => - switch JsExn.message(e) { - | Some(m) => m - | None => "Load failed" - } - | _ => "Load failed" - } - Promise.resolve(Error(msg)) - }) -} diff --git a/src/commands/WatcherCmd.affine b/src/commands/WatcherCmd.affine new file mode 100644 index 00000000..fb65474c --- /dev/null +++ b/src/commands/WatcherCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module WatcherCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/WatcherCmd.res b/src/commands/WatcherCmd.res deleted file mode 100644 index 1490af84..00000000 --- a/src/commands/WatcherCmd.res +++ /dev/null @@ -1,136 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL WatcherCmd — Backend command wrappers for filesystem observation. -/// -/// These wrap the Rust `watcher_*` commands using the same `callbacks.enqueue` -/// pattern as FarmCmd and other panel commands. The watcher runs in a Rust -/// background thread and emits `watcher://event` backend events. -/// -/// Pattern: `commandName(args..., tagger) => Tea_Cmd.t<'msg>` -/// where `tagger: result => 'msg` wraps the result into -/// the Watcher message type. - -open Model - -/// Backend invoke binding via RuntimeBridge. -let invoke = RuntimeBridge.invoke - -/// Parse a watch event kind string from JSON into the typed variant. -let parseEventKind = (kind: string): watchEventKind => { - switch kind { - | "created" => Created - | "modified" => Modified - | "removed" => Removed - | "renamed" => Renamed - | _ => Other - } -} - -/// Tea_Json decoder for a single watch event. -let watchEventDecoder: Tea_Json.decoder = { - open Decoders - map6((path, kindStr, isDir, timestamp, extension, filename): watchEvent => { - path, - kind: parseEventKind(kindStr), - isDir, - timestamp, - extension, - filename, - }, stringField( - "path", - ), stringField( - "kind", - ), boolField( - "is_dir", - ), floatField("timestamp"), stringField("extension"), stringField("filename")) -} - -/// Parse a raw JSON string into a `watchEvent`. -/// -/// The Rust side emits events as JSON-serialised `WatchEvent` structs. -/// This parses the JSON and maps field names (snake_case → camelCase). -let parseEvent = (jsonStr: string): option => - Decoders.decodeOption(watchEventDecoder, jsonStr) - -/// Start the filesystem watcher on the given paths. -/// -/// The watcher runs in a background Rust thread and emits events via the -/// backend event bus. Paths are watched recursively with 500ms debounce. -let start = (paths: array, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("watcher_start", {"paths": paths}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to start watcher"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Stop the filesystem watcher. Idempotent — safe to call when not running. -let stop = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("watcher_stop", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to stop watcher"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Get the current watcher status (running, watched paths, event count). -let status = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("watcher_status", ()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to get watcher status"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Add a path to the running watcher dynamically. -let addPath = (path: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("watcher_add_path", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to add watch path: ${path}`))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Remove a path from the running watcher. -let removePath = (path: string, tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("watcher_remove_path", {"path": path}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to remove watch path: ${path}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/WiringInspectorCmd.affine b/src/commands/WiringInspectorCmd.affine new file mode 100644 index 00000000..06e2f628 --- /dev/null +++ b/src/commands/WiringInspectorCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module WiringInspectorCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/WiringInspectorCmd.res b/src/commands/WiringInspectorCmd.res deleted file mode 100644 index c7b91ab0..00000000 --- a/src/commands/WiringInspectorCmd.res +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Wiring Inspector Command Wrappers — Backend bindings for PCC invocation. -/// -/// Two commands: -/// - `runVerification`: invoke PCC against all panel contracts. -/// - `runSingleVerification`: invoke PCC against a single panel contract. -/// -/// Both return JSON strings that the frontend parses with -/// WiringInspectorEngine.parseVerificationJson. - -let invoke = RuntimeBridge.invoke - -/// Run PCC verification against all panel contracts. -/// Returns JSON string with all panel verification results. -let runVerification = (tagger: result => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke("wiring_inspector_verify", Dict.make()) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to run PCC verification"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Run PCC verification against a single panel contract. -/// Returns JSON string with one panel verification result. -let runSingleVerification = (panelId: string, tagger: result => 'msg): Tea_Cmd.t< - 'msg, -> => { - Tea_Cmd.call(callbacks => { - invoke("wiring_inspector_verify_panel", {"panelId": panelId}) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error(`Failed to verify panel: ${panelId}`))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/commands/WizardCmd.affine b/src/commands/WizardCmd.affine new file mode 100644 index 00000000..30148929 --- /dev/null +++ b/src/commands/WizardCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module WizardCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/WizardCmd.res b/src/commands/WizardCmd.res deleted file mode 100644 index 9faaa6b2..00000000 --- a/src/commands/WizardCmd.res +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Wizard Commands — Backend command wrappers for plugin/panel generation. - -let invoke = RuntimeBridge.invoke - -/// Generate a new plugin or panel based on wizard configuration. -/// The backend handles the actual file generation and registration. -/// Returns a JSON-serialised generation result. -let generate = ( - creationType: string, - capabilities: string, - dependencies: string, - securityConfig: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "wizard_generate", - { - "creationType": creationType, - "capabilities": capabilities, - "dependencies": dependencies, - "securityConfig": securityConfig, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Failed to generate plugin/panel"))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Validate wizard configuration before generation. -/// Checks for capability conflicts, dependency issues, etc. -let validateConfig = ( - creationType: string, - capabilities: string, - dependencies: string, - tagger: result => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - invoke( - "wizard_validate_config", - { - "creationType": creationType, - "capabilities": capabilities, - "dependencies": dependencies, - }, - ) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(tagger(Error("Validation failed"))) - Promise.resolve() - }) - ->ignore - }) -} \ No newline at end of file diff --git a/src/commands/WorkspaceCmd.affine b/src/commands/WorkspaceCmd.affine new file mode 100644 index 00000000..974759a9 --- /dev/null +++ b/src/commands/WorkspaceCmd.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module WorkspaceCmd; + +// TODO: Complete semantic implementation diff --git a/src/commands/WorkspaceCmd.res b/src/commands/WorkspaceCmd.res deleted file mode 100644 index 4ffd740e..00000000 --- a/src/commands/WorkspaceCmd.res +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Workspace Commands — IPC wrappers for workspace operations. -/// -/// These functions bridge the ReScript TEA loop with the Rust backend -/// for workspace persistence (arrangements, sessions) and system info -/// queries (status bar widgets). Each function returns a Tea_Cmd that -/// dispatches a result message back into the update loop via -/// `callbacks.enqueue`. - -open Msg - -/// Backend invoke binding via RuntimeBridge. -/// All backend commands return Promise or throw on error. -let invoke = RuntimeBridge.invoke - -/// Save an arrangement to disk via backend. -let saveArrangement = (arrangementJson: string): Tea_Cmd.t => { - Tea_Cmd.call(_callbacks => { - invoke("save_arrangement", {"arrangement": arrangementJson})->ignore - // Fire-and-forget — arrangement save errors are non-critical. - }) -} - -/// Load all saved arrangements from disk. -let loadArrangements = (): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("load_arrangements", ()) - ->Promise.then(result => { - callbacks.enqueue(Workspace(ArrangementsLoaded(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Workspace(ArrangementsLoaded(Error("Failed to load arrangements")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete an arrangement from disk. -let deleteArrangement = (arrangementId: string): Tea_Cmd.t => { - Tea_Cmd.call(_callbacks => { - invoke("delete_arrangement", {"arrangementId": arrangementId})->ignore - }) -} - -/// Save a session to disk via backend. -let saveSession = (sessionJson: string): Tea_Cmd.t => { - Tea_Cmd.call(_callbacks => { - invoke("save_session", {"session": sessionJson})->ignore - }) -} - -/// Load all saved sessions from disk. -let loadSessions = (): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("load_sessions", ()) - ->Promise.then(result => { - callbacks.enqueue(Workspace(SessionsLoaded(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Workspace(SessionsLoaded(Error("Failed to load sessions")))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Delete a session from disk. -let deleteSession = (sessionId: string): Tea_Cmd.t => { - Tea_Cmd.call(_callbacks => { - invoke("delete_session", {"sessionId": sessionId})->ignore - }) -} - -/// Query system information (CPU, memory, disk, uptime) for status bar widgets. -let getSystemInfo = (): Tea_Cmd.t => { - Tea_Cmd.call(callbacks => { - invoke("get_system_info", ()) - ->Promise.then(result => { - callbacks.enqueue(Workspace(SystemInfoLoaded(Ok(result)))) - Promise.resolve() - }) - ->Promise.catch(_err => { - callbacks.enqueue(Workspace(SystemInfoLoaded(Error("Failed to get system info")))) - Promise.resolve() - }) - ->ignore - }) -} diff --git a/src/components/AccessibilityToolbar.affine b/src/components/AccessibilityToolbar.affine new file mode 100644 index 00000000..25f32d53 --- /dev/null +++ b/src/components/AccessibilityToolbar.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AccessibilityToolbar; + +// TODO: Complete semantic implementation diff --git a/src/components/AccessibilityToolbar.res b/src/components/AccessibilityToolbar.res deleted file mode 100644 index 63f90812..00000000 --- a/src/components/AccessibilityToolbar.res +++ /dev/null @@ -1,306 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AccessibilityToolbar — floating accessibility widget (FAB style). -/// -/// Renders as a fixed-position circular button in the bottom-right corner. -/// Clicking it opens a floating panel with categorised accessibility controls: -/// - Theme: Dark / Light / System -/// - Colour Palette: Standard / Deuteranopia / Protanopia / High Contrast -/// - Animation: On / Reduced / Off -/// - Font Size: S / M / L / XL -/// - Focus Indicators: Default / High Contrast / Thick / Dotted -/// -/// Inspired by the "All in One Accessibility" WordPress widget pattern: -/// a non-intrusive floating button that expands to a comprehensive panel. -/// -/// All dispatch goes through `AccessibilityCtrl(accessibilityMsg)`. -/// The widget reads from `accessibilityState` and uses `AccessibilityEngine` -/// for labels. Position is fixed so it floats above all content. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// Shared radio button helper -// =========================================================================== - -/// Render a single option button within a control group. -/// Active buttons get a highlighted ring + brighter text. -let renderOption = (label: string, tooltip: string, isActive: bool, onClick: msg): Tea_Vdom.t< - msg, -> => { - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded-md transition-all ${isActive - ? "bg-indigo-600 text-white ring-2 ring-indigo-400" - : "bg-gray-800 text-gray-400 hover:bg-gray-700 hover:text-gray-200"}`, - ), - Attrs.role("radio"), - Attrs.ariaSelected(isActive), - Attrs.title(tooltip), - Events.onClick(onClick), - }, - list{text(label)}, - ) -} - -// =========================================================================== -// Section header helper -// =========================================================================== - -/// Render a section heading inside the accessibility panel. -let renderSectionHeader = (title: string): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2 mt-3 first:mt-0", - ), - }, - list{text(title)}, - ) -} - -// =========================================================================== -// Control group renderers -// =========================================================================== - -/// Theme mode: Dark / Light / System. -let renderThemeSection = (active: themeMode): Tea_Vdom.t => { - let modes: array<(themeMode, string, string)> = [ - (ThemeDark, "Dark", "Dark background with light text"), - (ThemeLight, "Light", "Light background with dark text"), - (ThemeSystem, "System", "Follow your OS colour scheme"), - ] - div( - list{Attrs.role("radiogroup"), Attrs.ariaLabel("Theme mode")}, - list{ - renderSectionHeader("Theme"), - div( - list{Attrs.class_("flex flex-wrap gap-1.5")}, - modes - ->Array.map(((mode, label, tooltip)) => - renderOption(label, tooltip, mode === active, AccessibilityCtrl(SetThemeMode(mode))) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Colour palette: Standard / Deuteranopia / Protanopia / High Contrast. -let renderPaletteSection = (active: accessibilityPalette): Tea_Vdom.t => { - let palettes: array<(accessibilityPalette, string, string)> = [ - (StandardPalette, "Standard", "Default colour palette"), - (DeuteranopiaPalette, "Deutan.", "Red-green colour blindness safe"), - (ProtanopiaPalette, "Protan.", "Protanopia-safe palette"), - (HighContrastPalette, "Hi-Con", "Maximum contrast for low vision"), - ] - div( - list{Attrs.role("radiogroup"), Attrs.ariaLabel("Colour palette")}, - list{ - renderSectionHeader("Colour Vision"), - div( - list{Attrs.class_("flex flex-wrap gap-1.5")}, - palettes - ->Array.map(((palette, label, tooltip)) => - renderOption( - label, - tooltip, - palette === active, - AccessibilityCtrl(SetAccessibilityPalette(palette)), - ) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Animation preference: On / Reduced / Off. -let renderAnimationSection = (active: animationPreference): Tea_Vdom.t => { - let prefs: array<(animationPreference, string, string)> = [ - (AnimationsOn, "On", "All animations enabled"), - (AnimationsReduced, "Reduced", "Slower, fewer animations"), - (AnimationsOff, "Off", "No animations or transitions"), - ] - div( - list{Attrs.role("radiogroup"), Attrs.ariaLabel("Animation preference")}, - list{ - renderSectionHeader("Motion"), - div( - list{Attrs.class_("flex flex-wrap gap-1.5")}, - prefs - ->Array.map(((pref, label, tooltip)) => - renderOption(label, tooltip, pref === active, AccessibilityCtrl(SetAnimations(pref))) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Font size: S / M / L / XL. -let renderFontSizeSection = (active: fontSizePreset): Tea_Vdom.t => { - let sizes: array<(fontSizePreset, string)> = [ - (FontSmall, "S"), - (FontMedium, "M"), - (FontLarge, "L"), - (FontExtraLarge, "XL"), - ] - div( - list{Attrs.role("radiogroup"), Attrs.ariaLabel("Font size")}, - list{ - renderSectionHeader("Text Size"), - div( - list{Attrs.class_("flex flex-wrap gap-1.5")}, - sizes - ->Array.map(((preset, shortLabel)) => - renderOption( - shortLabel, - AccessibilityEngine.fontSizeLabel(preset), - preset === active, - AccessibilityCtrl(SetFontSize(preset)), - ) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Focus indicator style: Default / High Contrast / Thick / Dotted. -let renderFocusSection = (active: focusIndicatorStyle): Tea_Vdom.t => { - let styles: array<(focusIndicatorStyle, string)> = [ - (FocusDefault, "Default"), - (FocusHighContrast, "Hi-Con"), - (FocusThick, "Thick"), - (FocusDotted, "Dotted"), - ] - div( - list{Attrs.role("radiogroup"), Attrs.ariaLabel("Focus indicator style")}, - list{ - renderSectionHeader("Focus Ring"), - div( - list{Attrs.class_("flex flex-wrap gap-1.5")}, - styles - ->Array.map(((style, shortLabel)) => - renderOption( - shortLabel, - AccessibilityEngine.focusStyleLabel(style), - style === active, - AccessibilityCtrl(SetFocusStyle(style)), - ) - ) - ->List.fromArray, - ), - }, - ) -} - -// =========================================================================== -// Main view: FAB + floating panel -// =========================================================================== - -/// The floating accessibility panel that appears when the FAB is clicked. -let renderPanel = (state: accessibilityState): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "fixed bottom-20 right-4 w-72 max-h-[80vh] overflow-y-auto bg-gray-900 border border-gray-700 rounded-xl shadow-2xl shadow-black/50 z-[9999] p-4", - ), - Attrs.role("dialog"), - Attrs.ariaLabel("Accessibility settings"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div( - list{Attrs.class_("text-sm font-semibold text-gray-200")}, - list{text("Accessibility")}, - ), - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300 transition-colors p-1"), - Attrs.title("Close accessibility panel"), - Attrs.ariaLabel("Close accessibility panel"), - Events.onClick(AccessibilityCtrl(ToggleAccessibilityToolbar)), - KeyboardNav.onActivate(AccessibilityCtrl(ToggleAccessibilityToolbar)), - }, - list{text("X")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mb-3")}, - list{text("Adjust display and interaction preferences")}, - ), - // Divider - div(list{Attrs.class_("border-t border-gray-800 mb-1")}, list{}), - // Control sections - renderThemeSection(state.theme), - renderPaletteSection(state.palette), - renderAnimationSection(state.animations), - renderFontSizeSection(state.fontSize), - renderFocusSection(state.focusStyle), - // Reset link - div( - list{Attrs.class_("mt-4 pt-3 border-t border-gray-800 text-center")}, - list{ - button( - list{ - Attrs.class_("text-xs text-gray-600 hover:text-gray-400 transition-colors"), - Attrs.title("Reset all accessibility settings to defaults"), - Events.onClick(AccessibilityCtrl(SetThemeMode(ThemeDark))), - }, - list{text("Reset to defaults")}, - ), - }, - ), - }, - ) -} - -/// Main view entry point for the floating accessibility widget. -/// -/// Renders a fixed-position FAB (Floating Action Button) in the bottom-right -/// corner. When `toolbarExpanded` is true, the floating panel appears above -/// the FAB with all accessibility controls. -/// -/// @param state The current accessibility state from the model -let view = (state: accessibilityState): Tea_Vdom.t => { - div( - list{Attrs.class_("fixed bottom-4 right-4 z-[9999] flex flex-col items-end gap-2")}, - list{ - // Floating panel (when expanded) - if state.toolbarExpanded { - renderPanel(state) - } else { - noNode - }, - // FAB button (always visible) - button( - list{ - Attrs.class_( - "w-12 h-12 rounded-full bg-indigo-600 hover:bg-indigo-500 text-white shadow-lg shadow-indigo-900/50 flex items-center justify-center transition-all hover:scale-110 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-offset-2 focus:ring-offset-gray-950", - ), - Attrs.title( - state.toolbarExpanded ? "Close accessibility settings" : "Open accessibility settings", - ), - Attrs.ariaLabel( - state.toolbarExpanded ? "Close accessibility settings" : "Open accessibility settings", - ), - Events.onClick(AccessibilityCtrl(ToggleAccessibilityToolbar)), - KeyboardNav.onActivate(AccessibilityCtrl(ToggleAccessibilityToolbar)), - }, - list{ - // Accessibility icon (universal access symbol approximation using text) - span(list{Attrs.class_("text-lg font-bold")}, list{text("A")}), - }, - ), - }, - ) -} diff --git a/src/components/Aerie.affine b/src/components/Aerie.affine new file mode 100644 index 00000000..2cf1b3dd --- /dev/null +++ b/src/components/Aerie.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Aerie; + +// TODO: Complete semantic implementation diff --git a/src/components/Aerie.res b/src/components/Aerie.res deleted file mode 100644 index 1c5482be..00000000 --- a/src/components/Aerie.res +++ /dev/null @@ -1,701 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Aerie Component — Network diagnostics dashboard. -/// -/// Latency gauges, speed test results, BGP route analysis, -/// probe configuration. Backend: V-lang API at :4000. - -open Model -open Msg -open Tea.Html - -/// Render a single latency measurement card showing RTT, jitter, quality, and packet loss. -let renderLatencyCard = (result: latencyResult): Tea_Vdom.t => { - let color = AerieEngine.latencyColor(result.rttMs) - let quality = AerieEngine.latencyQuality(result.rttMs) - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4"), - Attrs.role("article"), - Attrs.ariaLabel(`${result.endpoint}: ${Float.toFixed(result.rttMs, ~digits=1)}ms`), - }, - list{ - div(list{Attrs.class_("text-xs text-gray-500 truncate mb-1")}, list{text(result.endpoint)}), - div( - list{Attrs.class_(`text-2xl font-light ${color}`)}, - list{text(`${Float.toFixed(result.rttMs, ~digits=1)}ms`)}, - ), - div( - list{Attrs.class_("flex justify-between text-xs text-gray-500 mt-2")}, - list{ - span(list{}, list{text(quality)}), - span(list{}, list{text(`jitter: ${Float.toFixed(result.jitterMs, ~digits=1)}ms`)}), - }, - ), - if result.packetLoss > 0.0 { - div( - list{Attrs.class_("text-xs text-red-400 mt-1")}, - list{text(`${Float.toFixed(result.packetLoss, ~digits=1)}% loss`)}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the category tab bar (Dashboard, Speed Tests, BGP, Probes). -let renderTabs = (active: aerieCategory): Tea_Vdom.t => { - let tabs: array = [AerieDashboard, AerieSpeedTests, AerieBgp, AerieProbes] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-violet-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Aerie(SetAerieCategory(tab))), - }, - list{text(AerieEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Main Aerie panel view — full-screen overlay for network diagnostics and BGP forensics. -let view = (aerie: aerieState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Aerie network diagnostics panel"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("Aerie")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("network diagnostics & BGP forensics")}, - ), - if aerie.bgpAnomalyCount > 0 { - span( - list{Attrs.class_("text-xs text-red-400 ml-2")}, - list{text(`${Int.toString(aerie.bgpAnomalyCount)} BGP anomalies`)}, - ) - } else { - noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-violet-600 text-white rounded hover:bg-violet-500", - ), - Events.onClick(Aerie(LoadAerie)), - KeyboardNav.onActivate(Aerie(LoadAerie)), - }, - list{text("Refresh")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - if !aerie.loaded && !aerie.loading { - div( - list{Attrs.class_("text-center text-gray-500 mt-12")}, - list{ - div(list{Attrs.class_("text-4xl mb-2")}, list{text("Aerie")}), - div( - list{Attrs.class_("text-sm mb-6")}, - list{text("Network health, speed tests, BGP analysis, proof envelopes")}, - ), - button( - list{ - Attrs.class_("px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-500"), - Events.onClick(Aerie(LoadAerie)), - KeyboardNav.onActivate(Aerie(LoadAerie)), - }, - list{text("Connect to Aerie")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderTabs(aerie.activeCategory), - switch aerie.activeCategory { - | AerieDashboard => - div( - list{Attrs.class_("space-y-6")}, - list{ - { - let avg = AerieEngine.avgLatency(aerie.latencyResults) - let maxRtt = - aerie.latencyResults->Array.reduce(0.0, (acc, r) => - Math.max(acc, r.rttMs) - ) - let minRtt = if Array.length(aerie.latencyResults) > 0 { - aerie.latencyResults->Array.reduce(99999.0, (acc, r) => - Math.min(acc, r.rttMs) - ) - } else { - 0.0 - } - let totalLoss = - aerie.latencyResults->Array.reduce(0.0, (acc, r) => acc +. r.packetLoss) - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(Array.length(aerie.probes))} probes`)}, - ), - div( - list{Attrs.class_(AerieEngine.latencyColor(avg))}, - list{text(`avg: ${Float.toFixed(avg, ~digits=1)}ms`)}, - ), - div( - list{Attrs.class_("text-green-400")}, - list{text(`min: ${Float.toFixed(minRtt, ~digits=1)}ms`)}, - ), - div( - list{Attrs.class_(AerieEngine.latencyColor(maxRtt))}, - list{text(`max: ${Float.toFixed(maxRtt, ~digits=1)}ms`)}, - ), - if totalLoss > 0.0 { - div( - list{Attrs.class_("text-red-400")}, - list{text(`loss: ${Float.toFixed(totalLoss, ~digits=1)}%`)}, - ) - } else { - div(list{Attrs.class_("text-green-500")}, list{text("0% loss")}) - }, - div( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `jitter: ${Float.toFixed( - AerieEngine.avgJitter(aerie.latencyResults), - ~digits=1, - )}ms`, - ), - }, - ), - div( - list{Attrs.class_(AerieEngine.mtuColor(aerie.mtuResult))}, - list{text(`MTU: ${AerieEngine.mtuStatus(aerie.mtuResult)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Int.toString( - AerieEngine.interfacesUp(aerie.interfaces), - )}/${Int.toString(Array.length(aerie.interfaces))} ifaces up`, - ), - }, - ), - if aerie.bgpAnomalyCount > 0 { - div( - list{Attrs.class_("text-red-400 font-medium")}, - list{text(`${Int.toString(aerie.bgpAnomalyCount)} BGP anomalies`)}, - ) - } else { - noNode - }, - }, - ) - }, - // Interface summary cards - if Array.length(aerie.interfaces) > 0 { - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - aerie.interfaces - ->Array.map(iface => - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3"), - }, - list{ - div( - list{Attrs.class_("flex justify-between items-center mb-1")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-200 font-mono")}, - list{text(iface.name)}, - ), - span( - list{ - Attrs.class_( - `text-xs ${iface.isUp - ? "text-green-400" - : "text-red-400"}`, - ), - }, - list{ - text( - if iface.isUp { - "UP" - } else { - "DOWN" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text(iface.linkType), - switch iface.ipAddress { - | Some(ip) => span(list{Attrs.class_("ml-2")}, list{text(ip)}) - | None => - span( - list{Attrs.class_("ml-2 text-gray-600")}, - list{text("no IP")}, - ) - }, - switch iface.signalDbm { - | Some(dbm) => - span( - list{Attrs.class_("ml-2")}, - list{text(`${Int.toString(dbm)} dBm`)}, - ) - | None => noNode - }, - }, - ), - }, - ) - ) - ->List.fromArray, - ) - } else { - noNode - }, - // Latency cards - div( - list{ - Attrs.class_("grid grid-cols-4 gap-3"), - Attrs.role("list"), - Attrs.ariaLabel("Latency measurements"), - }, - aerie.latencyResults->Array.map(r => renderLatencyCard(r))->List.fromArray, - ), - // Latency histogram (bar chart of measurements) - if Array.length(aerie.latencyResults) > 0 { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("Latency Distribution")}, - ), - div( - list{Attrs.class_("flex items-end gap-1 h-24")}, - aerie.latencyResults - ->Array.map(r => { - let maxH = 96.0 // h-24 = 6rem = 96px - let maxRttForScale = - aerie.latencyResults->Array.reduce(1.0, (acc, r2) => - Math.max(acc, r2.rttMs) - ) - let barH = Math.max(4.0, r.rttMs /. maxRttForScale *. maxH) - let barColor = if r.rttMs < 20.0 { - "bg-green-500" - } else if r.rttMs < 50.0 { - "bg-emerald-500" - } else if r.rttMs < 100.0 { - "bg-amber-500" - } else { - "bg-red-500" - } - div( - list{ - Attrs.class_( - `flex-1 ${barColor} rounded-t transition-all cursor-default`, - ), - Attrs.prop( - "style", - `height: ${Float.toFixed(barH, ~digits=0)}px`, - ), - Attrs.title( - `${r.endpoint}: ${Float.toFixed(r.rttMs, ~digits=1)}ms`, - ), - }, - list{}, - ) - }) - ->List.fromArray, - ), - // Endpoint labels - div( - list{Attrs.class_("flex gap-1 mt-1")}, - aerie.latencyResults - ->Array.map(r => - div( - list{ - Attrs.class_( - "flex-1 text-[8px] text-gray-600 truncate text-center", - ), - }, - list{text(r.endpoint)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ) - | AerieSpeedTests => - div( - list{Attrs.class_("space-y-3")}, - aerie.speedTests - ->Array.map(st => - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-4 flex justify-between", - ), - }, - list{ - div( - list{}, - list{ - div( - list{Attrs.class_("text-green-400 text-lg")}, - list{ - text(`${Float.toFixed(st.downloadMbps, ~digits=1)} Mbps down`), - }, - ), - div( - list{Attrs.class_("text-blue-400 text-sm")}, - list{text(`${Float.toFixed(st.uploadMbps, ~digits=1)} Mbps up`)}, - ), - }, - ), - div( - list{Attrs.class_("text-right")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(st.serverLocation)}, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Float.toFixed(st.pingMs, ~digits=0)}ms ping`)}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - | AerieBgp => - div( - list{Attrs.class_("space-y-4")}, - list{ - { - let total = Array.length(aerie.bgpRoutes) - let anomalous = - aerie.bgpRoutes->Array.filter(r => r.anomalous)->Array.length - let clean = total - anomalous - div( - list{Attrs.class_("flex gap-4 text-xs")}, - list{ - span( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(total)} routes`)}, - ), - span( - list{Attrs.class_("text-green-400")}, - list{text(`${Int.toString(clean)} clean`)}, - ), - if anomalous > 0 { - span( - list{Attrs.class_("text-red-400 font-medium")}, - list{text(`${Int.toString(anomalous)} anomalous`)}, - ) - } else { - span(list{Attrs.class_("text-green-500")}, list{text("No anomalies")}) - }, - }, - ) - }, - // BGP route table - if Array.length(aerie.bgpRoutes) > 0 { - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 bg-gray-900 border-b border-gray-700 text-xs text-gray-500", - ), - }, - list{ - span(list{Attrs.class_("w-8")}, list{text("")}), - span(list{Attrs.class_("w-40")}, list{text("Prefix")}), - span(list{Attrs.class_("flex-1")}, list{text("AS Path")}), - span(list{Attrs.class_("w-32")}, list{text("Next Hop")}), - span(list{Attrs.class_("w-48")}, list{text("Detail")}), - }, - ), - // Routes - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - aerie.bgpRoutes - ->Array.map(route => { - let rowBg = route.anomalous - ? "bg-red-900/20" - : "hover:bg-gray-900/50" - div( - list{ - Attrs.class_( - `flex items-center gap-3 p-3 border-b border-gray-800 ${rowBg}`, - ), - Attrs.role("row"), - }, - list{ - // Anomaly indicator - span( - list{ - Attrs.class_( - `w-8 text-center text-xs font-bold ${route.anomalous - ? "text-red-400" - : "text-green-500"}`, - ), - }, - list{text(route.anomalous ? "!" : "ok")}, - ), - // Prefix - span( - list{Attrs.class_("w-40 text-sm text-gray-300 font-mono")}, - list{text(route.prefix)}, - ), - // AS Path - span( - list{ - Attrs.class_( - "flex-1 text-xs text-gray-400 font-mono truncate", - ), - }, - list{ - text( - route.asPath - ->Array.map(asn => Int.toString(asn)) - ->Array.join(" → "), - ), - }, - ), - // Next hop - span( - list{Attrs.class_("w-32 text-xs text-gray-500 font-mono")}, - list{text(route.nextHop)}, - ), - // Anomaly detail - switch route.anomalyDetail { - | Some(detail) => - span( - list{Attrs.class_("w-48 text-xs text-red-400 truncate")}, - list{text(detail)}, - ) - | None => - span( - list{Attrs.class_("w-48 text-xs text-gray-700")}, - list{text("-")}, - ) - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else { - div( - list{Attrs.class_("text-center py-12")}, - list{ - div( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("No BGP route data available")}, - ), - div( - list{Attrs.class_("text-xs text-gray-700 mt-1")}, - list{text("Connect to the Aerie backend for live BGP analysis")}, - ), - }, - ) - }, - }, - ) - | AerieProbes => - div( - list{Attrs.class_("space-y-4")}, - list{ - { - let active = aerie.probes->Array.filter(p => p.active)->Array.length - let inactive = Array.length(aerie.probes) - active - div( - list{Attrs.class_("flex gap-4 text-xs")}, - list{ - span( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Int.toString(Array.length(aerie.probes))} probes configured`, - ), - }, - ), - span( - list{Attrs.class_("text-green-400")}, - list{text(`${Int.toString(active)} active`)}, - ), - if inactive > 0 { - span( - list{Attrs.class_("text-gray-600")}, - list{text(`${Int.toString(inactive)} inactive`)}, - ) - } else { - noNode - }, - }, - ) - }, - // Probe list - if Array.length(aerie.probes) > 0 { - div( - list{Attrs.class_("space-y-2")}, - aerie.probes - ->Array.map(probe => { - let statusColor = probe.active ? "bg-green-500" : "bg-gray-600" - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 bg-gray-900 border border-gray-700 rounded-lg", - ), - }, - list{ - // Active indicator - div( - list{Attrs.class_(`w-2 h-2 rounded-full ${statusColor}`)}, - list{}, - ), - // Label - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(probe.label)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(probe.endpoint)}, - ), - }, - ), - // Protocol badge - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-gray-400 rounded border border-gray-700", - ), - }, - list{text(probe.protocol)}, - ), - // Toggle button - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded transition-colors ${probe.active - ? "bg-red-900/40 text-red-400 hover:bg-red-900/60" - : "bg-green-900/40 text-green-400 hover:bg-green-900/60"}`, - ), - Events.onClick(Aerie(ToggleProbe(probe.endpoint))), - }, - list{text(probe.active ? "Disable" : "Enable")}, - ), - }, - ) - }) - ->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-center py-12")}, - list{ - div( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("No probes configured")}, - ), - div( - list{Attrs.class_("text-xs text-gray-700 mt-1")}, - list{text("Connect to Aerie backend to configure network probes")}, - ), - }, - ) - }, - }, - ) - }, - }, - ) - }, - switch aerie.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/AgentCoordination.affine b/src/components/AgentCoordination.affine new file mode 100644 index 00000000..421b2cf3 --- /dev/null +++ b/src/components/AgentCoordination.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentCoordination; + +// TODO: Complete semantic implementation diff --git a/src/components/AgentCoordination.res b/src/components/AgentCoordination.res deleted file mode 100644 index 955d113a..00000000 --- a/src/components/AgentCoordination.res +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent Coordination View Panel — multi-agent topology and -/// coordination strategy display. -/// -/// Layout: Strategy selector (6 cards) at top, agent topology diagram -/// below showing nodes connected by labelled edges. Memory type -/// indicators appear as badges on each agent node. - -open Msg -open AgentCoordinationModel -open AgentCoordinationEngine -open Tea.Html - -// ============================================================================ -// Memory Type Badges -// ============================================================================ - -/// A small badge for a memory type indicator on an agent node. -let memoryBadge = (mem: memoryType): Tea_Vdom.t => { - let colorClass = memoryColor(mem) - span( - list{Attrs.class_(`px-1 py-0.5 text-xs rounded font-mono ${colorClass}`)}, - list{text(memoryIndicator(mem))}, - ) -} - -// ============================================================================ -// Strategy Card -// ============================================================================ - -/// A single strategy card in the selector grid. -let strategyCard = (strategy: coordination, isSelected: bool): Tea_Vdom.t => { - let borderClass = strategyBorderColor(strategy, isSelected) - div( - list{ - Attrs.class_( - `flex flex-col p-3 rounded-lg border-2 cursor-pointer transition-all ${borderClass} bg-gray-900/50 hover:brightness-110`, - ), - }, - list{ - // Strategy name - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("font-semibold text-sm text-gray-100")}, - list{text(strategyDisplayName(strategy))}, - ), - if isSelected { - span(list{Attrs.class_("text-xs text-emerald-400 font-mono")}, list{text("ACTIVE")}) - } else { - noNode - }, - }, - ), - // Multi-agent indicator - if isMultiAgent(strategy) { - span(list{Attrs.class_("text-xs text-purple-400 mb-1")}, list{text("Multi-Agent")}) - } else { - span(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Single Agent")}) - }, - // Description - p( - list{Attrs.class_("text-xs text-gray-400 leading-relaxed")}, - list{text(strategyDescription(strategy))}, - ), - }, - ) -} - -// ============================================================================ -// Agent Node -// ============================================================================ - -/// A single agent node in the topology diagram. -let agentNodeView = (node: agentNode): Tea_Vdom.t => { - let stateColor = nodeStateColor(node.state) - let borderColor = nodeBorderColor(node.state) - div( - list{ - Attrs.class_( - `flex flex-col items-center p-3 rounded-lg border ${borderColor} bg-gray-900/50 min-w-32`, - ), - }, - list{ - // State indicator dot - span(list{Attrs.class_(`w-2 h-2 rounded-full mb-1 ${stateColor}`)}, list{}), - // Agent name - span(list{Attrs.class_("text-sm font-semibold text-gray-100 mb-1")}, list{text(node.name)}), - // State label - span( - list{Attrs.class_(`text-xs ${stateColor} mb-2`)}, - list{text(nodeStateLabel(node.state))}, - ), - // Strategy badge - span( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(strategyDisplayName(node.strategy))}, - ), - // Memory type badges - div( - list{Attrs.class_("flex gap-1 flex-wrap justify-center")}, - node.memoryTypes->Array.map(memoryBadge)->List.fromArray, - ), - }, - ) -} - -// ============================================================================ -// Topology Edge -// ============================================================================ - -/// An edge label between two nodes (displayed as text since full SVG -/// topology rendering is deferred to a future version). -let edgeLabel = (edge: topologyEdge): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2 text-xs text-gray-500")}, - list{ - span(list{Attrs.class_("font-mono")}, list{text(edge.fromId)}), - span(list{Attrs.class_("text-gray-600")}, list{text("--" ++ edge.label ++ "-->")}), - span(list{Attrs.class_("font-mono")}, list{text(edge.toId)}), - }, - ) -} - -// ============================================================================ -// Topology Diagram -// ============================================================================ - -/// The topology diagram showing agent nodes and their connections. -let topologyDiagram = (nodes: array, edges: array): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-4")}, - list{ - // Agent nodes (flex wrapped) - div( - list{Attrs.class_("flex gap-3 flex-wrap")}, - nodes->Array.map(agentNodeView)->List.fromArray, - ), - // Edge labels - if Array.length(edges) > 0 { - div( - list{ - Attrs.class_("flex flex-col gap-1 p-3 bg-gray-900/30 rounded border border-gray-800"), - }, - list{ - h4( - list{Attrs.class_("text-xs font-semibold text-gray-400 mb-1")}, - list{text("Connections")}, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - edges->Array.map(edgeLabel)->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// All 6 coordination strategies for the selector grid. -let allStrategies: array = [Solo, Pipeline, Broadcast, Consensus, Hierarchy, Swarm] - -/// Top-level view for the Agent Coordination View panel. -let view = (state: agentCoordinationState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full p-3 bg-gray-950 text-gray-100")}, - list{ - // Panel header - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold")}, list{text("Agent Coordination View")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.nodes))} agents`)}, - ), - }, - ), - // Strategy selector (3x2 grid) - div( - list{Attrs.class_("grid grid-cols-3 gap-2 mb-4")}, - allStrategies - ->Array.map(s => strategyCard(s, state.selectedStrategy == Some(s))) - ->List.fromArray, - ), - // Topology diagram - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - if state.loading { - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-600")}, - list{text("Loading topology...")}, - ) - } else if Array.length(state.nodes) == 0 { - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-600")}, - list{text("No agents registered")}, - ) - } else { - topologyDiagram(state.nodes, state.edges) - }, - }, - ), - }, - ) -} diff --git a/src/components/AgentOoda.affine b/src/components/AgentOoda.affine new file mode 100644 index 00000000..437afbee --- /dev/null +++ b/src/components/AgentOoda.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentOoda; + +// TODO: Complete semantic implementation diff --git a/src/components/AgentOoda.res b/src/components/AgentOoda.res deleted file mode 100644 index 96cd8929..00000000 --- a/src/components/AgentOoda.res +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL OODA Session Monitor Panel — agent OODA loop lifecycle tracking -/// display. -/// -/// Layout: Session list (left column), selected session detail (right column) -/// showing OODA state diagram with current state highlighted, loop count, -/// and advance/halt control buttons. - -open Msg -open AgentOodaModel -open AgentOodaEngine -open Tea.Html - -// ============================================================================ -// OODA State Diagram -// ============================================================================ - -/// A single state node in the OODA diagram. -let stateNode = (state: agentState, isCurrent: bool): Tea_Vdom.t => { - let bgColor = if isCurrent { - stateColor(state) - } else { - "bg-gray-800 text-gray-400" - } - let borderClass = if isCurrent { - stateBorderColor(state) ++ " ring-2 ring-offset-1 ring-offset-gray-950" - } else { - "border-gray-700" - } - div( - list{ - Attrs.class_( - `flex flex-col items-center p-3 rounded-lg border ${borderClass} ${bgColor} min-w-20`, - ), - }, - list{span(list{Attrs.class_("text-sm font-semibold")}, list{text(stateLabel(state))})}, - ) -} - -/// Arrow connector between OODA states. -let stateArrow: Tea_Vdom.t = { - span(list{Attrs.class_("text-gray-600 text-lg self-center")}, list{text("->")}) -} - -/// The full OODA state diagram showing all 4 states + Halted. -let oodaDiagram = (currentState: agentState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-3")}, - list{ - // Main OODA loop (horizontal) - div( - list{Attrs.class_("flex items-center gap-2 flex-wrap")}, - list{ - stateNode(Observing, currentState == Observing), - stateArrow, - stateNode(Orienting, currentState == Orienting), - stateArrow, - stateNode(Deciding, currentState == Deciding), - stateArrow, - stateNode(Acting, currentState == Acting), - }, - ), - // Halted state (separate, below) - div( - list{Attrs.class_("flex items-center gap-2 mt-2")}, - list{stateNode(Halted, currentState == Halted)}, - ), - }, - ) -} - -// ============================================================================ -// Session List Item -// ============================================================================ - -/// A single session row in the left-hand session list. -let sessionListItem = (session: oodaSession, isSelected: bool): Tea_Vdom.t => { - let health = sessionHealth(session) - let healthCls = healthColor(health) - let selectedCls = if isSelected { - "bg-gray-800 border-emerald-500" - } else { - "bg-gray-900/50 border-gray-800 hover:bg-gray-800/50" - } - div( - list{Attrs.class_(`flex items-center gap-3 p-3 rounded border cursor-pointer ${selectedCls}`)}, - list{ - // Health indicator dot - span(list{Attrs.class_(`w-2 h-2 rounded-full ${healthCls} shrink-0`)}, list{}), - // Agent name + state - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm font-semibold text-gray-100 truncate")}, - list{text(session.agentName)}, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_(`text-xs ${stateTextColor(session.state)}`)}, - list{text(stateLabel(session.state))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(session.loopCount)} loops`)}, - ), - }, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Session Detail View -// ============================================================================ - -/// Detailed view of the selected session (right column). -let sessionDetailView = (detail: sessionDetail): Tea_Vdom.t => { - let session = detail.session - div( - list{Attrs.class_("flex flex-col gap-4")}, - list{ - // Session header - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - h3( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text(session.agentName)}, - ), - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(session.id)}), - }, - ), - // OODA state diagram - oodaDiagram(session.state), - // Stats row - div( - list{Attrs.class_("flex gap-4 p-3 bg-gray-900/50 rounded-lg")}, - list{ - div( - list{Attrs.class_("flex flex-col items-center")}, - list{ - span( - list{Attrs.class_("text-xl font-bold font-mono text-gray-100")}, - list{text(Int.toString(session.loopCount))}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Loops")}), - }, - ), - div( - list{Attrs.class_("flex flex-col items-center")}, - list{ - span( - list{Attrs.class_("text-xl font-bold font-mono text-gray-100")}, - list{text(Float.toFixed(loopRate(detail), ~digits=2) ++ "/s")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Loop Rate")}), - }, - ), - div( - list{Attrs.class_("flex flex-col items-center")}, - list{ - span( - list{Attrs.class_("text-xl font-bold font-mono text-gray-100")}, - list{text(Float.toFixed(detail.avgLoopMs, ~digits=0) ++ "ms")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Avg Loop")}), - }, - ), - }, - ), - // Control buttons - div( - list{Attrs.class_("flex gap-2")}, - list{ - if session.state != Halted { - button( - list{ - Attrs.class_("px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-500"), - }, - list{text("Advance")}, - ) - } else { - noNode - }, - if session.state != Halted { - button( - list{ - Attrs.class_("px-4 py-2 text-sm rounded bg-red-600 text-white hover:bg-red-500"), - }, - list{text("Halt")}, - ) - } else { - noNode - }, - }, - ), - // Halt reason (if halted) - switch session.haltReason { - | Some(reason) => - div( - list{ - Attrs.class_("p-2 rounded bg-red-900/30 border border-red-500/40 text-xs text-red-400"), - }, - list{text("Halt reason: " ++ reason)}, - ) - | None => noNode - }, - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Top-level view for the OODA Session Monitor panel. -let view = (state: agentOodaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex h-full p-3 gap-3 bg-gray-950 text-gray-100")}, - list{ - // Left column: session list - div( - list{Attrs.class_("w-64 flex flex-col gap-2 overflow-y-auto shrink-0")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold mb-2")}, list{text("OODA Sessions")}), - div( - list{Attrs.class_("flex flex-col gap-1")}, - state.sessions - ->Array.map(session => - sessionListItem(session, state.selectedSessionId == Some(session.id)) - ) - ->List.fromArray, - ), - }, - ), - // Right column: session detail - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - switch state.selectedDetail { - | Some(detail) => sessionDetailView(detail) - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a session to view details")}, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/AgentSafety.affine b/src/components/AgentSafety.affine new file mode 100644 index 00000000..144c83b1 --- /dev/null +++ b/src/components/AgentSafety.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentSafety; + +// TODO: Complete semantic implementation diff --git a/src/components/AgentSafety.res b/src/components/AgentSafety.res deleted file mode 100644 index db5d8cc9..00000000 --- a/src/components/AgentSafety.res +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent Safety Gate Panel — tool call safety review and approval -/// queue display. -/// -/// Layout: Pending approvals queue at top (red/amber cards with Approve/Deny -/// buttons), event history below, stats sidebar showing approved/denied/ -/// escalated counts. - -open Msg -open AgentSafetyModel -open AgentSafetyEngine -open Tea.Html - -// ============================================================================ -// Stats Sidebar -// ============================================================================ - -/// A single stat counter in the sidebar. -let statCounter = (label: string, count: int, colorClass: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center justify-between py-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(label)}), - span( - list{Attrs.class_(`text-sm font-mono font-bold ${colorClass}`)}, - list{text(Int.toString(count))}, - ), - }, - ) -} - -/// Stats sidebar showing aggregate counts. -let statsSidebar = (stats: safetyStats): Tea_Vdom.t => { - div( - list{Attrs.class_("w-48 shrink-0 p-3 bg-gray-900/50 rounded-lg border border-gray-800")}, - list{ - h3( - list{Attrs.class_("text-sm font-semibold text-gray-200 mb-3")}, - list{text("Safety Stats")}, - ), - statCounter("Total Events", stats.totalEvents, "text-gray-200"), - statCounter("Auto-Approved", stats.autoApproved, "text-emerald-400"), - statCounter("Human Approved", stats.humanApproved, "text-emerald-400"), - statCounter("Denied", stats.denied, "text-red-400"), - statCounter("Escalated", stats.escalated, "text-orange-400"), - statCounter("Policy Blocked", stats.policyBlocked, "text-red-400"), - div( - list{Attrs.class_("border-t border-gray-700 mt-2 pt-2")}, - list{statCounter("Pending", stats.pendingCount, "text-amber-400")}, - ), - }, - ) -} - -// ============================================================================ -// Pending Approval Card -// ============================================================================ - -/// A pending approval card with Approve and Deny buttons. -let pendingCard = (event: safetyEvent): Tea_Vdom.t => { - let cardColor = eventColor(event.outcome) - div( - list{Attrs.class_(`flex flex-col p-3 rounded-lg border ${cardColor}`)}, - list{ - // Header: tool call type + agent - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-semibold text-gray-100")}, - list{text(toolCallLabel(event.toolCall))}, - ), - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(event.agentId)}), - }, - ), - // Description - p(list{Attrs.class_("text-xs text-gray-300 mb-1")}, list{text(event.description)}), - // Resource - div( - list{Attrs.class_("text-xs text-gray-500 font-mono mb-3 truncate")}, - list{text(event.resource)}, - ), - // Side effects warning - if hasSideEffects(event.toolCall) { - div( - list{Attrs.class_("text-xs text-amber-400 mb-2")}, - list{text("This operation has side effects")}, - ) - } else { - noNode - }, - // Action buttons - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-500", - ), - }, - list{text("Approve")}, - ), - button( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 text-sm rounded bg-red-600 text-white hover:bg-red-500", - ), - }, - list{text("Deny")}, - ), - }, - ), - // Timestamp - span(list{Attrs.class_("text-xs text-gray-600 mt-2")}, list{text(event.timestamp)}), - }, - ) -} - -// ============================================================================ -// History Row -// ============================================================================ - -/// A single event row in the history list. -let historyRow = (event: safetyEvent): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-3 py-2 border-b border-gray-800 hover:bg-gray-800/30", - ), - }, - list{ - // Outcome badge - span( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded font-mono shrink-0 ${outcomeTextColor(event.outcome)}`, - ), - }, - list{text(outcomeLabel(event.outcome))}, - ), - // Tool call type - span( - list{Attrs.class_("text-xs text-gray-300 w-24 shrink-0")}, - list{text(toolCallLabel(event.toolCall))}, - ), - // Description (truncated) - span( - list{Attrs.class_("text-xs text-gray-400 flex-1 truncate")}, - list{text(event.description)}, - ), - // Agent - span( - list{Attrs.class_("text-xs text-gray-500 font-mono shrink-0")}, - list{text(event.agentId)}, - ), - // Timestamp - span( - list{Attrs.class_("text-xs text-gray-600 font-mono shrink-0")}, - list{text(event.timestamp)}, - ), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Top-level view for the Agent Safety Gate panel. -let view = (state: agentSafetyState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex h-full p-3 gap-3 bg-gray-950 text-gray-100")}, - list{ - // Main content area - div( - list{Attrs.class_("flex-1 flex flex-col overflow-hidden")}, - list{ - // Panel header - h2(list{Attrs.class_("text-lg font-semibold mb-3")}, list{text("Agent Safety Gate")}), - // Pending approvals section - if Array.length(state.pendingEvents) > 0 { - div( - list{Attrs.class_("mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-semibold text-amber-400 mb-2")}, - list{ - text(`Pending Approvals (${Int.toString(Array.length(state.pendingEvents))})`), - }, - ), - div( - list{Attrs.class_("grid grid-cols-1 gap-2 max-h-64 overflow-y-auto")}, - state.pendingEvents->Array.map(pendingCard)->List.fromArray, - ), - }, - ) - } else { - div( - list{ - Attrs.class_( - "p-3 mb-4 rounded bg-emerald-900/20 border border-emerald-500/30 text-xs text-emerald-400", - ), - }, - list{text("No pending approvals")}, - ) - }, - // History section - h3( - list{Attrs.class_("text-sm font-semibold text-gray-300 mb-2")}, - list{text("Event History")}, - ), - div( - list{Attrs.class_("flex-1 overflow-y-auto border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("divide-y divide-gray-800")}, - state.historyEvents->Array.map(historyRow)->List.fromArray, - ), - }, - ), - }, - ), - // Stats sidebar (right) - statsSidebar(state.stats), - }, - ) -} diff --git a/src/components/AgenticBridge.affine b/src/components/AgenticBridge.affine new file mode 100644 index 00000000..20fc1a93 --- /dev/null +++ b/src/components/AgenticBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgenticBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/AgenticBridge.res b/src/components/AgenticBridge.res deleted file mode 100644 index 04d5f27f..00000000 --- a/src/components/AgenticBridge.res +++ /dev/null @@ -1,345 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Agentic Bridge Component — automated playtesting agents with OODA loop -/// phases. Displays agent lists with phase badges, configuration panel, -/// execution log, and findings. - -open Model -open Msg -open Tea.Html - -/// Render an OODA phase badge with colour coding. -/// Observe=blue, Orient=yellow, Decide=orange, Act=green. -let oodaPhaseBadge = (phase: oodaPhase): Tea_Vdom.t => { - let (color, label) = switch phase { - | Observe => ("bg-blue-700 text-blue-100", "Observe") - | Orient => ("bg-yellow-700 text-yellow-100", "Orient") - | Decide => ("bg-orange-700 text-orange-100", "Decide") - | Act => ("bg-green-700 text-green-100", "Act") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render an agent status indicator. -let agentStatusBadge = (status: agentStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | AgentIdle => ("text-gray-500", "Idle") - | AgentRunning => ("text-green-400 animate-pulse", "Running") - | AgentPaused => ("text-yellow-400", "Paused") - | AgentCompleted => ("text-blue-400", "Completed") - | AgentFailed => ("text-red-400", "Failed") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a finding severity badge. -let findingSevBadge = (sev: findingSeverity): Tea_Vdom.t => { - let (color, label) = switch sev { - | FindingCritical => ("bg-red-700 text-red-100", "Critical") - | FindingMajor => ("bg-orange-700 text-orange-100", "Major") - | FindingMinor => ("bg-yellow-700 text-yellow-100", "Minor") - | FindingObservation => ("bg-gray-700 text-gray-300", "Obs") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Main view function for the Agentic Bridge panel. -let view = (state: agenticBridgeState): Tea_Vdom.t => { - let totalAgents = Array.length(state.agents) - let runningAgents = state.agents->Array.filter(a => a.status == AgentRunning)->Array.length - let allFindings = state.agents->Array.flatMap(a => a.findings) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Agentic Bridge — Automated Playtesting Agents"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-amber-300")}, - list{text("Agentic Bridge")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(totalAgents) ++ - " agents, " ++ - Int.toString(runningAgents) ++ " active", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-amber-800 hover:bg-amber-700 text-white rounded", - ), - Events.onClick(AgenticBridge(AbStarted)), - KeyboardNav.onActivate(AgenticBridge(AbStarted)), - }, - list{text("Launch All")}, - ), - }, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Agents { - "bg-amber-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AgenticBridge(SetAbTab(Agents))), - }, - list{text("Agents")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Config { - "bg-amber-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AgenticBridge(SetAbTab(Config))), - }, - list{text("Config")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Execution { - "bg-amber-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AgenticBridge(SetAbTab(Execution))), - }, - list{text("Execution")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Results { - "bg-amber-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AgenticBridge(SetAbTab(Results))), - }, - list{text("Results")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(AgenticBridge(DismissAbError)), - KeyboardNav.onActivate(AgenticBridge(DismissAbError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Agents => - div( - list{Attrs.class_("space-y-2")}, - state.agents - ->Array.map(agent => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-gray-200")}, - list{text(agent.name)}, - ), - oodaPhaseBadge(agent.oodaPhase), - agentStatusBadge(agent.status), - }, - ), - div( - list{Attrs.class_("flex gap-4 mt-1 text-xs text-gray-500")}, - list{ - span( - list{}, - list{text(Int.toString(Array.length(agent.actions)) ++ " actions")}, - ), - span( - list{}, - list{text(Int.toString(Array.length(agent.findings)) ++ " findings")}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - | Config => - div( - list{Attrs.class_("space-y-2")}, - state.agentConfigs - ->Array.map(cfg => { - let strategyLabel = switch cfg.strategy { - | StrategyRandom => "Random" - | StrategyExhaustive => "Exhaustive" - | StrategyAdversarial => "Adversarial" - | StrategyReplay => "Replay" - } - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-mono mb-1")}, - list{text("Agent: " ++ cfg.agentId)}, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-gray-400")}, - list{ - span( - list{}, - list{text("Speed: " ++ Float.toFixed(cfg.speed, ~digits=1) ++ "x")}, - ), - span(list{}, list{text("Strategy: " ++ strategyLabel)}), - span( - list{}, - list{ - text( - "Thoroughness: " ++ - Float.toFixed(cfg.thoroughness *. 100.0, ~digits=0) ++ "%", - ), - }, - ), - span(list{}, list{text("Max cycles: " ++ Int.toString(cfg.maxCycles))}), - span(list{}, list{text("Seed: " ++ Int.toString(cfg.randomSeed))}), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - | Execution => - div( - list{Attrs.class_("space-y-1")}, - state.agents - ->Array.filter(a => a.status == AgentRunning || a.status == AgentCompleted) - ->Array.flatMap(a => - a.actions->Array.map(act => - div( - list{Attrs.class_("flex items-center gap-3 py-1 border-b border-gray-800/30")}, - list{ - oodaPhaseBadge(act.phase), - span( - list{Attrs.class_("text-sm text-gray-300 flex-1")}, - list{text(act.description)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(act.targetPath)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Float.toFixed(act.timestampMs, ~digits=0) ++ "ms")}, - ), - }, - ) - ) - ) - ->List.fromArray, - ) - | Results => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{ - text( - Int.toString( - Array.length(allFindings), - ) ++ " total findings across all agents", - ), - }, - ), - div( - list{}, - allFindings - ->Array.map(f => - div( - list{ - Attrs.class_("px-3 py-2 mb-2 bg-gray-900 border border-gray-800 rounded"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - findingSevBadge(f.severity), - span( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(f.summary)}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{text(f.detail)}), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono mt-1")}, - list{text(f.location)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Ai.affine b/src/components/Ai.affine new file mode 100644 index 00000000..b7f04ce1 --- /dev/null +++ b/src/components/Ai.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Ai; + +// TODO: Complete semantic implementation diff --git a/src/components/Ai.res b/src/components/Ai.res deleted file mode 100644 index 1b828bab..00000000 --- a/src/components/Ai.res +++ /dev/null @@ -1,755 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AI Panel Component — Three-region multi-provider neural interface. -/// -/// Layout: -/// +--sidebar--+ +--main area-----+ -/// | Panel-L | | Conversation | -/// | Constraints| | or Providers | -/// | Context | | or SystemPrompt| -/// +-----------+ +--input area----+ -/// -/// The AI panel embeds AI providers (Claude, Gemini, Mistral, GPT, local) inside -/// the PanLL environment with full repo context, active panel awareness, VoiceTag -/// data, and provenance information. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// Sidebar: Context summary + constraint indicators -// =========================================================================== - -/// Render the left sidebar showing current context and constraint indicators. -let renderSidebar = (ai: aiState): Tea_Vdom.t => { - div( - list{Attrs.class_("w-64 border-r border-gray-800 p-4 overflow-y-auto flex-shrink-0")}, - list{ - // Section: Active provider - div( - list{Attrs.class_("mb-6")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Active Provider")}, - ), - { - let selected = AiEngine.selectProvider(ai.providers) - switch selected { - | Some(p) => - div( - list{ - Attrs.class_( - `px-3 py-2 rounded ${AiEngine.providerBgColour(p.id)} text-sm font-medium`, - ), - }, - list{text(`${AiEngine.providerShortLabel(p.id)} / ${p.selectedModel}`)}, - ) - | None => - div( - list{Attrs.class_("px-3 py-2 rounded bg-red-500/20 text-red-300 text-sm")}, - list{text("No provider available")}, - ) - } - }, - }, - ), - // Section: Token usage - div( - list{Attrs.class_("mb-6")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Token Usage")}, - ), - div( - list{Attrs.class_("text-sm text-gray-400 space-y-1")}, - list{ - div(list{}, list{text(`In: ${AiEngine.formatTokens(ai.totalInputTokens)}`)}), - div(list{}, list{text(`Out: ${AiEngine.formatTokens(ai.totalOutputTokens)}`)}), - }, - ), - }, - ), - // Section: Provider status summary - div( - list{Attrs.class_("mb-6")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Providers")}, - ), - div( - list{Attrs.class_("space-y-2")}, - ai.providers - ->AiEngine.sortByPriority - ->Array.map(p => { - let status = AiEngine.getProviderStatus(ai.providerStatuses, p.id) - div( - list{Attrs.class_("flex items-center gap-2 text-sm")}, - list{ - div( - list{Attrs.class_(`w-2 h-2 rounded-full ${AiEngine.statusDotClass(status)}`)}, - list{}, - ), - div( - list{Attrs.class_(p.enabled ? "text-gray-300" : "text-gray-600")}, - list{text(AiEngine.providerShortLabel(p.id))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - { - if ai.autoContext !== "" { - div( - list{Attrs.class_("mb-6")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Repo Context")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 max-h-40 overflow-y-auto")}, - list{text(String.slice(ai.autoContext, ~start=0, ~end=200) ++ "...")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("mb-6")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Repo Context")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("No repo loaded — use Repo Loader to load context")}, - ), - }, - ) - } - }, - }, - ) -} - -// =========================================================================== -// Category tab bar -// =========================================================================== - -/// Render a single category tab. -let renderCategoryTab = (cat: aiCategory, isActive: bool): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm transition-colors ${isActive - ? "text-gray-100 border-b-2 border-orange-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(Ai(SetAiCategory(cat))), - }, - list{text(AiEngine.categoryLabel(cat))}, - ) -} - -/// Render the category tab bar. -let renderCategoryTabBar = (activeCategory: aiCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex border-b border-gray-800")}, - AiEngine.allCategories - ->Array.map(cat => renderCategoryTab(cat, cat === activeCategory)) - ->List.fromArray, - ) -} - -// =========================================================================== -// Conversation view -// =========================================================================== - -/// Render a single message in the conversation stream. -let renderMessage = (msg: aiMessage): Tea_Vdom.t => { - let isUser = msg.role === User - let alignment = isUser ? "justify-end" : "justify-start" - let bgClass = isUser ? "bg-gray-800" : "bg-gray-900" - let borderClass = switch msg.provider { - | Some(id) => AiEngine.providerColour(id) - | None => "border-gray-700" - } - - div( - list{Attrs.class_(`flex ${alignment} mb-3`)}, - list{ - div( - list{Attrs.class_(`max-w-3xl ${bgClass} border ${borderClass} rounded-lg px-4 py-3`)}, - list{ - { - if !isUser { - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - { - switch msg.provider { - | Some(id) => - span( - list{ - Attrs.class_( - `text-xs px-2 py-0.5 rounded ${AiEngine.providerBgColour(id)}`, - ), - }, - list{text(AiEngine.providerShortLabel(id))}, - ) - | None => noNode - } - }, - { - switch msg.model { - | Some(m) => span(list{Attrs.class_("text-xs text-gray-600")}, list{text(m)}) - | None => noNode - } - }, - { - if msg.outputTokens > 0 { - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${AiEngine.formatTokens(msg.outputTokens)} tokens`)}, - ) - } else { - noNode - } - }, - }, - ) - } else { - noNode - } - }, - // Message content - div( - list{Attrs.class_("text-sm text-gray-200 whitespace-pre-wrap")}, - list{text(msg.content)}, - ), - }, - ), - }, - ) -} - -/// Render the conversation stream. -let renderConversation = (ai: aiState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{ - { - if Array.length(ai.messages) === 0 { - div( - list{Attrs.class_("flex items-center justify-center h-full")}, - list{ - div( - list{Attrs.class_("text-center text-gray-600")}, - list{ - div(list{Attrs.class_("text-2xl mb-4")}, list{text("Neural Interface")}), - div( - list{Attrs.class_("text-sm")}, - list{text("Start a conversation with your AI providers.")}, - ), - div( - list{Attrs.class_("text-sm mt-2")}, - list{text("Load a repo to give the AI full project context.")}, - ), - }, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - ai.messages->Array.map(renderMessage)->List.fromArray, - ) - } - }, - { - if ai.loading { - div( - list{Attrs.class_("flex justify-start mb-3")}, - list{ - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg px-4 py-3")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-500 animate-pulse")}, - list{text("Thinking...")}, - ), - }, - ), - }, - ) - } else { - noNode - } - }, - { - switch ai.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mx-4 mb-2 px-3 py-2 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - }, - list{text(e)}, - ) - | None => noNode - } - }, - }, - ) -} - -// =========================================================================== -// Input area -// =========================================================================== - -/// Render the message input area with send button and provider indicator. -let renderInputArea = (ai: aiState): Tea_Vdom.t => { - let selectedProvider = AiEngine.selectProvider(ai.providers) - div( - list{Attrs.class_("border-t border-gray-800 p-4")}, - list{ - // Input row - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-orange-500", - ), - Attrs.placeholder("Type a message..."), - Attrs.value(ai.inputText), - Events.onInput(text => Ai(SetAiInput(text))), - KeyboardUtil.onEnterOrSpace(Ai(SendMessage)), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-6 py-3 bg-orange-600 hover:bg-orange-500 text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed", - ), - Attrs.disabled(ai.loading || ai.inputText === ""), - Events.onClick(Ai(SendMessage)), - KeyboardNav.onActivate(Ai(SendMessage)), - }, - list{text("Send")}, - ), - }, - ), - // Status bar - div( - list{Attrs.class_("flex items-center justify-between mt-2 text-xs text-gray-600")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - { - switch selectedProvider { - | Some(p) => - span( - list{}, - list{text(`${AiEngine.providerShortLabel(p.id)}: ${p.selectedModel}`)}, - ) - | None => span(list{}, list{text("No provider")}) - } - }, - span( - list{}, - list{ - text( - `${AiEngine.formatTokens(ai.totalInputTokens)} in / ${AiEngine.formatTokens( - ai.totalOutputTokens, - )} out`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - `px-2 py-1 rounded transition-colors ${ai.broadcastMode - ? "bg-orange-500/20 text-orange-300" - : "text-gray-600 hover:text-gray-400"}`, - ), - Events.onClick(Ai(ToggleBroadcast)), - KeyboardNav.onActivate(Ai(ToggleBroadcast)), - }, - list{text("Broadcast")}, - ), - button( - list{ - Attrs.class_("text-gray-600 hover:text-gray-400 transition-colors"), - Events.onClick(Ai(ClearAiHistory)), - KeyboardNav.onActivate(Ai(ClearAiHistory)), - }, - list{text("Clear")}, - ), - }, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Providers view -// =========================================================================== - -/// Render the provider management view. -let renderProviders = (ai: aiState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{ - div( - list{Attrs.class_("space-y-4")}, - ai.providers - ->AiEngine.sortByPriority - ->Array.map(p => { - let status = AiEngine.getProviderStatus(ai.providerStatuses, p.id) - div( - list{ - Attrs.class_( - `border ${p.enabled ? "border-gray-700" : "border-gray-800"} rounded-lg p-4`, - ), - }, - list{ - // Provider header - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_(`w-3 h-3 rounded-full ${AiEngine.statusDotClass(status)}`), - }, - list{}, - ), - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text(AiEngine.providerLabel(p.id))}, - ), - span( - list{ - Attrs.class_( - `text-xs px-2 py-0.5 rounded ${AiEngine.providerBgColour(p.id)}`, - ), - }, - list{text(`#${Int.toString(p.priority)}`)}, - ), - }, - ), - button( - list{ - Attrs.class_( - `px-3 py-1 rounded text-sm transition-colors ${p.enabled - ? "bg-green-500/20 text-green-300 hover:bg-green-500/30" - : "bg-gray-700 text-gray-400 hover:bg-gray-600"}`, - ), - Events.onClick(Ai(ToggleAiProvider(p.id))), - }, - list{text(p.enabled ? "Enabled" : "Disabled")}, - ), - }, - ), - // Model selector - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Model:")}), - div( - list{Attrs.class_("flex gap-1 flex-wrap")}, - AiEngine.providerModels(p.id) - ->Array.map(m => { - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded transition-colors ${p.selectedModel === m - ? "bg-orange-500/20 text-orange-300" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-800"}`, - ), - Events.onClick(Ai(SetAiModel(p.id, m))), - }, - list{text(m)}, - ) - }) - ->List.fromArray, - ), - }, - ), - // Status line - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`Status: ${AiEngine.statusLabel(status)} | Env: ${p.envVar}`)}, - ), - // Health check button - button( - list{ - Attrs.class_("mt-2 text-xs text-gray-500 hover:text-gray-300 transition-colors"), - Events.onClick(Ai(CheckProvider(p.id))), - }, - list{text("Check health")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -// =========================================================================== -// System prompt view -// =========================================================================== - -/// Render the system prompt editor. -let renderSystemPrompt = (ai: aiState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{ - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("System Prompt (editable)")}, - ), - textarea( - list{ - Attrs.class_( - "w-full h-48 bg-gray-900 border border-gray-700 rounded-lg px-4 py-3 text-sm text-gray-200 font-mono resize-y focus:outline-none focus:border-orange-500", - ), - Attrs.value(ai.systemPrompt), - Events.onInput(text => Ai(SetSystemPrompt(text))), - }, - list{}, - ), - }, - ), - { - if ai.autoContext !== "" { - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Auto-generated Context (from loaded repo)")}, - ), - div( - list{ - Attrs.class_( - "w-full bg-gray-900/50 border border-gray-800 rounded-lg px-4 py-3 text-xs text-gray-500 font-mono max-h-96 overflow-y-auto whitespace-pre-wrap", - ), - }, - list{text(ai.autoContext)}, - ), - }, - ) - } else { - noNode - } - }, - }, - ) -} - -// =========================================================================== -// Context view -// =========================================================================== - -/// Render the context inspector. -let renderContext = (ai: aiState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-4")}, - list{text("What the AI knows about the current session:")}, - ), - div( - list{Attrs.class_("space-y-4")}, - list{ - // Context sections - div( - list{Attrs.class_("border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Conversation History")}, - ), - div( - list{Attrs.class_("text-sm text-gray-300")}, - list{text(`${Int.toString(Array.length(ai.messages))} messages`)}, - ), - }, - ), - div( - list{Attrs.class_("border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("System Prompt Size")}, - ), - div( - list{Attrs.class_("text-sm text-gray-300")}, - list{text(`${Int.toString(String.length(ai.systemPrompt))} characters`)}, - ), - }, - ), - div( - list{Attrs.class_("border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Auto Context")}, - ), - div( - list{Attrs.class_("text-sm text-gray-300")}, - list{ - text( - ai.autoContext === "" - ? "None — load a repo to populate" - : `${Int.toString(String.length(ai.autoContext))} characters from repo`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Broadcast Mode")}, - ), - div( - list{Attrs.class_("text-sm text-gray-300")}, - list{ - text( - ai.broadcastMode - ? "Enabled — sending to multiple providers" - : "Disabled — single provider", - ), - }, - ), - }, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Main view (full panel overlay) -// =========================================================================== - -/// Render the full AI panel overlay with three-region layout. -let view = (ai: aiState): Tea_Vdom.t => { - div( - list{Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col")}, - list{ - // Header bar - div( - list{Attrs.class_("flex items-center justify-between px-6 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div(list{Attrs.class_("text-lg font-light text-gray-200")}, list{text("AI Panel")}), - { - let selected = AiEngine.selectProvider(ai.providers) - switch selected { - | Some(p) => - span( - list{ - Attrs.class_( - `text-xs px-2 py-0.5 rounded ${AiEngine.providerBgColour(p.id)}`, - ), - }, - list{text(AiEngine.providerShortLabel(p.id))}, - ) - | None => noNode - } - }, - { - if ai.broadcastMode { - span( - list{ - Attrs.class_("text-xs px-2 py-0.5 rounded bg-orange-500/20 text-orange-300"), - }, - list{text("Broadcast")}, - ) - } else { - noNode - } - }, - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Category tabs - renderCategoryTabBar(ai.activeCategory), - // Main content area (sidebar + main region) - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Left sidebar (always visible) - renderSidebar(ai), - // Main content (switches by category) - div( - list{Attrs.class_("flex-1 flex flex-col")}, - list{ - { - switch ai.activeCategory { - | Conversation => renderConversation(ai) - | SystemPrompt => renderSystemPrompt(ai) - | Providers => renderProviders(ai) - | Context => renderContext(ai) - } - }, - { - if ai.activeCategory === Conversation { - renderInputArea(ai) - } else { - noNode - } - }, - }, - ), - }, - ), - }, - ) -} diff --git a/src/components/AmbientOps.affine b/src/components/AmbientOps.affine new file mode 100644 index 00000000..0dcdbf14 --- /dev/null +++ b/src/components/AmbientOps.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AmbientOps; + +// TODO: Complete semantic implementation diff --git a/src/components/AmbientOps.res b/src/components/AmbientOps.res deleted file mode 100644 index f53da444..00000000 --- a/src/components/AmbientOps.res +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AmbientOps Component — hospital-model sysadmin operations panel. -/// -/// Integrates the AmbientOps framework departments: -/// - Clinician: AI-assisted sysadmin diagnostics (Rust) -/// - Network Ambulance: Network repair (Ada/SPARK + bash) -/// - Hardware Crash Team: GPU/PCIe diagnostics (Rust) -/// - Emergency Room: Panic-safe intake (V) -/// - Observatory: System weather (Elixir) - -open Model -open Msg -open Tea.Html - -/// Render a severity badge. -let severityBadge = (severity: diagnosticSeverity): Tea_Vdom.t => { - let (color, label) = switch severity { - | Info => ("text-blue-400", "INFO") - | Warning => ("text-yellow-400", "WARN") - | Error => ("text-red-400", "ERROR") - | Critical => ("text-red-500 font-bold", "CRIT") - } - span(list{Attrs.class_("text-xs font-mono px-1 rounded " ++ color)}, list{text(label)}) -} - -/// Render a finding row. -let findingRow = (finding: diagnosticFinding): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex items-start gap-3 py-2 px-3 border-b border-gray-800"), - Attrs.role("row"), - }, - list{ - severityBadge(finding.severity), - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text(finding.summary)}), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{ - text(AmbientOpsEngine.departmentLabel(finding.department)), - if finding.repairAvailable { - span(list{Attrs.class_("ml-2 text-green-500")}, list{text("[repair available]")}) - } else { - Tea_Html.noNode - }, - }, - ), - }, - ), - span(list{Attrs.class_("text-xs text-gray-600 shrink-0")}, list{text(finding.timestamp)}), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: ambientOpsTab, target: ambientOpsTab, label: string): Tea_Vdom.t => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-teal-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(AmbientOps(SetOpsTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Availability indicator. -let availDot = (available: bool, label: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{ - Attrs.class_( - "w-2 h-2 rounded-full " ++ if available { - "bg-green-400" - } else { - "bg-red-500" - }, - ), - }, - list{}, - ), - span( - list{ - Attrs.class_( - if available { - "text-gray-300" - } else { - "text-gray-600" - }, - ), - }, - list{text(label)}, - ), - }, - ) -} - -/// Main view function for the AmbientOps panel. -let view = (state: ambientOpsState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("AmbientOps — Hospital-Model Sysadmin"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - h2(list{Attrs.class_("text-lg font-bold text-teal-300")}, list{text("AmbientOps")}), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-teal-700 text-white hover:bg-teal-600", - ), - Events.onClick(AmbientOps(RunDiagnostics)), - KeyboardNav.onActivate(AmbientOps(RunDiagnostics)), - }, - list{ - text( - if state.scanning { - "Scanning..." - } else { - "Run Diagnostics" - }, - ), - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - list{ - tabBtn(state.activeTab, TabDashboard, "Dashboard"), - tabBtn(state.activeTab, TabClinician, "Clinician"), - tabBtn(state.activeTab, TabNetwork, "Network"), - tabBtn(state.activeTab, TabHardware, "Hardware"), - tabBtn(state.activeTab, TabEmergency, "Emergency"), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - Events.onClick(AmbientOps(DismissOpsError)), - KeyboardNav.onActivate(AmbientOps(DismissOpsError)), - }, - list{text(err)}, - ) - | None => Tea_Html.noNode - }, - // Tool availability bar - div( - list{Attrs.class_("flex gap-6 px-4 py-2 border-b border-gray-800")}, - list{ - availDot(state.clinicianAvailable, "Clinician"), - availDot(state.networkRepairAvailable, "Network Repair"), - availDot(state.hardwareCrashTeamAvailable, "HW Crash Team"), - }, - ), - // Findings count summary - div( - list{Attrs.class_("flex gap-4 px-4 py-1 text-xs text-gray-400 border-b border-gray-800")}, - list{ - span( - list{}, - list{ - text( - "Critical: " ++ - Int.toString(AmbientOpsEngine.countBySeverity(state.findings, Critical)), - ), - }, - ), - span( - list{}, - list{ - text( - "Errors: " ++ Int.toString(AmbientOpsEngine.countBySeverity(state.findings, Error)), - ), - }, - ), - span( - list{}, - list{ - text( - "Warnings: " ++ - Int.toString(AmbientOpsEngine.countBySeverity(state.findings, Warning)), - ), - }, - ), - span(list{}, list{text("Total: " ++ Int.toString(Array.length(state.findings)))}), - }, - ), - // Content — filtered findings - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - { - let filtered = switch state.activeTab { - | TabDashboard => state.findings - | TabClinician => AmbientOpsEngine.findingsForDepartment(state.findings, Clinician) - | TabNetwork => AmbientOpsEngine.findingsForDepartment(state.findings, NetworkAmbulance) - | TabHardware => AmbientOpsEngine.findingsForDepartment(state.findings, HardwareCrashTeam) - | TabEmergency => AmbientOpsEngine.findingsForDepartment(state.findings, EmergencyRoom) - } - if Array.length(filtered) == 0 { - list{ - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-600 text-sm")}, - list{text("No findings. Run diagnostics to scan.")}, - ), - } - } else { - filtered->Array.map(findingRow)->List.fromArray - } - }, - ), - }, - ) -} diff --git a/src/components/ArchitectMode.affine b/src/components/ArchitectMode.affine new file mode 100644 index 00000000..ede1ceeb --- /dev/null +++ b/src/components/ArchitectMode.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ArchitectMode; + +// TODO: Complete semantic implementation diff --git a/src/components/ArchitectMode.res b/src/components/ArchitectMode.res deleted file mode 100644 index 630cf700..00000000 --- a/src/components/ArchitectMode.res +++ /dev/null @@ -1,441 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Architect Mode Component — PixiJS fine-grained level editor. -/// Displays tool palette, canvas placeholder, property inspector for -/// selected entities, AI suggestion list, and undo/redo buttons. - -open Model -open Msg -open Tea.Html - -/// Render a tool label from architectEditorTool. -let toolLabel = (tool: architectEditorTool): string => { - switch tool { - | SelectTool => "Select" - | PlaceTool(_) => "Place" - | EraseTool => "Erase" - | WireTool => "Wire" - | ZoneTool => "Zone" - | PanTool => "Pan" - } -} - -/// Render a tool palette button. -let toolButton = ( - currentTool: architectEditorTool, - tool: architectEditorTool, - label: string, -): Tea_Vdom.t => { - let isActive = toolLabel(currentTool) == toolLabel(tool) - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs rounded border " ++ if isActive { - "bg-fuchsia-700 border-fuchsia-600 text-white" - } else { - "bg-gray-800 border-gray-700 text-gray-400 hover:text-gray-200" - }, - ), - }, - list{text(label)}, - ) -} - -/// Main view function for the Architect Mode panel. -let view = (state: architectModeState): Tea_Vdom.t => { - let entityCount = Array.length(state.entities) - let zoneCount = Array.length(state.zones) - let undoCount = Array.length(state.undoStack) - let redoCount = Array.length(state.redoStack) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Architect Mode — PixiJS Level Editor"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-fuchsia-300")}, - list{text("Architect Mode")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(entityCount) ++ - " entities, " ++ - Int.toString(zoneCount) ++ " zones", - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Zoom: " ++ Float.toFixed(state.zoom, ~digits=1) ++ "x")}, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs rounded " ++ if undoCount > 0 { - "bg-gray-700 text-gray-200 hover:bg-gray-600" - } else { - "bg-gray-800 text-gray-600 cursor-not-allowed" - }, - ), - }, - list{text("Undo (" ++ Int.toString(undoCount) ++ ")")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs rounded " ++ if redoCount > 0 { - "bg-gray-700 text-gray-200 hover:bg-gray-600" - } else { - "bg-gray-800 text-gray-600 cursor-not-allowed" - }, - ), - }, - list{text("Redo (" ++ Int.toString(redoCount) ++ ")")}, - ), - }, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Canvas { - "bg-fuchsia-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ArchitectMode(SetArchModeCategory(Canvas))), - }, - list{text("Canvas")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Properties { - "bg-fuchsia-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ArchitectMode(SetArchModeCategory(Properties))), - }, - list{text("Properties")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == AiSuggestions { - "bg-fuchsia-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ArchitectMode(SetArchModeCategory(AiSuggestions))), - }, - list{text("AI Suggestions")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Validation { - "bg-fuchsia-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ArchitectMode(SetArchModeCategory(Validation))), - }, - list{text("Validation")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(ArchitectMode(DismissArchModeError)), - KeyboardNav.onActivate(ArchitectMode(DismissArchModeError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Canvas => - div( - list{Attrs.class_("space-y-3")}, - list{ - // Tool palette - div( - list{Attrs.class_("flex gap-2 flex-wrap")}, - list{ - toolButton(state.selectedTool, SelectTool, "Select"), - toolButton(state.selectedTool, PlaceTool(""), "Place"), - toolButton(state.selectedTool, EraseTool, "Erase"), - toolButton(state.selectedTool, WireTool, "Wire"), - toolButton(state.selectedTool, ZoneTool, "Zone"), - toolButton(state.selectedTool, PanTool, "Pan"), - }, - ), - // Canvas placeholder - div( - list{ - Attrs.class_( - "w-full h-64 bg-gray-900 border border-gray-800 rounded flex items-center justify-center", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{ - text( - "PixiJS canvas — " ++ - Int.toString(entityCount) ++ - " entities, " ++ - Int.toString(zoneCount) ++ " zones", - ), - }, - ), - }, - ), - // Grid controls - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - span( - list{}, - list{ - text( - "Grid: " ++ if state.gridVisible { - "visible" - } else { - "hidden" - }, - ), - }, - ), - span( - list{}, - list{ - text( - "Snap: " ++ if state.snapToGrid { - "on" - } else { - "off" - }, - ), - }, - ), - span(list{}, list{text("Cell: " ++ Int.toString(state.gridSize) ++ "px")}), - }, - ), - }, - ) - | Properties => - switch state.selectedEntityId { - | Some(eid) => - switch state.entities->Array.find(e => e.id == eid) { - | Some(entity) => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-fuchsia-300")}, - list{text(entity.kind)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(entity.id)}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - span(list{Attrs.class_("text-gray-400")}, list{text("X:")}), - span( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(Float.toFixed(entity.x, ~digits=1))}, - ), - span(list{Attrs.class_("text-gray-400")}, list{text("Y:")}), - span( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(Float.toFixed(entity.y, ~digits=1))}, - ), - span(list{Attrs.class_("text-gray-400")}, list{text("Rotation:")}), - span( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(Float.toFixed(entity.rotation, ~digits=0) ++ " deg")}, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Entity not found: " ++ eid)}, - ) - } - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Select an entity on the canvas to inspect its properties.")}, - ) - } - | AiSuggestions => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{text(Int.toString(Array.length(state.aiSuggestions)) ++ " AI suggestions")}, - ), - div( - list{}, - state.aiSuggestions - ->Array.map(s => - div( - list{ - Attrs.class_( - "px-3 py-2 mb-2 border rounded " ++ if s.applied { - "bg-green-900/20 border-green-800" - } else { - "bg-gray-900 border-gray-800" - }, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(s.description)}, - ), - span( - list{Attrs.class_("text-xs font-mono text-fuchsia-400")}, - list{text(Float.toFixed(s.confidence *. 100.0, ~digits=0) ++ "%")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{ - text(Int.toString(Array.length(s.entities)) ++ " entities suggested"), - }, - ), - if s.applied { - span( - list{Attrs.class_("text-xs text-green-400 mt-1")}, - list{text("Applied")}, - ) - } else { - button( - list{ - Attrs.class_("text-xs text-fuchsia-400 hover:text-fuchsia-300 mt-1"), - }, - list{text("Apply")}, - ) - }, - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | Validation => - div( - list{Attrs.class_("space-y-3")}, - list{ - // Entity list for validation - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{ - text( - Int.toString(entityCount) ++ - " entities, " ++ - Int.toString(zoneCount) ++ " zones on canvas", - ), - }, - ), - // Show entities without zones - div( - list{Attrs.class_("space-y-1")}, - state.entities - ->Array.filter(e => e.selected) - ->Array.map(e => - div( - list{ - Attrs.class_( - "flex items-center gap-2 text-xs px-2 py-1 bg-gray-900 rounded", - ), - }, - list{ - span(list{Attrs.class_("text-fuchsia-300")}, list{text(e.kind)}), - span( - list{Attrs.class_("text-gray-500 font-mono")}, - list{ - text( - "(" ++ - Float.toFixed(e.x, ~digits=0) ++ - ", " ++ - Float.toFixed(e.y, ~digits=0) ++ ")", - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/AssetManager.affine b/src/components/AssetManager.affine new file mode 100644 index 00000000..27381f05 --- /dev/null +++ b/src/components/AssetManager.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AssetManager; + +// TODO: Complete semantic implementation diff --git a/src/components/AssetManager.res b/src/components/AssetManager.res deleted file mode 100644 index 4b5b61c1..00000000 --- a/src/components/AssetManager.res +++ /dev/null @@ -1,350 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Asset Manager Component — PixiJS sprites, sounds, level templates. -/// Displays asset grid with thumbnails/icons, filter by kind, collection -/// organiser, and usage tracker. - -open Model -open Msg -open Tea.Html - -/// Render an asset kind label. -let assetKindLabel = (kind: assetKind): string => { - switch kind { - | Sprite => "Sprite" - | SpriteSheet => "SpriteSheet" - | Sound => "Sound" - | Music => "Music" - | Font => "Font" - | LevelTemplate => "Template" - | ParticleEffect => "Particle" - } -} - -/// Render an asset kind icon/colour. -let assetKindColor = (kind: assetKind): string => { - switch kind { - | Sprite => "text-pink-400" - | SpriteSheet => "text-pink-300" - | Sound => "text-yellow-400" - | Music => "text-purple-400" - | Font => "text-gray-400" - | LevelTemplate => "text-green-400" - | ParticleEffect => "text-cyan-400" - } -} - -/// Format file size in human-readable form. -let formatSize = (bytes: int): string => { - if bytes >= 1048576 { - Float.toFixed(Int.toFloat(bytes) /. 1048576.0, ~digits=1) ++ " MB" - } else if bytes >= 1024 { - Float.toFixed(Int.toFloat(bytes) /. 1024.0, ~digits=1) ++ " KB" - } else { - Int.toString(bytes) ++ " B" - } -} - -/// Main view function for the Asset Manager panel. -let view = (state: assetManagerState): Tea_Vdom.t => { - let totalAssets = Array.length(state.assets) - let collectionCount = Array.length(state.collections) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Asset Manager — Game Asset Library"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-pink-300")}, - list{text("Asset Manager")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(totalAssets) ++ - " assets, " ++ - Int.toString(collectionCount) ++ " collections", - ), - }, - ), - if state.importing { - span( - list{Attrs.class_("text-xs text-yellow-400 animate-pulse")}, - list{text("Importing...")}, - ) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-pink-800 hover:bg-pink-700 text-white rounded"), - Events.onClick(AssetManager(AmStarted)), - KeyboardNav.onActivate(AssetManager(AmStarted)), - }, - list{text("Import")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Browse { - "bg-pink-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AssetManager(SetAmCategory(Browse))), - }, - list{text("Browse")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Import { - "bg-pink-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AssetManager(SetAmCategory(Import))), - }, - list{text("Import")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Collections { - "bg-pink-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AssetManager(SetAmCategory(Collections))), - }, - list{text("Collections")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Usage { - "bg-pink-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AssetManager(SetAmCategory(Usage))), - }, - list{text("Usage")}, - ), - }, - ), - // Filter bar - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-2 border-b border-gray-800")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-200", - ), - Attrs.value(state.filter), - Attrs.placeholder("Search assets..."), - }, - list{}, - ), - // Kind filter badges - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Kind:")}), - switch state.kindFilter { - | Some(k) => - span( - list{Attrs.class_("px-2 py-0.5 text-xs bg-pink-900/50 text-pink-300 rounded")}, - list{text(assetKindLabel(k))}, - ) - | None => span(list{Attrs.class_("text-xs text-gray-500")}, list{text("All")}) - }, - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(AssetManager(DismissAmError)), - KeyboardNav.onActivate(AssetManager(DismissAmError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Browse => - // Asset grid - div( - list{Attrs.class_("grid grid-cols-3 gap-2")}, - state.assets - ->Array.map(a => { - let isSelected = state.selectedAsset == Some(a.id) - div( - list{ - Attrs.class_( - "px-2 py-2 border rounded cursor-pointer " ++ if isSelected { - "bg-pink-900/20 border-pink-700" - } else { - "bg-gray-900 border-gray-800 hover:border-gray-700" - }, - ), - }, - list{ - // Thumbnail placeholder - div( - list{ - Attrs.class_( - "w-full h-16 bg-gray-800 rounded flex items-center justify-center mb-1", - ), - }, - list{ - span( - list{Attrs.class_("text-xs font-mono " ++ assetKindColor(a.kind))}, - list{text(assetKindLabel(a.kind))}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-200 truncate")}, list{text(a.name)}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(formatSize(a.sizeBytes))}, - ), - if Array.length(a.tags) > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - a.tags - ->Array.map(t => - span( - list{Attrs.class_("px-1 text-xs bg-gray-800 text-gray-500 rounded")}, - list{text(t)}, - ) - ) - ->List.fromArray, - ) - } else { - Tea_Html.noNode - }, - }, - ) - }) - ->List.fromArray, - ) - | Import => - div( - list{Attrs.class_("text-center py-8 space-y-4")}, - list{ - div( - list{ - Attrs.class_( - "w-full h-32 border-2 border-dashed border-gray-700 rounded flex items-center justify-center", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-500 text-sm")}, - list{text("Drop files here or click Import to select")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Supports: PNG, WebP, WAV, OGG, MP3, TTF, WOFF2, JSON")}, - ), - }, - ) - | Collections => - div( - list{Attrs.class_("space-y-3")}, - state.collections - ->Array.map(c => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-pink-300")}, - list{text(c.name)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Int.toString(Array.length(c.assets)) ++ " assets")}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(c.description)}), - }, - ) - ) - ->List.fromArray, - ) - | Usage => - div( - list{Attrs.class_("space-y-1")}, - state.assets - ->Array.filter(a => Array.length(a.usedInLevels) > 0) - ->Array.map(a => - div( - list{Attrs.class_("flex items-center gap-3 py-1 border-b border-gray-800/50")}, - list{ - span( - list{Attrs.class_("text-xs " ++ assetKindColor(a.kind))}, - list{text(assetKindLabel(a.kind))}, - ), - span(list{Attrs.class_("text-sm text-gray-200 flex-1")}, list{text(a.name)}), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text("Used in " ++ Int.toString(Array.length(a.usedInLevels)) ++ " levels"), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/AutomationBridge.affine b/src/components/AutomationBridge.affine new file mode 100644 index 00000000..643ec0f3 --- /dev/null +++ b/src/components/AutomationBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AutomationBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/AutomationBridge.res b/src/components/AutomationBridge.res deleted file mode 100644 index a5cbd60c..00000000 --- a/src/components/AutomationBridge.res +++ /dev/null @@ -1,381 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Automation Bridge Component — CI/CD pipeline orchestration for game -/// builds. Displays pipeline lists with step progress, trigger rules, build -/// status, and history table. - -open Model -open Msg -open Tea.Html - -/// Render a pipeline status badge. -let pipelineStatusBadge = (status: automationPipelineStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | PipelineIdle => ("bg-gray-700 text-gray-300", "Idle") - | PipelineQueued => ("bg-blue-700 text-blue-100", "Queued") - | PipelineRunning => ("bg-yellow-700 text-yellow-100", "Running") - | PipelineSucceeded => ("bg-green-700 text-green-100", "Passed") - | PipelineFailed => ("bg-red-700 text-red-100", "Failed") - | PipelineCancelled => ("bg-gray-600 text-gray-300", "Cancelled") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render a trigger event label. -let triggerEventLabel = (evt: automationTriggerEvent): string => { - switch evt { - | TriggerPush => "Push" - | TriggerPullRequest => "PR" - | TriggerTag => "Tag" - | TriggerSchedule => "Cron" - | TriggerManual => "Manual" - | TriggerFileChange => "FileChange" - } -} - -/// Main view function for the Automation Bridge panel. -let view = (state: automationBridgeState): Tea_Vdom.t => { - let totalPipelines = Array.length(state.pipelines) - let runningCount = state.pipelines->Array.filter(p => p.status == PipelineRunning)->Array.length - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Automation Bridge — CI/CD Pipeline Orchestration"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-emerald-300")}, - list{text("Automation Bridge")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(totalPipelines) ++ - " pipelines, " ++ - Int.toString(runningCount) ++ " running", - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-emerald-800 hover:bg-emerald-700 text-white rounded", - ), - Events.onClick(AutomationBridge(AutoBStarted)), - KeyboardNav.onActivate(AutomationBridge(AutoBStarted)), - }, - list{text("Trigger Build")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Pipelines { - "bg-emerald-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AutomationBridge(SetAutoBTab(Pipelines))), - }, - list{text("Pipelines")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Triggers { - "bg-emerald-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AutomationBridge(SetAutoBTab(Triggers))), - }, - list{text("Triggers")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Status { - "bg-emerald-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AutomationBridge(SetAutoBTab(Status))), - }, - list{text("Status")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == History { - "bg-emerald-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(AutomationBridge(SetAutoBTab(History))), - }, - list{text("History")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(AutomationBridge(DismissAutoBError)), - KeyboardNav.onActivate(AutomationBridge(DismissAutoBError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Pipelines => - div( - list{Attrs.class_("space-y-3")}, - state.pipelines - ->Array.map(pipe => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-gray-200")}, - list{text(pipe.name)}, - ), - pipelineStatusBadge(pipe.status), - }, - ), - // Step progress - div( - list{Attrs.class_("space-y-1")}, - pipe.steps - ->Array.map(step => - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - "w-2 h-2 rounded-full " ++ - switch step.status { - | PipelineSucceeded => "bg-green-500" - | PipelineFailed => "bg-red-500" - | PipelineRunning => "bg-yellow-500 animate-pulse" - | _ => "bg-gray-600" - }, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 flex-1")}, - list{text(step.name)}, - ), - switch step.durationMs { - | Some(d) => - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Float.toFixed(d, ~digits=0) ++ "ms")}, - ) - | None => Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ), - }, - ) - ) - ->List.fromArray, - ) - | Triggers => - div( - list{Attrs.class_("space-y-2")}, - state.triggers - ->Array.map(t => - div( - list{Attrs.class_("flex items-center gap-3 py-2 border-b border-gray-800/50")}, - list{ - span( - list{ - Attrs.class_( - "w-2 h-2 rounded-full " ++ if t.enabled { - "bg-green-500" - } else { - "bg-gray-600" - }, - ), - }, - list{}, - ), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text(t.description)}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(triggerEventLabel(t.event) ++ " | " ++ t.pattern)}, - ), - }, - ), - span( - list{Attrs.class_("text-xs font-mono text-gray-500")}, - list{text(t.pipelineId)}, - ), - }, - ) - ) - ->List.fromArray, - ) - | Status => - div( - list{Attrs.class_("space-y-2")}, - state.pipelines - ->Array.filter(p => p.status == PipelineRunning || p.status == PipelineQueued) - ->Array.map(pipe => { - let completedSteps = - pipe.steps->Array.filter(s => s.status == PipelineSucceeded)->Array.length - let totalSteps = Array.length(pipe.steps) - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(pipe.name)}), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(completedSteps) ++ - "/" ++ - Int.toString(totalSteps) ++ " steps", - ), - }, - ), - }, - ), - // Progress bar - div( - list{Attrs.class_("w-full h-2 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-emerald-500 transition-all"), - Attrs.style( - "width", - Float.toFixed( - if totalSteps > 0 { - Int.toFloat(completedSteps) /. Int.toFloat(totalSteps) *. 100.0 - } else { - 0.0 - }, - ~digits=1, - ) ++ "%", - ), - }, - list{}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - | History => - div( - list{}, - list{ - // History table header - div( - list{ - Attrs.class_( - "flex gap-2 text-xs text-gray-500 font-mono border-b border-gray-800 pb-1 mb-2", - ), - }, - list{ - span(list{Attrs.class_("w-24")}, list{text("Build")}), - span(list{Attrs.class_("flex-1")}, list{text("Pipeline")}), - span(list{Attrs.class_("w-16")}, list{text("Trigger")}), - span(list{Attrs.class_("w-20")}, list{text("Duration")}), - span(list{Attrs.class_("w-20")}, list{text("Status")}), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - state.buildHistory - ->Array.map(entry => - div( - list{Attrs.class_("flex gap-2 text-xs py-1 border-b border-gray-800/30")}, - list{ - span( - list{Attrs.class_("w-24 font-mono text-gray-500 truncate")}, - list{text(entry.commitSha->String.slice(~start=0, ~end=7))}, - ), - span( - list{Attrs.class_("flex-1 text-gray-300")}, - list{text(entry.pipelineName)}, - ), - span( - list{Attrs.class_("w-16 text-gray-500")}, - list{text(triggerEventLabel(entry.triggeredBy))}, - ), - span( - list{Attrs.class_("w-20 text-gray-500")}, - list{text(Float.toFixed(entry.durationMs /. 1000.0, ~digits=1) ++ "s")}, - ), - span(list{Attrs.class_("w-20")}, list{pipelineStatusBadge(entry.status)}), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/AutomationRouter.affine b/src/components/AutomationRouter.affine new file mode 100644 index 00000000..7fb3c38a --- /dev/null +++ b/src/components/AutomationRouter.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AutomationRouter; + +// TODO: Complete semantic implementation diff --git a/src/components/AutomationRouter.res b/src/components/AutomationRouter.res deleted file mode 100644 index c26bb3b5..00000000 --- a/src/components/AutomationRouter.res +++ /dev/null @@ -1,639 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Automation Router Component — view for cross-panel workflow -/// orchestration with event-driven rules and hybrid approval gates. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: automationRouterCategory, - active: automationRouterCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(AutomationRouter(SetRouterCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render dashboard — stats cards, global toggle, recent executions. -let renderDashboard = (state: automationRouterState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Stats row - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-cyan-400")}, - list{text(Int.toString(AutomationRouterEngine.enabledCount(state.rules)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Active Rules")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-amber-400")}, - list{text(Int.toString(AutomationRouterEngine.pendingCount(state.pendingActions)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Pending Approval")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-emerald-400")}, - list{ - text( - AutomationRouterEngine.formatSuccessRate( - AutomationRouterEngine.successRate(state.executionLog), - ), - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Success Rate")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(Array.length(state.executionLog)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Executions")}), - }, - ), - }, - ), - // Global toggle - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - if state.globalEnabled { - "px-4 py-2 text-sm bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer" - } else { - "px-4 py-2 text-sm bg-red-800 text-red-200 rounded hover:bg-red-700 cursor-pointer" - }, - ), - Events.onClick(AutomationRouter(ToggleGlobalEnabled)), - KeyboardNav.onActivate(AutomationRouter(ToggleGlobalEnabled)), - }, - list{ - text( - if state.globalEnabled { - "Automation: ON" - } else { - "Automation: OFF" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.rules))} rules configured`)}, - ), - }, - ), - // Recent executions - if Array.length(state.executionLog) > 0 { - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text("Recent Executions")}), - ...state.executionLog - ->Array.slice(~start=0, ~end=5) - ->Array.map(entry => - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span( - list{ - Attrs.class_( - if entry.success { - "text-emerald-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if entry.success { - "OK" - } else { - "FAIL" - }, - ), - }, - ), - span(list{Attrs.class_("text-gray-200 flex-1")}, list{text(entry.ruleName)}), - span(list{Attrs.class_("text-gray-500")}, list{text(entry.detail)}), - }, - ) - ) - ->List.fromArray, - }, - ) - } else { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No executions yet — rules will appear here when they fire")}, - ) - }, - }, - ) -} - -/// Render rules list. -let renderRules = (state: automationRouterState): Tea_Vdom.t => { - let filtered = AutomationRouterEngine.filterRules( - state.rules, - state.filterText, - state.showDisabled, - ) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700", - ), - Attrs.placeholder("Filter rules..."), - Attrs.value(state.filterText), - Events.onInput(text => AutomationRouter(SetRouterFilter(text))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - if state.showDisabled { - "px-2 py-1 text-xs bg-gray-600 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(AutomationRouter(ToggleShowDisabled)), - KeyboardNav.onActivate(AutomationRouter(ToggleShowDisabled)), - }, - list{text("Show Disabled")}, - ), - }, - ), - // Rule cards - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text( - "No rules configured — load from .machine_readable/ENSAID_CONFIG.a2ml or create manually", - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - filtered - ->Array.map(rule => { - let triggerCls = AutomationRouterEngine.triggerColour(rule.trigger) - let approvalCls = AutomationRouterEngine.approvalColour(rule.approval) - div( - list{ - Attrs.class_( - `p-3 bg-gray-800 rounded border ${if rule.enabled { - "border-gray-700" - } else { - "border-gray-800 opacity-50" - }}`, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-100 font-medium flex-1")}, - list{text(rule.name)}, - ), - span( - list{Attrs.class_(`text-xs ${triggerCls} font-mono`)}, - list{text(AutomationRouterEngine.triggerKindLabel(rule.trigger))}, - ), - span( - list{Attrs.class_(`text-xs ${approvalCls}`)}, - list{text(AutomationRouterEngine.approvalLabel(rule.approval))}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text(rule.description)}), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Trigger: ${AutomationRouterEngine.triggerLabel(rule.trigger)}`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`Fired: ${Int.toString(rule.firedCount)}x`)}, - ), - button( - list{ - Attrs.class_( - if rule.enabled { - "ml-auto px-2 py-1 text-xs bg-emerald-800 text-emerald-200 rounded cursor-pointer" - } else { - "ml-auto px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(AutomationRouter(ToggleRule(rule.id))), - }, - list{ - text( - if rule.enabled { - "Enabled" - } else { - "Disabled" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-cyan-800 text-cyan-200 rounded hover:bg-cyan-700 cursor-pointer", - ), - Events.onClick(AutomationRouter(ExecuteRule(rule.id))), - }, - list{text("Run")}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render pending approval actions. -let renderPending = (state: automationRouterState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - if Array.length(state.pendingActions) > 0 { - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(AutomationRouter(ApproveAll)), - KeyboardNav.onActivate(AutomationRouter(ApproveAll)), - }, - list{text("Approve All")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-red-800 text-red-200 rounded hover:bg-red-700 cursor-pointer", - ), - Events.onClick(AutomationRouter(RejectAll)), - KeyboardNav.onActivate(AutomationRouter(RejectAll)), - }, - list{text("Reject All")}, - ), - }, - ) - } else { - noNode - }, - if Array.length(state.pendingActions) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No pending actions — rules with approval gates will queue here")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.pendingActions - ->Array.mapWithIndex((action, idx) => - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-amber-800/50")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-sm text-amber-300 font-medium")}, - list{text(action.ruleName)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto")}, - list{text(action.triggerDetail)}, - ), - }, - ), - div( - list{Attrs.class_("space-y-1 mb-2")}, - action.actions - ->Array.map(a => - div( - list{Attrs.class_("text-xs text-gray-400 font-mono")}, - list{text(`${a.panelId} -> ${a.message}`)}, - ) - ) - ->List.fromArray, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(AutomationRouter(ApproveAction(idx))), - }, - list{text("Approve")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-red-800 text-red-200 rounded hover:bg-red-700 cursor-pointer", - ), - Events.onClick(AutomationRouter(RejectAction(idx))), - }, - list{text("Reject")}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render execution history. -let renderHistory = (state: automationRouterState): Tea_Vdom.t => { - if Array.length(state.executionLog) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No execution history")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - state.executionLog - ->Array.map(entry => - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span( - list{ - Attrs.class_( - if entry.success { - "text-emerald-400 w-8" - } else { - "text-red-400 w-8" - }, - ), - }, - list{ - text( - if entry.success { - "OK" - } else { - "FAIL" - }, - ), - }, - ), - span(list{Attrs.class_("text-gray-200 flex-1")}, list{text(entry.ruleName)}), - span(list{Attrs.class_("text-gray-500")}, list{text(entry.detail)}), - span( - list{Attrs.class_("text-gray-600 font-mono")}, - list{text(AutomationRouterEngine.formatRelativeTime(entry.triggeredAt))}, - ), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render settings view. -let renderSettings = (state: automationRouterState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Config source - div( - list{Attrs.class_("p-4 bg-gray-800 rounded border border-gray-700")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 mb-3")}, list{text("Configuration Source")}), - div( - list{Attrs.class_("text-xs text-gray-400 mb-3")}, - list{ - text( - "Rules can be loaded from the repo's .machine_readable/ENSAID_CONFIG.a2ml or stored locally in PanLL.", - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if state.configSource === "repo" { - "px-3 py-1.5 text-xs bg-cyan-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded cursor-pointer hover:bg-gray-600" - }, - ), - Events.onClick(AutomationRouter(LoadFromRepo)), - KeyboardNav.onActivate(AutomationRouter(LoadFromRepo)), - }, - list{text("Load from Repo")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(AutomationRouter(SaveRules)), - KeyboardNav.onActivate(AutomationRouter(SaveRules)), - }, - list{text("Save Rules")}, - ), - }, - ), - }, - ), - // Show disabled toggle - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if state.showDisabled { - "px-3 py-1.5 text-xs bg-gray-600 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(AutomationRouter(ToggleShowDisabled)), - KeyboardNav.onActivate(AutomationRouter(ToggleShowDisabled)), - }, - list{text("Show Disabled Rules")}, - ), - }, - ), - }, - ) -} - -/// Main view function. -let view = (state: automationRouterState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Automation Router panel"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Automation Router")}, - ), - if state.globalEnabled { - span(list{Attrs.class_("text-xs text-emerald-400")}, list{text("ACTIVE")}) - } else { - span(list{Attrs.class_("text-xs text-red-400")}, list{text("PAUSED")}) - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if state.bojRouting { - "px-3 py-1.5 text-xs bg-blue-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600" - }, - ), - Attrs.ariaLabel( - if state.bojRouting { - "Disable BoJ routing" - } else { - "Enable BoJ routing" - }, - ), - Events.onClick(AutomationRouter(ToggleAutomationBojRouting)), - KeyboardNav.onActivate(AutomationRouter(ToggleAutomationBojRouting)), - }, - list{ - text( - if state.bojRouting { - "BoJ On" - } else { - "BoJ" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(AutomationRouter(LoadRules)), - KeyboardNav.onActivate(AutomationRouter(LoadRules)), - }, - list{text("Refresh")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Dashboard", RouterDashboard, state.activeCategory), - renderTab("Rules", RouterRules, state.activeCategory), - renderTab("Pending", RouterPending, state.activeCategory), - renderTab("History", RouterHistory, state.activeCategory), - renderTab("Settings", RouterSettings, state.activeCategory), - }, - ), - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | RouterDashboard => renderDashboard(state) - | RouterRules => renderRules(state) - | RouterPending => renderPending(state) - | RouterHistory => renderHistory(state) - | RouterSettings => renderSettings(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/BalanceAnalyser.affine b/src/components/BalanceAnalyser.affine new file mode 100644 index 00000000..72ff5bf6 --- /dev/null +++ b/src/components/BalanceAnalyser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BalanceAnalyser; + +// TODO: Complete semantic implementation diff --git a/src/components/BalanceAnalyser.res b/src/components/BalanceAnalyser.res deleted file mode 100644 index 477a6281..00000000 --- a/src/components/BalanceAnalyser.res +++ /dev/null @@ -1,521 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL BalanceAnalyser — game balance statistical analysis and Monte Carlo -/// simulation for IDApTIK level tuning. -/// -/// Five tabs: Overview (level stats table with difficulty scores and win rates), -/// Distributions (placeholder charts), Simulations (Monte Carlo results), -/// Recommendations (suggested parameter changes), and Difficulty Curve -/// (placeholder chart for the intended difficulty arc). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for balanceTab variants. -let tabLabel = (tab: balanceTab): string => - switch tab { - | TabOverview => "Overview" - | TabDistributions => "Distributions" - | TabSimulations => "Simulations" - | TabRecommendations => "Recommendations" - | TabDifficultyCurve => "Difficulty Curve" - } - -/// Render the tab bar. -let renderTabs = (active: balanceTab): Tea_Vdom.t => { - let tabs: array = [ - TabOverview, - TabDistributions, - TabSimulations, - TabRecommendations, - TabDifficultyCurve, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(BalanceAnalyser(SetBaTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Difficulty colour from score (0-10 scale). -let difficultyColour = (score: float): string => - if score >= 8.0 { - "text-red-400" - } else if score >= 5.0 { - "text-amber-400" - } else if score >= 3.0 { - "text-emerald-400" - } else { - "text-blue-400" - } - -/// Win rate colour (higher is greener, lower is redder). -let winRateColour = (rate: float): string => - if rate >= 0.7 { - "text-emerald-400" - } else if rate >= 0.4 { - "text-amber-400" - } else { - "text-red-400" - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Overview tab: level stats table with difficulty scores and estimated win rates. -let renderOverviewTab = (state: balanceAnalyserState): Tea_Vdom.t => { - if Array.length(state.levelStats) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No level statistics loaded. Run a simulation to generate balance data.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - // Summary - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.levelStats))} level(s) analysed`)}, - ), - // Table header - div( - list{ - Attrs.class_( - "grid grid-cols-6 gap-2 px-3 py-2 text-xs text-gray-500 font-medium border-b border-gray-800", - ), - }, - list{ - span(list{}, list{text("Level")}), - span(list{Attrs.class_("text-right")}, list{text("Difficulty")}), - span(list{Attrs.class_("text-right")}, list{text("Win Rate")}), - span(list{Attrs.class_("text-right")}, list{text("Guard Rate")}), - span(list{Attrs.class_("text-right")}, list{text("Alert Threshold")}), - span(list{Attrs.class_("text-right")}, list{text("Outlier")}), - }, - ), - // Table rows - div( - list{Attrs.class_("flex flex-col gap-1 max-h-80 overflow-y-auto")}, - state.levelStats - ->Array.map(level => { - let isSelected = state.selectedLevel === Some(level.levelId) - let bgCls = isSelected ? "bg-gray-750 border border-cyan-700" : "bg-gray-800" - let diffColour = difficultyColour(level.difficultyScore) - let wrColour = winRateColour(level.estimatedWinRate) - let outlierColour = if level.outlierScore > 2.0 { - "text-red-400" - } else if level.outlierScore > 1.0 { - "text-amber-400" - } else { - "text-gray-400" - } - div( - list{ - Attrs.class_( - `grid grid-cols-6 gap-2 px-3 py-2 text-sm rounded cursor-pointer hover:bg-gray-750 ${bgCls}`, - ), - Events.onClick(BalanceAnalyser(SelectLevel(level.levelId))), - }, - list{ - span(list{Attrs.class_("text-gray-300 truncate")}, list{text(level.levelName)}), - span( - list{Attrs.class_(`text-right font-mono ${diffColour}`)}, - list{text(Float.toFixed(level.difficultyScore, ~digits=1))}, - ), - span( - list{Attrs.class_(`text-right font-mono ${wrColour}`)}, - list{text(`${Float.toFixed(level.estimatedWinRate *. 100.0, ~digits=0)}%`)}, - ), - span( - list{Attrs.class_("text-right font-mono text-gray-400")}, - list{text(Float.toFixed(level.guardSpawnRate, ~digits=2))}, - ), - span( - list{Attrs.class_("text-right font-mono text-gray-400")}, - list{text(Float.toFixed(level.alertThreshold, ~digits=2))}, - ), - span( - list{Attrs.class_(`text-right font-mono ${outlierColour}`)}, - list{text(Float.toFixed(level.outlierScore, ~digits=2))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Distributions tab: placeholder for distribution charts. -let renderDistributionsTab = (state: balanceAnalyserState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-40 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{ - text( - `Difficulty distribution chart (${Int.toString( - Array.length(state.distributions), - )} buckets)`, - ), - }, - ), - }, - ), - // Distribution data summary - if Array.length(state.distributions) > 0 { - div( - list{Attrs.class_("flex flex-col gap-1 max-h-48 overflow-y-auto")}, - state.distributions - ->Array.map(point => { - let widthPct = Int.toString(Int.fromFloat(point.percentage)) - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-16 text-gray-400 font-mono text-right")}, - list{text(point.bucket)}, - ), - div( - list{Attrs.class_("flex-1 h-3 bg-gray-700 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full bg-cyan-600 transition-all duration-300 w-[${widthPct}%]`, - ), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-gray-500 font-mono w-12 text-right")}, - list{text(`${Int.toString(point.count)}`)}, - ), - }, - ) - }) - ->List.fromArray, - ) - } else { - noNode - }, - }, - ) -} - -/// Simulations tab: Monte Carlo simulation results. -let renderSimulationsTab = (state: balanceAnalyserState): Tea_Vdom.t => { - if Array.length(state.simulations) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No simulation results. Click Run Simulation to generate balance data.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{ - text( - `${Int.toString(Array.length(state.simulations))} simulation(s) (${Int.toString( - state.simulationRuns, - )} runs each)`, - ), - }, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-80 overflow-y-auto")}, - state.simulations - ->Array.map(sim => { - let wrColour = winRateColour(sim.winRate) - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(sim.levelId)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(sim.runs)} runs`)}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-3 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_(`${wrColour}`)}, - list{text(`Win: ${Float.toFixed(sim.winRate *. 100.0, ~digits=1)}%`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Avg: ${Float.toFixed(sim.avgCompletionTime, ~digits=0)}s`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`P95: ${Float.toFixed(sim.p95CompletionTime, ~digits=0)}s`)}, - ), - div( - list{Attrs.class_("text-red-400")}, - list{text(`Guard: ${Int.toString(sim.deathsByGuard)}`)}, - ), - div( - list{Attrs.class_("text-amber-400")}, - list{text(`Trap: ${Int.toString(sim.deathsByTrap)}`)}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text(`Timeout: ${Int.toString(sim.deathsByTimeout)}`)}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Recommendations tab: suggested parameter changes for balance improvement. -let renderRecommendationsTab = (state: balanceAnalyserState): Tea_Vdom.t => { - if Array.length(state.recommendations) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No recommendations yet. Run simulations to generate balance suggestions.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - state.recommendations - ->Array.map(item => { - let delta = item.suggestedValue -. item.currentValue - let deltaSign = delta >= 0.0 ? "+" : "" - let deltaColour = if Float.parseFloat(Float.toFixed(delta, ~digits=2)) === 0.0 { - "text-gray-400" - } else if delta > 0.0 { - "text-emerald-400" - } else { - "text-red-400" - } - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-300")}, - list{text(`${item.levelId}: ${item.parameter}`)}, - ), - span( - list{Attrs.class_(`text-xs font-mono ${deltaColour}`)}, - list{ - text( - `${Float.toFixed(item.currentValue, ~digits=2)} -> ${Float.toFixed( - item.suggestedValue, - ~digits=2, - )} (${deltaSign}${Float.toFixed(delta, ~digits=2)})`, - ), - }, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(item.reason)}), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(`Impact: ${item.impact}`)}), - // Apply button - div( - list{Attrs.class_("flex justify-end mt-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer", - ), - Events.onClick( - BalanceAnalyser( - ApplyRecommendation(item.levelId, item.parameter, item.suggestedValue), - ), - ), - }, - list{text("Apply")}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Difficulty Curve tab: placeholder chart for the intended difficulty arc. -let renderDifficultyCurveTab = (state: balanceAnalyserState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-48 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{ - text( - `Difficulty curve chart (${Int.toString(Array.length(state.levelStats))} levels)`, - ), - }, - ), - }, - ), - // Level difficulty as horizontal bars for visual reference - div( - list{Attrs.class_("flex flex-col gap-1 max-h-48 overflow-y-auto")}, - state.levelStats - ->Array.map(level => { - let widthPct = Int.toString(Int.fromFloat(level.difficultyScore *. 10.0)) - let barColour = if level.difficultyScore >= 8.0 { - "bg-red-500" - } else if level.difficultyScore >= 5.0 { - "bg-amber-500" - } else { - "bg-emerald-500" - } - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-24 text-gray-400 truncate text-right")}, - list{text(level.levelName)}, - ), - div( - list{Attrs.class_("flex-1 h-3 bg-gray-700 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full ${barColour} transition-all duration-300 w-[${widthPct}%]`, - ), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-gray-500 font-mono w-8 text-right")}, - list{text(Float.toFixed(level.difficultyScore, ~digits=1))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: balanceAnalyserState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabOverview => renderOverviewTab(state) - | TabDistributions => renderDistributionsTab(state) - | TabSimulations => renderSimulationsTab(state) - | TabRecommendations => renderRecommendationsTab(state) - | TabDifficultyCurve => renderDifficultyCurveTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with Run Simulation - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Balance Analyser")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(BalanceAnalyser(RunSimulation)), - KeyboardNav.onActivate(BalanceAnalyser(RunSimulation)), - }, - list{text("Run Simulation")}, - ), - }, - ), - // Running indicator - if state.running { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-amber-400 rounded-full animate-pulse")}, list{}), - span(list{Attrs.class_("text-sm text-amber-300")}, list{text("Simulation running...")}), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/BetaFeedbackHub.affine b/src/components/BetaFeedbackHub.affine new file mode 100644 index 00000000..e255edba --- /dev/null +++ b/src/components/BetaFeedbackHub.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BetaFeedbackHub; + +// TODO: Complete semantic implementation diff --git a/src/components/BetaFeedbackHub.res b/src/components/BetaFeedbackHub.res deleted file mode 100644 index 9309befc..00000000 --- a/src/components/BetaFeedbackHub.res +++ /dev/null @@ -1,459 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL BetaFeedbackHub — feedback-o-tron integration and player feedback -/// triage for IDApTIK beta testing programmes. -/// -/// Five tabs: Inbox (feedback list with upvote/downvote), Triaged (processed -/// entries), Sentiment (chart placeholder), Submit (new feedback form), and -/// Analytics (aggregate statistics). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for betaFeedbackTab variants. -let tabLabel = (tab: betaFeedbackTab): string => - switch tab { - | TabInbox => "Inbox" - | TabTriaged => "Triaged" - | TabSentiment => "Sentiment" - | TabSubmit => "Submit" - | TabAnalytics => "Analytics" - } - -/// Render the tab bar. -let renderTabs = (active: betaFeedbackTab): Tea_Vdom.t => { - let tabs: array = [TabInbox, TabTriaged, TabSentiment, TabSubmit, TabAnalytics] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(BetaFeedbackHub(SetBfhTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Category badge colour. -let categoryBadge = (cat: feedbackCategory): Tea_Vdom.t => { - let (colour, lbl) = switch cat { - | FeedbackBug => ("bg-red-600 text-white", "BUG") - | FeedbackFeature => ("bg-blue-600 text-white", "FEAT") - | FeedbackBalance => ("bg-purple-600 text-white", "BAL") - | FeedbackUx => ("bg-cyan-600 text-white", "UX") - | FeedbackPerformance => ("bg-amber-600 text-white", "PERF") - | FeedbackOther => ("bg-gray-600 text-gray-200", "OTHER") - } - span(list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded font-mono ${colour}`)}, list{text(lbl)}) -} - -/// Priority indicator. -let priorityIndicator = (priority: feedbackPriority): Tea_Vdom.t => { - let (colour, lbl) = switch priority { - | FeedbackCritical => ("text-red-400", "P0") - | FeedbackHigh => ("text-orange-400", "P1") - | FeedbackMedium => ("text-amber-400", "P2") - | FeedbackLow => ("text-gray-400", "P3") - } - span(list{Attrs.class_(`text-xs font-mono ${colour}`)}, list{text(lbl)}) -} - -/// Sentiment icon. -let sentimentIcon = (sentiment: feedbackSentiment): Tea_Vdom.t => { - let (colour, sym) = switch sentiment { - | SentimentPositive => ("text-emerald-400", "+") - | SentimentNeutral => ("text-gray-400", "=") - | SentimentNegative => ("text-red-400", "-") - | SentimentUnknown => ("text-gray-600", "?") - } - span(list{Attrs.class_(`text-xs font-mono ${colour}`)}, list{text(sym)}) -} - -/// Status label. -let statusLabel = (status: feedbackStatus): string => - switch status { - | FeedbackNew => "NEW" - | FeedbackTriaged => "TRIAGED" - | FeedbackInProgress => "IN PROGRESS" - | FeedbackResolved => "RESOLVED" - | FeedbackWontFix => "WON'T FIX" - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Render a feedback entry row with upvote/downvote buttons. -let feedbackRow = (entry: feedbackEntry): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-3 py-2 bg-gray-800 rounded text-sm cursor-pointer hover:bg-gray-750", - ), - Events.onClick(BetaFeedbackHub(SelectFeedback(entry.id))), - }, - list{ - // Vote buttons - div( - list{Attrs.class_("flex flex-col items-center gap-0.5")}, - list{ - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-emerald-400 cursor-pointer"), - Events.onClick(BetaFeedbackHub(Upvote(entry.id))), - }, - list{text("^")}, - ), - span( - list{Attrs.class_("text-xs font-mono text-gray-400")}, - list{text(Int.toString(entry.upvotes - entry.downvotes))}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-red-400 cursor-pointer"), - Events.onClick(BetaFeedbackHub(Downvote(entry.id))), - }, - list{text("v")}, - ), - }, - ), - // Priority and category - priorityIndicator(entry.priority), - categoryBadge(entry.category), - sentimentIcon(entry.sentiment), - // Title - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(entry.title)}), - // Submitter and platform - span( - list{Attrs.class_("text-gray-600 text-xs")}, - list{text(`${entry.submittedBy} (${entry.platform})`)}, - ), - }, - ) -} - -/// Inbox tab: all feedback entries sorted by newest or votes. -let renderInboxTab = (state: betaFeedbackHubState): Tea_Vdom.t => { - let newEntries = state.entries->Array.filter(e => - switch e.status { - | FeedbackNew => true - | _ => false - } - ) - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - // Controls - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`${Int.toString(Array.length(newEntries))} new feedback item(s)`)}, - ), - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded cursor-pointer ${state.sortByUpvotes - ? "bg-cyan-700 text-white" - : "bg-gray-700 text-gray-400"}`, - ), - Events.onClick(BetaFeedbackHub(ToggleSortByVotes)), - KeyboardNav.onActivate(BetaFeedbackHub(ToggleSortByVotes)), - }, - list{text(state.sortByUpvotes ? "Sort: Votes" : "Sort: Recent")}, - ), - }, - ), - // Entry list - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - newEntries->Array.map(entry => feedbackRow(entry))->List.fromArray, - ), - }, - ) -} - -/// Triaged tab: entries that have been processed. -let renderTriagedTab = (state: betaFeedbackHubState): Tea_Vdom.t => { - let processed = state.entries->Array.filter(e => - switch e.status { - | FeedbackNew => false - | _ => true - } - ) - if Array.length(processed) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No triaged feedback yet.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-1 p-4 max-h-96 overflow-y-auto")}, - processed - ->Array.map(entry => { - div( - list{Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-800 rounded text-sm")}, - list{ - categoryBadge(entry.category), - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(entry.title)}), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(statusLabel(entry.status))}, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Sentiment tab: chart placeholder showing positive/neutral/negative distribution. -let renderSentimentTab = (state: betaFeedbackHubState): Tea_Vdom.t => { - let positive = state.entries->Array.filter(e => e.sentiment === SentimentPositive)->Array.length - let neutral = state.entries->Array.filter(e => e.sentiment === SentimentNeutral)->Array.length - let negative = state.entries->Array.filter(e => e.sentiment === SentimentNegative)->Array.length - let total = Array.length(state.entries) - - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // Sentiment summary - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-emerald-400")}, - list{text(Int.toString(positive))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Positive")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(neutral))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Neutral")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-red-400")}, - list{text(Int.toString(negative))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Negative")}), - }, - ), - }, - ), - // Chart placeholder - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-32 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text(`Sentiment distribution chart (${Int.toString(total)} entries)`)}, - ), - }, - ), - }, - ) -} - -/// Submit tab: new feedback form. -let renderSubmitTab = (state: betaFeedbackHubState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Submit New Feedback")}, - ), - // Title input - div( - list{Attrs.class_("flex flex-col gap-1")}, - list{ - label(list{Attrs.class_("text-xs text-gray-500")}, list{text("Title")}), - input( - list{ - Attrs.class_( - "w-full bg-gray-800 text-gray-200 text-sm rounded px-3 py-2 border border-gray-700 focus:border-cyan-600 focus:outline-none", - ), - Attrs.placeholder("Brief description of the feedback..."), - Events.onInput(text => BetaFeedbackHub(UpdateSubmitTitle(text))), - }, - list{}, - ), - }, - ), - // Body textarea - div( - list{Attrs.class_("flex flex-col gap-1")}, - list{ - label(list{Attrs.class_("text-xs text-gray-500")}, list{text("Details")}), - textarea( - list{ - Attrs.class_( - "w-full h-24 bg-gray-800 text-gray-200 text-sm rounded p-3 border border-gray-700 focus:border-cyan-600 focus:outline-none resize-y", - ), - Attrs.placeholder("Describe the feedback in detail..."), - Events.onInput(text => BetaFeedbackHub(UpdateSubmitBody(text))), - }, - list{}, - ), - }, - ), - // Submit button - div( - list{Attrs.class_("flex justify-end")}, - list{ - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded font-medium cursor-pointer ${state.submitting - ? "bg-gray-600 text-gray-400" - : "bg-emerald-700 text-white hover:bg-emerald-600"}`, - ), - Events.onClick(BetaFeedbackHub(SubmitFeedback)), - KeyboardNav.onActivate(BetaFeedbackHub(SubmitFeedback)), - }, - list{text(state.submitting ? "Submitting..." : "Submit Feedback")}, - ), - }, - ), - }, - ) -} - -/// Analytics tab: aggregate statistics from all feedback. -let renderAnalyticsTab = (state: betaFeedbackHubState): Tea_Vdom.t => { - let total = Array.length(state.entries) - let bugs = state.entries->Array.filter(e => e.category === FeedbackBug)->Array.length - let features = state.entries->Array.filter(e => e.category === FeedbackFeature)->Array.length - let balance = state.entries->Array.filter(e => e.category === FeedbackBalance)->Array.length - - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(total))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Total")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-red-400")}, - list{text(Int.toString(bugs))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Bugs")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-blue-400")}, - list{text(Int.toString(features))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Features")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-purple-400")}, - list{text(Int.toString(balance))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Balance")}), - }, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: betaFeedbackHubState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabInbox => renderInboxTab(state) - | TabTriaged => renderTriagedTab(state) - | TabSentiment => renderSentimentTab(state) - | TabSubmit => renderSubmitTab(state) - | TabAnalytics => renderAnalyticsTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Beta Feedback Hub")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.entries))} entries`)}, - ), - }, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/Boj.affine b/src/components/Boj.affine new file mode 100644 index 00000000..28029766 --- /dev/null +++ b/src/components/Boj.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Boj; + +// TODO: Complete semantic implementation diff --git a/src/components/Boj.res b/src/components/Boj.res deleted file mode 100644 index 2a81f967..00000000 --- a/src/components/Boj.res +++ /dev/null @@ -1,1317 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL BoJ Component — view for the Bundle of Joy cartridge server panel. -/// -/// 5 tabs: Dashboard, Cartridges (17×10 matrix), Topology, Federation, Invoke. -/// All views use Tea_Html (no JSX). Accessible with ARIA roles and labels. - -open Msg -open BojModel -open BojEngine -open Tea.Html - -// =========================================================================== -// TypeLL Cross-Panel Type Intelligence -// =========================================================================== - -/// Render TypeLL cross-panel type intelligence result (if available). -/// Parses the raw JSON via TypeLLEngine.parseCheckResult and displays an -/// evangeliser-style narrative with proof obligations and linearity notes. -let viewTypeCheckResult = (lastTypeCheck: option): Tea_Vdom.t => { - switch lastTypeCheck { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let narrative = TypeLLEngine.generateNarrative(result) - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - let statusText = if result.valid { - "Type-safe" - } else { - "Type issues detected" - } - div( - list{Attrs.class_("mt-4 p-3 rounded-lg border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL")}, - ), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono mb-1")}, - list{text(result.typeSignature)}, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(narrative.celebrate)}), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -/// Render a tab button. -let renderTab = (label: string, active: bool, onClick: msg): Tea_Vdom.t => { - let baseClass = "px-3 py-1.5 text-xs rounded-t border-b-2 transition-colors" - let activeClass = active - ? `${baseClass} text-cyan-300 border-cyan-400 bg-gray-800` - : `${baseClass} text-gray-500 border-transparent hover:text-gray-300` - button( - list{ - Attrs.class_(activeClass), - Events.onClick(onClick), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Render the connection status indicator. -let renderConnectionStatus = (state: bojState): Tea_Vdom.t => { - let (dotClass, label) = if state.connected { - ("bg-green-400", "Connected") - } else { - ("bg-red-400", "Disconnected") - } - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - div(list{Attrs.class_(`w-2 h-2 rounded-full ${dotClass}`)}, list{}), - span(list{Attrs.class_("text-gray-400")}, list{text(label)}), - span(list{Attrs.class_("text-gray-600 ml-2")}, list{text(state.serverUrl)}), - }, - ) -} - -/// Render a stat card. -let renderStat = (label: string, value: string, colour: string): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-800/50 border border-gray-700 rounded p-3")}, - list{ - div(list{Attrs.class_(`text-lg font-bold ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text(label)}), - }, - ) -} - -// ============================================================================ -// Dashboard Tab -// ============================================================================ - -let renderDashboard = (state: bojState): Tea_Vdom.t => { - let total = Array.length(state.cartridges) - let loaded = loadedCount(state.cartridges) - let gradeD = countByGrade(state.cartridges, GradeD) - let gradeC = countByGrade(state.cartridges, GradeC) - let gradeB = countByGrade(state.cartridges, GradeB) - let gradeA = countByGrade(state.cartridges, GradeA) - let peerCount = Array.length(state.umoja.peers) - - div( - list{Attrs.class_("space-y-4")}, - list{ - // Stats grid - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - renderStat("Total Cartridges", Int.toString(total), "text-cyan-300"), - renderStat("Loaded", Int.toString(loaded), "text-green-300"), - renderStat("Umoja Peers", Int.toString(peerCount), "text-indigo-300"), - renderStat("Federation", state.umoja.active ? "Active" : "Inactive", "text-amber-300"), - }, - ), - // Grade breakdown - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-700 rounded p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{text("Cartridge Readiness Grades")}, - ), - div( - list{Attrs.class_("flex gap-4 text-xs")}, - list{ - span( - list{Attrs.class_("text-yellow-300")}, - list{text(`D(Alpha): ${Int.toString(gradeD)}`)}, - ), - span( - list{Attrs.class_("text-blue-300")}, - list{text(`C(Beta): ${Int.toString(gradeC)}`)}, - ), - span( - list{Attrs.class_("text-emerald-300")}, - list{text(`B(RC): ${Int.toString(gradeB)}`)}, - ), - span( - list{Attrs.class_("text-green-300")}, - list{text(`A(Prod): ${Int.toString(gradeA)}`)}, - ), - }, - ), - }, - ), - // Architecture summary - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-700 rounded p-3")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text("Architecture")}), - div( - list{Attrs.class_("text-xs text-gray-300 font-mono space-y-1")}, - list{ - div( - list{}, - list{ - text( - "Idris2 ABI (dependent types) → Zig FFI (C-compatible) → V-lang (REST+gRPC+GraphQL)", - ), - }, - ), - div( - list{}, - list{text("Umoja: gossip protocol, SHA-256 attestation, distributed federation")}, - ), - div(list{}, list{text("Hot-reload: unmount → verify hash → remount")}), - }, - ), - }, - ), - // Latency log visualization - if Array.length(state.latencyLog) > 0 { - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-700 rounded p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-3")}, - list{text("Invocation Latency (last 20)")}, - ), - // Histogram bars - div( - list{Attrs.class_("flex items-end gap-0.5 h-16")}, - state.latencyLog - ->Array.slice(~start=0, ~end=20) - ->Array.map(entry => { - let maxMs = 500.0 - let heightPct = Math.min(entry.durationMs /. maxMs *. 100.0, 100.0) - let colour = if entry.durationMs < 50.0 { - "bg-emerald-500" - } else if entry.durationMs < 200.0 { - "bg-amber-500" - } else { - "bg-red-500" - } - div( - list{ - Attrs.class_(`flex-1 ${colour} rounded-t transition-all min-w-1`), - Attrs.style("height", `${Float.toFixed(heightPct, ~digits=0)}%`), - Attrs.title( - `${entry.cartridge}/${entry.tool}: ${Float.toFixed( - entry.durationMs, - ~digits=1, - )}ms`, - ), - }, - list{}, - ) - }) - ->List.fromArray, - ), - { - let totalMs = state.latencyLog->Array.reduce(0.0, (acc, e) => acc +. e.durationMs) - let count = Float.fromInt(Array.length(state.latencyLog)) - let avgMs = totalMs /. count - let maxEntry = state.latencyLog->Array.reduce(state.latencyLog->Array.getUnsafe(0), ( - best, - e, - ) => - if e.durationMs > best.durationMs { - e - } else { - best - } - ) - let minEntry = state.latencyLog->Array.reduce(state.latencyLog->Array.getUnsafe(0), ( - best, - e, - ) => - if e.durationMs < best.durationMs { - e - } else { - best - } - ) - div( - list{Attrs.class_("flex gap-4 mt-2 text-[10px] text-gray-600")}, - list{ - span(list{}, list{text(`Avg: ${Float.toFixed(avgMs, ~digits=1)}ms`)}), - span( - list{}, - list{text(`Min: ${Float.toFixed(minEntry.durationMs, ~digits=1)}ms`)}, - ), - span( - list{}, - list{text(`Max: ${Float.toFixed(maxEntry.durationMs, ~digits=1)}ms`)}, - ), - span( - list{}, - list{text(`Total: ${Int.toString(Array.length(state.latencyLog))} calls`)}, - ), - }, - ) - }, - }, - ) - } else { - noNode - }, - // Hot-reload pipeline indicator - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-700 rounded p-3")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text("Hot-Reload Pipeline")}), - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{ - Attrs.class_( - "px-2 py-0.5 bg-red-900/30 text-red-400 rounded border border-red-800", - ), - }, - list{text("1. Unmount")}, - ), - span(list{Attrs.class_("text-gray-700")}, list{text("→")}), - span( - list{ - Attrs.class_( - "px-2 py-0.5 bg-amber-900/30 text-amber-400 rounded border border-amber-800", - ), - }, - list{text("2. Verify SHA-256")}, - ), - span(list{Attrs.class_("text-gray-700")}, list{text("→")}), - span( - list{ - Attrs.class_( - "px-2 py-0.5 bg-emerald-900/30 text-emerald-400 rounded border border-emerald-800", - ), - }, - list{text("3. Remount")}, - ), - span( - list{Attrs.class_("text-gray-600 ml-auto text-[10px]")}, - list{text("Zero-downtime cartridge replacement")}, - ), - }, - ), - }, - ), - // Refresh button - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-cyan-900/50 text-cyan-300 rounded border border-cyan-700 hover:bg-cyan-800/50", - ), - Events.onClick(Boj(RefreshHealth)), - KeyboardNav.onActivate(Boj(RefreshHealth)), - }, - list{text("Check Health")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded border border-gray-600 hover:bg-gray-600", - ), - Events.onClick(Boj(RefreshCartridges)), - KeyboardNav.onActivate(Boj(RefreshCartridges)), - }, - list{text("Refresh Cartridges")}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Cartridges Tab — Matrix View -// ============================================================================ - -let renderCartridgeRow = (cartridge: bojCartridge): Tea_Vdom.t => { - let gradeBadge = { - let colour = gradeColour(cartridge.grade) - span( - list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded border ${colour}`)}, - list{text(gradeLabel(cartridge.grade))}, - ) - } - let loadedBadge = if cartridge.loaded { - span(list{Attrs.class_("text-green-400 text-xs")}, list{text("●")}) - } else { - span(list{Attrs.class_("text-gray-600 text-xs")}, list{text("○")}) - } - let protoCells = - allProtocols - ->Array.map(proto => { - let has = hasProtocol(cartridge, proto) - td( - list{Attrs.class_("px-1 py-1 text-center text-xs")}, - list{ - if has { - span(list{Attrs.class_("text-cyan-400")}, list{text("██")}) - } else { - span(list{Attrs.class_("text-gray-800")}, list{text(" ")}) - }, - }, - ) - }) - ->List.fromArray - - tr( - list{ - Attrs.class_("border-b border-gray-800 hover:bg-gray-800/30 cursor-pointer"), - Events.onClick(Boj(SelectCartridge(cartridge.name))), - }, - list{ - td(list{Attrs.class_("px-2 py-1 text-xs")}, list{loadedBadge}), - td(list{Attrs.class_("px-2 py-1 text-xs text-gray-200")}, list{text(cartridge.displayName)}), - td(list{Attrs.class_("px-2 py-1")}, list{gradeBadge}), - td( - list{Attrs.class_("px-2 py-1 text-xs text-gray-500")}, - list{text(layerProgress(cartridge.layers))}, - ), - ...protoCells, - }, - ) -} - -let renderCartridgesMatrix = (state: bojState): Tea_Vdom.t => { - let filtered = filterCartridges(state.cartridges, state.filterText) - let protoHeaders = - allProtocols - ->Array.map(proto => { - th( - list{Attrs.class_("px-1 py-1 text-xs text-gray-500 font-normal text-center")}, - list{text(protocolShort(proto))}, - ) - }) - ->List.fromArray - - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700", - ), - Attrs.placeholder("Filter cartridges..."), - Attrs.value(state.filterText), - Events.onInput(t => Boj(SetBojFilter(t))), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(Array.length(filtered))} of ${Int.toString( - Array.length(state.cartridges), - )}`, - ), - }, - ), - }, - ), - // Matrix table - div( - list{Attrs.class_("overflow-x-auto")}, - list{ - table( - list{ - Attrs.class_("w-full text-left"), - Attrs.role("grid"), - Attrs.ariaLabel("Cartridge capability matrix"), - }, - list{ - thead( - list{}, - list{ - tr( - list{Attrs.class_("border-b border-gray-700")}, - list{ - th(list{Attrs.class_("px-2 py-1 text-xs text-gray-500 font-normal")}, list{}), - th( - list{Attrs.class_("px-2 py-1 text-xs text-gray-500 font-normal")}, - list{text("Cartridge")}, - ), - th( - list{Attrs.class_("px-2 py-1 text-xs text-gray-500 font-normal")}, - list{text("Grade")}, - ), - th( - list{Attrs.class_("px-2 py-1 text-xs text-gray-500 font-normal")}, - list{text("Layers")}, - ), - ...protoHeaders, - }, - ), - }, - ), - tbody(list{}, filtered->Array.map(renderCartridgeRow)->List.fromArray), - }, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Cartridge Detail (shown when a cartridge is selected) -// ============================================================================ - -let renderCartridgeDetail = (state: bojState, name: string): Tea_Vdom.t => { - let cartOpt = state.cartridges->Array.find(c => c.name === name) - switch cartOpt { - | None => div(list{}, list{text("Cartridge not found")}) - | Some(cart) => - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-700 rounded p-4 space-y-3")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(cart.displayName)}, - ), - span( - list{ - Attrs.class_(`px-1.5 py-0.5 text-xs rounded border ${gradeColour(cart.grade)}`), - }, - list{text(gradeLabel(cart.grade))}, - ), - }, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-300"), - Events.onClick(Boj(SelectCartridge(""))), - Attrs.ariaLabel("Close detail"), - }, - list{text("✕")}, - ), - }, - ), - // Description - div(list{Attrs.class_("text-xs text-gray-400")}, list{text(cart.description)}), - // Layer status - div( - list{Attrs.class_("grid grid-cols-4 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_(cart.layers.abiReady ? "text-green-400" : "text-gray-600")}, - list{text(cart.layers.abiReady ? "✓ ABI (Idris2)" : "○ ABI (Idris2)")}, - ), - div( - list{Attrs.class_(cart.layers.ffiReady ? "text-green-400" : "text-gray-600")}, - list{text(cart.layers.ffiReady ? "✓ FFI (Zig)" : "○ FFI (Zig)")}, - ), - div( - list{Attrs.class_(cart.layers.adapterReady ? "text-green-400" : "text-gray-600")}, - list{text(cart.layers.adapterReady ? "✓ Adapter (V)" : "○ Adapter (V)")}, - ), - div( - list{Attrs.class_(cart.layers.sharedLibReady ? "text-green-400" : "text-gray-600")}, - list{text(cart.layers.sharedLibReady ? "✓ .so built" : "○ .so pending")}, - ), - }, - ), - // Ports - if cart.restPort > 0 || cart.grpcPort > 0 || cart.graphqlPort > 0 { - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500")}, - list{ - if cart.restPort > 0 { - span(list{}, list{text(`REST :${Int.toString(cart.restPort)}`)}) - } else { - noNode - }, - if cart.grpcPort > 0 { - span(list{}, list{text(`gRPC :${Int.toString(cart.grpcPort)}`)}) - } else { - noNode - }, - if cart.graphqlPort > 0 { - span(list{}, list{text(`GraphQL :${Int.toString(cart.graphqlPort)}`)}) - } else { - noNode - }, - }, - ) - } else { - noNode - }, - // Load/Unload buttons - div( - list{Attrs.class_("flex gap-2")}, - list{ - if cart.loaded { - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-red-900/50 text-red-300 rounded border border-red-700 hover:bg-red-800/50", - ), - Events.onClick(Boj(UnloadCartridge(cart.name))), - }, - list{text("Unload")}, - ) - } else { - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-cyan-900/50 text-cyan-300 rounded border border-cyan-700 hover:bg-cyan-800/50", - ), - Events.onClick(Boj(LoadCartridge(cart.name))), - }, - list{text("Load")}, - ) - }, - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded border border-gray-600 hover:bg-gray-600", - ), - Events.onClick(Boj(SetBojCategory(Invoke))), - }, - list{text("Invoke →")}, - ), - }, - ), - // SHA hash - if cart.soHash !== "" { - div( - list{Attrs.class_("text-xs text-gray-600 font-mono truncate")}, - list{text(`SHA-256: ${cart.soHash}`)}, - ) - } else { - noNode - }, - }, - ) - } -} - -// ============================================================================ -// Topology Tab — Interactive Layered Architecture View -// ============================================================================ - -/// Render a single topology layer card with detail metrics. -let renderTopologyLayer = ( - title: string, - subtitle: string, - colour: string, - borderColour: string, - metrics: array<(string, string)>, - readyCount: int, - totalCount: int, -): Tea_Vdom.t => { - let pct = if totalCount > 0 { - readyCount * 100 / totalCount - } else { - 0 - } - div( - list{ - Attrs.class_( - `bg-gray-800/40 border rounded-lg p-4 ${borderColour} hover:bg-gray-800/60 transition-colors`, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_(`text-sm font-bold ${colour}`)}, list{text(title)}), - span(list{Attrs.class_("text-[10px] text-gray-600")}, list{text(subtitle)}), - }, - ), - // Readiness gauge - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("w-20 h-2 bg-gray-700 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full ${if pct >= 80 { - "bg-emerald-500" - } else if pct >= 50 { - "bg-amber-500" - } else { - "bg-red-500" - }}`, - ), - Attrs.style("width", `${Int.toString(pct)}%`), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-[10px] text-gray-500 font-mono")}, - list{text(`${Int.toString(readyCount)}/${Int.toString(totalCount)}`)}, - ), - }, - ), - }, - ), - // Metrics grid - div( - list{Attrs.class_("grid grid-cols-3 gap-2 mt-2")}, - metrics - ->Array.map(((label, value)) => - div( - list{Attrs.class_("text-xs")}, - list{ - div(list{Attrs.class_("text-gray-600")}, list{text(label)}), - div(list{Attrs.class_("text-gray-300 font-mono")}, list{text(value)}), - }, - ) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Render the data flow arrow between layers. -let renderFlowArrow = (label: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center justify-center py-1")}, - list{ - div( - list{Attrs.class_("text-gray-700 text-xs flex items-center gap-1")}, - list{ - span(list{}, list{text("▼")}), - span(list{Attrs.class_("text-[10px] text-gray-600")}, list{text(label)}), - span(list{}, list{text("▼")}), - }, - ), - }, - ) -} - -let renderTopology = (state: bojState): Tea_Vdom.t => { - let abiReady = state.cartridges->Array.filter(c => c.layers.abiReady)->Array.length - let ffiReady = state.cartridges->Array.filter(c => c.layers.ffiReady)->Array.length - let adapterReady = state.cartridges->Array.filter(c => c.layers.adapterReady)->Array.length - let soReady = state.cartridges->Array.filter(c => c.layers.sharedLibReady)->Array.length - let total = Array.length(state.cartridges) - let restPorts = state.cartridges->Array.filter(c => c.restPort > 0)->Array.length - let grpcPorts = state.cartridges->Array.filter(c => c.grpcPort > 0)->Array.length - let gqlPorts = state.cartridges->Array.filter(c => c.graphqlPort > 0)->Array.length - - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-3")}, - list{text("BoJ Architecture — Interactive 3-Layer Cartridge Stack")}, - ), - // Layer 1: V-lang Adapter - renderTopologyLayer( - "V-lang Triple Adapter", - "REST + gRPC + GraphQL", - "text-violet-400", - "border-violet-800", - [ - ("REST endpoints", Int.toString(restPorts)), - ("gRPC endpoints", Int.toString(grpcPorts)), - ("GraphQL endpoints", Int.toString(gqlPorts)), - ], - adapterReady, - total, - ), - renderFlowArrow("C ABI calls"), - // Layer 2: Zig FFI - renderTopologyLayer( - "Zig FFI", - "C-compatible, zero-cost", - "text-amber-400", - "border-amber-800", - [ - (".so files built", Int.toString(soReady)), - ("State machines", Int.toString(ffiReady)), - ("Hash verified", Int.toString(soReady)), - ], - ffiReady, - total, - ), - renderFlowArrow("dependent type proofs"), - // Layer 3: Idris2 ABI - renderTopologyLayer( - "Idris2 ABI", - "Dependent types, formal proofs", - "text-cyan-400", - "border-cyan-800", - [ - ("Definitions ready", Int.toString(abiReady)), - ("Safe* modules", "7"), - ("Proof strategy", "compile-time"), - ], - abiReady, - total, - ), - renderFlowArrow("UDP gossip protocol"), - // Layer 4: Umoja Federation - renderTopologyLayer( - "Umoja Federation", - "IPv6 UDP gossip, SHA-256 attestation", - "text-indigo-400", - "border-indigo-800", - [ - ("Active peers", Int.toString(Array.length(state.umoja.peers))), - ("Gossip round", Int.toString(state.umoja.currentRound)), - ( - "Status", - if state.umoja.active { - "Active" - } else { - "Inactive" - }, - ), - ], - if state.umoja.active { - 1 - } else { - 0 - }, - 1, - ), - // Refresh - div( - list{Attrs.class_("flex gap-2 mt-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded border border-gray-600 hover:bg-gray-600", - ), - Events.onClick(Boj(RefreshTopology)), - KeyboardNav.onActivate(Boj(RefreshTopology)), - }, - list{text("Refresh Topology")}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Federation Tab -// ============================================================================ - -/// Format a relative time string from a Unix timestamp (seconds). -let relativeTime = (timestamp: float): string => { - let now = Date.now() /. 1000.0 - let diff = now -. timestamp - if diff < 0.0 { - "just now" - } else if diff < 60.0 { - `${Int.toString(Float.toInt(diff))}s ago` - } else if diff < 3600.0 { - `${Int.toString(Float.toInt(diff /. 60.0))}m ago` - } else if diff < 86400.0 { - `${Int.toString(Float.toInt(diff /. 3600.0))}h ago` - } else { - `${Int.toString(Float.toInt(diff /. 86400.0))}d ago` - } -} - -/// Determine catalogue sync status by comparing a peer's digest against -/// the local node's digest (first peer with Verified state, or local node). -let catalogueSyncLabel = (state: bojState, peer: umojaPeer): (string, string) => { - // Compare against the first verified peer's digest as a proxy for local. - // If the local catalogue digest is not tracked separately, we compare - // against the most common digest among verified peers. - let localDigest = - state.umoja.peers - ->Array.find(p => p.state === PeerVerified && p.nodeId !== peer.nodeId) - ->Option.map(p => p.catalogueDigest) - ->Option.getOr("") - if peer.catalogueDigest === "" { - ("unknown", "text-gray-600") - } else if localDigest === "" || peer.catalogueDigest === localDigest { - ("in sync", "text-green-400") - } else { - ("differs", "text-amber-400") - } -} - -/// Render per-peer action buttons based on peer state. -let renderPeerActions = (peer: umojaPeer): Tea_Vdom.t => { - let disconnectBtn = switch peer.state { - | PeerVerified | PeerExchanged => - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-red-900/40 text-red-300 rounded border border-red-800 hover:bg-red-800/50", - ), - Events.onClick(Boj(UmojaDisconnectPeer(peer.nodeId))), - Attrs.ariaLabel(`Disconnect peer ${peer.nodeId}`), - }, - list{text("Disconnect")}, - ) - | PeerPending | PeerRejected | PeerStale => noNode - } - let syncBtn = switch peer.state { - | PeerVerified => - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-cyan-900/40 text-cyan-300 rounded border border-cyan-800 hover:bg-cyan-800/50", - ), - Events.onClick(Boj(UmojaSyncCatalogue(peer.nodeId))), - Attrs.ariaLabel(`Sync catalogue with peer ${peer.nodeId}`), - }, - list{text("Sync Catalogue")}, - ) - | PeerPending | PeerExchanged | PeerRejected | PeerStale => noNode - } - let metricsBtn = button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-700 text-gray-300 rounded border border-gray-600 hover:bg-gray-600", - ), - Events.onClick(Boj(UmojaPeerMetrics(peer.nodeId))), - Attrs.ariaLabel(`View metrics for peer ${peer.nodeId}`), - }, - list{text("Metrics")}, - ) - div(list{Attrs.class_("flex gap-1 ml-auto")}, list{syncBtn, disconnectBtn, metricsBtn}) -} - -let renderFederation = (state: bojState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Status bar - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_( - state.umoja.active - ? "px-2 py-1 text-xs bg-green-900/50 text-green-300 rounded border border-green-700" - : "px-2 py-1 text-xs bg-gray-800 text-gray-500 rounded border border-gray-700", - ), - }, - list{text(state.umoja.active ? "Federation Active" : "Federation Inactive")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Node: ${state.umoja.localNodeId}`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Round: ${Int.toString(state.umoja.currentRound)}`)}, - ), - }, - ), - // Add Peer section - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-700 rounded p-3")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text("Add Peer")}), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700", - ), - Attrs.placeholder("Peer address (e.g. 192.168.1.100:9876 or [::1]:9876)"), - Attrs.value(state.umojaAddPeerInput), - Events.onInput(v => Boj(UmojaAddPeerInput(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-indigo-900/50 text-indigo-300 rounded border border-indigo-700 hover:bg-indigo-800/50", - ), - Events.onClick(Boj(UmojaAddPeer(state.umojaAddPeerInput))), - Attrs.disabled(state.umojaAddPeerInput === ""), - Attrs.ariaLabel("Add peer to federation"), - }, - list{text("Add")}, - ), - }, - ), - }, - ), - // Peer list with management controls - if Array.length(state.umoja.peers) > 0 { - div( - list{Attrs.class_("space-y-1")}, - state.umoja.peers - ->Array.map(peer => { - let (syncStatus, syncColour) = catalogueSyncLabel(state, peer) - div( - list{ - Attrs.class_( - "flex items-center gap-3 bg-gray-800/30 border border-gray-700 rounded px-3 py-2", - ), - }, - list{ - // Peer state badge - span( - list{Attrs.class_(`text-xs font-medium ${peerStateColour(peer.state)}`)}, - list{text(peerStateLabel(peer.state))}, - ), - // Node ID - span(list{Attrs.class_("text-xs text-gray-300")}, list{text(peer.nodeId)}), - // Address - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(peer.address)}), - // Last seen (relative time) - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(peer.lastSeen > 0.0 ? relativeTime(peer.lastSeen) : "never")}, - ), - // Catalogue sync status - span(list{Attrs.class_(`text-xs ${syncColour}`)}, list{text(syncStatus)}), - // Catalogue digest (truncated) - span( - list{Attrs.class_("text-xs text-gray-700 font-mono truncate max-w-32")}, - list{text(peer.catalogueDigest)}, - ), - // Per-peer action buttons - renderPeerActions(peer), - }, - ) - }) - ->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-xs text-gray-500 italic")}, - list{ - text( - "No peers discovered. Add a peer or start the Umoja federation layer to begin gossip.", - ), - }, - ) - }, - // Action buttons row - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-amber-900/50 text-amber-300 rounded border border-amber-700 hover:bg-amber-800/50", - ), - Events.onClick(Boj(UmojaTriggerGossip)), - KeyboardNav.onActivate(Boj(UmojaTriggerGossip)), - Attrs.ariaLabel("Trigger manual gossip round"), - }, - list{text("Trigger Gossip Round")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-indigo-900/50 text-indigo-300 rounded border border-indigo-700 hover:bg-indigo-800/50", - ), - Events.onClick(Boj(RefreshUmoja)), - KeyboardNav.onActivate(Boj(RefreshUmoja)), - }, - list{text("Refresh Federation")}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Invoke Tab -// ============================================================================ - -let renderInvoke = (state: bojState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - // Cartridge selector - div( - list{Attrs.class_("space-y-1")}, - list{ - label(list{Attrs.class_("text-xs text-gray-400")}, list{text("Cartridge")}), - select( - list{ - Attrs.class_( - "w-full px-3 py-1.5 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700", - ), - Events.onChange(v => Boj(SetInvokeCartridge(v))), - }, - list{ - option'(list{Attrs.value("")}, list{text("Select cartridge...")}), - ...state.cartridges - ->Array.filter(c => c.loaded) - ->Array.map(c => option'(list{Attrs.value(c.name)}, list{text(c.displayName)})) - ->List.fromArray, - }, - ), - }, - ), - // Tool name - div( - list{Attrs.class_("space-y-1")}, - list{ - label(list{Attrs.class_("text-xs text-gray-400")}, list{text("Tool")}), - input( - list{ - Attrs.class_( - "w-full px-3 py-1.5 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700", - ), - Attrs.placeholder("Tool name (e.g. query, connect, status)"), - Attrs.value(state.invokeTool), - Events.onInput(v => Boj(SetInvokeTool(v))), - }, - list{}, - ), - }, - ), - // Args (JSON) - div( - list{Attrs.class_("space-y-1")}, - list{ - label(list{Attrs.class_("text-xs text-gray-400")}, list{text("Arguments (JSON)")}), - textarea( - list{ - Attrs.class_( - "w-full px-3 py-1.5 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700 font-mono h-20", - ), - Attrs.placeholder(`{"key": "value"}`), - Events.onInput(v => Boj(SetInvokeArgs(v))), - }, - list{}, - ), - }, - ), - // Execute button - button( - list{ - Attrs.class_( - "px-4 py-2 text-xs bg-cyan-900/50 text-cyan-300 rounded border border-cyan-700 hover:bg-cyan-800/50", - ), - Events.onClick(Boj(ExecuteInvoke)), - KeyboardNav.onActivate(Boj(ExecuteInvoke)), - Attrs.disabled(state.invokeCartridge === "" || state.invokeTool === ""), - }, - list{text(state.loading ? "Invoking..." : "Invoke")}, - ), - // Result - switch state.invokeResult { - | None => noNode - | Some(result) => - div( - list{ - Attrs.class_( - result.success - ? "bg-green-900/20 border border-green-800 rounded p-3" - : "bg-red-900/20 border border-red-800 rounded p-3", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{ - Attrs.class_( - result.success ? "text-xs text-green-400" : "text-xs text-red-400", - ), - }, - list{text(result.success ? "Success" : "Failed")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(result.durationMs)}ms`)}, - ), - }, - ), - pre( - list{ - Attrs.class_( - "text-xs text-gray-300 font-mono whitespace-pre-wrap overflow-auto max-h-40", - ), - }, - list{text(result.payload)}, - ), - }, - ) - }, - // TypeLL type-check result - viewTypeCheckResult(state.lastTypeCheck), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -let view = (state: bojState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 z-40 bg-gray-950/95 flex flex-col overflow-hidden"), - Attrs.role("dialog"), - Attrs.ariaLabel("Bundle of Joy — Cartridge Server"), - }, - list{ - // Header - div( - list{Attrs.class_("px-4 py-3 border-b border-gray-800 flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-cyan-300")}, - list{text("BoJ — Bundle of Joy")}, - ), - renderConnectionStatus(state), - }, - ), - // Close button - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300 text-sm"), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - Attrs.ariaLabel("Close BoJ panel"), - }, - list{text("✕")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("px-4 pt-2 flex gap-1 border-b border-gray-800"), Attrs.role("tablist")}, - list{ - renderTab( - "Dashboard", - state.activeCategory === Dashboard, - Boj(SetBojCategory(Dashboard)), - ), - renderTab( - "Cartridges", - state.activeCategory === Cartridges, - Boj(SetBojCategory(Cartridges)), - ), - renderTab("Topology", state.activeCategory === Topology, Boj(SetBojCategory(Topology))), - renderTab( - "Federation", - state.activeCategory === Federation, - Boj(SetBojCategory(Federation)), - ), - renderTab("Invoke", state.activeCategory === Invoke, Boj(SetBojCategory(Invoke))), - }, - ), - // Error banner - switch state.error { - | None => noNode - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/30 border border-red-800 rounded text-xs text-red-300 flex justify-between", - ), - }, - list{ - span(list{}, list{text(err)}), - button( - list{ - Attrs.class_("text-red-500 hover:text-red-300 ml-2"), - Events.onClick(Boj(DismissBojError)), - KeyboardNav.onActivate(Boj(DismissBojError)), - }, - list{text("✕")}, - ), - }, - ) - }, - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - // Selected cartridge detail (shown above matrix) - switch state.selectedCartridge { - | Some(name) if name !== "" => renderCartridgeDetail(state, name) - | _ => noNode - }, - // Tab content - switch state.activeCategory { - | Dashboard => renderDashboard(state) - | Cartridges => renderCartridgesMatrix(state) - | Topology => renderTopology(state) - | Federation => renderFederation(state) - | Invoke => renderInvoke(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/BuildDashboard.affine b/src/components/BuildDashboard.affine new file mode 100644 index 00000000..799b1ef0 --- /dev/null +++ b/src/components/BuildDashboard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BuildDashboard; + +// TODO: Complete semantic implementation diff --git a/src/components/BuildDashboard.res b/src/components/BuildDashboard.res deleted file mode 100644 index 3e021253..00000000 --- a/src/components/BuildDashboard.res +++ /dev/null @@ -1,454 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Build Dashboard Component — view for monitoring build processes, -/// test results, and compilation status across IDApTIK sub-projects. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: buildDashboardCategory, - active: buildDashboardCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(BuildDashboard(SetBuildCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render overview — target cards with status. -let renderOverview = (state: buildDashboardState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Stats row - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-red-400")}, - list{text(Int.toString(BuildDashboardEngine.errorCount(state.messages)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Errors")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-amber-400")}, - list{text(Int.toString(BuildDashboardEngine.warningCount(state.messages)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Warnings")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-emerald-400")}, - list{ - text( - `${Int.toString( - BuildDashboardEngine.passedTestCount(state.testResults), - )}/${Int.toString(Array.length(state.testResults))}`, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Tests Passed")}), - }, - ), - }, - ), - // Target cards - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-3")}, - state.targets - ->Array.map(((target, status)) => { - let colourCls = BuildDashboardEngine.targetColour(target) - let statusCls = BuildDashboardEngine.statusColour(status) - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_(`text-sm font-medium ${colourCls}`)}, - list{text(BuildDashboardEngine.targetLabel(target))}, - ), - span( - list{Attrs.class_(`text-xs ${statusCls}`)}, - list{text(BuildDashboardEngine.statusLabel(status))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(BuildDashboard(TriggerBuild(target))), - }, - list{text("Build")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer", - ), - Events.onClick(BuildDashboard(RunTests(target))), - }, - list{text("Test")}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - // Controls - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - if state.watchMode { - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - }, - ), - Events.onClick(BuildDashboard(ToggleWatchMode)), - KeyboardNav.onActivate(BuildDashboard(ToggleWatchMode)), - }, - list{ - text( - if state.watchMode { - "Watch Mode On" - } else { - "Watch Mode" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - if state.autoRebuild { - "px-3 py-1.5 text-xs bg-amber-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - }, - ), - Events.onClick(BuildDashboard(ToggleAutoRebuild)), - KeyboardNav.onActivate(BuildDashboard(ToggleAutoRebuild)), - }, - list{ - text( - if state.autoRebuild { - "Auto-Rebuild On" - } else { - "Auto-Rebuild" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - if state.bojRouting { - "px-3 py-1.5 text-xs bg-blue-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600" - }, - ), - Attrs.ariaLabel( - if state.bojRouting { - "Disable BoJ routing" - } else { - "Enable BoJ routing" - }, - ), - Events.onClick(BuildDashboard(ToggleBuildBojRouting)), - KeyboardNav.onActivate(BuildDashboard(ToggleBuildBojRouting)), - }, - list{ - text( - if state.bojRouting { - "BoJ On" - } else { - "BoJ" - }, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render errors/warnings list. -let renderErrors = (state: buildDashboardState): Tea_Vdom.t => { - if Array.length(state.messages) === 0 { - div( - list{Attrs.class_("text-center text-emerald-400 text-sm py-8")}, - list{text("No build errors or warnings")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - state.messages - ->Array.map(m => { - let sevCls = switch m.severity { - | "error" => "border-red-800 bg-red-900/20" - | "warning" => "border-amber-800 bg-amber-900/20" - | _ => "border-gray-700 bg-gray-800" - } - let targetCls = BuildDashboardEngine.targetColour(m.target) - div( - list{ - Attrs.class_(`p-2 rounded border ${sevCls} cursor-pointer hover:opacity-80`), - Events.onClick(EditorBridge(OpenFileInEditor(m.filePath, m.line))), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1 text-xs")}, - list{ - span( - list{Attrs.class_(`font-bold ${targetCls}`)}, - list{text(BuildDashboardEngine.targetLabel(m.target))}, - ), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(`${m.filePath}:${Int.toString(m.line)}:${Int.toString(m.col)}`)}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-300")}, list{text(m.message)}), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Render test results. -let renderTests = (state: buildDashboardState): Tea_Vdom.t => { - let results = if state.showPassedTests { - state.testResults - } else { - state.testResults->Array.filter(r => !r.passed) - } - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if state.showPassedTests { - "px-2 py-1 text-xs bg-gray-600 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(BuildDashboard(ToggleShowPassed)), - KeyboardNav.onActivate(BuildDashboard(ToggleShowPassed)), - }, - list{text("Show Passed")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${Int.toString( - BuildDashboardEngine.passedTestCount(state.testResults), - )} passed, ${Int.toString( - BuildDashboardEngine.failedTestCount(state.testResults), - )} failed`, - ), - }, - ), - }, - ), - if Array.length(results) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No test results")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - results - ->Array.map(r => - div( - list{ - Attrs.class_( - `flex items-center gap-3 p-2 rounded text-xs ${if r.passed { - "bg-gray-800" - } else { - "bg-red-900/20 border border-red-800" - }}`, - ), - }, - list{ - span( - list{ - Attrs.class_( - if r.passed { - "text-emerald-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if r.passed { - "PASS" - } else { - "FAIL" - }, - ), - }, - ), - span(list{Attrs.class_("text-gray-200 flex-1")}, list{text(r.name)}), - span(list{Attrs.class_("text-gray-500")}, list{text(r.suite)}), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(`${Float.toString(r.durationMs)}ms`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render build history. -let renderHistory = (state: buildDashboardState): Tea_Vdom.t => { - if Array.length(state.history) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No build history")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.history - ->Array.map(entry => { - let statusCls = BuildDashboardEngine.statusColour(entry.status) - let targetCls = BuildDashboardEngine.targetColour(entry.target) - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span( - list{Attrs.class_(`w-20 ${targetCls}`)}, - list{text(BuildDashboardEngine.targetLabel(entry.target))}, - ), - span( - list{Attrs.class_(statusCls)}, - list{text(BuildDashboardEngine.statusLabel(entry.status))}, - ), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(`${Float.toString(entry.durationMs)}ms`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(entry.errorCount)}E ${Int.toString(entry.warningCount)}W`)}, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Main view function. -let view = (state: buildDashboardState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Build Dashboard panel"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Build Dashboard")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(BuildDashboard(RefreshBuildStatus)), - KeyboardNav.onActivate(BuildDashboard(RefreshBuildStatus)), - }, - list{text("Refresh")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Overview", BuildOverview, state.activeCategory), - renderTab("Errors", BuildErrors, state.activeCategory), - renderTab("Tests", BuildTests, state.activeCategory), - renderTab("History", BuildHistory, state.activeCategory), - }, - ), - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | BuildOverview => renderOverview(state) - | BuildErrors => renderErrors(state) - | BuildTests => renderTests(state) - | BuildHistory => renderHistory(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/BurbleRoomMonitor.affine b/src/components/BurbleRoomMonitor.affine new file mode 100644 index 00000000..9736f16c --- /dev/null +++ b/src/components/BurbleRoomMonitor.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BurbleRoomMonitor; + +// TODO: Complete semantic implementation diff --git a/src/components/BurbleRoomMonitor.res b/src/components/BurbleRoomMonitor.res deleted file mode 100644 index 818f9a76..00000000 --- a/src/components/BurbleRoomMonitor.res +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Burble Room Monitor Component — Active room dashboard. -/// -/// Displays active voice rooms with participant counts, recording status, -/// and speaking indicators per participant. Emits PanelBus events for -/// cross-panel communication: -/// - BurbleSpeechStarted(userId, displayName) -/// - BurbleSpeechEnded(userId) -/// - BurbleVoiceStarted(panelContext, roomId) -/// - BurbleVoiceEnded(panelContext) -/// -/// Uses the groove discovery protocol for room listing via -/// GET /api/v1/servers/:id/rooms. -/// -/// TEA pattern: -/// Model: BurbleModel.burbleState (participants, huddle state) -/// Cmd: BurbleCmd.listRooms, BurbleCmd.joinHuddle -/// View: This file — renders room list and participant detail - -open Msg -open Tea.Html - -// =========================================================================== -// Room card — displays a single room with participant list -// =========================================================================== - -/// Render the "current room" section when the user is in a huddle. -let rec renderCurrentRoom = (state: BurbleModel.burbleState): Tea_Vdom.t => { - switch state.currentHuddle { - | None => - div( - list{Attrs.class_("px-4 py-6 text-center border-b border-gray-800")}, - list{ - div(list{Attrs.class_("text-gray-500 text-sm mb-2")}, list{text("Not in a room")}), - div( - list{Attrs.class_("text-gray-600 text-xs")}, - list{text("Join a huddle to see room activity and participant details.")}, - ), - }, - ) - | Some(huddleId) => - let participants = BurbleEngine.getParticipants(state) - let speakingCount = Array.length(BurbleEngine.speakingParticipants(state)) - - div( - list{Attrs.class_("border-b border-gray-800")}, - list{ - // Room header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 bg-gray-900/50")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Active indicator - div( - list{Attrs.class_("w-2.5 h-2.5 rounded-full bg-emerald-400 animate-pulse")}, - list{}, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text(huddleId)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(Array.length(participants))} participants, ${Int.toString( - speakingCount, - )} speaking`, - ), - }, - ), - }, - ), - }, - ), - // Leave button - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-red-900/50 text-red-300 rounded hover:bg-red-800/50 border border-red-700 transition-colors", - ), - Events.onClick(Burble(BurbleModel.LeftHuddle)), - KeyboardNav.onActivate(Burble(BurbleModel.LeftHuddle)), - Attrs.title("Leave room"), - }, - list{text("Leave")}, - ), - }, - ), - // Recording status - div( - list{ - Attrs.class_( - "flex items-center gap-2 px-4 py-2 border-t border-gray-800/50 bg-gray-900/30", - ), - }, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full bg-gray-600")}, list{}), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Recording: Off")}), - div(list{Attrs.class_("flex-1")}, list{}), - span(list{Attrs.class_("text-xs text-gray-600")}, list{text("Consent: Avow")}), - }, - ), - // Participant list - div( - list{Attrs.class_("max-h-64 overflow-y-auto")}, - participants - ->Array.map(p => renderParticipantRow(p)) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render a single participant row with speaking indicator. -and renderParticipantRow = (participant: BurbleModel.participant): Tea_Vdom.t => { - let speakingIndicator = participant.isSpeaking - ? "border-l-2 border-l-emerald-500 bg-gray-900/30" - : "border-l-2 border-l-transparent" - let voiceLabel = switch participant.voiceState { - | BurbleModel.Connected => "Active" - | BurbleModel.Muted => "Muted" - | BurbleModel.Deafened => "Deaf" - } - let voiceColour = switch participant.voiceState { - | BurbleModel.Connected => "text-emerald-400" - | BurbleModel.Muted => "text-amber-400" - | BurbleModel.Deafened => "text-red-400" - } - - div( - list{ - Attrs.class_(`flex items-center gap-3 py-2 px-4 ${speakingIndicator} transition-all`), - Attrs.ariaLabel(`${participant.displayName}: ${voiceLabel}`), - }, - list{ - // Speaking animation - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full transition-colors ${participant.isSpeaking - ? "bg-emerald-400" - : "bg-gray-700"}`, - ), - }, - list{}, - ), - // Name - span(list{Attrs.class_("text-sm text-gray-300 flex-1")}, list{text(participant.displayName)}), - // Voice state badge - span(list{Attrs.class_(`text-xs ${voiceColour}`)}, list{text(voiceLabel)}), - // Volume level (small bar) - div( - list{Attrs.class_("w-16 h-1.5 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full ${participant.isSpeaking - ? "bg-emerald-500" - : "bg-gray-600"} transition-all duration-100`, - ), - Attrs.style("width", `${Float.toFixed(participant.volume *. 100.0, ~digits=0)}%`), - }, - list{}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Room actions — join/create room controls -// =========================================================================== - -/// Render room action buttons. -let renderRoomActions = (inHuddle: bool): Tea_Vdom.t => { - if inHuddle { - noNode - } else { - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-3 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-900/50 text-emerald-300 rounded hover:bg-emerald-800/50 border border-emerald-700 transition-colors", - ), - Events.onClick(Burble(BurbleModel.ConnectionChanged(BurbleModel.Connecting))), - Attrs.title("Connect to Burble server"), - }, - list{text("Connect")}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("Connect to see available rooms")}, - ), - }, - ) - } -} - -// =========================================================================== -// Groove room discovery section -// =========================================================================== - -/// Render the groove-discovered room list. -/// Currently shows the current huddle. The listRooms command would populate -/// additional rooms from the Burble API. -let renderDiscoveredRooms = (_state: BurbleModel.burbleState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - // Section header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-300 font-medium")}, - list{text("Discovered Rooms")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded hover:bg-gray-700 border border-gray-700 transition-colors", - ), - Events.onClick(Burble(BurbleModel.ConnectionChanged(BurbleModel.Connecting))), - Attrs.title("Refresh room list from groove endpoint"), - }, - list{text("Refresh")}, - ), - }, - ), - // Placeholder for additional rooms from the API - div( - list{Attrs.class_("px-4 py-4 text-center")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("Room discovery via groove: GET /api/v1/servers/:id/rooms")}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Local voice controls — mute/deafen -// =========================================================================== - -/// Render local voice control buttons. -let renderVoiceControls = (state: BurbleModel.burbleState): Tea_Vdom.t => { - if !BurbleEngine.isInHuddle(state) { - noNode - } else { - div( - list{ - Attrs.class_("flex items-center gap-3 px-4 py-2 border-t border-gray-800 bg-gray-900/30"), - }, - list{ - // Mute toggle - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded border transition-colors ${state.isMuted - ? "bg-amber-900/50 text-amber-300 border-amber-700" - : "bg-gray-800 text-gray-300 border-gray-700 hover:bg-gray-700"}`, - ), - Events.onClick(Burble(BurbleModel.MuteToggled(!state.isMuted))), - }, - list{text(state.isMuted ? "Unmute" : "Mute")}, - ), - // Deafen toggle - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded border transition-colors ${state.isDeafened - ? "bg-red-900/50 text-red-300 border-red-700" - : "bg-gray-800 text-gray-300 border-gray-700 hover:bg-gray-700"}`, - ), - Events.onClick(Burble(BurbleModel.DeafenToggled(!state.isDeafened))), - }, - list{text(state.isDeafened ? "Undeafen" : "Deafen")}, - ), - // Status - div(list{Attrs.class_("flex-1")}, list{}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(BurbleEngine.localVoiceLabel(state))}, - ), - }, - ) - } -} - -// =========================================================================== -// Main view — full-screen panel overlay -// =========================================================================== - -/// Main Burble Room Monitor panel view. -/// -/// Layout: -/// Header (title, close) -/// Room actions (connect/join when not in huddle) -/// Current room (participant list with speaking indicators) -/// Discovered rooms (from groove discovery) -/// Voice controls (mute/deafen when in huddle) -let view = (state: BurbleModel.burbleState): Tea_Vdom.t => { - let inHuddle = BurbleEngine.isInHuddle(state) - - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Burble Room Monitor panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Room Monitor")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Burble")}), - if inHuddle { - span(list{Attrs.class_("text-xs text-emerald-400")}, list{text("LIVE")}) - } else { - noNode - }, - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 border-b border-red-800 text-xs text-red-400"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Room actions (when not in huddle) - renderRoomActions(inHuddle), - // Current room detail - renderCurrentRoom(state), - // Discovered rooms from groove - renderDiscoveredRooms(state), - // Voice controls (when in huddle) - renderVoiceControls(state), - }, - ) -} diff --git a/src/components/BurbleServerStatus.affine b/src/components/BurbleServerStatus.affine new file mode 100644 index 00000000..7f3a03c4 --- /dev/null +++ b/src/components/BurbleServerStatus.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BurbleServerStatus; + +// TODO: Complete semantic implementation diff --git a/src/components/BurbleServerStatus.res b/src/components/BurbleServerStatus.res deleted file mode 100644 index 6bf5d0f0..00000000 --- a/src/components/BurbleServerStatus.res +++ /dev/null @@ -1,291 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Burble Server Status Component — Groove-aware health panel. -/// -/// Displays Burble server connection state, active rooms, total participants, -/// uptime, and a groove capability summary showing which services are connected. -/// -/// Uses the groove discovery protocol to probe localhost:6473/.well-known/groove -/// and enriches the view with capability details (voice, text, presence, TTS, -/// STT, recording, spatial audio). -/// -/// TEA pattern: -/// Model: BurbleModel.burbleState + grooveManifest from BurbleCmd -/// Update: BurbleEngine.update (pure state transitions) -/// Cmd: BurbleCmd.checkGroove, BurbleCmd.getHealth -/// View: This file — renders server status and groove discovery - -open Msg -open Tea.Html - -// =========================================================================== -// Groove capability row — coloured pill with endpoint and protocol -// =========================================================================== - -/// Capability status from groove manifest. -type grooveCapability = { - name: string, - capType: string, - protocol: string, - endpoint: string, - panelCompatible: bool, -} - -/// Render a single groove capability as a status row. -let renderCapability = (cap: grooveCapability): Tea_Vdom.t => { - let compatClass = cap.panelCompatible ? "text-emerald-400" : "text-gray-600" - let protocolBadge = switch cap.protocol { - | "webrtc" => "bg-purple-900/50 text-purple-300 border-purple-700" - | "websocket" => "bg-blue-900/50 text-blue-300 border-blue-700" - | "http" => "bg-amber-900/50 text-amber-300 border-amber-700" - | _ => "bg-gray-800 text-gray-400 border-gray-700" - } - - div( - list{Attrs.class_("flex items-center gap-3 py-1.5 px-3 border-b border-gray-800/50")}, - list{ - // Capability name - span(list{Attrs.class_("text-sm text-gray-300 w-28")}, list{text(cap.name)}), - // Protocol badge - span( - list{Attrs.class_(`text-xs px-1.5 py-0.5 rounded border ${protocolBadge}`)}, - list{text(cap.protocol)}, - ), - // Endpoint - span(list{Attrs.class_("text-xs text-gray-500 font-mono flex-1")}, list{text(cap.endpoint)}), - // Panel compatible indicator - span( - list{ - Attrs.class_(`text-xs ${compatClass}`), - Attrs.title(cap.panelCompatible ? "Panel-compatible" : "Not panel-compatible"), - }, - list{text(cap.panelCompatible ? "PANEL" : "---")}, - ), - }, - ) -} - -// =========================================================================== -// Connection status indicator -// =========================================================================== - -/// Render the server connection status header. -let renderConnectionStatus = (connection: BurbleModel.connectionState): Tea_Vdom.t => { - let (dotClass, label) = switch connection { - | Disconnected => ("bg-gray-600", "Disconnected") - | Connecting => ("bg-amber-400 animate-pulse", "Connecting...") - | Connected => ("bg-emerald-400", "Connected") - | Reconnecting => ("bg-amber-400 animate-pulse", "Reconnecting...") - | Failed(reason) => ("bg-red-500", `Failed: ${reason}`) - } - - div( - list{Attrs.class_("flex items-center gap-3 px-4 py-3 border-b border-gray-800")}, - list{ - div(list{Attrs.class_(`w-3 h-3 rounded-full ${dotClass}`)}, list{}), - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 font-medium")}, list{text("Burble Server")}), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(label)}), - }, - ), - // Server URL - span(list{Attrs.class_("text-xs text-gray-600 font-mono")}, list{text("localhost:6473")}), - }, - ) -} - -// =========================================================================== -// Stats grid — rooms, participants, uptime -// =========================================================================== - -/// Render a stat card in the overview grid. -let statCard = (label: string, value: string, colour: string): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-3 text-center")}, - list{ - div(list{Attrs.class_(`text-2xl font-bold ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text(label)}), - }, - ) -} - -/// Render the server stats overview grid. -let renderStatsGrid = (state: BurbleModel.burbleState): Tea_Vdom.t => { - let participantCount = Int.toString(Array.length(Dict.valuesToArray(state.participants))) - let roomLabel = switch state.currentHuddle { - | Some(_) => "1" - | None => "0" - } - - div( - list{Attrs.class_("grid grid-cols-3 gap-3 px-4 py-3 border-b border-gray-800")}, - list{ - statCard("Active Rooms", roomLabel, "text-blue-400"), - statCard("Participants", participantCount, "text-emerald-400"), - statCard("Connection", BurbleEngine.localVoiceLabel(state), "text-amber-400"), - }, - ) -} - -// =========================================================================== -// Groove capabilities section -// =========================================================================== - -/// The known Burble groove capabilities from the /.well-known/groove manifest. -/// These are rendered as a summary of what the Burble server offers. -let burbleCapabilities: array = [ - {name: "Voice", capType: "voice", protocol: "webrtc", endpoint: "/voice", panelCompatible: true}, - { - name: "Text", - capType: "text", - protocol: "websocket", - endpoint: "/socket/websocket", - panelCompatible: true, - }, - { - name: "Presence", - capType: "presence", - protocol: "websocket", - endpoint: "/socket/websocket", - panelCompatible: true, - }, - { - name: "Spatial", - capType: "spatial-audio", - protocol: "webrtc", - endpoint: "/voice", - panelCompatible: false, - }, - { - name: "Recording", - capType: "recording", - protocol: "http", - endpoint: "/api/v1/recordings", - panelCompatible: true, - }, - {name: "TTS", capType: "tts", protocol: "http", endpoint: "/api/v1/tts", panelCompatible: false}, - {name: "STT", capType: "stt", protocol: "http", endpoint: "/api/v1/stt", panelCompatible: false}, -] - -/// Render the groove capabilities section. -let renderGrooveCapabilities = (connected: bool): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - // Section header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-300 font-medium")}, - list{text("Groove Capabilities")}, - ), - span(list{Attrs.class_("text-xs text-gray-600")}, list{text("/.well-known/groove")}), - }, - ), - // Refresh button - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded hover:bg-gray-700 border border-gray-700 transition-colors", - ), - Events.onClick(Burble(BurbleModel.ConnectionChanged(BurbleModel.Connecting))), - Attrs.title("Re-probe groove endpoint"), - }, - list{text("Probe")}, - ), - }, - ), - // Capability list (dimmed if disconnected) - div( - list{Attrs.class_(connected ? "" : "opacity-50")}, - burbleCapabilities - ->Array.map(cap => renderCapability(cap)) - ->List.fromArray - ->List.toArray - ->List.fromArray, - ), - }, - ) -} - -// =========================================================================== -// Error display -// =========================================================================== - -/// Render error bar if present. -let renderError = (error: option): Tea_Vdom.t => { - switch error { - | Some(err) => - div( - list{Attrs.class_("px-4 py-2 bg-red-900/30 border-b border-red-800 text-xs text-red-400")}, - list{text(err)}, - ) - | None => noNode - } -} - -// =========================================================================== -// Main view — full-screen panel overlay -// =========================================================================== - -/// Main Burble Server Status panel view. -/// -/// Layout: -/// Header (title, close) -/// Connection status (dot, label, URL) -/// Stats grid (rooms, participants, uptime) -/// Error bar (if any) -/// Groove capabilities (capability list with protocols) -let view = (state: BurbleModel.burbleState): Tea_Vdom.t => { - let isConnected = BurbleEngine.isConnected(state) - - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Burble Server Status panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Burble Server Status")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("groove-aware")}), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Connection status - renderConnectionStatus(state.connection), - // Stats grid - renderStatsGrid(state), - // Error display - renderError(state.error), - // Groove capabilities - renderGrooveCapabilities(isConnected), - }, - ) -} diff --git a/src/components/BurbleVoiceQuality.affine b/src/components/BurbleVoiceQuality.affine new file mode 100644 index 00000000..4ab7ec90 --- /dev/null +++ b/src/components/BurbleVoiceQuality.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module BurbleVoiceQuality; + +// TODO: Complete semantic implementation diff --git a/src/components/BurbleVoiceQuality.res b/src/components/BurbleVoiceQuality.res deleted file mode 100644 index 80dd8de6..00000000 --- a/src/components/BurbleVoiceQuality.res +++ /dev/null @@ -1,351 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Burble Voice Quality Component — WebRTC audio metrics panel. -/// -/// Displays real-time audio quality indicators for the active Burble session: -/// - Latency (round-trip time in ms) -/// - Jitter (variation in packet arrival time) -/// - Packet loss (percentage of dropped audio packets) -/// - Bitrate (current Opus encode bitrate) -/// - Per-participant audio level indicators (volume bars) -/// - Codec info (Opus configuration, sample rate, channels) -/// - Spatial audio status (enabled/disabled, coordinate system) -/// -/// When no session is active, shows a disconnected state with instructions. -/// -/// TEA pattern: -/// Model: BurbleModel.burbleState (participant volumes, connection state) -/// Cmd: BurbleCmd.getVoiceStats (WebRTC stats query) -/// View: This file — renders quality metrics and participant levels - -open Msg -open Tea.Html - -// =========================================================================== -// Metric card — single stat with colour-coded quality threshold -// =========================================================================== - -/// Quality rating for colour coding. -type qualityRating = - | Good - | Fair - | Poor - -/// Get CSS classes for a quality rating. -let qualityColour = (rating: qualityRating): string => { - switch rating { - | Good => "text-emerald-400" - | Fair => "text-amber-400" - | Poor => "text-red-400" - } -} - -/// Render a single metric card with value and quality indicator. -let metricCard = (label: string, value: string, unit: string, rating: qualityRating): Tea_Vdom.t< - msg, -> => { - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-3")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text(label)}), - div( - list{Attrs.class_("flex items-baseline gap-1")}, - list{ - span(list{Attrs.class_(`text-xl font-bold ${qualityColour(rating)}`)}, list{text(value)}), - span(list{Attrs.class_("text-xs text-gray-600")}, list{text(unit)}), - }, - ), - }, - ) -} - -// =========================================================================== -// Audio quality metrics grid -// =========================================================================== - -/// Render the audio quality metrics grid. -/// These are representative values based on connection state since actual -/// WebRTC stats come from the Rust backend via BurbleCmd.getVoiceStats. -let renderMetrics = (connected: bool): Tea_Vdom.t => { - // When connected, show representative good-quality metrics. - // The Update function would replace these with real WebRTC stats. - let (latency, jitter, loss, bitrate) = if connected { - ("23", "4", "0.1", "48") - } else { - ("--", "--", "--", "--") - } - let latencyRating = if connected { - Good - } else { - Poor - } - let jitterRating = if connected { - Good - } else { - Poor - } - let lossRating = if connected { - Good - } else { - Poor - } - let bitrateRating = if connected { - Good - } else { - Poor - } - - div( - list{Attrs.class_("grid grid-cols-4 gap-3 px-4 py-3 border-b border-gray-800")}, - list{ - metricCard("Latency", latency, "ms", latencyRating), - metricCard("Jitter", jitter, "ms", jitterRating), - metricCard("Packet Loss", loss, "%", lossRating), - metricCard("Bitrate", bitrate, "kbps", bitrateRating), - }, - ) -} - -// =========================================================================== -// Per-participant audio level bars -// =========================================================================== - -/// Render a single participant's audio level indicator. -let renderParticipantLevel = (participant: BurbleModel.participant): Tea_Vdom.t => { - let volumePercent = Float.toFixed(participant.volume *. 100.0, ~digits=0) - let barWidth = `${volumePercent}%` - let speakingClass = participant.isSpeaking ? "border-emerald-600" : "border-gray-800" - let stateLabel = switch participant.voiceState { - | BurbleModel.Connected => "Active" - | BurbleModel.Muted => "Muted" - | BurbleModel.Deafened => "Deafened" - } - let stateColour = switch participant.voiceState { - | BurbleModel.Connected => "text-emerald-400" - | BurbleModel.Muted => "text-amber-400" - | BurbleModel.Deafened => "text-red-400" - } - - div( - list{ - Attrs.class_(`flex items-center gap-3 py-2 px-4 border-b ${speakingClass} transition-colors`), - Attrs.ariaLabel(`${participant.displayName} audio level`), - }, - list{ - // Speaking indicator dot - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${participant.isSpeaking - ? "bg-emerald-400 animate-pulse" - : "bg-gray-700"}`, - ), - }, - list{}, - ), - // Name and state - div( - list{Attrs.class_("w-32")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300 truncate")}, - list{text(participant.displayName)}, - ), - div(list{Attrs.class_(`text-xs ${stateColour}`)}, list{text(stateLabel)}), - }, - ), - // Volume bar - div( - list{Attrs.class_("flex-1 h-3 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full transition-all duration-150 ${participant.isSpeaking - ? "bg-emerald-500" - : "bg-gray-600"}`, - ), - Attrs.style("width", barWidth), - }, - list{}, - ), - }, - ), - // Volume percentage - span( - list{Attrs.class_("text-xs text-gray-500 w-10 text-right font-mono")}, - list{text(`${volumePercent}%`)}, - ), - }, - ) -} - -/// Render the participant audio levels section. -let renderParticipants = (state: BurbleModel.burbleState): Tea_Vdom.t => { - let participants = BurbleEngine.getParticipants(state) - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - // Section header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-300 font-medium")}, - list{text("Participant Audio Levels")}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(Array.length(participants))} participants`)}, - ), - }, - ), - // Participant rows - if Array.length(participants) === 0 { - div( - list{Attrs.class_("flex items-center justify-center py-8")}, - list{ - div( - list{Attrs.class_("text-gray-600 text-sm text-center")}, - list{text("No participants in huddle. Join a room to see audio levels.")}, - ), - }, - ) - } else { - div( - list{}, - participants - ->Array.map(p => renderParticipantLevel(p)) - ->List.fromArray, - ) - }, - }, - ) -} - -// =========================================================================== -// Codec info section -// =========================================================================== - -/// Render codec and spatial audio info. -let renderCodecInfo = (connected: bool): Tea_Vdom.t => { - div( - list{Attrs.class_("px-4 py-3 border-t border-gray-800 bg-gray-900/30")}, - list{ - div( - list{Attrs.class_("flex items-center gap-6 text-xs text-gray-500")}, - list{ - // Codec - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("text-gray-600")}, list{text("Codec:")}), - span( - list{Attrs.class_(connected ? "text-gray-300" : "text-gray-600")}, - list{text(connected ? "Opus 48kHz" : "---")}, - ), - }, - ), - // Channels - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("text-gray-600")}, list{text("Channels:")}), - span( - list{Attrs.class_(connected ? "text-gray-300" : "text-gray-600")}, - list{text(connected ? "Stereo" : "---")}, - ), - }, - ), - // Spatial audio - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("text-gray-600")}, list{text("Spatial:")}), - span(list{Attrs.class_("text-gray-400")}, list{text("Off (Workspace profile)")}), - }, - ), - // E2EE - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("text-gray-600")}, list{text("E2EE:")}), - span( - list{Attrs.class_(connected ? "text-emerald-400" : "text-gray-600")}, - list{text(connected ? "ON" : "---")}, - ), - }, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Main view — full-screen panel overlay -// =========================================================================== - -/// Main Burble Voice Quality panel view. -/// -/// Layout: -/// Header (title, close) -/// Metrics grid (latency, jitter, packet loss, bitrate) -/// Participant audio levels (volume bars, speaking indicators) -/// Codec info bar (Opus config, spatial status, E2EE) -let view = (state: BurbleModel.burbleState): Tea_Vdom.t => { - let connected = BurbleEngine.isConnected(state) - - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Burble Voice Quality panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Voice Quality")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("WebRTC Metrics")}), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Metrics grid - renderMetrics(connected), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 border-b border-red-800 text-xs text-red-400"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Participant audio levels - renderParticipants(state), - // Codec info bar - renderCodecInfo(connected), - }, - ) -} diff --git a/src/components/Capture.affine b/src/components/Capture.affine new file mode 100644 index 00000000..ac9bfa15 --- /dev/null +++ b/src/components/Capture.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Capture; + +// TODO: Complete semantic implementation diff --git a/src/components/Capture.res b/src/components/Capture.res deleted file mode 100644 index 261ee265..00000000 --- a/src/components/Capture.res +++ /dev/null @@ -1,276 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Capture Panel — gallery, recordings, demos, cloning, comparisons (DD-022). -/// -/// The capture panel provides a gallery view of all screenshots and recordings, -/// demo package management, panel clone tracking, and comparison mode controls. - -open Model -open Msg -open Tea.Html - -/// Render the capture gallery — thumbnails of all screenshots. -let renderGallery = (capture: captureState): Tea_Vdom.t => { - if Array.length(capture.captures) === 0 { - div( - list{ - Attrs.class_("p-8 text-center text-gray-600 text-sm"), - Attrs.title("Use the capture bar (camera icon) on any panel to take screenshots"), - }, - list{text("No captures yet — use the capture bar on any panel to start")}, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-3 gap-4")}, - Array.map(capture.captures, entry => - div( - list{ - Attrs.class_( - "bg-gray-900 rounded border border-gray-800 p-3 hover:border-gray-600 transition-colors", - ), - Attrs.title( - `${entry.label} — ${entry.panelId} (${Float.toFixed(entry.timestamp, ~digits=0)})`, - ), - }, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(entry.label)}), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(entry.panelId ++ " " ++ (entry.isRecording ? "recording" : "screenshot"))}, - ), - button( - list{ - Attrs.class_("mt-2 text-xs text-red-500 hover:text-red-400"), - Events.onClick(Capture(RemoveCapture(entry.id))), - }, - list{text("Remove")}, - ), - }, - ) - )->List.fromArray, - ) - } -} - -/// Render recording status. -let renderRecordingStatus = (recording: recordingState): Tea_Vdom.t => { - switch recording { - | NotRecording => div(list{Attrs.class_("text-xs text-gray-600")}, list{text("Not recording")}) - | Recording(panelId, _startTime) => - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full bg-red-500 animate-pulse")}, list{}), - div(list{Attrs.class_("text-xs text-red-400")}, list{text("Recording: " ++ panelId)}), - button( - list{ - Attrs.class_("text-xs text-gray-400 hover:text-gray-300"), - Events.onClick(Capture(StopRecording)), - KeyboardNav.onActivate(Capture(StopRecording)), - }, - list{text("Stop")}, - ), - }, - ) - | Paused(panelId, _elapsed) => - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full bg-yellow-500")}, list{}), - div(list{Attrs.class_("text-xs text-yellow-400")}, list{text("Paused: " ++ panelId)}), - button( - list{ - Attrs.class_("text-xs text-gray-400 hover:text-gray-300"), - Events.onClick(Capture(TogglePauseRecording)), - KeyboardNav.onActivate(Capture(TogglePauseRecording)), - }, - list{text("Resume")}, - ), - }, - ) - } -} - -/// Full Capture panel view. -let view = (capture: captureState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 overflow-auto"), - Attrs.role("dialog"), - Attrs.ariaLabel("Capture panel"), - }, - list{ - // Header - div( - list{ - Attrs.class_( - "sticky top-0 bg-gray-950 border-b border-gray-800 p-4 flex items-center justify-between z-10", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div(list{Attrs.class_("text-lg font-light text-gray-300")}, list{text("Capture")}), - renderRecordingStatus(capture.recording), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Int.toString(Array.length(capture.captures)) ++ " captures")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - `px-2 py-1 rounded text-xs ${capture.captureBarVisible - ? "bg-blue-700 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(Capture(ToggleCaptureBar)), - KeyboardNav.onActivate(Capture(ToggleCaptureBar)), - Attrs.title("Toggle capture bar visibility on all panels"), - }, - list{text("Capture Bars")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 bg-gray-800 text-gray-400 rounded hover:bg-gray-700 transition-colors text-sm", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Body - div( - list{Attrs.class_("p-6 max-w-5xl mx-auto")}, - list{ - // Category tabs - div( - list{Attrs.class_("flex gap-2 mb-6")}, - [ - (CaptureGallery, "Gallery"), - (CaptureRecordings, "Recordings"), - (CaptureDemos, "Demos"), - (CaptureClones, "Clones"), - (CaptureComparison, "Comparison"), - ] - ->Array.map(((cat, label)) => - button( - list{ - Attrs.class_( - `px-3 py-1 rounded text-xs ${capture.activeCategory === cat - ? "bg-blue-700 text-white" - : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`, - ), - Events.onClick(Capture(SetCaptureCategory(cat))), - }, - list{text(label)}, - ) - ) - ->List.fromArray, - ), - // Content based on active tab - switch capture.activeCategory { - | CaptureGallery => renderGallery(capture) - | CaptureRecordings => - div( - list{ - Attrs.class_( - "p-4 bg-gray-900/50 rounded border border-gray-800 text-xs text-gray-600", - ), - Attrs.title( - "Start a recording from any panel's capture bar (record icon on panel edge)", - ), - }, - list{ - text( - "Recordings captured via panel capture bars appear here. Use the record button on any panel edge.", - ), - }, - ) - | CaptureDemos => - div( - list{ - Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800"), - Attrs.title( - "Demo packages: instructor records steps, student replays and compares", - ), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(Int.toString(Array.length(capture.demos)) ++ " demos loaded")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text( - "Record a panel session as a .panll-demo package for teaching. Students load the demo, see golden output in a locked reference panel, and work alongside it.", - ), - }, - ), - }, - ) - | CaptureClones => - div( - list{ - Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800"), - Attrs.title( - "Clone a panel to create an independent copy for before/after comparison", - ), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(Int.toString(Array.length(capture.clones)) ++ " clones")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text( - "Clone any panel's state for before/after analysis. Clones are independent — changes in one don't affect the other.", - ), - }, - ), - }, - ) - | CaptureComparison => - div( - list{ - Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800"), - Attrs.title( - "Compare two panels side-by-side, or a student's work against a demo's golden output", - ), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text( - switch capture.comparison { - | NoComparison => "No comparison active — select two panels or a demo to compare" - | SideBySide(l, r) => "Side-by-side: " ++ l ++ " vs " ++ r - | DemoComparison(s, d) => - "Demo comparison: student " ++ s ++ " vs golden " ++ d - | BeforeAfter(b, a) => "Before/after: " ++ b ++ " vs " ++ a - }, - ), - }, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/CaptureBar.affine b/src/components/CaptureBar.affine new file mode 100644 index 00000000..b59c04fa --- /dev/null +++ b/src/components/CaptureBar.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CaptureBar; + +// TODO: Complete semantic implementation diff --git a/src/components/CaptureBar.res b/src/components/CaptureBar.res deleted file mode 100644 index 3ef1e798..00000000 --- a/src/components/CaptureBar.res +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Capture Bar — reusable vertical capture strip for panels (DD-022). -/// -/// Per the user's design: icons are SIDE-ORIENTED (vertical strip on the panel -/// edge) so people don't instinctively click them thinking they're selfie buttons. -/// The strip appears on the right edge of each panel/pane. -/// -/// Icons: -/// - Camera: screenshot this panel -/// - Record: start/stop recording this panel -/// - Clone: duplicate this panel's state -/// - Compare: enter comparison mode with this panel - -open Msg -open Tea.Html - -/// A single capture bar button. -let captureButton = ( - label: string, - icon: string, - tooltip: string, - onClick: msg, - active: bool, -): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - `w-6 h-6 flex items-center justify-center rounded text-xs ${active - ? "bg-red-700 text-white" - : "bg-gray-800/80 text-gray-500 hover:text-gray-300 hover:bg-gray-700"} transition-colors`, - ), - Events.onClick(onClick), - Attrs.title(tooltip), - Attrs.ariaLabel(label), - }, - list{text(icon)}, - ) -} - -/// Render the capture bar for a panel. `panelId` identifies which panel -/// this bar is attached to. `isRecording` indicates if this panel is -/// currently being recorded. -let view = (panelId: string, isRecording: bool, visible: bool): Tea_Vdom.t => { - if !visible { - noNode - } else { - div( - list{ - Attrs.class_( - "absolute right-0 top-1/2 -translate-y-1/2 flex flex-col gap-1 p-1 bg-gray-950/70 rounded-l z-30", - ), - Attrs.role("toolbar"), - Attrs.ariaLabel("Capture controls for " ++ panelId), - }, - list{ - captureButton( - "Screenshot " ++ panelId, - "C", // Camera icon placeholder (would be SVG in production) - "Screenshot this panel", - Capture(CaptureScreenshot(panelId)), - false, - ), - captureButton( - (isRecording ? "Stop recording " : "Record ") ++ panelId, - "R", // Record icon placeholder - isRecording ? "Stop recording this panel" : "Start recording this panel", - isRecording ? Capture(StopRecording) : Capture(StartRecording(panelId)), - isRecording, - ), - captureButton( - "Clone " ++ panelId, - "D", // Duplicate/clone icon placeholder - "Clone this panel's state into an independent copy", - Capture(ClonePanel(panelId)), - false, - ), - captureButton( - "Compare " ++ panelId, - "=", // Compare icon placeholder - "Enter comparison mode with this panel", - Capture(ToggleCaptureSelection(panelId)), - false, - ), - }, - ) - } -} diff --git a/src/components/CladeBrowser.affine b/src/components/CladeBrowser.affine new file mode 100644 index 00000000..37064cce --- /dev/null +++ b/src/components/CladeBrowser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CladeBrowser; + +// TODO: Complete semantic implementation diff --git a/src/components/CladeBrowser.res b/src/components/CladeBrowser.res deleted file mode 100644 index 92786477..00000000 --- a/src/components/CladeBrowser.res +++ /dev/null @@ -1,654 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Clade Browser — panel component for exploring and customising panel clades. -/// -/// Renders four tabs: Overview (stats grid + clade list), By Kind (grouped view), -/// Traits (trait matrix), Panel Map (which panels belong to which clades). -/// Uses Tailwind CSS classes (same as all other PanLL panels). - -open Msg -open CladeBrowserModel -open CladeBrowserEngine -open Tea.Html - -/// Render a tab button. -let renderTab = (label: string, active: bool, cat: cladeBrowserCategory): Tea_Vdom.t => { - let baseClass = "px-3 py-1.5 text-xs rounded-t border-b-2 transition-colors cursor-pointer" - let cls = active - ? `${baseClass} text-cyan-300 border-cyan-400 bg-gray-800` - : `${baseClass} text-gray-500 border-transparent hover:text-gray-300` - button( - list{ - Attrs.class_(cls), - Events.onClick(CladeBrowser(SetCladeCategory(cat))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Render a trait badge (green if active, gray if not). -let traitBadge = (label: string, active: bool): Tea_Vdom.t => { - let cls = active - ? "inline-block px-2 py-0.5 text-xs rounded border border-green-500/30 bg-green-500/10 text-green-400" - : "inline-block px-2 py-0.5 text-xs rounded border border-gray-600/30 bg-gray-700/20 text-gray-500" - span(list{Attrs.class_(cls)}, list{text(label)}) -} - -/// Render trait badges for a clade entry. -let traitBadges = (traits: cladeTraits): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - list{ - traitBadge("Persistence", traits.hasPersistence), - traitBadge("Backend", traits.hasBackend), - traitBadge("Work Items", traits.hasWorkItems), - traitBadge("Real-Time", traits.hasRealTime), - traitBadge("Ambient", traits.isAmbient), - }, - ) -} - -/// Render a kind badge with colour-coded background. -let kindBadge = (kind: string): Tea_Vdom.t => { - let cls = switch kind { - | "ai" => "bg-violet-500/20 text-violet-400 border-violet-500/30" - | "bridge" => "bg-blue-500/20 text-blue-400 border-blue-500/30" - | "builder" => "bg-amber-500/20 text-amber-400 border-amber-500/30" - | "database" => "bg-emerald-500/20 text-emerald-400 border-emerald-500/30" - | "directive" => "bg-red-500/20 text-red-400 border-red-500/30" - | "loader" => "bg-indigo-500/20 text-indigo-400 border-indigo-500/30" - | "meta" => "bg-gray-500/20 text-gray-400 border-gray-500/30" - | "network" => "bg-teal-500/20 text-teal-400 border-teal-500/30" - | "scanner" => "bg-orange-500/20 text-orange-400 border-orange-500/30" - | "terminal" => "bg-lime-500/20 text-lime-400 border-lime-500/30" - | "viewer" => "bg-purple-500/20 text-purple-400 border-purple-500/30" - | _ => "bg-gray-500/20 text-gray-400 border-gray-500/30" - } - span( - list{Attrs.class_(`inline-block px-2 py-0.5 text-xs rounded-full border ${cls}`)}, - list{text(kind)}, - ) -} - -/// Render a stat card for the overview grid. -let statCard = (value: string, label: string, colour: string): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-800 rounded-lg p-4 text-center")}, - list{ - div(list{Attrs.class_(`text-2xl font-bold ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{text(label)}), - }, - ) -} - -/// Render a protocol badge. -let protocolBadge = (proto: cladeProtocol): Tea_Vdom.t => { - span( - list{ - Attrs.class_( - "inline-block px-1.5 py-0.5 text-xs rounded border border-sky-500/30 bg-sky-500/10 text-sky-400", - ), - }, - list{text(protocolLabel(proto))}, - ) -} - -/// Render a capability badge. -let capBadge = (cap: cladeCapability): Tea_Vdom.t => { - span( - list{ - Attrs.class_( - "inline-block px-1.5 py-0.5 text-xs rounded border border-amber-500/30 bg-amber-500/10 text-amber-400", - ), - }, - list{text(capabilityLabel(cap))}, - ) -} - -/// Render an isolation level badge with colour coding. -let isolationBadgeView = (iso: cladeIsolation): Tea_Vdom.t => { - let (cls, label) = switch iso { - | IsolationNone => ("border-red-500/30 bg-red-500/10 text-red-400", "None") - | IsolationSoft => ("border-yellow-500/30 bg-yellow-500/10 text-yellow-400", "Soft") - | IsolationProcess => ("border-blue-500/30 bg-blue-500/10 text-blue-400", "Process") - | IsolationContainer => ("border-emerald-500/30 bg-emerald-500/10 text-emerald-400", "Container") - } - span( - list{Attrs.class_(`inline-block px-1.5 py-0.5 text-xs rounded border ${cls}`)}, - list{text(label)}, - ) -} - -/// Render a single clade card. -let cladeCard = (entry: cladeEntry, isSelected: bool): Tea_Vdom.t => { - let borderCls = isSelected ? "border-cyan-500 bg-gray-800/80" : "border-gray-700 bg-gray-800/40" - div( - list{ - Attrs.class_( - `p-3 mb-2 rounded-lg border ${borderCls} cursor-pointer hover:border-gray-500 transition-colors`, - ), - Events.onClick(CladeBrowser(SelectClade(Some(entry.id)))), - }, - list{ - // Header row: name + kind badge + version + isolation - div( - list{Attrs.class_("flex justify-between items-center mb-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - strong(list{Attrs.class_("text-sm text-gray-100")}, list{text(entry.name)}), - kindBadge(entry.kind), - isolationBadgeView(entry.isolation), - span(list{Attrs.class_("text-xs text-gray-600")}, list{text("v" ++ entry.version)}), - }, - ), - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(entry.id)}), - }, - ), - // Summary - p(list{Attrs.class_("text-xs text-gray-300 my-1")}, list{text(entry.summary)}), - // Traits - traitBadges(entry.traits), - // Protocols (Tier 1.1) - if entry.protocols->Array.length > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - entry.protocols->Array.map(protocolBadge)->List.fromArray, - ) - } else { - noNode - }, - // Capabilities (Tier 1.2) - if entry.capabilities->Array.length > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - entry.capabilities->Array.map(capBadge)->List.fromArray, - ) - } else { - noNode - }, - // Dependencies (Tier 1.3) - if entry.requires->Array.length > 0 { - div( - list{Attrs.class_("mt-1 text-xs text-red-400")}, - list{text("requires: " ++ entry.requires->Array.map(d => d.cladeId)->Array.join(", "))}, - ) - } else { - noNode - }, - // Enhances (Tier 1.3) - if entry.enhances->Array.length > 0 { - div( - list{Attrs.class_("mt-1 text-xs text-indigo-400")}, - list{text("enhances: " ++ entry.enhances->Array.join(", "))}, - ) - } else { - noNode - }, - // Panel IDs - if entry.panelIds->Array.length > 0 { - div( - list{Attrs.class_("mt-1 text-xs text-gray-500")}, - list{text("Panels: " ++ entry.panelIds->Array.join(", "))}, - ) - } else { - noNode - }, - }, - ) -} - -/// Overview tab — stats grid + full clade list. -let viewOverview = (state: cladeBrowserState): Tea_Vdom.t => { - let filtered = filterClades(state.clades, state.kindFilter, state.searchQuery) - let total = state.clades->Array.length - - div( - list{}, - list{ - // Stats grid (row 1: counts, row 2: Tier 1 metrics) - div( - list{Attrs.class_("grid grid-cols-4 gap-3 mb-3")}, - list{ - statCard(Int.toString(total), "Total Clades", "text-cyan-400"), - statCard( - Int.toString(countWithTrait(state.clades, t => t.hasBackend)), - "With Backend", - "text-green-400", - ), - statCard( - Int.toString(countWithTrait(state.clades, t => t.hasRealTime)), - "Real-Time", - "text-amber-400", - ), - statCard( - Int.toString(countWithTrait(state.clades, t => t.isAmbient)), - "Ambient", - "text-violet-400", - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-4 gap-3 mb-5")}, - list{ - statCard( - Int.toString(countWithProtocols(state.clades)), - "With Protocols", - "text-sky-400", - ), - statCard( - Int.toString(countWithCapabilities(state.clades)), - "With Capabilities", - "text-amber-400", - ), - statCard( - Int.toString( - countByIsolation(state.clades, IsolationProcess) + - countByIsolation(state.clades, IsolationContainer), - ), - "Process/Container", - "text-blue-400", - ), - statCard( - Int.toString(countWithParent(state.clades)), - "With Inheritance", - "text-indigo-400", - ), - }, - ), - // Kind distribution - div( - list{Attrs.class_("flex flex-wrap gap-3 mb-4 text-xs")}, - allKinds - ->Array.filter(k => k !== KindAll) - ->Array.map(k => { - let count = countByKind(state.clades, k) - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("w-2 h-2 rounded-full bg-current")}, list{}), - text(kindLabel(k) ++ " (" ++ Int.toString(count) ++ ")"), - }, - ) - }) - ->List.fromArray, - ), - // Clade list - div( - list{}, - filtered - ->Array.map(entry => cladeCard(entry, state.selectedClade === Some(entry.id))) - ->List.fromArray, - ), - }, - ) -} - -/// By Kind tab — clades grouped by their kind. -let viewByKind = (state: cladeBrowserState): Tea_Vdom.t => { - let kinds = allKinds->Array.filter(k => k !== KindAll) - div( - list{}, - kinds - ->Array.map(kind => { - let kindsClades = filterByKind(state.clades, kind) - if kindsClades->Array.length > 0 { - div( - list{Attrs.class_("mb-5")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - kindBadge(kindLabel(kind)->String.toLowerCase), - h3( - list{Attrs.class_("text-sm font-semibold text-gray-200 m-0")}, - list{ - text(kindLabel(kind) ++ " (" ++ Int.toString(kindsClades->Array.length) ++ ")"), - }, - ), - }, - ), - div( - list{}, - kindsClades - ->Array.map(entry => cladeCard(entry, state.selectedClade === Some(entry.id))) - ->List.fromArray, - ), - }, - ) - } else { - noNode - } - }) - ->List.fromArray, - ) -} - -/// Traits tab — matrix of all clades vs traits. -let viewTraits = (state: cladeBrowserState): Tea_Vdom.t => { - let check = (v: bool): Tea_Vdom.t => - if v { - span(list{Attrs.class_("text-green-400")}, list{text("Yes")}) - } else { - span(list{Attrs.class_("text-gray-600")}, list{text("-")}) - } - - table( - list{Attrs.class_("w-full text-xs")}, - list{ - thead( - list{}, - list{ - tr( - list{Attrs.class_("border-b border-gray-700")}, - list{ - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Clade")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Kind")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Persist")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Backend")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Work Items")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Real-Time")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Ambient")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Isolation")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Protocols")}), - th(list{Attrs.class_("text-left p-2 text-gray-400")}, list{text("Capabilities")}), - }, - ), - }, - ), - tbody( - list{}, - state.clades - ->Array.map(entry => - tr( - list{Attrs.class_("border-b border-gray-800 hover:bg-gray-800/50")}, - list{ - td(list{Attrs.class_("p-2 font-medium text-gray-200")}, list{text(entry.name)}), - td(list{Attrs.class_("p-2")}, list{kindBadge(entry.kind)}), - td(list{Attrs.class_("p-2")}, list{check(entry.traits.hasPersistence)}), - td(list{Attrs.class_("p-2")}, list{check(entry.traits.hasBackend)}), - td(list{Attrs.class_("p-2")}, list{check(entry.traits.hasWorkItems)}), - td(list{Attrs.class_("p-2")}, list{check(entry.traits.hasRealTime)}), - td(list{Attrs.class_("p-2")}, list{check(entry.traits.isAmbient)}), - td(list{Attrs.class_("p-2")}, list{isolationBadgeView(entry.isolation)}), - td( - list{Attrs.class_("p-2")}, - list{ - span( - list{Attrs.class_("text-sky-400")}, - list{text(Int.toString(entry.protocols->Array.length))}, - ), - }, - ), - td( - list{Attrs.class_("p-2")}, - list{ - span( - list{Attrs.class_("text-amber-400")}, - list{text(Int.toString(entry.capabilities->Array.length))}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Panel Map tab — which panels belong to which clades, with inheritance chains. -let viewPanelMap = (state: cladeBrowserState): Tea_Vdom.t => { - let withParent = countWithParent(state.clades) - let roots = rootClades(state.clades)->Array.length - div( - list{}, - list{ - p( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{ - text( - "Panel-to-clade assignments. Each panel inherits traits from its clade and ancestors.", - ), - }, - ), - // Inheritance stats - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500 mb-4")}, - list{ - span(list{}, list{text(`${Int.toString(roots)} root clades`)}), - span(list{}, list{text(`${Int.toString(withParent)} with inheritance`)}), - }, - ), - div( - list{}, - state.clades - ->Array.filter(c => c.panelIds->Array.length > 0) - ->Array.map(entry => { - let chain = inheritanceLabel(state.clades, entry.id) - let effectiveTraits = resolveTraits(state.clades, entry.id) - div( - list{Attrs.class_("py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-baseline gap-3")}, - list{ - div( - list{Attrs.class_("min-w-[180px] flex items-center gap-2")}, - list{ - strong(list{Attrs.class_("text-sm text-gray-200")}, list{text(entry.name)}), - kindBadge(entry.kind), - }, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - entry.panelIds - ->Array.map(pid => - span( - list{Attrs.class_("bg-gray-700 px-2 py-0.5 rounded text-xs text-gray-200")}, - list{text(pid)}, - ) - ) - ->List.fromArray, - ), - }, - ), - // Inheritance chain - if chain != entry.id { - div( - list{Attrs.class_("mt-1 ml-[180px] flex items-center gap-2")}, - list{span(list{Attrs.class_("text-xs text-indigo-400")}, list{text(chain)})}, - ) - } else { - noNode - }, - // Effective traits (from inheritance) - switch effectiveTraits { - | Some(traits) => - div( - list{Attrs.class_("mt-1 ml-[180px] flex gap-2 text-xs")}, - list{ - if traits.hasPersistence { - span(list{Attrs.class_("text-green-500")}, list{text("persist")}) - } else { - noNode - }, - if traits.hasBackend { - span(list{Attrs.class_("text-blue-500")}, list{text("backend")}) - } else { - noNode - }, - if traits.hasWorkItems { - span(list{Attrs.class_("text-amber-500")}, list{text("work")}) - } else { - noNode - }, - if traits.hasRealTime { - span(list{Attrs.class_("text-cyan-500")}, list{text("realtime")}) - } else { - noNode - }, - if traits.isAmbient { - span(list{Attrs.class_("text-purple-500")}, list{text("ambient")}) - } else { - noNode - }, - }, - ) - | None => noNode - }, - // Sibling clades - if entry.siblingClades->Array.length > 0 { - div( - list{Attrs.class_("mt-1 ml-[180px] text-xs text-gray-600")}, - list{text("siblings: " ++ entry.siblingClades->Array.join(", "))}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Render the permission rules for a clade. -let permissionBadge = (rules: array, cladeId: string): Tea_Vdom.t => { - let rule = CladeBrowserEngine.findPermissionRule(rules, cladeId) - switch rule { - | None => - span( - list{ - Attrs.class_("text-xs text-emerald-500 cursor-pointer"), - Events.onClick(CladeBrowser(SetCladePermission(cladeId, PermitNone))), - }, - list{text("open")}, - ) - | Some({permission: PermitAll}) => - span( - list{ - Attrs.class_("text-xs text-emerald-500 cursor-pointer"), - Events.onClick(CladeBrowser(SetCladePermission(cladeId, PermitNone))), - }, - list{text("open")}, - ) - | Some({permission: PermitNone}) => - span( - list{ - Attrs.class_("text-xs text-red-400 cursor-pointer"), - Events.onClick(CladeBrowser(RemoveCladePermission(cladeId))), - }, - list{text("locked")}, - ) - | Some({permission: PermitOnly(allowed)}) => - span( - list{ - Attrs.class_("text-xs text-amber-400 cursor-pointer"), - Events.onClick(CladeBrowser(RemoveCladePermission(cladeId))), - }, - list{text(`restricted (${Int.toString(Array.length(allowed))})`)}, - ) - } -} - -/// Render the permission rules section. -let viewPermissions = (state: cladeBrowserState): Tea_Vdom.t => { - div( - list{Attrs.class_("mt-4 pt-4 border-t border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - h3( - list{Attrs.class_("text-sm font-semibold text-gray-200 m-0")}, - list{text("Cross-Clade Permissions")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.permissionRules))} rules active`)}, - ), - }, - ), - p( - list{Attrs.class_("text-xs text-gray-500 mb-3")}, - list{ - text( - "Controls which clades may cross-reference each other via the Panel Bus. Click to toggle.", - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - state.clades - ->Array.map(entry => - div( - list{ - Attrs.class_( - "flex items-center justify-between py-1 px-2 rounded hover:bg-gray-800/50", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-300 font-mono w-36 truncate")}, - list{text(entry.id)}, - ), - kindBadge(entry.kind), - }, - ), - permissionBadge(state.permissionRules, entry.id), - }, - ) - ) - ->List.fromArray, - ), - }, - ) -} - -/// Main view function — renders the complete clade browser panel. -let view = (state: cladeBrowserState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("p-5 text-gray-200 font-mono overflow-y-auto max-h-screen"), - Attrs.role("region"), - Attrs.ariaLabel("Clade Browser"), - }, - list{ - // Header - div( - list{Attrs.class_("flex justify-between items-center mb-4")}, - list{ - h2(list{Attrs.class_("text-lg font-bold m-0")}, list{text("Clade Browser")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Int.toString(state.clades->Array.length) ++ " clades loaded")}, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 mb-4 border-b border-gray-700 pb-2")}, - allCategories - ->Array.map(cat => renderTab(categoryLabel(cat), state.category === cat, cat)) - ->List.fromArray, - ), - // Content - switch state.category { - | CategoryOverview => viewOverview(state) - | CategoryByKind => viewByKind(state) - | CategoryTraits => viewTraits(state) - | CategoryPanelMap => div(list{}, list{viewPanelMap(state), viewPermissions(state)}) - }, - }, - ) -} diff --git a/src/components/CloudGuard.affine b/src/components/CloudGuard.affine new file mode 100644 index 00000000..4e3a4db2 --- /dev/null +++ b/src/components/CloudGuard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuard; + +// TODO: Complete semantic implementation diff --git a/src/components/CloudGuard.res b/src/components/CloudGuard.res deleted file mode 100644 index 8013c361..00000000 --- a/src/components/CloudGuard.res +++ /dev/null @@ -1,559 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CloudGuard — Main Cloudflare domain security management panel. -/// -/// Full-screen overlay panel (like VAB) that provides the Panel-W dashboard -/// for managing Cloudflare domains. Contains the domain selector ribbon, -/// category tab bar, settings toggle grid, action bar, and side panels -/// for audit results and config diffs. -/// -/// Layout (see plan for full ASCII art): -/// +-------------------------------------------------------+ -/// | [Domain Ribbon: checkboxes for each domain] | -/// | [Select All] [None] [Filter: _____] | -/// +-------------------------------------------------------+ -/// | SSL/TLS | Headers | WAF | Bot | DNS | ... | DNSSEC | <-- Category tabs -/// +---------------------------+---------------------------+ -/// | Settings Toggle Grid | Compliance Audit | -/// | (toggles, dropdowns, | (passed/failed/warnings, | -/// | number inputs per | findings list, | -/// | category) | config diff summary) | -/// +---------------------------+---------------------------+ -/// | [Harden All] [Push Changes] [Download] [Audit] | <-- Action bar -/// | Progress: 28/36 domains hardened | -/// +-------------------------------------------------------+ - -open Msg -open Model -open Tea.Html - -// ============================================================================ -// Category tab bar -// ============================================================================ - -/// All setting categories in display order. -let allCategories: array = [ - SslTls, - Headers, - Waf, - BotDefense, - Dns, - EmailSec, - Performance, - Network, - Pages, - Dnssec, -] - -/// Render a single category tab button. -let renderCategoryTab = (cat: settingCategory, isActive: bool): Tea_Vdom.t => { - let activeClass = isActive - ? "border-indigo-500 text-indigo-300 bg-gray-800/50" - : "border-transparent text-gray-500 hover:text-gray-300 hover:border-gray-600" - - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium border-b-2 cursor-pointer transition-colors ${activeClass}`, - ), - Attrs.ariaSelected(isActive), - Attrs.role("tab"), - Events.onClick(CloudGuard(SetCategory(cat))), - }, - list{text(CloudGuardCatalog.categoryLabel(cat))}, - ) -} - -/// Render the full category tab bar. -let renderCategoryTabBar = (activeCategory: settingCategory): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex border-b border-gray-800 overflow-x-auto"), - Attrs.role("tablist"), - Attrs.ariaLabel("Setting categories"), - }, - allCategories - ->Array.map(cat => renderCategoryTab(cat, cat === activeCategory)) - ->List.fromArray, - ) -} - -// ============================================================================ -// Connection status bar -// ============================================================================ - -/// Render the connection status indicator. -let renderConnectionStatus = (connection: cfConnectionStatus): Tea_Vdom.t => { - let (dotClass, statusText) = switch connection { - | Disconnected => ("bg-gray-500", "Not connected") - | Connecting => ("bg-yellow-400 animate-pulse", "Connecting...") - | Connected(info) => ("bg-green-400", `Connected: ${info}`) - | ConnectionError(err) => ("bg-red-400", `Error: ${err}`) - } - - div( - list{Attrs.class_("flex items-center gap-2 px-3 py-1.5")}, - list{ - span(list{Attrs.class_(`w-2 h-2 rounded-full ${dotClass}`)}, list{}), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ) -} - -// ============================================================================ -// Audit side panel -// ============================================================================ - -/// Render the audit results summary in the right side panel. -let renderAuditPanel = (auditResult: option, loading: bool): Tea_Vdom.t => { - div( - list{Attrs.class_("w-72 border-l border-gray-800 p-3 overflow-y-auto")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-3 font-medium")}, - list{text("COMPLIANCE AUDIT")}, - ), - switch auditResult { - | None => - if loading { - div(list{Attrs.class_("text-sm text-gray-500 italic")}, list{text("Running audit...")}) - } else { - div( - list{Attrs.class_("text-sm text-gray-600 italic")}, - list{text("Click 'Audit' to check compliance")}, - ) - } - | Some(result) => - div( - list{}, - list{ - // Score summary - div( - list{Attrs.class_("flex items-center gap-3 mb-3")}, - list{ - div( - list{Attrs.class_("text-2xl font-bold text-indigo-300")}, - list{text(`${Float.toFixed(result.score *. 100.0, ~digits=0)}%`)}, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-xs text-green-400")}, - list{text(`${Int.toString(result.passed)} passed`)}, - ), - div( - list{Attrs.class_("text-xs text-red-400")}, - list{text(`${Int.toString(result.failed)} failed`)}, - ), - }, - ), - }, - ), - // Severity summary bar - div( - list{Attrs.class_("flex gap-1 mb-3")}, - list{ - { - let critical = - result.findings - ->Array.filter(f => CloudGuardEngine.severityLabel(f.severity) == "CRITICAL") - ->Array.length - let high = - result.findings - ->Array.filter(f => CloudGuardEngine.severityLabel(f.severity) == "HIGH") - ->Array.length - let medium = - result.findings - ->Array.filter(f => CloudGuardEngine.severityLabel(f.severity) == "MEDIUM") - ->Array.length - let low = - result.findings - ->Array.filter(f => CloudGuardEngine.severityLabel(f.severity) == "LOW") - ->Array.length - div( - list{Attrs.class_("flex gap-2 text-[10px]")}, - list{ - if critical > 0 { - span( - list{Attrs.class_("text-red-400")}, - list{text(`${Int.toString(critical)} critical`)}, - ) - } else { - noNode - }, - if high > 0 { - span( - list{Attrs.class_("text-orange-400")}, - list{text(`${Int.toString(high)} high`)}, - ) - } else { - noNode - }, - if medium > 0 { - span( - list{Attrs.class_("text-amber-400")}, - list{text(`${Int.toString(medium)} medium`)}, - ) - } else { - noNode - }, - if low > 0 { - span( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(low)} low`)}, - ) - } else { - noNode - }, - }, - ) - }, - }, - ), - // Findings list with drill-down - div( - list{Attrs.class_("space-y-2")}, - result.findings - ->CloudGuardEngine.sortFindingsBySeverity - ->Array.map(finding => - div( - list{ - Attrs.class_( - "text-xs p-2 bg-gray-800/50 rounded hover:bg-gray-800/70 transition-colors", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-1.5 mb-1")}, - list{ - span( - list{ - Attrs.class_( - `font-bold ${CloudGuardEngine.severityColour(finding.severity)}`, - ), - }, - list{text(CloudGuardEngine.severityLabel(finding.severity))}, - ), - span(list{Attrs.class_("text-gray-400")}, list{text(finding.settingId)}), - }, - ), - div(list{Attrs.class_("text-gray-400")}, list{text(finding.message)}), - // Remediation suggestion - div( - list{Attrs.class_("mt-1.5 pl-2 border-l-2 border-gray-700")}, - list{ - div( - list{ - Attrs.class_( - "text-[10px] text-gray-600 uppercase tracking-wider mb-0.5", - ), - }, - list{text("Remediation")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{ - text( - `Set ${finding.settingId} from "${finding.currentValue}" to "${finding.expectedValue}"`, - ), - }, - ), - }, - ), - // Fix action button - button( - list{ - Attrs.class_( - "mt-1.5 px-2 py-0.5 text-[10px] bg-indigo-900/40 text-indigo-300 rounded border border-indigo-800 hover:bg-indigo-800/50 transition-colors", - ), - Events.onClick(CloudGuard(HardenSetting(finding.settingId))), - Attrs.ariaLabel(`Fix ${finding.settingId}`), - }, - list{text("Apply Fix")}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ) -} - -// ============================================================================ -// Action bar -// ============================================================================ - -/// Render the bottom action bar with Harden All, Push, Download, Audit buttons. -let renderActionBar = ( - selectedCount: int, - totalCount: int, - loading: bool, - bulkProgress: option, -): Tea_Vdom.t => { - let buttonClass = "px-3 py-1.5 text-sm font-medium rounded cursor-pointer transition-colors" - let primaryClass = `${buttonClass} bg-indigo-600 hover:bg-indigo-500 text-white` - let secondaryClass = `${buttonClass} bg-gray-700 hover:bg-gray-600 text-gray-200` - let disabledClass = `${buttonClass} bg-gray-800 text-gray-600 cursor-not-allowed` - - div( - list{Attrs.class_("border-t border-gray-800 px-4 py-3 flex items-center justify-between")}, - list{ - // Action buttons - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if selectedCount > 0 && !loading { - primaryClass - } else { - disabledClass - }, - ), - Attrs.ariaLabel("Harden all selected domains"), - if selectedCount > 0 && !loading { - Events.onClick(CloudGuard(HardenSelected)) - } else { - Attrs.noProp - }, - }, - list{text("Harden All")}, - ), - button( - list{ - Attrs.class_( - if !loading { - secondaryClass - } else { - disabledClass - }, - ), - Attrs.ariaLabel("Push local changes to Cloudflare"), - if !loading { - Events.onClick(CloudGuard(PushChanges)) - } else { - Attrs.noProp - }, - }, - list{text("Push Changes")}, - ), - button( - list{ - Attrs.class_( - if !loading { - secondaryClass - } else { - disabledClass - }, - ), - Attrs.ariaLabel("Download offline config"), - if !loading { - Events.onClick(CloudGuard(DownloadConfig)) - } else { - Attrs.noProp - }, - }, - list{text("Download")}, - ), - button( - list{ - Attrs.class_( - if selectedCount > 0 && !loading { - secondaryClass - } else { - disabledClass - }, - ), - Attrs.ariaLabel("Run compliance audit"), - if selectedCount > 0 && !loading { - Events.onClick(CloudGuard(RunAudit)) - } else { - Attrs.noProp - }, - }, - list{text("Audit")}, - ), - }, - ), - // Progress indicator - switch bulkProgress { - | None => - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(selectedCount)}/${Int.toString(totalCount)} domains selected`)}, - ) - | Some(progress) => - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Progress bar - div( - list{Attrs.class_("w-40 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500 transition-all"), - Attrs.style( - "width", - `${Float.toFixed( - Int.toFloat(progress.completed) /. - Int.toFloat( - if progress.total > 0 { - progress.total - } else { - 1 - }, - ) *. 100.0, - ~digits=0, - )}%`, - ), - }, - list{}, - ), - }, - ), - // Progress text - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text(`${Int.toString(progress.completed)}/${Int.toString(progress.total)} domains`), - }, - ), - // Current domain - switch progress.currentDomain { - | Some(domain) => span(list{Attrs.class_("text-xs text-gray-500")}, list{text(domain)}) - | None => noNode - }, - }, - ) - }, - }, - ) -} - -// ============================================================================ -// Main panel view -// ============================================================================ - -/// Render the complete CloudGuard panel as a full-screen overlay. -/// This is the Panel-W component for the CloudGuard module. -let view = (state: cloudguardState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 z-50 bg-gray-950 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("CloudGuard — Cloudflare Domain Security Management"), - }, - list{ - // Header bar with title, connection status, and close button - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-900/80", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-lg font-semibold text-gray-200")}, - list{text("CloudGuard")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Cloudflare Domain Security")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - renderConnectionStatus(state.connection), - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300 cursor-pointer text-lg px-2"), - Attrs.ariaLabel("Close CloudGuard"), - Events.onClick(CloudGuard(ToggleCloudGuard)), - }, - list{text("x")}, - ), - }, - ), - }, - ), - // Domain selector ribbon - div( - list{Attrs.class_("px-4 py-2")}, - list{CloudGuardDomainList.view(state.zones, state.selectedZoneIds, state.filterText)}, - ), - // Category tab bar - div(list{Attrs.class_("px-4")}, list{renderCategoryTabBar(state.activeCategory)}), - // Main content area: settings grid + audit side panel - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Main content (left) — DNS editor for DNS tab, settings grid otherwise - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - { - let currentDomain = switch state.selectedZoneIds[0] { - | Some(zoneId) => - switch state.zones->Array.find(z => z.id === zoneId) { - | Some(zone) => Some(zone.name) - | None => None - } - | None => None - } - switch state.activeCategory { - | Dns => - // DNS tab shows the inline record editor - CloudGuardDnsEditor.view( - state.dnsRecords, - state.dnsEditingId, - Array.length(state.selectedZoneIds) > 0, - state.loading, - ) - | Pages => - // Pages tab shows the Pages setup component - CloudGuardPagesSetup.view(state.pagesProjects, currentDomain, state.loading) - | _ => - // All other tabs show the settings toggle grid - CloudGuardSettingsGrid.view( - state.settings, - state.activeCategory, - state.settingFilter, - state.exceptions, - currentDomain, - ) - } - }, - }, - ), - // Side panel (right) — audit results or diff viewer - if state.showAudit { - renderAuditPanel(state.auditResult, state.loading) - } else if state.showDiff { - CloudGuardDiffViewer.view(state.configDiff, state.loading) - } else { - noNode - }, - }, - ), - // Action bar (bottom) - renderActionBar( - Array.length(state.selectedZoneIds), - Array.length(state.zones), - state.loading, - state.bulkProgress, - ), - }, - ) -} diff --git a/src/components/CloudGuardDiffViewer.affine b/src/components/CloudGuardDiffViewer.affine new file mode 100644 index 00000000..9b8d91b8 --- /dev/null +++ b/src/components/CloudGuardDiffViewer.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuardDiffViewer; + +// TODO: Complete semantic implementation diff --git a/src/components/CloudGuardDiffViewer.res b/src/components/CloudGuardDiffViewer.res deleted file mode 100644 index 79bffc6f..00000000 --- a/src/components/CloudGuardDiffViewer.res +++ /dev/null @@ -1,264 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CloudGuard Diff Viewer — Three-way config diff display. -/// -/// Shows differences between offline (saved), live (Cloudflare), and policy -/// (Trustfile/Nickel) values for zone settings. Each diff entry displays the -/// three values side by side with colour-coded resolution indicators. -/// -/// Layout: -/// +-------------+-------------+-------------+-------------+ -/// | Setting | Offline | Live | Policy | -/// +-------------+-------------+-------------+-------------+ -/// | ssl.mode | full_strict | flexible | full_strict | <-- red: live differs -/// | min_tls | 1.2 | 1.2 | 1.2 | <-- green: all match -/// | brotli | on | off | on | <-- red: live differs -/// +-------------+-------------+-------------+-------------+ -/// | 2 drifts found | 0 conflicts | 12 settings match | -/// +------------------------------------------------------------+ - -open Msg -open Model -open Tea.Html - -// ============================================================================ -// Diff entry rendering -// ============================================================================ - -/// CSS class for a diff value based on whether it matches the policy. -let valueClass = (value: option, policyValue: option): string => { - switch (value, policyValue) { - | (Some(v), Some(p)) => - if v === p { - "text-green-400" - } else { - "text-red-400" - } - | (None, _) => "text-gray-600 italic" - | (_, None) => "text-gray-400" - } -} - -/// Render a single diff entry row. -let renderDiffEntry = (entry: configDiffEntry): Tea_Vdom.t => { - let offlineDisplay = switch entry.offlineValue { - | Some(v) => v - | None => "—" - } - let liveDisplay = switch entry.liveValue { - | Some(v) => v - | None => "—" - } - let policyDisplay = switch entry.policyValue { - | Some(v) => v - | None => "—" - } - - // Determine if this is a drift (live != offline) or conflict (all three differ) - let isDrift = entry.offlineValue !== entry.liveValue - let isConflict = - isDrift && entry.offlineValue !== entry.policyValue && entry.liveValue !== entry.policyValue - - let rowBg = if isConflict { - " bg-red-950/20" - } else if isDrift { - " bg-yellow-950/20" - } else { - "" - } - - div( - list{Attrs.class_(`flex hover:bg-gray-800/30${rowBg}`)}, - list{ - // Setting ID - div( - list{Attrs.class_("py-1.5 px-2 text-sm text-gray-300 font-mono flex-1")}, - list{text(entry.settingId)}, - ), - // Offline value - div( - list{ - Attrs.class_( - `py-1.5 px-2 text-sm font-mono w-28 ${valueClass( - entry.offlineValue, - entry.policyValue, - )}`, - ), - }, - list{text(offlineDisplay)}, - ), - // Live value - div( - list{ - Attrs.class_( - `py-1.5 px-2 text-sm font-mono w-28 ${valueClass(entry.liveValue, entry.policyValue)}`, - ), - }, - list{text(liveDisplay)}, - ), - // Policy value - div( - list{Attrs.class_("py-1.5 px-2 text-sm font-mono w-28 text-gray-500")}, - list{text(policyDisplay)}, - ), - // Status indicator - div( - list{Attrs.class_("py-1.5 px-2 w-20")}, - list{ - if isConflict { - span(list{Attrs.class_("text-xs text-red-400 font-medium")}, list{text("CONFLICT")}) - } else if isDrift { - span(list{Attrs.class_("text-xs text-yellow-400 font-medium")}, list{text("DRIFT")}) - } else { - span(list{Attrs.class_("text-xs text-green-400")}, list{text("OK")}) - }, - }, - ), - }, - ) -} - -// ============================================================================ -// Table header -// ============================================================================ - -/// Render the diff table header as a flex row. -let renderDiffHeader = (): Tea_Vdom.t => { - let headerCell = (label: string, extraClass: string) => - div( - list{Attrs.class_(`text-left text-xs text-gray-500 font-medium py-2 px-2 ${extraClass}`)}, - list{text(label)}, - ) - - div( - list{Attrs.class_("flex border-b border-gray-800")}, - list{ - headerCell("Setting", "flex-1"), - headerCell("Offline", "w-28"), - headerCell("Live", "w-28"), - headerCell("Policy", "w-28"), - headerCell("Status", "w-20"), - }, - ) -} - -// ============================================================================ -// Main diff viewer -// ============================================================================ - -/// Render the complete diff viewer panel. -/// Shows the three-way diff table when a diff is available, or a prompt to -/// download configs first. -let view = (configDiff: option, _loading: bool): Tea_Vdom.t => { - div( - list{ - Attrs.class_("w-72 border-l border-gray-800 p-3 overflow-y-auto"), - Attrs.role("region"), - Attrs.ariaLabel("Configuration Diff Viewer"), - }, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-3 font-medium")}, list{text("CONFIG DIFF")}), - switch configDiff { - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic")}, - list{text("Download a config first, then compare to see diffs.")}, - ) - | Some(diff) => - div( - list{}, - list{ - // Summary bar - div( - list{Attrs.class_("flex items-center gap-3 mb-3 text-xs")}, - list{ - span( - list{Attrs.class_("text-yellow-400")}, - list{text(`${Int.toString(diff.driftCount)} drifts`)}, - ), - span( - list{Attrs.class_("text-red-400")}, - list{text(`${Int.toString(diff.conflictCount)} conflicts`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{ - text( - `${Int.toString( - Array.length(diff.entries) - diff.driftCount - diff.conflictCount, - )} OK`, - ), - }, - ), - }, - ), - // Diff entries (compact list for the side panel) - div( - list{Attrs.class_("space-y-1")}, - diff.entries - ->Array.filter(e => e.offlineValue !== e.liveValue) - ->Array.map(entry => { - let liveDisplay = switch entry.liveValue { - | Some(v) => v - | None => "—" - } - let offlineDisplay = switch entry.offlineValue { - | Some(v) => v - | None => "—" - } - div( - list{Attrs.class_("text-xs p-2 bg-gray-800/50 rounded")}, - list{ - div( - list{Attrs.class_("font-mono text-gray-300 mb-0.5")}, - list{text(entry.settingId)}, - ), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text(offlineDisplay)}), - span(list{Attrs.class_("text-gray-600")}, list{text(" -> ")}), - span(list{Attrs.class_("text-yellow-400")}, list{text(liveDisplay)}), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - }, - }, - ) -} - -/// Render the diff viewer as a full-width table (for modal/expanded view). -/// This is an alternative layout for when the diff is shown in the main content area. -let viewExpanded = (configDiff: option): Tea_Vdom.t => { - switch configDiff { - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic px-3 py-4")}, - list{text("No config diff available. Download a config, then compare.")}, - ) - | Some(diff) => - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - div( - list{Attrs.class_("w-full text-left")}, - list{ - renderDiffHeader(), - div( - list{}, - diff.entries - ->Array.map(renderDiffEntry) - ->List.fromArray, - ), - }, - ), - }, - ) - } -} diff --git a/src/components/CloudGuardDnsEditor.affine b/src/components/CloudGuardDnsEditor.affine new file mode 100644 index 00000000..31886f4c --- /dev/null +++ b/src/components/CloudGuardDnsEditor.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuardDnsEditor; + +// TODO: Complete semantic implementation diff --git a/src/components/CloudGuardDnsEditor.res b/src/components/CloudGuardDnsEditor.res deleted file mode 100644 index 570f7b2c..00000000 --- a/src/components/CloudGuardDnsEditor.res +++ /dev/null @@ -1,499 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CloudGuard DNS Editor — Inline DNS record table with security templates. -/// -/// Renders a tabular view of all DNS records for the selected zone with inline -/// editing, creation, deletion, and one-click security record templates. -/// -/// Security templates provide quick setup of: -/// - SPF: `v=spf1 -all` (deny all for domains that don't send email) -/// - DMARC: `v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; pct=100; fo=1` -/// - DKIM revocation: `v=DKIM1; p=` (revoke all DKIM keys) -/// - CAA: `0 issue "letsencrypt.org"` (restrict CA to Let's Encrypt) -/// - TLS-RPT: `v=TLSRPTv1; rua=mailto:tlsrpt@domain` -/// -/// Layout: -/// +---------+--------------------+----------------------------+-----+-------+--------+ -/// | Type | Name | Content | TTL | Proxy | Actions| -/// +---------+--------------------+----------------------------+-----+-------+--------+ -/// | A | example.com | 192.0.2.1 | Auto| ON | [Edit][Del] -/// | AAAA | example.com | 2001:db8::1 | Auto| ON | [Edit][Del] -/// | TXT | example.com | v=spf1 -all | Auto| -- | [Edit][Del] -/// | TXT | _dmarc.example.com | v=DMARC1; p=reject; ... | Auto| -- | [Edit][Del] -/// | CAA | example.com | 0 issue "letsencrypt.org" | Auto| -- | [Edit][Del] -/// +---------+--------------------+----------------------------+-----+-------+--------+ -/// [+ Add Record] [SPF] [DMARC] [DKIM Revoke] [CAA] [TLS-RPT] <-- Security templates - -open Msg -open Model -open Tea.Html - -// ============================================================================ -// DNS record type display helpers -// ============================================================================ - -/// Human-readable label for a DNS record type. -let recordTypeLabel = (rt: dnsRecordType): string => { - switch rt { - | A => "A" - | AAAA => "AAAA" - | CNAME => "CNAME" - | MX => "MX" - | TXT => "TXT" - | SRV => "SRV" - | NS => "NS" - | CAA => "CAA" - | TLSA => "TLSA" - | HTTPS => "HTTPS" - | SVCB => "SVCB" - | PTR => "PTR" - | LOC => "LOC" - } -} - -/// CSS colour class for a DNS record type badge. -let recordTypeBadgeClass = (rt: dnsRecordType): string => { - switch rt { - | A | AAAA => "bg-blue-900/50 text-blue-300 border-blue-700/50" - | CNAME => "bg-green-900/50 text-green-300 border-green-700/50" - | MX => "bg-purple-900/50 text-purple-300 border-purple-700/50" - | TXT => "bg-yellow-900/50 text-yellow-300 border-yellow-700/50" - | CAA => "bg-orange-900/50 text-orange-300 border-orange-700/50" - | NS => "bg-gray-800/50 text-gray-300 border-gray-700/50" - | SRV => "bg-indigo-900/50 text-indigo-300 border-indigo-700/50" - | _ => "bg-gray-800/50 text-gray-400 border-gray-700/50" - } -} - -// ============================================================================ -// Security status indicators -// ============================================================================ - -/// Check if a record is a security-relevant TXT record (SPF, DMARC, DKIM, TLSRPT). -let isSecurityRecord = (record: cfDnsRecord): bool => { - switch record.recordType { - | TXT => - String.includes(record.content, "v=spf1") || - String.includes(record.name, "_dmarc") && String.includes(record.content, "v=DMARC1") || - String.includes(record.name, "_domainkey") && String.includes(record.content, "v=DKIM1") || - (String.includes(record.name, "_smtp._tls") && String.includes(record.content, "v=TLSRPTv1")) - | CAA => true - | _ => false - } -} - -/// Get a security label for a record if it's a known security record. -let securityLabel = (record: cfDnsRecord): option => { - switch record.recordType { - | TXT => - if String.includes(record.content, "v=spf1") { - Some("SPF") - } else if String.includes(record.name, "_dmarc") { - Some("DMARC") - } else if String.includes(record.name, "_domainkey") { - Some("DKIM") - } else if String.includes(record.name, "_smtp._tls") { - Some("TLS-RPT") - } else { - None - } - | CAA => Some("CAA") - | _ => None - } -} - -// ============================================================================ -// Table header -// ============================================================================ - -/// Render the DNS records table header row (div-based flex layout since -/// Tea_Html does not provide th/thead/tr elements). -let renderTableHeader = (): Tea_Vdom.t => { - let headerCell = (label: string, width: string) => - div( - list{Attrs.class_(`text-left text-xs text-gray-500 font-medium py-2 px-2 ${width}`)}, - list{text(label)}, - ) - - div( - list{Attrs.class_("flex border-b border-gray-800")}, - list{ - headerCell("Type", "w-16"), - headerCell("Name", "w-48"), - headerCell("Content", "flex-1"), - headerCell("TTL", "w-16"), - headerCell("Proxy", "w-14"), - headerCell("", "w-20"), - }, - ) -} - -// ============================================================================ -// Individual record row -// ============================================================================ - -/// Render a single DNS record row in the table. -let renderRecordRow = (record: cfDnsRecord, isEditing: bool): Tea_Vdom.t => { - let editingClass = isEditing ? " bg-gray-800/80 ring-1 ring-indigo-500/50" : "" - let secLabel = securityLabel(record) - - div( - list{ - Attrs.class_(`flex hover:bg-gray-800/30 transition-colors${editingClass}`), - Attrs.ariaLabel(`DNS record ${recordTypeLabel(record.recordType)} ${record.name}`), - }, - list{ - // Type badge - div( - list{Attrs.class_("py-1.5 px-2 w-16")}, - list{ - span( - list{ - Attrs.class_( - `text-xs font-mono px-1.5 py-0.5 rounded border ${recordTypeBadgeClass( - record.recordType, - )}`, - ), - }, - list{text(recordTypeLabel(record.recordType))}, - ), - }, - ), - // Name - div( - list{Attrs.class_("py-1.5 px-2 w-48")}, - list{ - div( - list{Attrs.class_("flex items-center gap-1.5")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-200 font-mono truncate max-w-48")}, - list{text(record.name)}, - ), - // Security label badge - switch secLabel { - | Some(label) => - span( - list{ - Attrs.class_( - "text-xs text-green-400 font-medium px-1 py-0.5 bg-green-900/30 rounded", - ), - }, - list{text(label)}, - ) - | None => noNode - }, - }, - ), - }, - ), - // Content (truncated for long TXT records) - div( - list{Attrs.class_("py-1.5 px-2 flex-1")}, - list{ - div( - list{ - Attrs.class_("text-sm text-gray-300 font-mono truncate max-w-96"), - Attrs.title(record.content), // Full content on hover - }, - list{text(record.content)}, - ), - }, - ), - // TTL - div( - list{Attrs.class_("py-1.5 px-2 w-16 text-xs text-gray-500")}, - list{ - text( - if record.ttl === 1 { - "Auto" - } else { - Int.toString(record.ttl) - }, - ), - }, - ), - // Proxied status - div( - list{Attrs.class_("py-1.5 px-2 w-14")}, - list{ - switch record.recordType { - | A | AAAA | CNAME => - span( - list{ - Attrs.class_( - if record.proxied { - "text-xs text-orange-400 font-medium" - } else { - "text-xs text-gray-500" - }, - ), - }, - list{ - text( - if record.proxied { - "ON" - } else { - "OFF" - }, - ), - }, - ) - | _ => span(list{Attrs.class_("text-xs text-gray-600")}, list{text("--")}) - }, - }, - ), - // Actions - div( - list{Attrs.class_("py-1.5 px-2 w-20")}, - list{ - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - // Edit button - if !record.locked { - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-indigo-400 cursor-pointer px-1"), - Attrs.ariaLabel(`Edit ${record.name}`), - Events.onClick(CloudGuard(StartEditingDnsRecord(record.id))), - }, - list{text("Edit")}, - ) - } else { - noNode - }, - // Delete button - if !record.locked { - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-red-400 cursor-pointer px-1"), - Attrs.ariaLabel(`Delete ${record.name}`), - Events.onClick(CloudGuard(DeleteDnsRecord(record.zoneId, record.id))), - }, - list{text("Del")}, - ) - } else { - span(list{Attrs.class_("text-xs text-gray-600 italic")}, list{text("Locked")}) - }, - }, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Security template buttons -// ============================================================================ - -/// Render the security record template buttons. -/// These provide one-click creation of SPF, DMARC, DKIM revocation, CAA, TLS-RPT records. -let renderSecurityTemplates = (records: array, hasSelectedZone: bool): Tea_Vdom.t< - msg, -> => { - // Check which security records already exist - let hasSpf = records->Array.some(r => - switch r.recordType { - | TXT => String.includes(r.content, "v=spf1") - | _ => false - } - ) - let hasDmarc = records->Array.some(r => - switch r.recordType { - | TXT => String.includes(r.name, "_dmarc") && String.includes(r.content, "v=DMARC1") - | _ => false - } - ) - let hasDkim = records->Array.some(r => - switch r.recordType { - | TXT => String.includes(r.name, "_domainkey") - | _ => false - } - ) - let hasCaa = records->Array.some(r => - switch r.recordType { - | CAA => true - | _ => false - } - ) - let hasTlsrpt = records->Array.some(r => - switch r.recordType { - | TXT => String.includes(r.name, "_smtp._tls") - | _ => false - } - ) - - /// Render a single template button. Green if record exists, amber if missing. - let templateButton = (label: string, templateName: string, exists: bool) => { - let (bgClass, labelSuffix) = if exists { - ("bg-green-900/30 text-green-400 border-green-700/40 cursor-default", " OK") - } else { - ( - "bg-amber-900/30 text-amber-400 border-amber-700/40 hover:bg-amber-900/50 cursor-pointer", - "", - ) - } - - button( - list{ - Attrs.class_(`text-xs font-medium px-2 py-1 rounded border ${bgClass}`), - Attrs.ariaLabel( - if exists { - `${label} record already exists` - } else { - `Add ${label} security record` - }, - ), - if !exists && hasSelectedZone { - Events.onClick(CloudGuard(ApplySecurityTemplate(templateName))) - } else { - Attrs.noProp - }, - }, - list{text(`${label}${labelSuffix}`)}, - ) - } - - div( - list{Attrs.class_("flex items-center gap-2 flex-wrap")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500 mr-1")}, list{text("Security:")}), - templateButton("SPF", "spf", hasSpf), - templateButton("DMARC", "dmarc", hasDmarc), - templateButton("DKIM", "dkim_revoke", hasDkim), - templateButton("CAA", "caa", hasCaa), - templateButton("TLS-RPT", "tlsrpt", hasTlsrpt), - }, - ) -} - -// ============================================================================ -// Record count summary -// ============================================================================ - -/// Render a compact summary showing counts by record type. -let renderRecordSummary = (records: array): Tea_Vdom.t => { - let counts = CloudGuardEngine.countRecordsByType(records) - - div( - list{Attrs.class_("flex items-center gap-2 flex-wrap")}, - counts - ->Array.map(((typeName, count)) => - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${typeName}: ${Int.toString(count)}`)}, - ) - ) - ->List.fromArray, - ) -} - -// ============================================================================ -// Main DNS editor view -// ============================================================================ - -/// Render the complete DNS editor for the currently selected zone. -/// Shows the record table, security templates, and record count summary. -let view = ( - records: array, - editingId: option, - hasSelectedZone: bool, - loading: bool, -): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex-1 flex flex-col"), - Attrs.role("region"), - Attrs.ariaLabel("DNS Record Editor"), - }, - list{ - // Header with record count and security templates - div( - list{ - Attrs.class_("flex items-center justify-between px-3 py-2 border-b border-gray-800/50"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-300 font-medium")}, - list{text(`${Int.toString(Array.length(records))} records`)}, - ), - renderRecordSummary(records), - }, - ), - renderSecurityTemplates(records, hasSelectedZone), - }, - ), - // Loading indicator - if loading { - div( - list{Attrs.class_("text-sm text-gray-500 italic px-3 py-2")}, - list{text("Loading DNS records...")}, - ) - } else { - noNode - }, - // Records table - if Array.length(records) > 0 { - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - div( - list{Attrs.class_("w-full text-left")}, - list{ - renderTableHeader(), - div( - list{}, - records - ->Array.map(record => { - let isEditing = switch editingId { - | Some(id) => id === record.id - | None => false - } - renderRecordRow(record, isEditing) - }) - ->List.fromArray, - ), - }, - ), - }, - ) - } else if !loading { - div( - list{Attrs.class_("text-sm text-gray-600 italic px-3 py-4")}, - list{text("No DNS records found for this zone.")}, - ) - } else { - noNode - }, - { - let missing = CloudGuardEngine.checkEmailSecurityRecords(records) - if Array.length(missing) > 0 && Array.length(records) > 0 { - div( - list{Attrs.class_("border-t border-gray-800/50 px-3 py-2")}, - list{ - div( - list{Attrs.class_("text-xs text-amber-400 font-medium mb-1")}, - list{text("Missing Security Records:")}, - ), - div( - list{Attrs.class_("space-y-0.5")}, - missing - ->Array.map(msg => - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(`- ${msg}`)}) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - } - }, - }, - ) -} diff --git a/src/components/CloudGuardDomainList.affine b/src/components/CloudGuardDomainList.affine new file mode 100644 index 00000000..0bcaba65 --- /dev/null +++ b/src/components/CloudGuardDomainList.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuardDomainList; + +// TODO: Complete semantic implementation diff --git a/src/components/CloudGuardDomainList.res b/src/components/CloudGuardDomainList.res deleted file mode 100644 index 74c66f12..00000000 --- a/src/components/CloudGuardDomainList.res +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CloudGuard Domain List — Horizontal domain selector ribbon. -/// -/// Renders a scrollable horizontal ribbon of domain checkboxes at the top of -/// the CloudGuard panel. Users can select/deselect individual domains, use -/// "Select All" / "None" shortcuts, and filter by name. -/// -/// Layout: -/// [Select All] [None] [Filter: ________] -/// [x wokelang.org] [x axel-protocol.org] [ betlang.org] [x cc-studio.dev] ... -/// -/// Selected domains are highlighted with an indigo border. Domains with -/// compliance issues show a small red/yellow dot indicator. - -open Msg -open Model -open Tea.Html - -/// Render a single domain chip in the ribbon. -/// Selected chips have an indigo border, deselected have a gray border. -let renderDomainChip = (zone: cfZone, isSelected: bool): Tea_Vdom.t => { - let borderClass = isSelected - ? "border-indigo-500 bg-indigo-950/30" - : "border-gray-700 bg-gray-800/30" - - let statusDot = switch zone.status { - | "active" => - span(list{Attrs.class_("w-2 h-2 rounded-full bg-green-400 inline-block mr-1.5")}, list{}) - | "pending" => - span(list{Attrs.class_("w-2 h-2 rounded-full bg-yellow-400 inline-block mr-1.5")}, list{}) - | _ => span(list{Attrs.class_("w-2 h-2 rounded-full bg-gray-500 inline-block mr-1.5")}, list{}) - } - - let planBadge = switch zone.plan { - | Free => noNode - | Pro => span(list{Attrs.class_("ml-1.5 text-xs text-orange-400 font-medium")}, list{text("PRO")}) - | Business => - span(list{Attrs.class_("ml-1.5 text-xs text-purple-400 font-medium")}, list{text("BIZ")}) - | Enterprise => - span(list{Attrs.class_("ml-1.5 text-xs text-blue-400 font-medium")}, list{text("ENT")}) - } - - button( - list{ - Attrs.class_( - `inline-flex items-center px-3 py-1.5 rounded border text-sm font-mono cursor-pointer transition-colors ${borderClass} hover:border-indigo-400`, - ), - Attrs.ariaPressed(isSelected), - Attrs.ariaLabel(`${isSelected ? "Deselect" : "Select"} ${zone.name}`), - Events.onClick(CloudGuard(ToggleZoneSelection(zone.id))), - }, - list{statusDot, span(list{Attrs.class_("text-gray-200")}, list{text(zone.name)}), planBadge}, - ) -} - -/// Render the domain filter input. -let renderFilterInput = (filterText: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Filter:")}), - input( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-gray-300 w-40 focus:border-indigo-500 focus:outline-none", - ), - Attrs.type_("text"), - Attrs.value(filterText), - Attrs.placeholder("domain name..."), - Attrs.ariaLabel("Filter domains"), - Events.onInput(text => CloudGuard(SetFilterText(text))), - }, - list{}, - ), - }, - ) -} - -/// Render the "Select All" and "None" shortcut buttons. -let renderSelectionControls = (): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_("text-xs text-indigo-400 hover:text-indigo-300 cursor-pointer font-medium"), - Attrs.ariaLabel("Select all domains"), - Events.onClick(CloudGuard(SelectAllZones)), - KeyboardNav.onActivate(CloudGuard(SelectAllZones)), - }, - list{text("Select All")}, - ), - span(list{Attrs.class_("text-gray-600")}, list{text("|")}), - button( - list{ - Attrs.class_("text-xs text-gray-400 hover:text-gray-300 cursor-pointer font-medium"), - Attrs.ariaLabel("Deselect all domains"), - Events.onClick(CloudGuard(DeselectAllZones)), - KeyboardNav.onActivate(CloudGuard(DeselectAllZones)), - }, - list{text("None")}, - ), - }, - ) -} - -/// Render the complete domain ribbon: controls bar + scrollable chip list. -let view = (zones: array, selectedZoneIds: array, filterText: string): Tea_Vdom.t< - msg, -> => { - let filteredZones = CloudGuardEngine.filterZones(zones, filterText) - - div( - list{ - Attrs.class_("border-b border-gray-800 pb-3"), - Attrs.role("region"), - Attrs.ariaLabel("Domain selector"), - }, - list{ - // Controls row: Select All | None | Filter - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - renderSelectionControls(), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(Array.length(selectedZoneIds))}/${Int.toString( - Array.length(zones), - )} selected`, - ), - }, - ), - renderFilterInput(filterText), - }, - ), - }, - ), - // Domain chips ribbon (scrollable) - div( - list{ - Attrs.class_("flex flex-wrap gap-1.5 max-h-20 overflow-y-auto"), - Attrs.role("listbox"), - Attrs.ariaLabel("Domains"), - Attrs.prop("aria-multiselectable", "true"), - }, - filteredZones - ->Array.map(zone => { - let isSelected = Array.includes(selectedZoneIds, zone.id) - renderDomainChip(zone, isSelected) - }) - ->List.fromArray, - ), - }, - ) -} diff --git a/src/components/CloudGuardPagesSetup.affine b/src/components/CloudGuardPagesSetup.affine new file mode 100644 index 00000000..aee20e8f --- /dev/null +++ b/src/components/CloudGuardPagesSetup.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuardPagesSetup; + +// TODO: Complete semantic implementation diff --git a/src/components/CloudGuardPagesSetup.res b/src/components/CloudGuardPagesSetup.res deleted file mode 100644 index e093a53e..00000000 --- a/src/components/CloudGuardPagesSetup.res +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CloudGuard Pages Setup — Cloudflare Pages integration panel. -/// -/// Provides a workflow for connecting GitHub repositories to Cloudflare Pages -/// projects with automatic SSG framework detection, CNAME record creation, -/// custom domain binding, and security headers setup via `_headers` file. -/// -/// Layout: -/// +-----------------------------------------------+ -/// | Existing Pages Projects | -/// | +-------------------------------------------+ | -/// | | project-name.pages.dev | | -/// | | custom-domain.com (CNAME active) | | -/// | | Framework: Jekyll | Branch: main | | -/// | +-------------------------------------------+ | -/// | | -/// | [+ New Pages Project] | -/// | | -/// | Security Headers Template | -/// | [Generate _headers file] [Preview] | -/// +-----------------------------------------------+ - -open Msg -open Model -open Tea.Html - -// ============================================================================ -// Pages project card -// ============================================================================ - -/// Render a single Pages project card showing project metadata and status. -let renderProjectCard = (project: cfPagesProject): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "border border-gray-700 rounded-lg p-3 mb-2 hover:border-gray-600 transition-colors", - ), - }, - list{ - // Project header - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(project.name)}, - ), - span( - list{Attrs.class_("text-xs text-indigo-400 font-mono")}, - list{text(`${project.subdomain}.pages.dev`)}, - ), - }, - ), - // Framework badge - switch project.framework { - | Some(fw) => - span( - list{ - Attrs.class_( - "text-xs text-purple-400 font-medium px-1.5 py-0.5 bg-purple-900/30 rounded", - ), - }, - list{text(fw)}, - ) - | None => noNode - }, - }, - ), - // Custom domains - if Array.length(project.customDomains) > 0 { - div( - list{Attrs.class_("mb-1.5")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-0.5")}, list{text("Custom domains:")}), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - project.customDomains - ->Array.map(domain => - span( - list{ - Attrs.class_( - "text-xs text-green-400 font-mono px-1.5 py-0.5 bg-green-900/20 rounded", - ), - }, - list{text(domain)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Branch info - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Production branch: ${project.productionBranch}`)}, - ), - }, - ) -} - -// ============================================================================ -// Security headers template preview -// ============================================================================ - -/// Generate a `_headers` file content for security hardening. -/// This is a free alternative to Workers for setting security headers on -/// Cloudflare Pages deployments. -let securityHeadersTemplate = (domain: string): string => { - `/* - X-Frame-Options: DENY - X-Content-Type-Options: nosniff - X-XSS-Protection: 1; mode=block - Referrer-Policy: strict-origin-when-cross-origin - Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=() - Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' - Strict-Transport-Security: max-age=31536000; includeSubDomains; preload - Cross-Origin-Embedder-Policy: require-corp - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Resource-Policy: same-origin - -# ${domain} — Generated by CloudGuard -` -} - -/// Render the security headers template section. -let renderHeadersTemplate = (currentDomain: option): Tea_Vdom.t => { - let domain = switch currentDomain { - | Some(d) => d - | None => "example.com" - } - - div( - list{Attrs.class_("border border-gray-700 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300 font-medium mb-2")}, - list{text("Security Headers Template (_headers)")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{ - text( - "Add this _headers file to your Pages project root for free security headers (no Workers needed).", - ), - }, - ), - // Template preview - pre( - list{ - Attrs.class_( - "text-xs text-gray-400 bg-gray-900 rounded p-2 overflow-x-auto max-h-40 font-mono whitespace-pre", - ), - }, - list{text(securityHeadersTemplate(domain))}, - ), - }, - ) -} - -// ============================================================================ -// Main Pages setup view -// ============================================================================ - -/// Render the Pages setup panel. Shows existing projects and the security -/// headers template generator. -let view = ( - pagesProjects: array, - currentDomain: option, - _loading: bool, -): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex-1 flex flex-col gap-4"), - Attrs.role("region"), - Attrs.ariaLabel("Cloudflare Pages Setup"), - }, - list{ - // Existing projects section - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 font-medium mb-2")}, - list{text("Pages Projects")}, - ), - if Array.length(pagesProjects) > 0 { - div( - list{Attrs.class_("space-y-2")}, - pagesProjects - ->Array.map(renderProjectCard) - ->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-xs text-gray-600 italic py-2")}, - list{text("No Pages projects found for this account.")}, - ) - }, - }, - ), - // Security headers template - renderHeadersTemplate(currentDomain), - }, - ) -} diff --git a/src/components/CloudGuardSettingsGrid.affine b/src/components/CloudGuardSettingsGrid.affine new file mode 100644 index 00000000..b8a5137c --- /dev/null +++ b/src/components/CloudGuardSettingsGrid.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CloudGuardSettingsGrid; + +// TODO: Complete semantic implementation diff --git a/src/components/CloudGuardSettingsGrid.res b/src/components/CloudGuardSettingsGrid.res deleted file mode 100644 index 3c5c4d36..00000000 --- a/src/components/CloudGuardSettingsGrid.res +++ /dev/null @@ -1,302 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CloudGuard Settings Grid — Grouped toggle/switch grid for zone settings. -/// -/// Renders Cloudflare zone settings as an interactive grid of toggles, dropdowns, -/// and number inputs, grouped by category. Settings unavailable on the user's plan -/// are greyed out with a "Requires Pro/Business/Enterprise" badge. -/// -/// Modified settings (different from last-pushed state) show an orange dot indicator. -/// Settings that differ from the policy default show a yellow warning icon. -/// -/// Layout (within a category tab): -/// +-----------------------------------------+ -/// | SSL Mode [Full (Strict) v] | -/// | Min TLS Version [1.2 v] | -/// | Always HTTPS [================ON] | -/// | Auto Rewrites [================ON] | -/// | Opportunistic Enc [================ON] | -/// | TLS 1.3 [zrt (0-RTT) v] | -/// +-----------------------------------------+ - -open Msg -open Model -open Tea.Html - -/// Render a toggle switch for on/off settings. -/// The toggle is a styled checkbox that looks like a sliding switch. -let renderToggle = (setting: cfSetting): Tea_Vdom.t => { - let isOn = CloudGuardEngine.isSettingEnabled(setting.value) - let bgClass = isOn ? "bg-indigo-600" : "bg-gray-600" - let translateClass = isOn ? "translate-x-5" : "translate-x-0" - - div( - list{Attrs.class_("flex items-center justify-between py-2 px-3 hover:bg-gray-800/50 rounded")}, - list{ - // Label + description - div( - list{Attrs.class_("flex-1 mr-4")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 font-medium")}, list{text(setting.label)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, list{text(setting.description)}), - }, - ), - // Toggle switch - button( - list{ - Attrs.class_( - `relative inline-flex h-6 w-11 items-center rounded-full transition-colors cursor-pointer ${bgClass}`, - ), - Attrs.role("switch"), - Attrs.ariaChecked(isOn), - Attrs.ariaLabel(`Toggle ${setting.label}`), - Events.onClick(CloudGuard(ToggleSetting(setting.id))), - }, - list{ - span( - list{ - Attrs.class_( - `inline-block h-4 w-4 rounded-full bg-white transition-transform ${translateClass}`, - ), - Attrs.style("margin-left", "2px"), - }, - list{}, - ), - }, - ), - // Modified indicator - if setting.modified { - span( - list{ - Attrs.class_("w-2 h-2 rounded-full bg-orange-400 ml-2"), - Attrs.title("Setting has been modified"), - }, - list{}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render a dropdown select for enum settings. -let renderSelect = (setting: cfSetting, options: array): Tea_Vdom.t => { - let currentValue = CloudGuardEngine.settingValueToString(setting.value) - - div( - list{Attrs.class_("flex items-center justify-between py-2 px-3 hover:bg-gray-800/50 rounded")}, - list{ - // Label + description - div( - list{Attrs.class_("flex-1 mr-4")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 font-medium")}, list{text(setting.label)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, list{text(setting.description)}), - }, - ), - // Select dropdown - select( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-gray-200 cursor-pointer focus:border-indigo-500 focus:outline-none", - ), - Attrs.value(currentValue), - Attrs.ariaLabel(`Select ${setting.label}`), - Events.onChange(value => CloudGuard(UpdateSettingValue(setting.id, value))), - }, - options - ->Array.map(opt => { - option'( - list{ - Attrs.value(opt), - if opt === currentValue { - Attrs.selected(true) - } else { - Attrs.noProp - }, - }, - list{text(opt)}, - ) - }) - ->List.fromArray, - ), - // Modified indicator - if setting.modified { - span( - list{ - Attrs.class_("w-2 h-2 rounded-full bg-orange-400 ml-2"), - Attrs.title("Setting has been modified"), - }, - list{}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render a number input for numeric settings. -let renderNumberInput = (setting: cfSetting): Tea_Vdom.t => { - let currentValue = CloudGuardEngine.settingValueToString(setting.value) - - div( - list{Attrs.class_("flex items-center justify-between py-2 px-3 hover:bg-gray-800/50 rounded")}, - list{ - div( - list{Attrs.class_("flex-1 mr-4")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 font-medium")}, list{text(setting.label)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, list{text(setting.description)}), - }, - ), - input( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-600 rounded px-2 py-1 text-sm text-gray-200 w-24 focus:border-indigo-500 focus:outline-none", - ), - Attrs.type_("number"), - Attrs.value(currentValue), - Attrs.ariaLabel(`Set ${setting.label}`), - Events.onInput(value => CloudGuard(UpdateSettingValue(setting.id, value))), - }, - list{}, - ), - }, - ) -} - -/// Render an exception indicator badge showing that this setting has a -/// per-domain override. Displays the override reason on hover via title attr. -let renderExceptionBadge = (domainExc: domainException): Tea_Vdom.t => { - span( - list{ - Attrs.class_( - "text-xs text-yellow-400 font-medium px-1.5 py-0.5 border border-yellow-500/30 rounded ml-2", - ), - Attrs.title(`Exception: ${domainExc.reason}`), - }, - list{text("EXC")}, - ) -} - -/// Render a single setting row, choosing the appropriate input type. -/// Unavailable settings are rendered greyed-out with a plan badge. -/// If an exception exists for the current domain, shows a yellow "EXC" badge. -let renderSettingRow = (setting: cfSetting, domainExc: option): Tea_Vdom.t< - msg, -> => { - // Check availability from catalog - let catalogEntry = CloudGuardCatalog.findById(setting.id) - - // The core row content depends on availability and value type - let rowContent = switch catalogEntry { - | None => renderToggle(setting) // Fallback to toggle for unknown settings - | Some(entry) => - switch entry.availability { - | Unavailable(tier) => - // Greyed-out setting with plan badge - div( - list{ - Attrs.class_("flex items-center justify-between py-2 px-3 opacity-40 cursor-not-allowed"), - }, - list{ - div( - list{Attrs.class_("flex-1 mr-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 font-medium")}, - list{text(setting.label)}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-0.5")}, - list{text(setting.description)}, - ), - }, - ), - span( - list{ - Attrs.class_( - "text-xs text-amber-500/60 font-medium px-2 py-0.5 border border-amber-500/30 rounded", - ), - }, - list{text(`Requires ${CloudGuardCatalog.planLabel(tier)}`)}, - ), - }, - ) - | Available | Limited(_) => - switch entry.valueType { - | "toggle" => renderToggle(setting) - | "select" => - switch entry.options { - | Some(opts) => renderSelect(setting, opts) - | None => renderToggle(setting) - } - | "number" => renderNumberInput(setting) - | _ => renderToggle(setting) // Fallback - } - } - } - - // Wrap with exception badge if this setting has a per-domain override - switch domainExc { - | None => rowContent - | Some(exc) => div(list{Attrs.class_("relative")}, list{rowContent, renderExceptionBadge(exc)}) - } -} - -/// Render the settings grid for a given category. -/// Shows all settings in the category, filtered by search text. -/// Exceptions are used to show per-domain override indicators on individual rows. -let view = ( - settings: array, - activeCategory: settingCategory, - settingFilter: string, - exceptions: array, - currentDomain: option, -): Tea_Vdom.t => { - // Filter settings to the active category - let categorySettings = settings->Array.filter(s => s.category === activeCategory) - - // Apply text filter - let filteredSettings = if String.length(settingFilter) > 0 { - let lower = String.toLowerCase(settingFilter) - categorySettings->Array.filter(s => - String.includes(String.toLowerCase(s.label), lower) || - String.includes(String.toLowerCase(s.id), lower) - ) - } else { - categorySettings - } - - div( - list{ - Attrs.class_("flex-1 overflow-y-auto"), - Attrs.role("list"), - Attrs.ariaLabel(`${CloudGuardCatalog.categoryLabel(activeCategory)} settings`), - }, - list{ - if Array.length(filteredSettings) === 0 { - div( - list{Attrs.class_("text-gray-600 text-sm italic py-4 px-3")}, - list{text(`No ${CloudGuardCatalog.categoryLabel(activeCategory)} settings found`)}, - ) - } else { - div( - list{Attrs.class_("divide-y divide-gray-800/50")}, - filteredSettings - ->Array.map(setting => { - // Look up per-domain exception for this setting - let domainExc = switch currentDomain { - | Some(domain) => CloudGuardEngine.findException(exceptions, domain, setting.id) - | None => None - } - renderSettingRow(setting, domainExc) - }) - ->List.fromArray, - ) - }, - }, - ) -} diff --git a/src/components/CodeReview.affine b/src/components/CodeReview.affine new file mode 100644 index 00000000..241812c1 --- /dev/null +++ b/src/components/CodeReview.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CodeReview; + +// TODO: Complete semantic implementation diff --git a/src/components/CodeReview.res b/src/components/CodeReview.res deleted file mode 100644 index f93b2925..00000000 --- a/src/components/CodeReview.res +++ /dev/null @@ -1,311 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CodeReview — pull request review, inline comments, and approval gates. -/// Scanner clade panel for team code review workflows. -/// -/// Four tabs: Pull Requests (list with status badges), File Changes (summary), -/// Comments (threaded inline review), and Approval Gate (merge readiness). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Human-readable label for a PR status. -let statusLabel = (status: prStatus): string => - switch status { - | PrOpen => "Open" - | PrApproved => "Approved" - | PrChangesRequested => "Changes Requested" - | PrMerged => "Merged" - | PrClosed => "Closed" - } - -/// Tailwind colour class for a PR status badge. -let statusColour = (status: prStatus): string => - switch status { - | PrOpen => "bg-blue-600 text-blue-100" - | PrApproved => "bg-emerald-600 text-emerald-100" - | PrChangesRequested => "bg-amber-600 text-amber-100" - | PrMerged => "bg-purple-600 text-purple-100" - | PrClosed => "bg-gray-600 text-gray-300" - } - -/// Tab bar rendering. -let renderTabs = (active: codeReviewTab): Tea_Vdom.t => { - let tabs = CodeReviewEngine.allTabs - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(CodeReview(SetCrTab(tab))), - }, - list{text(CodeReviewEngine.tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Pull Requests tab: list with status badges, file counts, and diff stats. -let renderPullRequestsTab = (state: codeReviewState): Tea_Vdom.t => { - let filtered = CodeReviewEngine.filterPrs(state.pullRequests, state.filter) - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No pull requests found. Open a PR to begin code review.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - filtered - ->Array.map(pr => { - let isSelected = state.selectedPr === Some(pr.id) - let bgCls = isSelected ? "bg-gray-750 border border-cyan-700" : "bg-gray-800" - div( - list{ - Attrs.class_( - `p-3 rounded border border-gray-700 cursor-pointer hover:bg-gray-750 ${bgCls}`, - ), - Events.onClick(CodeReview(SelectPr(pr.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(pr.title)}), - span( - list{Attrs.class_(`text-xs px-2 py-0.5 rounded-full ${statusColour(pr.status)}`)}, - list{text(statusLabel(pr.status))}, - ), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500")}, - list{ - span(list{}, list{text(pr.author)}), - span(list{}, list{text(`${pr.branch}`)}), - span(list{}, list{text(`${Int.toString(pr.filesChanged)} files`)}), - span( - list{Attrs.class_("text-emerald-500")}, - list{text(`+${Int.toString(pr.additions)}`)}, - ), - span( - list{Attrs.class_("text-red-400")}, - list{text(`-${Int.toString(pr.deletions)}`)}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// File Changes tab: summary of changed files in the selected PR. -let renderFileChangesTab = (_state: codeReviewState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-48 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Select a pull request to view file changes.")}, - ), - }, - ), - }, - ) -} - -/// Comments tab: threaded inline review comments. -let renderCommentsTab = (state: codeReviewState): Tea_Vdom.t => { - let unresolvedCount = CodeReviewEngine.countUnresolved(state.comments) - if Array.length(state.comments) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No review comments yet.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{ - text( - `${Int.toString(Array.length(state.comments))} comment(s), ${Int.toString( - unresolvedCount, - )} unresolved`, - ), - }, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-80 overflow-y-auto")}, - state.comments - ->Array.map(comment => { - let resolvedCls = comment.resolved ? "border-emerald-800 opacity-60" : "border-gray-700" - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${resolvedCls}`)}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400 font-mono")}, - list{text(`${comment.filePath}:${Int.toString(comment.lineNumber)}`)}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(comment.author)}), - }, - ), - div(list{Attrs.class_("text-sm text-gray-300")}, list{text(comment.body)}), - if comment.resolved { - div(list{Attrs.class_("text-xs text-emerald-500 mt-1")}, list{text("Resolved")}) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Approval Gate tab: merge readiness check and approve button. -let renderApprovalGateTab = (state: codeReviewState): Tea_Vdom.t => { - let unresolvedCount = CodeReviewEngine.countUnresolved(state.comments) - let openCount = CodeReviewEngine.countByStatus(state.pullRequests, PrOpen) - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 border border-gray-700")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-2")}, - list{text("Merge Readiness")}, - ), - div( - list{Attrs.class_("flex flex-col gap-1 text-xs")}, - list{ - div( - list{ - Attrs.class_( - if openCount === 0 { - "text-emerald-400" - } else { - "text-amber-400" - }, - ), - }, - list{text(`Open PRs: ${Int.toString(openCount)}`)}, - ), - div( - list{ - Attrs.class_( - if unresolvedCount === 0 { - "text-emerald-400" - } else { - "text-red-400" - }, - ), - }, - list{text(`Unresolved comments: ${Int.toString(unresolvedCount)}`)}, - ), - }, - ), - }, - ), - if unresolvedCount === 0 && state.selectedPr !== None { - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(CodeReview(ApprovePr)), - KeyboardNav.onActivate(CodeReview(ApprovePr)), - }, - list{text("Approve Selected PR")}, - ) - } else { - noNode - }, - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function for the Code Review panel. -let view = (state: codeReviewState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabPullRequests => renderPullRequestsTab(state) - | TabFileChanges => renderFileChangesTab(state) - | TabComments => renderCommentsTab(state) - | TabApprovalGate => renderApprovalGateTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold text-cyan-300")}, list{text("Code Review")}), - // Filter input - input( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-gray-300 w-48 placeholder-gray-600", - ), - Attrs.placeholder("Filter PRs..."), - Attrs.value(state.filter), - Events.onInput(value => CodeReview(SetCrFilter(value))), - }, - list{}, - ), - }, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/CompatibilityMatrix.affine b/src/components/CompatibilityMatrix.affine new file mode 100644 index 00000000..4bf08b43 --- /dev/null +++ b/src/components/CompatibilityMatrix.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module CompatibilityMatrix; + +// TODO: Complete semantic implementation diff --git a/src/components/CompatibilityMatrix.res b/src/components/CompatibilityMatrix.res deleted file mode 100644 index 08da4de8..00000000 --- a/src/components/CompatibilityMatrix.res +++ /dev/null @@ -1,439 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL CompatibilityMatrix — browser/device cross-testing matrix with -/// pass/fail/untested cell colouring and failure detail drill-down. -/// -/// Four tabs: Matrix (grid layout with coloured cells), Failures (detail view -/// of failing cells), Screenshots (captured evidence), and Targets (browser -/// and device configuration). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for compatibilityTab variants. -let tabLabel = (tab: compatibilityTab): string => - switch tab { - | TabMatrix => "Matrix" - | TabFailures => "Failures" - | TabScreenshots => "Screenshots" - | TabTargets => "Targets" - } - -/// Render the tab bar. -let renderTabs = (active: compatibilityTab): Tea_Vdom.t => { - let tabs: array = [TabMatrix, TabFailures, TabScreenshots, TabTargets] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(CompatibilityMatrix(SetCmTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Cell background colour based on compatibility result. -let cellColour = (result: compatResult): string => - switch result { - | CompatPassing => "bg-emerald-700" - | CompatFailing(_) => "bg-red-700" - | CompatWarning(_) => "bg-amber-700" - | CompatUntested => "bg-gray-700" - | CompatSkipped(_) => "bg-gray-600" - } - -/// Cell short label for the matrix grid. -let cellLabel = (result: compatResult): string => - switch result { - | CompatPassing => "OK" - | CompatFailing(_) => "FAIL" - | CompatWarning(_) => "WARN" - | CompatUntested => "--" - | CompatSkipped(_) => "SKIP" - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Matrix tab: grid of browsers (columns) x devices (rows) with coloured cells. -let renderMatrixTab = (state: compatibilityMatrixState): Tea_Vdom.t => { - let browserCount = Array.length(state.browsers) - let deviceCount = Array.length(state.devices) - let passing = - state.cells - ->Array.filter(c => - switch c.result { - | CompatPassing => true - | _ => false - } - ) - ->Array.length - let failing = - state.cells - ->Array.filter(c => - switch c.result { - | CompatFailing(_) => true - | _ => false - } - ) - ->Array.length - let totalCells = Array.length(state.cells) - - div( - list{Attrs.class_("flex flex-col gap-3 p-4")}, - list{ - // Summary - div( - list{Attrs.class_("flex gap-4 text-sm")}, - list{ - span( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Int.toString(browserCount)} browser(s) x ${Int.toString( - deviceCount, - )} device(s) = ${Int.toString(totalCells)} cell(s)`, - ), - }, - ), - span(list{Attrs.class_("text-emerald-400")}, list{text(`${Int.toString(passing)} pass`)}), - span(list{Attrs.class_("text-red-400")}, list{text(`${Int.toString(failing)} fail`)}), - }, - ), - // Matrix header row (browser names) - div( - list{Attrs.class_("overflow-x-auto")}, - list{ - div( - list{Attrs.class_("inline-flex flex-col gap-1 min-w-max")}, - list{ - // Header row - div( - list{Attrs.class_("flex gap-1")}, - list{ - // Empty corner cell - div(list{Attrs.class_("w-28 h-8 flex-shrink-0")}, list{}), - // Browser column headers - fragment( - state.browsers - ->Array.map(browser => { - div( - list{ - Attrs.class_( - "w-16 h-8 flex items-center justify-center text-xs text-gray-400 font-mono", - ), - }, - list{text(browser.name)}, - ) - }) - ->List.fromArray, - ), - }, - ), - // Device rows with cells - fragment( - state.devices - ->Array.map(device => { - div( - list{Attrs.class_("flex gap-1")}, - list{ - // Device label - div( - list{ - Attrs.class_( - "w-28 h-8 flex items-center text-xs text-gray-400 font-mono flex-shrink-0 truncate", - ), - }, - list{text(device.name)}, - ), - // Matrix cells for this device - fragment( - state.browsers - ->Array.map(browser => { - let cell = - state.cells->Array.find( - c => c.browser.name === browser.name && c.device.name === device.name, - ) - let result = switch cell { - | Some(c) => c.result - | None => CompatUntested - } - div( - list{ - Attrs.class_( - `w-16 h-8 flex items-center justify-center rounded text-xs text-white font-mono cursor-pointer hover:opacity-80 ${cellColour( - result, - )}`, - ), - Events.onClick( - CompatibilityMatrix(SelectCell(browser.name, device.name)), - ), - }, - list{text(cellLabel(result))}, - ) - }) - ->List.fromArray, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ), - }, - ) -} - -/// Failures tab: detail view of failing cells with error messages. -let renderFailuresTab = (state: compatibilityMatrixState): Tea_Vdom.t => { - let failures = state.cells->Array.filter(c => - switch c.result { - | CompatFailing(_) => true - | _ => false - } - ) - if Array.length(failures) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No failures detected. All tested cells are passing.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - failures - ->Array.map(cell => { - let errMsg = switch cell.result { - | CompatFailing(msg) => msg - | _ => "" - } - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-red-800")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(`${cell.browser.name} / ${cell.device.name}`)}, - ), - span( - list{ - Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-red-600 text-white font-mono"), - }, - list{text("FAIL")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-red-300 font-mono whitespace-pre-wrap")}, - list{text(errMsg)}, - ), - if cell.notes !== "" { - div( - list{Attrs.class_("text-xs text-gray-500 mt-2")}, - list{text(`Notes: ${cell.notes}`)}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Screenshots tab: grid of captured evidence images. -let renderScreenshotsTab = (state: compatibilityMatrixState): Tea_Vdom.t => { - let withScreenshots = state.cells->Array.filter(c => Option.isSome(c.screenshotPath)) - if Array.length(withScreenshots) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No screenshots captured yet. Run tests to generate evidence.")}, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-3 gap-3 p-4 max-h-96 overflow-y-auto")}, - withScreenshots - ->Array.map(cell => { - div( - list{Attrs.class_("bg-gray-800 rounded p-2 border border-gray-700")}, - list{ - div( - list{Attrs.class_("bg-gray-900 rounded h-24 flex items-center justify-center mb-2")}, - list{span(list{Attrs.class_("text-gray-600 text-xs")}, list{text("[screenshot]")})}, - ), - div( - list{Attrs.class_("text-xs text-gray-400 text-center")}, - list{text(`${cell.browser.name} / ${cell.device.name}`)}, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Targets tab: browser and device configuration lists. -let renderTargetsTab = (state: compatibilityMatrixState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // Browsers - div( - list{Attrs.class_("flex flex-col gap-2")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text(`Browsers (${Int.toString(Array.length(state.browsers))})`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - state.browsers - ->Array.map(browser => { - div( - list{Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-800 rounded text-xs")}, - list{ - span(list{Attrs.class_("text-gray-300 font-medium")}, list{text(browser.name)}), - span(list{Attrs.class_("text-gray-500")}, list{text(`v${browser.version}`)}), - span(list{Attrs.class_("text-gray-600")}, list{text(browser.engine)}), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Devices - div( - list{Attrs.class_("flex flex-col gap-2")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text(`Devices (${Int.toString(Array.length(state.devices))})`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - state.devices - ->Array.map(device => { - let (w, h) = device.resolution - div( - list{Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-800 rounded text-xs")}, - list{ - span(list{Attrs.class_("text-gray-300 font-medium")}, list{text(device.name)}), - span(list{Attrs.class_("text-gray-500")}, list{text(device.category)}), - span( - list{Attrs.class_("text-gray-600 font-mono")}, - list{ - text( - `${Int.toString(w)}x${Int.toString(h)} @${Float.toFixed( - device.pixelRatio, - ~digits=0, - )}x`, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: compatibilityMatrixState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabMatrix => renderMatrixTab(state) - | TabFailures => renderFailuresTab(state) - | TabScreenshots => renderScreenshotsTab(state) - | TabTargets => renderTargetsTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with Run All - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Compatibility Matrix")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(CompatibilityMatrix(RunAll)), - KeyboardNav.onActivate(CompatibilityMatrix(RunAll)), - }, - list{text("Run All")}, - ), - }, - ), - // Running indicator - if state.running { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-amber-400 rounded-full animate-pulse")}, list{}), - span( - list{Attrs.class_("text-sm text-amber-300")}, - list{text("Running compatibility tests...")}, - ), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/ContractileCompleteness.affine b/src/components/ContractileCompleteness.affine new file mode 100644 index 00000000..9d1febae --- /dev/null +++ b/src/components/ContractileCompleteness.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ContractileCompleteness; + +// TODO: Complete semantic implementation diff --git a/src/components/ContractileCompleteness.res b/src/components/ContractileCompleteness.res deleted file mode 100644 index 04edddb5..00000000 --- a/src/components/ContractileCompleteness.res +++ /dev/null @@ -1,266 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Contractile Completeness Component — Mustfile/Trustfile/Dustfile/K9 coverage. -/// -/// Two-column layout: left sidebar with repo list, right content with -/// detail view showing which contractile files are present or missing. - -open Model -open Msg -open Tea.Html - -/// Render a presence indicator. -let presenceIcon = (present: bool): Tea_Vdom.t => { - let (color, label) = if present { - ("text-green-400", "Y") - } else { - ("text-red-400", "N") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a repo row in the sidebar. -let repoRow = (repo: repoContractileStatus, selected: bool): Tea_Vdom.t => { - let complete = repo.hasMustfile && repo.hasTrustfile && repo.hasDustfile && repo.hasK9 - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-2 border-b border-gray-800 hover:bg-gray-800/60 transition-colors " ++ if ( - selected - ) { - "bg-gray-800/80 border-l-2 border-l-blue-500" - } else { - "" - }, - ), - Events.onClick(ContractileCompleteness(SelectRepo(repo.repoName))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(repo.repoName)}), - span( - list{ - Attrs.class_( - if complete { - "text-xs text-green-400" - } else { - "text-xs text-amber-400" - }, - ), - }, - list{ - text( - if complete { - "Complete" - } else { - "Incomplete" - }, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render a tab button. -let tabBtn = ( - current: contractileCompletenessTab, - target: contractileCompletenessTab, - label: string, -): Tea_Vdom.t => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(ContractileCompleteness(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Contractile Completeness panel. -let view = (state: contractileCompletenessState): Tea_Vdom.t => { - let complete = ContractileCompletenessEngine.fullyCompleteCount(state.repos) - let total = Array.length(state.repos) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Contractile Completeness — Mustfile/Trustfile/Dustfile/K9 Coverage"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-amber-300")}, - list{text("Contractile Completeness")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(complete)}/${Int.toString(total)} complete`)}, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600"), - Events.onClick(ContractileCompleteness(ScanRepos)), - KeyboardNav.onActivate(ContractileCompleteness(ScanRepos)), - }, - list{ - text( - if state.scanning { - "Scanning..." - } else { - "Scan" - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - ContractileCompletenessEngine.allTabs - ->Array.map(t => tabBtn(state.activeTab, t, ContractileCompletenessEngine.tabLabel(t))) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar — repo list - div( - list{Attrs.class_("w-64 border-r border-gray-800 overflow-y-auto")}, - state.repos - ->Array.map(r => repoRow(r, state.selectedRepo == Some(r.repoName))) - ->List.fromArray, - ), - // Right content — detail view - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.selectedRepo { - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a repo to view contractile file details")}, - ) - | Some(name) => - switch state.repos->Array.find(r => r.repoName == name) { - | None => div(list{}, list{text("Repo not found")}) - | Some(repo) => - div( - list{}, - list{ - h3( - list{Attrs.class_("text-md font-semibold text-gray-200 mb-3")}, - list{text(repo.repoName)}, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-24")}, - list{text("Mustfile:")}, - ), - presenceIcon(repo.hasMustfile), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-24")}, - list{text("Trustfile:")}, - ), - presenceIcon(repo.hasTrustfile), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-24")}, - list{text("Dustfile:")}, - ), - presenceIcon(repo.hasDustfile), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-24")}, - list{text("K9:")}, - ), - presenceIcon(repo.hasK9), - if repo.hasK9 { - span( - list{Attrs.class_("text-xs text-gray-500 ml-2")}, - list{text(`(${Int.toString(repo.k9Count)} configs)`)}, - ) - } else { - noNode - }, - }, - ), - }, - ), - }, - ) - } - }, - }, - ), - }, - ), - // Footer - div( - list{Attrs.class_("px-4 py-2 border-t border-gray-800 text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - ContractileCompletenessEngine.incompleteCount(state.repos), - )} repos incomplete`, - ), - }, - ), - }, - ) -} diff --git a/src/components/ContractileManager.affine b/src/components/ContractileManager.affine new file mode 100644 index 00000000..c8cdca0f --- /dev/null +++ b/src/components/ContractileManager.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ContractileManager; + +// TODO: Complete semantic implementation diff --git a/src/components/ContractileManager.res b/src/components/ContractileManager.res deleted file mode 100644 index a692c087..00000000 --- a/src/components/ContractileManager.res +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Contractile Manager Component — cognitive governance dashboard. -/// -/// Displays all 11 built-in contractiles from the Cognitive Governance Stack -/// (DD-007) with colour-coded status badges, elasticity levels, and the -/// current vexation index. Contractiles are the elastic, adaptive state-shapes -/// between the Operator and the Machine. -/// -/// Status colours: green=Satisfied, red=Violated, yellow=Pending, grey=Suspended. - -open Model -open Msg -open Tea.Html - -/// Render a contractile status badge with colour coding. -let statusBadge = (status: contractStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | Satisfied => ("bg-green-700 text-green-100", "Satisfied") - | Violated(_) => ("bg-red-700 text-red-100", "Violated") - | Pending => ("bg-yellow-700 text-yellow-100", "Pending") - | Suspended => ("bg-gray-700 text-gray-300", "Suspended") - } - span( - list{ - Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color), - Attrs.ariaLabel("Status: " ++ label), - }, - list{text(label)}, - ) -} - -/// Render an enforcement level indicator. -let enforcementLabel = (level: enforcementLevel): Tea_Vdom.t => { - let (color, label) = switch level { - | Strict => ("text-red-400", "Strict") - | Adaptive => ("text-amber-400", "Adaptive") - | Warn => ("text-blue-400", "Warn") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render an elasticity bar — visual indicator of how flexible the contractile is. -/// 0.0 = rigid (no give), 1.0 = fully elastic (maximum flexibility). -let elasticityBar = (elasticity: float): Tea_Vdom.t => { - let pct = Float.toFixed(elasticity *. 100.0, ~digits=0) - let barColor = if elasticity == 0.0 { - "bg-red-500" - } else if elasticity < 0.3 { - "bg-amber-500" - } else { - "bg-green-500" - } - div( - list{Attrs.class_("flex items-center gap-2"), Attrs.ariaLabel("Elasticity: " ++ pct ++ "%")}, - list{ - div( - list{Attrs.class_("w-16 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full rounded-full transition-all " ++ barColor), - Attrs.prop("style", "width: " ++ pct ++ "%"), - }, - list{}, - ), - }, - ), - span(list{Attrs.class_("text-xs text-gray-500 w-8")}, list{text(pct ++ "%")}), - }, - ) -} - -/// Render a single contractile row. -let renderContractile = (c: contractile): Tea_Vdom.t => { - let violationDetail = switch c.status { - | Violated(reason) => - div( - list{Attrs.class_("text-xs text-red-300 mt-1 pl-2 border-l-2 border-red-800")}, - list{text(reason)}, - ) - | _ => Tea_Html.noNode - } - - div( - list{ - Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded"), - Attrs.role("listitem"), - Attrs.ariaLabel(c.name ++ " contractile"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - statusBadge(c.status), - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(c.name)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, list{text(c.description)}), - }, - ), - enforcementLabel(c.enforcement), - elasticityBar(c.elasticity), - }, - ), - violationDetail, - }, - ) -} - -/// Render the vexation index indicator — shows current operator friction level. -let vexationIndicator = (vexometer: vexometerState): Tea_Vdom.t => { - let pct = Float.toFixed(vexometer.index *. 100.0, ~digits=0) - let color = if vexometer.index > 0.7 { - "text-red-400" - } else if vexometer.index > 0.4 { - "text-amber-400" - } else { - "text-green-400" - } - div( - list{ - Attrs.class_("flex items-center gap-3 px-4 py-2 bg-gray-900 border border-gray-800 rounded"), - Attrs.role("status"), - Attrs.ariaLabel("Vexation index: " ++ pct ++ "%"), - }, - list{ - span(list{Attrs.class_("text-sm text-gray-400")}, list{text("Vexation Index")}), - span(list{Attrs.class_("text-lg font-bold font-mono " ++ color)}, list{text(pct ++ "%")}), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - "h-full rounded-full transition-all " ++ if vexometer.index > 0.7 { - "bg-red-500" - } else if vexometer.index > 0.4 { - "bg-amber-500" - } else { - "bg-green-500" - }, - ), - Attrs.prop("style", "width: " ++ pct ++ "%"), - }, - list{}, - ), - }, - ), - }, - ) -} - -/// Main view function for the Contractile Manager panel. -let view = (contractiles: array, vexometer: vexometerState): Tea_Vdom.t => { - let totalCount = Array.length(contractiles) - let satisfiedCount = contractiles->Array.filter(c => c.status == Satisfied)->Array.length - let violatedCount = - contractiles - ->Array.filter(c => - switch c.status { - | Violated(_) => true - | _ => false - } - ) - ->Array.length - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Contractile Manager — Cognitive Governance Dashboard"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-purple-300")}, - list{text("Contractile Manager")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(satisfiedCount) ++ - "/" ++ - Int.toString(totalCount) ++ - " satisfied" ++ if violatedCount > 0 { - ", " ++ Int.toString(violatedCount) ++ " violated" - } else { - "" - }, - ), - }, - ), - }, - ), - }, - ), - // Vexation indicator - div(list{Attrs.class_("px-4 pt-3")}, list{vexationIndicator(vexometer)}), - // Contractile list - div( - list{ - Attrs.class_("flex-1 overflow-y-auto px-4 py-3 space-y-2"), - Attrs.role("list"), - Attrs.ariaLabel("Contractile governance stack"), - }, - contractiles->Array.map(c => renderContractile(c))->List.fromArray, - ), - }, - ) -} diff --git a/src/components/Coprocessors.affine b/src/components/Coprocessors.affine new file mode 100644 index 00000000..25487e60 --- /dev/null +++ b/src/components/Coprocessors.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Coprocessors; + +// TODO: Complete semantic implementation diff --git a/src/components/Coprocessors.res b/src/components/Coprocessors.res deleted file mode 100644 index 65a183ef..00000000 --- a/src/components/Coprocessors.res +++ /dev/null @@ -1,1006 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Coprocessors Component — view for monitoring IDApTIK's -/// coprocessor backends. Dashboard, call log, heatmap, and settings. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: coprocessorsCategory, - active: coprocessorsCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(Coprocessors(SetCoprocCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render a single backend metrics card. -let renderMetricsCard = (metrics: coprocMetrics): Tea_Vdom.t => { - let colourCls = CoprocessorsEngine.backendColour(metrics.backend) - let healthCls = CoprocessorsEngine.healthColour(metrics.health) - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_(`text-sm font-medium ${colourCls}`)}, - list{text(CoprocessorsEngine.backendLabel(metrics.backend))}, - ), - span( - list{Attrs.class_(`text-xs ${healthCls}`)}, - list{text(CoprocessorsEngine.healthLabel(metrics.health))}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Total Calls")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(Int.toString(metrics.totalCalls))}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Avg Duration")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(`${Float.toString(metrics.avgDurationMs)}ms`)}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Max Duration")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(`${Float.toString(metrics.maxDurationMs)}ms`)}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Error Rate")}), - div( - list{ - Attrs.class_( - if metrics.errorRate > 0.1 { - "text-red-400 font-mono" - } else { - "text-gray-200 font-mono" - }, - ), - }, - list{text(`${Float.toString(metrics.errorRate *. 100.0)}%`)}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render a discovered compute device card. -let renderDeviceCard = (device: computeDevice): Tea_Vdom.t => { - let engineLabel = CoprocessorsEngine.engineLabel(device.engineId) - let statusCls = if device.available { - "text-emerald-400" - } else { - "text-gray-500" - } - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(device.deviceName)}, - ), - span( - list{Attrs.class_(`text-xs ${statusCls}`)}, - list{ - text( - if device.available { - "Online" - } else { - "Offline" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${engineLabel} / ${device.deviceType}`)}, - ), - }, - ) -} - -/// Render the last compute result. -let renderComputeResult = (result: computeQueryResult): Tea_Vdom.t => { - let engineLabel = CoprocessorsEngine.engineLabel(result.engineId) - let borderCls = if result.success { - "border-emerald-700" - } else { - "border-red-700" - } - div( - list{Attrs.class_(`p-3 bg-gray-800 rounded border ${borderCls}`)}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(`${engineLabel}: ${result.operation}`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 font-mono")}, - list{text(`${Float.toString(result.durationMs)}ms`)}, - ), - }, - ), - div( - list{ - Attrs.class_( - "text-xs text-gray-400 font-mono whitespace-pre-wrap max-h-32 overflow-y-auto", - ), - }, - list{text(result.result)}, - ), - }, - ) -} - -/// Render the Phase 2 FFI status indicator. -let renderFfiStatus = (localDispatch: localDispatchState): Tea_Vdom.t => { - let statusColour = if localDispatch.ffiLoaded { - "text-emerald-400" - } else { - "text-gray-500" - } - let statusText = if localDispatch.ffiLoaded { - "Loaded" - } else { - "Not Loaded" - } - let dotColour = if localDispatch.ffiLoaded { - "bg-emerald-400" - } else { - "bg-gray-600" - } - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700 space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_(`w-2 h-2 rounded-full ${dotColour}`)}, list{}), - span(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text("Zig FFI")}), - }, - ), - span(list{Attrs.class_(`text-xs font-mono ${statusColour}`)}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("CPU Cores")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(Int.toString(localDispatch.cpuCores))}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("CPU Load")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{text(`${Float.toFixed(localDispatch.cpuUtilisation *. 100.0, ~digits=1)}%`)}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("GPU Memory")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{ - text( - if localDispatch.gpuMemoryMb > 0 { - `${Int.toString(localDispatch.gpuMemoryMb)} MB` - } else { - "N/A" - }, - ), - }, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("FFI Backends")}), - div( - list{Attrs.class_("text-gray-200 font-mono")}, - list{ - text( - if Array.length(localDispatch.availableBackends) > 0 { - Int.toString(Array.length(localDispatch.availableBackends)) - } else { - "0" - }, - ), - }, - ), - }, - ), - }, - ), - // Show library path when loaded. - switch localDispatch.ffiLibPath { - | Some(path) => - div(list{Attrs.class_("text-xs text-gray-600 font-mono truncate")}, list{text(path)}) - | None => noNode - }, - }, - ) -} - -/// Render the dashboard view. -let renderDashboard = (state: coprocessorsState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Phase 2: FFI status indicator - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("LOCAL DISPATCH (PHASE 2)")}, - ), - renderFfiStatus(state.localDispatch), - }, - ), - // Control plane: discovered devices - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("COMPUTE ENGINES")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Coprocessors(DiscoverDevices)), - KeyboardNav.onActivate(Coprocessors(DiscoverDevices)), - }, - list{text("Discover")}, - ), - }, - ), - if Array.length(state.discoveredDevices) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-xs py-4")}, - list{ - text("No compute engines discovered. Click Discover to probe Axiom.jl and BoJ."), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-2")}, - state.discoveredDevices->Array.map(d => renderDeviceCard(d))->List.fromArray, - ) - }, - }, - ), - // Last compute result - switch state.lastComputeResult { - | Some(result) => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("LAST COMPUTE RESULT")}, - ), - renderComputeResult(result), - }, - ) - | None => noNode - }, - // Data plane: backend metrics (existing) - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("BACKEND METRICS")}, - ), - if Array.length(state.metrics) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-xs py-4")}, - list{ - text("No backend metrics available"), - button( - list{ - Attrs.class_( - "ml-2 px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Coprocessors(RefreshMetrics)), - KeyboardNav.onActivate(Coprocessors(RefreshMetrics)), - }, - list{text("Refresh")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3")}, - state.metrics->Array.map(m => renderMetricsCard(m))->List.fromArray, - ) - }, - }, - ), - }, - ) -} - -/// Render the call log view. -let renderCallLog = (state: coprocessorsState): Tea_Vdom.t => { - let entries = switch state.selectedBackend { - | Some(backend) => CoprocessorsEngine.filterByBackend(state.callLog, backend) - | None => state.callLog - } - div( - list{Attrs.class_("space-y-3")}, - list{ - // Backend filter chips - div( - list{Attrs.class_("flex items-center gap-1 flex-wrap")}, - list{ - button( - list{ - Attrs.class_( - if state.selectedBackend === None { - "px-2 py-1 text-xs bg-gray-600 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(Coprocessors(SelectBackendFilter(None))), - }, - list{text("All")}, - ), - ...CoprocessorsEngine.allBackends - ->Array.map(backend => { - let isActive = state.selectedBackend === Some(backend) - let colourCls = CoprocessorsEngine.backendColour(backend) - button( - list{ - Attrs.class_( - if isActive { - `px-2 py-1 text-xs bg-gray-600 ${colourCls} rounded` - } else { - "px-2 py-1 text-xs bg-gray-800 text-gray-500 rounded cursor-pointer hover:text-gray-300" - }, - ), - Events.onClick(Coprocessors(SelectBackendFilter(Some(backend)))), - }, - list{text(CoprocessorsEngine.backendShortLabel(backend))}, - ) - }) - ->List.fromArray, - }, - ), - // Log entries - if Array.length(entries) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No call log entries")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - entries - ->Array.map(entry => { - let colourCls = CoprocessorsEngine.backendColour(entry.backend) - div( - list{ - Attrs.class_( - `flex items-center gap-3 p-2 rounded text-xs ${if entry.success { - "bg-gray-800" - } else { - "bg-red-900/20" - }}`, - ), - }, - list{ - span( - list{Attrs.class_(`w-8 font-mono ${colourCls}`)}, - list{text(CoprocessorsEngine.backendShortLabel(entry.backend))}, - ), - span( - list{Attrs.class_("text-gray-200 w-32 truncate")}, - list{text(entry.operation)}, - ), - span( - list{Attrs.class_("text-gray-500 w-24 truncate")}, - list{text(entry.inputSummary)}, - ), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(`${Float.toString(entry.durationMs)}ms`)}, - ), - if entry.success { - span(list{Attrs.class_("text-emerald-400")}, list{text("OK")}) - } else { - span(list{Attrs.class_("text-red-400")}, list{text("ERR")}) - }, - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the heatmap view. -let renderHeatmap = (state: coprocessorsState): Tea_Vdom.t => { - if Array.length(state.heatmap) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No heatmap data — coprocessor call frequency will appear here during gameplay")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - CoprocessorsEngine.allBackends - ->Array.map(backend => { - let cells = state.heatmap->Array.filter(c => c.backend === backend) - let colourCls = CoprocessorsEngine.backendColour(backend) - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_(`w-8 text-xs font-mono ${colourCls}`)}, - list{text(CoprocessorsEngine.backendShortLabel(backend))}, - ), - div( - list{Attrs.class_("flex gap-px flex-1")}, - cells - ->Array.map(cell => { - let intensity = if cell.callCount === 0 { - "bg-gray-800" - } else if cell.callCount < 5 { - "bg-emerald-900" - } else if cell.callCount < 20 { - "bg-emerald-700" - } else if cell.callCount < 50 { - "bg-amber-700" - } else { - "bg-red-700" - } - div( - list{ - Attrs.class_(`w-4 h-6 rounded-sm ${intensity}`), - Attrs.title( - `Slot ${Int.toString(cell.timeSlot)}: ${Int.toString(cell.callCount)} calls`, - ), - }, - list{}, - ) - }) - ->List.fromArray, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Render a single routing decision row in the audit log. -let renderRoutingRow = (decision: routingDecision): Tea_Vdom.t => { - let routeColour = CoprocessorsEngine.routingColour(decision.chosenRoute) - let routeLabel = CoprocessorsEngine.routingLabel(decision.chosenRoute) - let category = SmartRouter.classifyOperation(decision.operation) - let catColour = SmartRouter.categoryColour(category) - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span( - list{Attrs.class_(`w-20 font-mono ${catColour}`)}, - list{text(SmartRouter.categoryLabel(category))}, - ), - span(list{Attrs.class_("text-gray-200 w-36 truncate")}, list{text(decision.operation)}), - span(list{Attrs.class_(`font-medium ${routeColour}`)}, list{text(routeLabel)}), - span( - list{Attrs.class_("text-gray-500 font-mono ml-auto")}, - list{text(`${Float.toFixed(decision.latencyEstimateMs, ~digits=1)}ms`)}, - ), - }, - ) -} - -/// Render the routing strategy selector buttons. -let renderStrategySelector = (activeStrategy: routingStrategy): Tea_Vdom.t => { - let strategies: array = [RouteAutomatic, RouteLocal, RouteRemote, RouteBoj] - div( - list{Attrs.class_("flex items-center gap-1")}, - strategies - ->Array.map(s => { - let isActive = s === activeStrategy - let colour = CoprocessorsEngine.routingColour(s) - let label = CoprocessorsEngine.routingLabel(s) - button( - list{ - Attrs.class_( - if isActive { - `px-3 py-1 text-xs font-medium ${colour} bg-gray-700 rounded border border-gray-600` - } else { - "px-3 py-1 text-xs text-gray-400 bg-gray-800 rounded border border-gray-700 hover:text-gray-200 cursor-pointer" - }, - ), - Events.onClick(Coprocessors(SetRoutingStrategy(s))), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the route distribution bar chart. -let renderRouteDistribution = (stats: array<(string, int, float)>): Tea_Vdom.t => { - let total = stats->Array.reduce(0, (acc, (_, count, _)) => acc + count) - div( - list{Attrs.class_("space-y-2")}, - stats - ->Array.map(((label, count, avgLatency)) => { - let pct = if total > 0 { - Float.fromInt(count) /. Float.fromInt(total) *. 100.0 - } else { - 0.0 - } - let widthPct = if total > 0 { - Float.toFixed(pct, ~digits=0) - } else { - "0" - } - div( - list{Attrs.class_("space-y-0.5")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between text-xs")}, - list{ - span(list{Attrs.class_("text-gray-300")}, list{text(label)}), - span( - list{Attrs.class_("text-gray-500 font-mono")}, - list{ - text( - `${Int.toString(count)} (${Float.toFixed(pct, ~digits=1)}%) avg ${Float.toFixed( - avgLatency, - ~digits=1, - )}ms`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("w-full bg-gray-800 rounded-full h-1.5")}, - list{ - div( - list{ - Attrs.class_("bg-cyan-500 h-1.5 rounded-full"), - Attrs.style("width", `${widthPct}%`), - }, - list{}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) -} - -/// Render the backend health indicators. -let renderBackendHealthRow = (health: SmartRouter.backendHealth): Tea_Vdom.t => { - let dotColour = if health.available { - "bg-emerald-400" - } else { - "bg-gray-600" - } - let statusText = if health.available { - "Online" - } else { - "Offline" - } - let statusColour = if health.available { - "text-emerald-400" - } else { - "text-gray-500" - } - div( - list{Attrs.class_("flex items-center justify-between p-2 bg-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_(`w-2 h-2 rounded-full ${dotColour}`)}, list{}), - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(health.name)}), - }, - ), - div( - list{Attrs.class_("flex items-center gap-4 text-xs")}, - list{ - span( - list{Attrs.class_("text-gray-500 font-mono")}, - list{text(`${Float.toFixed(health.avgLatencyMs, ~digits=1)}ms`)}, - ), - span(list{Attrs.class_(statusColour)}, list{text(statusText)}), - }, - ), - }, - ) -} - -/// Render the Phase 3 routing tab. -let renderRouting = (state: coprocessorsState): Tea_Vdom.t => { - let backends = SmartRouter.buildBackendHealth(state) - let stats = CoprocessorsEngine.currentRouteStats(state) - let catStats = CoprocessorsEngine.currentCategoryStats(state) - let recentDecisions = - state.routingHistory - ->Array.toReversed - ->Array.slice(~start=0, ~end=50) - - div( - list{Attrs.class_("space-y-4")}, - list{ - // Strategy selector - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("ROUTING STRATEGY")}, - ), - renderStrategySelector(state.routingStrategy), - }, - ), - // Backend health - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("BACKEND HEALTH")}, - ), - div( - list{Attrs.class_("space-y-1")}, - backends->Array.map(b => renderBackendHealthRow(b))->List.fromArray, - ), - }, - ), - // Route distribution - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("ROUTE DISTRIBUTION")}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(Array.length(state.routingHistory))} decisions`)}, - ), - }, - ), - if Array.length(state.routingHistory) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-xs py-3")}, - list{text("No routing decisions yet. Use Smart Dispatch to route operations.")}, - ) - } else { - renderRouteDistribution(stats) - }, - }, - ), - // Category distribution - if Array.length(catStats) > 0 { - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("OPERATION CATEGORIES")}, - ), - div( - list{Attrs.class_("flex items-center gap-2 flex-wrap")}, - catStats - ->Array.map(((label, count)) => { - div( - list{Attrs.class_("px-2 py-1 bg-gray-800 rounded text-xs")}, - list{ - span(list{Attrs.class_("text-gray-300")}, list{text(label)}), - span( - list{Attrs.class_("text-gray-500 ml-1 font-mono")}, - list{text(Int.toString(count))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Recent routing decisions (audit trail) - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("RECENT DECISIONS")}, - ), - if Array.length(recentDecisions) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-xs py-4")}, - list{text("No routing decisions recorded")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-72 overflow-y-auto")}, - recentDecisions->Array.map(d => renderRoutingRow(d))->List.fromArray, - ) - }, - }, - ), - }, - ) -} - -/// Render the settings view. -let renderSettings = (state: coprocessorsState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - // Auto-refresh toggle - div( - list{Attrs.class_("flex items-center justify-between p-3 bg-gray-800 rounded")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text("Auto-Refresh")}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Every ${Int.toString(state.refreshIntervalMs)}ms`)}, - ), - }, - ), - button( - list{ - Attrs.class_( - if state.autoRefresh { - "px-3 py-1 text-xs bg-emerald-700 text-white rounded" - } else { - "px-3 py-1 text-xs bg-gray-700 text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Coprocessors(ToggleAutoRefresh)), - KeyboardNav.onActivate(Coprocessors(ToggleAutoRefresh)), - }, - list{ - text( - if state.autoRefresh { - "Enabled" - } else { - "Disabled" - }, - ), - }, - ), - }, - ), - // Backend toggles - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text("Backend Toggles")}), - ...CoprocessorsEngine.allBackends - ->Array.map(backend => { - let isEnabled = state.enabledBackends->Array.includes(backend) - let colourCls = CoprocessorsEngine.backendColour(backend) - div( - list{Attrs.class_("flex items-center justify-between p-2 bg-gray-800/50 rounded")}, - list{ - span( - list{Attrs.class_(`text-sm ${colourCls}`)}, - list{text(CoprocessorsEngine.backendLabel(backend))}, - ), - button( - list{ - Attrs.class_( - if isEnabled { - "px-2 py-0.5 text-xs bg-emerald-700 text-white rounded" - } else { - "px-2 py-0.5 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(Coprocessors(ToggleCoprocBackend(backend))), - }, - list{ - text( - if isEnabled { - "On" - } else { - "Off" - }, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - }, - ), - }, - ) -} - -/// Main view function. -let view = (state: coprocessorsState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Coprocessors panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Coprocessors")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.enabledBackends))} of 10 active`)}, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Coprocessors(RefreshMetrics)), - KeyboardNav.onActivate(Coprocessors(RefreshMetrics)), - }, - list{text("Refresh")}, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Dashboard", CoprocDashboard, state.activeCategory), - renderTab("Call Log", CoprocCallLog, state.activeCategory), - renderTab("Heatmap", CoprocHeatmap, state.activeCategory), - renderTab("Routing", CoprocRouting, state.activeCategory), - renderTab("Settings", CoprocSettings, state.activeCategory), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(Coprocessors(DismissCoprocError)), - KeyboardNav.onActivate(Coprocessors(DismissCoprocError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Loading indicator - if state.loading { - div( - list{Attrs.class_("px-4 py-2 text-xs text-cyan-400 animate-pulse")}, - list{text("Loading coprocessor data...")}, - ) - } else { - noNode - }, - // Main content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | CoprocDashboard => renderDashboard(state) - | CoprocCallLog => renderCallLog(state) - | CoprocHeatmap => renderHeatmap(state) - | CoprocRouting => renderRouting(state) - | CoprocSettings => renderSettings(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/DatabaseBridge.affine b/src/components/DatabaseBridge.affine new file mode 100644 index 00000000..e60fd062 --- /dev/null +++ b/src/components/DatabaseBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DatabaseBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/DatabaseBridge.res b/src/components/DatabaseBridge.res deleted file mode 100644 index 1141616e..00000000 --- a/src/components/DatabaseBridge.res +++ /dev/null @@ -1,443 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Database Bridge Component — VeriSimDB game state persistence. -/// Displays schema tree, query history, game state snapshot viewer, and -/// proof obligation badges for data integrity. - -open Model -open Msg -open Tea.Html - -/// Render a proof obligation status badge. -let obligationBadge = (status: proofObligationStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | ObligationProven => ("bg-green-700 text-green-100", "Proven") - | ObligationUnproven => ("bg-gray-700 text-gray-300", "Unproven") - | ObligationViolated => ("bg-red-700 text-red-100", "Violated") - | ObligationTimeout => ("bg-yellow-700 text-yellow-100", "Timeout") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render a query status indicator. -let queryStatusIndicator = (status: queryStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | QuerySuccess => ("text-green-400", "OK") - | QueryFailed => ("text-red-400", "Fail") - | QueryRunning => ("text-yellow-400 animate-pulse", "Running") - | QueryCancelled => ("text-gray-500", "Cancelled") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a column data type label. -let colTypeLabel = (dt: columnDataType): string => { - switch dt { - | ColInt => "Int" - | ColFloat => "Float" - | ColString => "String" - | ColBool => "Bool" - | ColTimestamp => "Timestamp" - | ColBlob => "Blob" - | ColJson => "JSON" - } -} - -/// Main view function for the Database Bridge panel. -let view = (state: databaseBridgeState): Tea_Vdom.t => { - let provenCount = - state.proofObligations->Array.filter(o => o.status == ObligationProven)->Array.length - let totalObligations = Array.length(state.proofObligations) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Database Bridge — VeriSimDB Game State Persistence"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-teal-300")}, - list{text("Database Bridge")}, - ), - span( - list{ - Attrs.class_( - "text-xs " ++ if state.connected { - "text-green-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if state.connected { - "Connected" - } else { - "Disconnected" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(Int.toString(Array.length(state.schemas)) ++ " schemas")}, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-teal-800 hover:bg-teal-700 text-white rounded"), - Events.onClick(DatabaseBridge(DbBStarted)), - KeyboardNav.onActivate(DatabaseBridge(DbBStarted)), - }, - list{text("Refresh")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Schema { - "bg-teal-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DatabaseBridge(SetDbBTab(Schema))), - }, - list{text("Schema")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Queries { - "bg-teal-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DatabaseBridge(SetDbBTab(Queries))), - }, - list{text("Queries")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == GameState { - "bg-teal-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DatabaseBridge(SetDbBTab(GameState))), - }, - list{text("Game State")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == ProofObligations { - "bg-teal-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DatabaseBridge(SetDbBTab(ProofObligations))), - }, - list{ - text( - "Proofs (" ++ - Int.toString(provenCount) ++ - "/" ++ - Int.toString(totalObligations) ++ ")", - ), - }, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(DatabaseBridge(DismissDbBError)), - KeyboardNav.onActivate(DatabaseBridge(DismissDbBError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Schema => - div( - list{Attrs.class_("space-y-3")}, - state.schemas - ->Array.map(s => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("text-sm font-bold text-teal-300 mb-1")}, - list{text(s.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(s.description)}, - ), - div( - list{Attrs.class_("space-y-1")}, - s.columns - ->Array.map(col => - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{Attrs.class_("font-mono text-gray-300 w-32")}, - list{ - text( - col.name ++ if col.primaryKey { - " (PK)" - } else { - "" - }, - ), - }, - ), - span( - list{Attrs.class_("text-teal-400 w-16")}, - list{text(colTypeLabel(col.dataType))}, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{ - text( - if col.nullable { - "nullable" - } else { - "not null" - }, - ), - }, - ), - switch col.constraint_ { - | Some(c) => span(list{Attrs.class_("text-yellow-400")}, list{text(c)}) - | None => Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ), - if Array.length(s.invariants) > 0 { - div( - list{Attrs.class_("mt-2 pt-2 border-t border-gray-800")}, - s.invariants - ->Array.map(inv => - div( - list{Attrs.class_("text-xs text-yellow-400 font-mono")}, - list{text("INV: " ++ inv)}, - ) - ) - ->List.fromArray, - ) - } else { - Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ) - | Queries => - div( - list{Attrs.class_("space-y-1")}, - state.queries - ->Array.map(q => - div( - list{Attrs.class_("py-2 border-b border-gray-800/50")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - queryStatusIndicator(q.status), - span( - list{Attrs.class_("text-sm font-mono text-gray-300 flex-1 truncate")}, - list{text(q.queryText)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Int.toString(q.rowCount) ++ " rows")}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Float.toFixed(q.durationMs, ~digits=1) ++ "ms")}, - ), - }, - ), - if Array.length(q.optimisationHints) > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - q.optimisationHints - ->Array.map(h => - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-teal-900/50 text-teal-300 rounded", - ), - }, - list{text(h)}, - ) - ) - ->List.fromArray, - ) - } else { - Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ) - | GameState => - switch state.gameStateSnapshot { - | Some(snap) => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text("Snapshot: " ++ snap.snapshotId)}), - span(list{}, list{text("Tables: " ++ Int.toString(snap.tableCount))}), - span(list{}, list{text("Total rows: " ++ Int.toString(snap.totalRows))}), - span( - list{}, - list{text("Size: " ++ Int.toString(snap.sizeBytes / 1024) ++ " KB")}, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - snap.tableSizes - ->Array.map(((tName, rowCount)) => - div( - list{Attrs.class_("flex items-center gap-3 py-1")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-300 w-40")}, - list{text(tName)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Int.toString(rowCount) ++ " rows")}, - ), - // Simple bar - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-teal-600"), - Attrs.style( - "width", - Float.toFixed( - if snap.totalRows > 0 { - Int.toFloat(rowCount) /. - Int.toFloat(snap.totalRows) *. 100.0 - } else { - 0.0 - }, - ~digits=1, - ) ++ "%", - ), - }, - list{}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{ - text( - "No game state snapshot available. Take a snapshot to inspect persisted data.", - ), - }, - ) - } - | ProofObligations => - div( - list{Attrs.class_("space-y-2")}, - state.proofObligations - ->Array.map(o => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - obligationBadge(o.status), - span( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(o.description)}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono mt-1")}, - list{text(o.schemaName ++ ": " ++ o.statement)}, - ), - switch o.counterexample { - | Some(ce) => - div( - list{Attrs.class_("text-xs text-red-400 mt-1")}, - list{text("Counterexample: " ++ ce)}, - ) - | None => Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Databases.affine b/src/components/Databases.affine new file mode 100644 index 00000000..d1482525 --- /dev/null +++ b/src/components/Databases.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Databases; + +// TODO: Complete semantic implementation diff --git a/src/components/Databases.res b/src/components/Databases.res deleted file mode 100644 index 51ddc9c8..00000000 --- a/src/components/Databases.res +++ /dev/null @@ -1,1353 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Databases Component — unified database management panel. -/// -/// Manages VeriSimDB, QuandleDB, and LithoGlyph in a single view with: -/// - Multi-module connection dashboard with health cards -/// - Unified query console with per-module language switching -/// - Schema browser with entity detail drill-down -/// - Cross-modal drift heatmap with normalisation controls -/// - Opt-in telemetry dashboard with aggregate metrics -/// - TypeLL cross-panel type intelligence for query validation -/// -/// 5 tabs: Dashboard, Query, Schema, Drift, Telemetry. -/// All views use Tea_Html (no JSX). Accessible with ARIA roles and labels. - -open Msg -open DatabasesModel -open DatabasesEngine -open DatabaseModule -open Tea.Html - -// =========================================================================== -// TypeLL Cross-Panel Type Intelligence -// =========================================================================== - -/// Render TypeLL cross-panel type intelligence result (if available). -let viewTypeCheckResult = (lastTypeCheck: option): Tea_Vdom.t => { - switch lastTypeCheck { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let narrative = TypeLLEngine.generateNarrative(result) - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - let statusText = if result.valid { - "Type-safe" - } else { - "Type issues detected" - } - div( - list{Attrs.class_("mt-4 p-3 rounded-lg border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL")}, - ), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono mb-1")}, - list{text(result.typeSignature)}, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(narrative.celebrate)}), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -// =========================================================================== -// Shared Components -// =========================================================================== - -/// Render a tab button. -let renderTab = (label: string, active: bool, onClick: msg): Tea_Vdom.t => { - let baseClass = "px-3 py-1.5 text-xs rounded-t border-b-2 transition-colors" - let activeClass = active - ? `${baseClass} text-emerald-300 border-emerald-400 bg-gray-800` - : `${baseClass} text-gray-500 border-transparent hover:text-gray-300` - button( - list{ - Attrs.class_(activeClass), - Events.onClick(onClick), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Render a stat card. -let renderStat = (label: string, value: string, colour: string): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-800/50 border border-gray-700 rounded p-3")}, - list{ - div(list{Attrs.class_(`text-lg font-bold ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text(label)}), - }, - ) -} - -/// Render a module selector pill. -let renderModulePill = ( - config: moduleConfig, - selected: bool, - connStatus: connectionStatus, -): Tea_Vdom.t => { - let accent = moduleAccent(config.id) - let bg = if selected { - "bg-gray-700" - } else { - "bg-transparent hover:bg-gray-800" - } - let border = if selected { - `border` - } else { - "border border-transparent" - } - button( - list{ - Attrs.class_( - `flex items-center gap-2 px-3 py-2 rounded-lg ${bg} ${border} transition-colors`, - ), - Attrs.style( - "border-color", - if selected { - accent - } else { - "transparent" - }, - ), - Events.onClick(Databases(SelectModule(config.id))), - Attrs.ariaLabel(`Select ${config.name} database`), - }, - list{ - // Connection status dot - div( - list{Attrs.class_(`w-2 h-2 rounded-full flex-shrink-0 ${connectionColour(connStatus)}`)}, - list{}, - ), - // Module icon - span( - list{ - Attrs.class_("text-xs font-bold px-1.5 py-0.5 rounded"), - Attrs.style("background-color", accent ++ "20"), - Attrs.style("color", accent), - }, - list{text(moduleIcon(config.id))}, - ), - // Module name - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(config.name)}), - // Version badge - span(list{Attrs.class_("text-[10px] text-gray-600")}, list{text("v" ++ config.version)}), - }, - ) -} - -// =========================================================================== -// Dashboard Tab -// =========================================================================== - -/// Render capability badge. -let renderCapabilityBadge = (cap: capability, supported: bool): Tea_Vdom.t => { - let colour = if supported { - "text-emerald-400 bg-emerald-900/30 border-emerald-700" - } else { - "text-gray-600 bg-gray-900/30 border-gray-800" - } - span( - list{Attrs.class_(`text-[10px] px-2 py-0.5 rounded-full border ${colour}`)}, - list{text(capabilityLabel(cap))}, - ) -} - -/// Render the capability matrix row for a module. -let renderModuleRow = (ms: moduleState): Tea_Vdom.t => { - let allCaps = [ - QueryExecution, - DriftDetection, - ProofGeneration, - Normalisation, - Federation, - Telemetry, - Playground, - ] - let accent = moduleAccent(ms.config.id) - div( - list{Attrs.class_("flex items-center gap-3 py-2 border-b border-gray-800")}, - list{ - // Module badge - div( - list{Attrs.class_("w-24 flex-shrink-0")}, - list{ - span( - list{ - Attrs.class_("text-xs font-bold px-2 py-0.5 rounded"), - Attrs.style("background-color", accent ++ "20"), - Attrs.style("color", accent), - }, - list{text(ms.config.name)}, - ), - }, - ), - // Connection - div( - list{Attrs.class_("w-28 flex-shrink-0 flex items-center gap-1.5")}, - list{ - div( - list{Attrs.class_(`w-2 h-2 rounded-full ${connectionColour(ms.connection)}`)}, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 truncate")}, - list{ - text( - switch ms.connection { - | Disconnected => "Offline" - | Connecting => "Connecting" - | Connected(_) => "Online" - | Error(_) => "Error" - }, - ), - }, - ), - }, - ), - // Capabilities - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - allCaps - ->Array.map(cap => renderCapabilityBadge(cap, hasCapability(ms.config, cap))) - ->List.fromArray, - ), - }, - ) -} - -/// Render the Dashboard tab. -let viewDashboard = (state: databasesState): Tea_Vdom.t => { - let connected = connectedCount(state) - let total = Array.length(state.modules) - let caps = totalCapabilities(state) - let historyCount = Array.length(state.queryHistory) - - div( - list{Attrs.class_("space-y-6")}, - list{ - // Health summary cards - div( - list{Attrs.class_("grid grid-cols-4 gap-4")}, - list{ - renderStat("Modules", Int.toString(total), "text-emerald-400"), - renderStat( - "Connected", - `${Int.toString(connected)}/${Int.toString(total)}`, - if connected == total { - "text-emerald-400" - } else { - "text-amber-400" - }, - ), - renderStat("Capabilities", Int.toString(caps), "text-indigo-400"), - renderStat("Queries Run", Int.toString(historyCount), "text-cyan-400"), - }, - ), - // Module capability matrix - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3")}, - list{text("Module Capability Matrix")}, - ), - div( - list{Attrs.class_("space-y-1")}, - state.modules->Array.map(renderModuleRow)->List.fromArray, - ), - }, - ), - // Action bar - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 bg-emerald-700 hover:bg-emerald-600 text-white text-xs rounded transition-colors", - ), - Events.onClick(Databases(ConnectAll)), - KeyboardNav.onActivate(Databases(ConnectAll)), - Attrs.ariaLabel("Connect to all database modules"), - }, - list{text("Connect All")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-xs rounded transition-colors", - ), - Events.onClick(Databases(RefreshHealth)), - KeyboardNav.onActivate(Databases(RefreshHealth)), - Attrs.ariaLabel("Refresh database health"), - }, - list{text("Refresh Health")}, - ), - // BoJ routing toggle - div( - list{Attrs.class_("flex items-center gap-2 ml-auto")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("BoJ Routing")}), - button( - list{ - Attrs.class_( - if state.bojRouting { - "w-8 h-4 rounded-full bg-emerald-600 relative transition-colors" - } else { - "w-8 h-4 rounded-full bg-gray-700 relative transition-colors" - }, - ), - Events.onClick(Databases(ToggleBojRouting)), - KeyboardNav.onActivate(Databases(ToggleBojRouting)), - Attrs.ariaLabel("Toggle BoJ cartridge routing"), - }, - list{ - div( - list{ - Attrs.class_( - `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${if ( - state.bojRouting - ) { - "translate-x-4" - } else { - "translate-x-0.5" - }}`, - ), - }, - list{}, - ), - }, - ), - }, - ), - }, - ), - // Recent query history - if historyCount > 0 { - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{ - Attrs.class_("text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3"), - }, - list{text("Recent Queries")}, - ), - div( - list{Attrs.class_("space-y-1 max-h-48 overflow-y-auto")}, - state.queryHistory - ->Array.slice(~start=0, ~end=10) - ->Array.map(entry => { - let statusColour = if entry.success { - "text-emerald-400" - } else { - "text-red-400" - } - let moduleBadge = moduleAccent(entry.moduleId) - div( - list{ - Attrs.class_( - "flex items-center gap-2 py-1.5 border-b border-gray-800/50 text-xs", - ), - }, - list{ - span( - list{ - Attrs.class_("px-1.5 py-0.5 rounded font-mono"), - Attrs.style("background-color", moduleBadge ++ "20"), - Attrs.style("color", moduleBadge), - }, - list{text(moduleIcon(entry.moduleId))}, - ), - span( - list{Attrs.class_("text-gray-300 font-mono truncate flex-1")}, - list{text(entry.query)}, - ), - span( - list{Attrs.class_(statusColour)}, - list{ - text( - if entry.success { - "OK" - } else { - "ERR" - }, - ), - }, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text(Float.toString(entry.durationMs) ++ "ms")}, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text(Int.toString(entry.rowCount) ++ " rows")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ) -} - -// =========================================================================== -// Query Console Tab -// =========================================================================== - -/// Render the query console for the selected module. -let viewQuery = (state: databasesState): Tea_Vdom.t => { - let currentModule = selectedModuleState(state) - let playground = currentModule->Option.flatMap(m => m.config.playground) - let langName = playground->Option.map(p => p.languageName)->Option.getOr("SQL") - - div( - list{Attrs.class_("space-y-4")}, - list{ - // Module selector ribbon - div( - list{Attrs.class_("flex items-center gap-2 border-b border-gray-800 pb-3")}, - state.modules - ->Array.map(m => - renderModulePill(m.config, m.config.id == state.selectedModule, m.connection) - ) - ->List.fromArray, - ), - // Query editor - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-semibold text-gray-400 uppercase tracking-wider")}, - list{text(langName ++ " Editor")}, - ), - // Example query buttons - switch playground { - | Some(pg) => - div( - list{Attrs.class_("flex gap-1 ml-auto")}, - pg.exampleQueries - ->Array.slice(~start=0, ~end=4) - ->Array.map(eq => - button( - list{ - Attrs.class_( - "text-[10px] px-2 py-0.5 bg-gray-800 hover:bg-gray-700 text-gray-400 rounded transition-colors", - ), - Events.onClick(Databases(LoadExampleQuery(eq.query))), - Attrs.title(eq.query), - }, - list{ - text(eq.label), - if eq.isDependentType { - span(list{Attrs.class_("text-yellow-500 ml-0.5")}, list{text("DT")}) - } else { - noNode - }, - }, - ) - ) - ->List.fromArray, - ) - | None => noNode - }, - }, - ), - // Textarea - textarea( - list{ - Attrs.class_( - "w-full h-32 bg-gray-950 text-gray-200 text-sm font-mono p-3 rounded border border-gray-700 focus:border-emerald-600 focus:outline-none resize-y", - ), - Attrs.value(state.queryInput), - Attrs.placeholder(`Enter ${langName} query...`), - Events.onInput(value => Databases(SetQueryInput(value))), - Attrs.ariaLabel(`${langName} query input`), - Attrs.spellCheck(false), - }, - list{}, - ), - // Execute bar - div( - list{Attrs.class_("flex items-center gap-3 mt-2")}, - list{ - button( - list{ - Attrs.class_( - if state.queryLoading { - "px-4 py-2 bg-gray-600 text-gray-400 text-xs rounded cursor-not-allowed" - } else { - "px-4 py-2 bg-emerald-700 hover:bg-emerald-600 text-white text-xs rounded transition-colors" - }, - ), - Events.onClick(Databases(ExecuteQuery)), - KeyboardNav.onActivate(Databases(ExecuteQuery)), - Attrs.disabled(state.queryLoading || state.queryInput == ""), - Attrs.ariaLabel("Execute query"), - }, - list{ - text( - if state.queryLoading { - "Executing..." - } else { - "Execute" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-xs rounded transition-colors", - ), - Events.onClick(Databases(ClearQuery)), - KeyboardNav.onActivate(Databases(ClearQuery)), - Attrs.ariaLabel("Clear query"), - }, - list{text("Clear")}, - ), - // Language features - switch playground { - | Some(pg) => - div( - list{Attrs.class_("flex items-center gap-2 ml-auto text-[10px] text-gray-600")}, - list{ - if pg.linterAvailable { - span(list{Attrs.class_("text-emerald-600")}, list{text("Linter")}) - } else { - noNode - }, - if pg.formatterAvailable { - span(list{Attrs.class_("text-emerald-600")}, list{text("Formatter")}) - } else { - noNode - }, - if pg.hasDependentTypes { - span( - list{Attrs.class_("text-yellow-600")}, - list{text(pg.languageName ++ "-DT")}, - ) - } else { - noNode - }, - }, - ) - | None => noNode - }, - }, - ), - }, - ), - // Query result - switch currentModule { - | Some(ms) => - switch ms.queryResult { - | Some(result) => - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-3")}, - list{ - span( - list{Attrs.class_("text-xs font-semibold text-emerald-400")}, - list{text("Result")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(result.rowCount)} rows in ${Float.toString( - result.timingMs, - )}ms`, - ), - }, - ), - span( - list{Attrs.class_("text-[10px] text-gray-600 ml-auto")}, - list{text(result.statementType)}, - ), - }, - ), - // Column headers - if Array.length(result.columns) > 0 { - div( - list{Attrs.class_("overflow-x-auto")}, - list{ - table( - list{Attrs.class_("w-full text-xs"), Attrs.role("grid")}, - list{ - thead( - list{}, - list{ - tr( - list{Attrs.class_("border-b border-gray-700")}, - result.columns - ->Array.map(col => - th( - list{ - Attrs.class_( - "text-left py-1.5 px-2 text-gray-400 font-semibold", - ), - }, - list{text(col)}, - ) - ) - ->List.fromArray, - ), - }, - ), - tbody( - list{}, - result.rows - ->Array.map(row => - tr( - list{ - Attrs.class_("border-b border-gray-800/50 hover:bg-gray-800/30"), - }, - row - ->Array.map(cell => - td( - list{Attrs.class_("py-1.5 px-2 text-gray-300 font-mono")}, - list{text(cell)}, - ) - ) - ->List.fromArray, - ) - ) - ->List.fromArray, - ), - }, - ), - }, - ) - } else { - switch result.message { - | Some(msg) => div(list{Attrs.class_("text-xs text-gray-400")}, list{text(msg)}) - | None => noNode - } - }, - }, - ) - | None => - switch ms.queryError { - | Some(err) => - div( - list{Attrs.class_("bg-red-900/20 border border-red-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs font-semibold text-red-400 mb-1")}, - list{text("Query Error")}, - ), - div(list{Attrs.class_("text-xs text-red-300 font-mono")}, list{text(err)}), - }, - ) - | None => noNode - } - } - | None => noNode - }, - // TypeLL result - viewTypeCheckResult(state.lastTypeCheck), - }, - ) -} - -// =========================================================================== -// Schema Browser Tab -// =========================================================================== - -/// Render entity detail panel. -let viewEntityDetail = (state: databasesState): Tea_Vdom.t => { - switch state.selectedEntity { - | None => - div( - list{Attrs.class_("text-xs text-gray-600 text-center py-8")}, - list{text("Select an entity to view details")}, - ) - | Some(entityName) => - let entity = state.schemaEntities->Array.find(e => e.name == entityName) - switch entity { - | None => noNode - | Some(e) => - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-3")}, - list{ - span(list{Attrs.class_("text-sm font-bold text-gray-200")}, list{text(e.name)}), - span( - list{ - Attrs.class_("text-[10px] px-2 py-0.5 bg-gray-800 text-gray-500 rounded-full"), - }, - list{text(e.kind)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600 ml-auto")}, - list{text(Int.toString(e.entryCount) ++ " entries")}, - ), - }, - ), - // Fields - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Fields")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - e.fields - ->Array.map(field => - span( - list{ - Attrs.class_( - "text-xs font-mono px-2 py-0.5 bg-gray-800 text-gray-300 rounded border border-gray-700", - ), - }, - list{text(field)}, - ) - ) - ->List.fromArray, - ), - }, - ), - // Entity detail JSON (if loaded) - switch state.entityDetail { - | Some(detail) => - div( - list{Attrs.class_("mt-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Detail")}, - ), - pre( - list{ - Attrs.class_( - "text-xs text-gray-300 font-mono bg-gray-950 p-3 rounded overflow-x-auto max-h-48 overflow-y-auto", - ), - }, - list{text(detail)}, - ), - }, - ) - | None => noNode - }, - // Actions - div( - list{Attrs.class_("flex gap-2 mt-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-xs text-white rounded transition-colors", - ), - Events.onClick(Databases(LoadEntityDetail(entityName))), - }, - list{text("Load Detail")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 bg-gray-700 hover:bg-gray-600 text-xs text-white rounded transition-colors", - ), - Events.onClick( - Databases(LoadExampleQuery("SELECT * FROM " ++ entityName ++ " LIMIT 10")), - ), - }, - list{text("Query This")}, - ), - }, - ), - }, - ) - } - } -} - -/// Render the Schema Browser tab. -let viewSchema = (state: databasesState): Tea_Vdom.t => { - let entities = filteredEntities(state) - - div( - list{Attrs.class_("space-y-4")}, - list{ - // Module selector - div( - list{Attrs.class_("flex items-center gap-2 border-b border-gray-800 pb-3")}, - state.modules - ->Array.map(m => - renderModulePill(m.config, m.config.id == state.selectedModule, m.connection) - ) - ->List.fromArray, - ), - // Filter - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 text-gray-200 text-sm px-3 py-2 rounded border border-gray-700 focus:border-emerald-600 focus:outline-none", - ), - Attrs.placeholder("Filter entities..."), - Attrs.value(state.filterText), - Events.onInput(value => Databases(SetFilter(value))), - Attrs.ariaLabel("Filter schema entities"), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(Array.length(entities))} entities`)}, - ), - }, - ), - // Two-column: entity list + detail - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - // Entity list - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - entities - ->Array.map(entity => { - let isSelected = state.selectedEntity == Some(entity.name) - button( - list{ - Attrs.class_( - `flex items-center gap-2 w-full px-3 py-2 rounded text-left transition-colors ${if ( - isSelected - ) { - "bg-gray-700 text-white" - } else { - "hover:bg-gray-800 text-gray-300" - }}`, - ), - Events.onClick(Databases(SelectEntity(entity.name))), - Attrs.ariaLabel(`Select entity ${entity.name}`), - }, - list{ - span( - list{ - Attrs.class_("text-[10px] px-1.5 py-0.5 bg-gray-800 text-gray-500 rounded"), - }, - list{text(entity.kind)}, - ), - span( - list{Attrs.class_("text-sm flex-1 truncate font-mono")}, - list{text(entity.name)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Int.toString(entity.entryCount))}, - ), - span( - list{Attrs.class_("text-xs text-gray-700")}, - list{text(Int.toString(Array.length(entity.fields)) ++ " cols")}, - ), - }, - ) - }) - ->List.fromArray, - ), - // Entity detail panel - viewEntityDetail(state), - }, - ), - }, - ) -} - -// =========================================================================== -// Drift Monitor Tab -// =========================================================================== - -/// Render a single drift bar for a modality. -let renderDriftBar = (dimension: string, score: float): Tea_Vdom.t => { - let pct = Float.toInt(score *. 100.0) - let colour = if score < 0.1 { - "bg-emerald-500" - } else if score < 0.3 { - "bg-amber-500" - } else { - "bg-red-500" - } - let textColour = if score < 0.1 { - "text-emerald-400" - } else if score < 0.3 { - "text-amber-400" - } else { - "text-red-400" - } - - div( - list{Attrs.class_("flex items-center gap-3 py-1")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400 w-24 text-right capitalize")}, - list{text(dimension)}, - ), - div( - list{Attrs.class_("flex-1 h-3 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_(`h-full ${colour} rounded-full transition-all`), - Attrs.style("width", Int.toString(max(pct, 1)) ++ "%"), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_(`text-xs font-mono w-12 text-right ${textColour}`)}, - list{text(Float.toFixed(score, ~digits=3))}, - ), - }, - ) -} - -/// Render the Drift Monitor tab. -let viewDrift = (state: databasesState): Tea_Vdom.t => { - let currentModule = selectedModuleState(state) - let hasDrift = - currentModule->Option.map(m => hasCapability(m.config, DriftDetection))->Option.getOr(false) - - div( - list{Attrs.class_("space-y-4")}, - list{ - // Module selector - div( - list{Attrs.class_("flex items-center gap-2 border-b border-gray-800 pb-3")}, - state.modules - ->Array.map(m => - renderModulePill(m.config, m.config.id == state.selectedModule, m.connection) - ) - ->List.fromArray, - ), - if hasDrift { - switch currentModule { - | Some(ms) => - div( - list{Attrs.class_("space-y-4")}, - list{ - // Drift heatmap - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-4")}, - list{ - span( - list{ - Attrs.class_( - "text-xs font-semibold text-gray-400 uppercase tracking-wider", - ), - }, - list{text("Cross-Modal Drift Heatmap")}, - ), - button( - list{ - Attrs.class_( - "ml-auto px-3 py-1 bg-gray-700 hover:bg-gray-600 text-xs text-white rounded transition-colors", - ), - Events.onClick(Databases(RefreshDrift)), - KeyboardNav.onActivate(Databases(RefreshDrift)), - Attrs.ariaLabel("Refresh drift scores"), - }, - list{text("Refresh")}, - ), - }, - ), - switch ms.driftScores { - | Some(scores) => - div( - list{Attrs.class_("space-y-1")}, - scores - ->Array.map(ds => renderDriftBar(ds.dimension, ds.score)) - ->List.fromArray, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-600 text-center py-8")}, - list{text("Connect to backend to load drift scores")}, - ) - }, - }, - ), - // Proof obligations - if Array.length(ms.proofObligations) > 0 { - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{ - Attrs.class_( - "text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3", - ), - }, - list{text("Proof Obligations")}, - ), - div( - list{Attrs.class_("space-y-2")}, - ms.proofObligations - ->Array.map(po => { - let statusColour = switch po.status { - | "verified" => "text-emerald-400" - | "failed" => "text-red-400" - | _ => "text-amber-400" - } - div( - list{ - Attrs.class_( - "flex items-center gap-3 text-xs py-1 border-b border-gray-800/50", - ), - }, - list{ - span( - list{Attrs.class_(`font-semibold ${statusColour} w-16`)}, - list{text(po.status)}, - ), - span(list{Attrs.class_("text-gray-300")}, list{text(po.contractName)}), - span( - list{Attrs.class_("text-gray-600 ml-auto")}, - list{text(po.proofType)}, - ), - span( - list{Attrs.class_("text-gray-700 font-mono text-[10px]")}, - list{text(po.proofHash)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Normalise button - button( - list{ - Attrs.class_( - "px-4 py-2 bg-indigo-700 hover:bg-indigo-600 text-white text-xs rounded transition-colors", - ), - Events.onClick(Databases(NormaliseAll)), - KeyboardNav.onActivate(Databases(NormaliseAll)), - Attrs.ariaLabel("Normalise all drifted modalities"), - }, - list{text("Normalise Drifted Modalities")}, - ), - }, - ) - | None => noNode - } - } else { - div( - list{Attrs.class_("text-xs text-gray-600 text-center py-12")}, - list{text("Selected module does not support drift detection. Switch to VeriSimDB.")}, - ) - }, - }, - ) -} - -// =========================================================================== -// Telemetry Tab -// =========================================================================== - -/// Render the Telemetry tab. -let viewTelemetry = (state: databasesState): Tea_Vdom.t => { - let currentModule = selectedModuleState(state) - let hasTelemetry = - currentModule->Option.map(m => hasCapability(m.config, Telemetry))->Option.getOr(false) - - div( - list{Attrs.class_("space-y-4")}, - list{ - // Module selector - div( - list{Attrs.class_("flex items-center gap-2 border-b border-gray-800 pb-3")}, - state.modules - ->Array.map(m => - renderModulePill(m.config, m.config.id == state.selectedModule, m.connection) - ) - ->List.fromArray, - ), - if hasTelemetry { - switch currentModule { - | Some(ms) => - switch ms.telemetry { - | Some(t) => - div( - list{Attrs.class_("space-y-4")}, - list{ - // Summary cards - div( - list{Attrs.class_("grid grid-cols-4 gap-4")}, - list{ - renderStat("Entities", Int.toString(t.entityCount), "text-emerald-400"), - renderStat( - "Avg Query", - Float.toFixed(t.avgQueryDurationMs, ~digits=1) ++ "ms", - "text-cyan-400", - ), - renderStat( - "Drift Events", - Int.toString(t.driftDetectedCount), - "text-amber-400", - ), - renderStat( - "Normalise Rate", - Float.toFixed(t.normaliseSuccessRate *. 100.0, ~digits=0) ++ "%", - "text-indigo-400", - ), - }, - ), - // Modality heatmap - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{ - Attrs.class_( - "text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3", - ), - }, - list{text("Modality Usage Heatmap")}, - ), - div( - list{Attrs.class_("flex gap-2 flex-wrap")}, - t.modalityHeatmap - ->Array.map(((name, intensity)) => { - let opacity = Float.toFixed(Math.min(intensity, 1.0), ~digits=2) - div( - list{ - Attrs.class_("px-3 py-2 rounded text-xs text-white font-mono"), - Attrs.style("background-color", `rgba(52, 211, 153, ${opacity})`), - }, - list{ - div(list{}, list{text(name)}), - div( - list{Attrs.class_("text-[10px] opacity-80")}, - list{text(Float.toFixed(intensity *. 100.0, ~digits=0) ++ "%")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Query patterns - if Array.length(t.queryPatterns) > 0 { - div( - list{Attrs.class_("bg-gray-900/50 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{ - Attrs.class_( - "text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3", - ), - }, - list{text("Common Query Patterns")}, - ), - div( - list{Attrs.class_("space-y-1")}, - t.queryPatterns - ->Array.map(((pattern, count)) => - div( - list{Attrs.class_("flex items-center gap-2 text-xs py-1")}, - list{ - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(pattern)}), - span( - list{Attrs.class_("text-gray-600 font-mono")}, - list{text(Int.toString(count))}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Privacy notice - div( - list{Attrs.class_("text-[10px] text-gray-700 text-center")}, - list{ - text( - "Telemetry is opt-in and aggregate-only. No query content, entity data, or PII.", - ), - }, - ), - // Timestamp - div( - list{Attrs.class_("text-[10px] text-gray-700 text-center")}, - list{text("Generated: " ++ t.generatedAt)}, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center py-12")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-3")}, - list{text("No telemetry snapshot loaded")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white text-xs rounded transition-colors", - ), - Events.onClick(Databases(LoadTelemetry)), - KeyboardNav.onActivate(Databases(LoadTelemetry)), - Attrs.ariaLabel("Load telemetry snapshot"), - }, - list{text("Load Telemetry")}, - ), - }, - ) - } - | None => noNode - } - } else { - div( - list{Attrs.class_("text-xs text-gray-600 text-center py-12")}, - list{text("Selected module does not expose telemetry. Switch to VeriSimDB.")}, - ) - }, - }, - ) -} - -// =========================================================================== -// Main View -// =========================================================================== - -/// Root view for the Databases panel. -let view = (state: databasesState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 p-4 overflow-y-auto"), - Attrs.role("region"), - Attrs.ariaLabel("Databases panel — VeriSimDB, QuandleDB, LithoGlyph management"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center gap-3 mb-4")}, - list{ - h2(list{Attrs.class_("text-lg font-bold text-emerald-400")}, list{text("Databases")}), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(Array.length(state.modules))} modules registered`)}, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{Attrs.class_("ml-auto flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-xs text-red-400")}, list{text(err)}), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-300"), - Events.onClick(Databases(DismissError)), - KeyboardNav.onActivate(Databases(DismissError)), - }, - list{text("dismiss")}, - ), - }, - ) - | None => noNode - }, - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 mb-4 border-b border-gray-800"), Attrs.role("tablist")}, - list{ - renderTab( - "Dashboard", - state.activeCategory == DbDashboard, - Databases(SetCategory(DbDashboard)), - ), - renderTab("Query", state.activeCategory == DbQuery, Databases(SetCategory(DbQuery))), - renderTab("Schema", state.activeCategory == DbSchema, Databases(SetCategory(DbSchema))), - renderTab("Drift", state.activeCategory == DbDrift, Databases(SetCategory(DbDrift))), - renderTab( - "Telemetry", - state.activeCategory == DbTelemetry, - Databases(SetCategory(DbTelemetry)), - ), - }, - ), - // Active tab content - switch state.activeCategory { - | DbDashboard => viewDashboard(state) - | DbQuery => viewQuery(state) - | DbSchema => viewSchema(state) - | DbDrift => viewDrift(state) - | DbTelemetry => viewTelemetry(state) - }, - }, - ) -} diff --git a/src/components/DebuggingWorkbench.affine b/src/components/DebuggingWorkbench.affine new file mode 100644 index 00000000..5e8a97b7 --- /dev/null +++ b/src/components/DebuggingWorkbench.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DebuggingWorkbench; + +// TODO: Complete semantic implementation diff --git a/src/components/DebuggingWorkbench.res b/src/components/DebuggingWorkbench.res deleted file mode 100644 index 58b68dc0..00000000 --- a/src/components/DebuggingWorkbench.res +++ /dev/null @@ -1,374 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL DebuggingWorkbench — time-travel debugging, state inspection, watch -/// expressions, and console output for the TEA model. -/// Inspector clade panel for deep debugging workflows. -/// -/// Four tabs: Time Travel (snapshot slider), State Inspector (model tree), -/// Watch Expressions (live evaluation), and Console (log output). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab bar rendering. -let renderTabs = (active: debuggingWorkbenchTab): Tea_Vdom.t => { - let tabs = DebuggingWorkbenchEngine.allTabs - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(DebuggingWorkbench(SetDwTab(tab))), - }, - list{text(DebuggingWorkbenchEngine.tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Time Travel tab: snapshot slider with step controls and snapshot list. -let renderTimeTravelTab = (state: debuggingWorkbenchState): Tea_Vdom.t => { - let tt = state.timeTravel - let count = DebuggingWorkbenchEngine.snapshotCount(tt) - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // Controls - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium ${if ( - DebuggingWorkbenchEngine.canGoBack(tt) - ) { - "bg-cyan-700 text-white hover:bg-cyan-600 cursor-pointer" - } else { - "bg-gray-700 text-gray-500 cursor-not-allowed" - }}`, - ), - Events.onClick(DebuggingWorkbench(DwStepBack)), - KeyboardNav.onActivate(DebuggingWorkbench(DwStepBack)), - Attrs.disabled(!DebuggingWorkbenchEngine.canGoBack(tt)), - }, - list{text("Step Back")}, - ), - div( - list{Attrs.class_("text-sm text-gray-400 flex-1 text-center")}, - list{ - text( - if count > 0 { - `Snapshot ${Int.toString(tt.currentIndex + 1)} of ${Int.toString(count)}` - } else { - "No snapshots captured" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium ${if ( - DebuggingWorkbenchEngine.canGoForward(tt) - ) { - "bg-cyan-700 text-white hover:bg-cyan-600 cursor-pointer" - } else { - "bg-gray-700 text-gray-500 cursor-not-allowed" - }}`, - ), - Events.onClick(DebuggingWorkbench(DwStepForward)), - KeyboardNav.onActivate(DebuggingWorkbench(DwStepForward)), - Attrs.disabled(!DebuggingWorkbenchEngine.canGoForward(tt)), - }, - list{text("Step Forward")}, - ), - }, - ), - // Time-travelling indicator - if tt.isTimeTravelling { - div( - list{ - Attrs.class_( - "flex items-center gap-2 bg-amber-900/30 border border-amber-700 rounded p-2", - ), - }, - list{ - div(list{Attrs.class_("w-2.5 h-2.5 bg-amber-400 rounded-full animate-pulse")}, list{}), - span( - list{Attrs.class_("text-xs text-amber-300")}, - list{text("Time-travelling — state is read-only")}, - ), - }, - ) - } else { - noNode - }, - // Snapshot list - if count > 0 { - div( - list{Attrs.class_("flex flex-col gap-1 max-h-64 overflow-y-auto")}, - tt.snapshots - ->Array.mapWithIndex((snap, idx) => { - let isCurrent = idx === tt.currentIndex - let bgCls = isCurrent ? "bg-cyan-900/30 border-cyan-700" : "bg-gray-800 border-gray-700" - div( - list{ - Attrs.class_( - `flex items-center gap-2 p-2 rounded border cursor-pointer hover:bg-gray-750 ${bgCls}`, - ), - Events.onClick(DebuggingWorkbench(DwGoToSnapshot(idx))), - }, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-8 text-right")}, - list{text(`#${Int.toString(idx + 1)}`)}, - ), - span(list{Attrs.class_("text-sm text-gray-300 flex-1")}, list{text(snap.label)}), - span( - list{Attrs.class_("text-xs text-gray-600 font-mono")}, - list{text(Float.toFixed(snap.timestamp, ~digits=1))}, - ), - }, - ) - }) - ->List.fromArray, - ) - } else { - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-32 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Click \"Capture Snapshot\" to begin time-travel debugging.")}, - ), - }, - ) - }, - // Capture button - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium self-end", - ), - Events.onClick(DebuggingWorkbench(DwCaptureSnapshot)), - KeyboardNav.onActivate(DebuggingWorkbench(DwCaptureSnapshot)), - }, - list{text("Capture Snapshot")}, - ), - }, - ) -} - -/// State Inspector tab: JSON tree view of the current model state. -let renderStateInspectorTab = (state: debuggingWorkbenchState): Tea_Vdom.t => { - switch state.selectedSnapshot { - | Some(snapshotId) => { - let snap = state.timeTravel.snapshots->Array.find(s => s.id === snapshotId) - switch snap { - | Some(s) => - div( - list{Attrs.class_("p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text(`Inspecting: ${s.label}`)}, - ), - pre( - list{ - Attrs.class_( - "bg-gray-800 rounded p-4 text-xs font-mono text-gray-300 max-h-96 overflow-auto border border-gray-700", - ), - }, - list{text(s.modelJson)}, - ), - }, - ) - | None => - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("Snapshot not found.")}, - ) - } - } - | None => - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("Select a snapshot to inspect its state tree.")}, - ) - } -} - -/// Watch Expressions tab: live expression evaluation display. -let renderWatchExpressionsTab = (state: debuggingWorkbenchState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - if Array.length(state.watches) === 0 { - div( - list{Attrs.class_("text-gray-500 text-sm italic")}, - list{text("No watch expressions. Add one below.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 max-h-64 overflow-y-auto")}, - state.watches - ->Array.map(watch => { - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-xs font-mono text-cyan-400")}, - list{text(watch.expression)}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-600 hover:text-red-400 cursor-pointer"), - Events.onClick(DebuggingWorkbench(DwRemoveWatch(watch.id))), - }, - list{text("x")}, - ), - }, - ), - div( - list{Attrs.class_("text-sm font-mono text-gray-300")}, - list{text(watch.currentValue)}, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - // Add watch button - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer self-start", - ), - Events.onClick(DebuggingWorkbench(DwAddWatch)), - KeyboardNav.onActivate(DebuggingWorkbench(DwAddWatch)), - }, - list{text("+ Add Watch")}, - ), - }, - ) -} - -/// Console tab: scrollable log output. -let renderConsoleTab = (state: debuggingWorkbenchState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - if Array.length(state.consoleLog) === 0 { - div( - list{Attrs.class_("text-gray-500 text-sm italic")}, - list{text("Console output is empty.")}, - ) - } else { - div( - list{ - Attrs.class_( - "bg-gray-800 rounded p-4 font-mono text-xs text-gray-300 max-h-96 overflow-auto border border-gray-700", - ), - }, - state.consoleLog - ->Array.map(line => { - div(list{Attrs.class_("py-0.5")}, list{text(line)}) - }) - ->List.fromArray, - ) - }, - // Clear console button - if Array.length(state.consoleLog) > 0 { - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer self-end", - ), - Events.onClick(DebuggingWorkbench(DwClearConsole)), - KeyboardNav.onActivate(DebuggingWorkbench(DwClearConsole)), - }, - list{text("Clear Console")}, - ) - } else { - noNode - }, - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function for the Debugging Workbench panel. -let view = (state: debuggingWorkbenchState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabTimeTravel => renderTimeTravelTab(state) - | TabStateInspector => renderStateInspectorTab(state) - | TabWatchExpressions => renderWatchExpressionsTab(state) - | TabConsole => renderConsoleTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Debugging Workbench")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - DebuggingWorkbenchEngine.snapshotCount(state.timeTravel), - )} snapshots`, - ), - }, - ), - }, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/DeviceNetworkDesigner.affine b/src/components/DeviceNetworkDesigner.affine new file mode 100644 index 00000000..bfc81c27 --- /dev/null +++ b/src/components/DeviceNetworkDesigner.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DeviceNetworkDesigner; + +// TODO: Complete semantic implementation diff --git a/src/components/DeviceNetworkDesigner.res b/src/components/DeviceNetworkDesigner.res deleted file mode 100644 index e8794ab1..00000000 --- a/src/components/DeviceNetworkDesigner.res +++ /dev/null @@ -1,444 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Device Network Designer Component — wire devices, configure security -/// levels, and validate network topology. Displays device palette, canvas -/// placeholder, wiring mode toggle, and validation results panel. - -open Model -open Msg -open Tea.Html - -/// Main view function for the Device Network Designer panel. -let view = (state: deviceNetworkDesignerState): Tea_Vdom.t => { - let deviceCount = Array.length(state.devices) - let connectionCount = Array.length(state.connections) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Device Network Designer — Network Topology Editor"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-blue-300")}, - list{text("Device Network Designer")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(deviceCount) ++ - " devices, " ++ - Int.toString(connectionCount) ++ " connections", - ), - }, - ), - span( - list{ - Attrs.class_( - "text-xs px-2 py-0.5 rounded " ++ if state.wiringMode { - "bg-blue-700 text-blue-100" - } else { - "bg-gray-800 text-gray-400" - }, - ), - }, - list{ - text( - if state.wiringMode { - "Wiring ON" - } else { - "Wiring OFF" - }, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-blue-800 hover:bg-blue-700 text-white rounded"), - Events.onClick(DeviceNetworkDesigner(DndStarted)), - KeyboardNav.onActivate(DeviceNetworkDesigner(DndStarted)), - }, - list{text("Validate")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Designer { - "bg-blue-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DeviceNetworkDesigner(SetDndCategory(Designer))), - }, - list{text("Designer")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Devices { - "bg-blue-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DeviceNetworkDesigner(SetDndCategory(Devices))), - }, - list{text("Devices")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Wiring { - "bg-blue-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DeviceNetworkDesigner(SetDndCategory(Wiring))), - }, - list{text("Wiring")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Validation { - "bg-blue-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(DeviceNetworkDesigner(SetDndCategory(Validation))), - }, - list{text("Validation")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(DeviceNetworkDesigner(DismissDndError)), - KeyboardNav.onActivate(DeviceNetworkDesigner(DismissDndError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Designer => - div( - list{Attrs.class_("space-y-3")}, - list{ - // Device palette - div( - list{Attrs.class_("flex gap-2 flex-wrap mb-3")}, - ["Router", "Server", "Camera", "Firewall", "Switch", "Sensor"] - ->Array.map(dt => - span( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-800 border border-gray-700 text-gray-300 rounded cursor-pointer hover:border-blue-600", - ), - }, - list{text(dt)}, - ) - ) - ->List.fromArray, - ), - // Canvas placeholder - div( - list{ - Attrs.class_( - "w-full h-48 bg-gray-900 border border-gray-800 rounded flex items-center justify-center", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{ - text( - "Network graph canvas — " ++ - Int.toString(deviceCount) ++ - " nodes, " ++ - Int.toString(connectionCount) ++ " edges", - ), - }, - ), - }, - ), - }, - ) - | Devices => - div( - list{Attrs.class_("space-y-2")}, - state.devices - ->Array.map(d => { - let isSelected = state.selectedDevice == Some(d.id) - div( - list{ - Attrs.class_( - "px-3 py-2 border rounded " ++ if isSelected { - "bg-blue-900/20 border-blue-700" - } else { - "bg-gray-900 border-gray-800" - }, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-blue-300")}, - list{text(d.deviceType)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(d.id)}, - ), - }, - ), - span( - list{ - Attrs.class_("px-2 py-0.5 text-xs bg-gray-800 text-gray-300 rounded"), - }, - list{text(d.zone ++ " (L" ++ Int.toString(d.securityLevel) ++ ")")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1 font-mono")}, - list{ - text( - "(" ++ - Float.toFixed(d.x, ~digits=0) ++ - ", " ++ - Float.toFixed(d.y, ~digits=0) ++ ")", - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - | Wiring => - div( - list{Attrs.class_("space-y-1")}, - list{ - // Table header - div( - list{ - Attrs.class_( - "flex gap-2 text-xs text-gray-500 font-mono border-b border-gray-800 pb-1 mb-2", - ), - }, - list{ - span(list{Attrs.class_("w-20")}, list{text("From")}), - span(list{Attrs.class_("w-20")}, list{text("To")}), - span(list{Attrs.class_("w-16")}, list{text("Protocol")}), - span(list{Attrs.class_("w-16")}, list{text("BW")}), - span(list{Attrs.class_("w-12")}, list{text("Enc")}), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - state.connections - ->Array.map(c => { - let isSelected = state.selectedConnection == Some(c.id) - div( - list{ - Attrs.class_( - "flex gap-2 text-xs py-1 border-b border-gray-800/30 " ++ if isSelected { - "bg-blue-900/20" - } else { - "" - }, - ), - }, - list{ - span( - list{Attrs.class_("w-20 font-mono text-gray-400 truncate")}, - list{text(c.fromDevice)}, - ), - span( - list{Attrs.class_("w-20 font-mono text-gray-400 truncate")}, - list{text(c.toDevice)}, - ), - span(list{Attrs.class_("w-16 text-blue-400")}, list{text(c.protocol)}), - span(list{Attrs.class_("w-16 text-gray-500")}, list{text(c.bandwidth)}), - span( - list{ - Attrs.class_( - "w-12 " ++ if c.encrypted { - "text-green-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if c.encrypted { - "Yes" - } else { - "No" - }, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - | Validation => - switch state.validation { - | Some(v) => - div( - list{Attrs.class_("space-y-3")}, - list{ - // Validation summary - div( - list{Attrs.class_("flex items-center gap-3 mb-3")}, - list{ - span( - list{ - Attrs.class_( - "px-3 py-1 text-sm rounded font-bold " ++ if v.valid { - "bg-green-700 text-green-100" - } else { - "bg-red-700 text-red-100" - }, - ), - }, - list{ - text( - if v.valid { - "VALID" - } else { - "INVALID" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(v.deviceCount) ++ - " devices, " ++ - Int.toString(v.connectionCount) ++ " connections", - ), - }, - ), - }, - ), - // Errors - if Array.length(v.errors) > 0 { - div( - list{Attrs.class_("space-y-1")}, - list{ - h3(list{Attrs.class_("text-sm text-red-400 mb-1")}, list{text("Errors")}), - div( - list{}, - v.errors - ->Array.map(e => - div( - list{ - Attrs.class_( - "px-2 py-1 text-xs text-red-200 bg-red-900/30 rounded mb-1", - ), - }, - list{text(e)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - Tea_Html.noNode - }, - // Warnings - if Array.length(v.warnings) > 0 { - div( - list{Attrs.class_("space-y-1")}, - list{ - h3( - list{Attrs.class_("text-sm text-yellow-400 mb-1")}, - list{text("Warnings")}, - ), - div( - list{}, - v.warnings - ->Array.map(w => - div( - list{ - Attrs.class_( - "px-2 py-1 text-xs text-yellow-200 bg-yellow-900/30 rounded mb-1", - ), - }, - list{text(w)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - Tea_Html.noNode - }, - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Run validation to check the network topology for errors.")}, - ) - } - }, - }, - ), - }, - ) -} diff --git a/src/components/DlcWorkshop.affine b/src/components/DlcWorkshop.affine new file mode 100644 index 00000000..210fc820 --- /dev/null +++ b/src/components/DlcWorkshop.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DlcWorkshop; + +// TODO: Complete semantic implementation diff --git a/src/components/DlcWorkshop.res b/src/components/DlcWorkshop.res deleted file mode 100644 index 60fd0d96..00000000 --- a/src/components/DlcWorkshop.res +++ /dev/null @@ -1,588 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL DLC Workshop Component — view for creating, testing, and -/// packaging IDApTIK DLC puzzle packs. Puzzle browser, VM instruction -/// composer, solution test runner, asset browser, and packaging. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = (label: string, cat: dlcWorkshopCategory, active: dlcWorkshopCategory): Tea_Vdom.t< - msg, -> => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(DlcWorkshop(SetWorkshopCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render a puzzle card. -let renderPuzzleCard = (puzzle: dlcPuzzle, isSelected: bool): Tea_Vdom.t => { - let borderCls = if isSelected { - "border-cyan-400" - } else { - "border-gray-700" - } - let diffCls = DlcWorkshopEngine.difficultyColour(puzzle.difficulty) - let testCls = DlcWorkshopEngine.testStatusColour(puzzle.testStatus) - div( - list{ - Attrs.class_( - `p-3 bg-gray-800 rounded border ${borderCls} cursor-pointer hover:border-gray-500`, - ), - Events.onClick(DlcWorkshop(SelectPuzzle(puzzle.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-100")}, list{text(puzzle.name)}), - span( - list{Attrs.class_(`text-xs ${diffCls}`)}, - list{text(DlcWorkshopEngine.difficultyLabel(puzzle.difficulty))}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400 mb-2 line-clamp-2")}, - list{text(puzzle.description)}, - ), - div( - list{Attrs.class_("flex items-center gap-3 text-xs")}, - list{ - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(Array.length(puzzle.instructions))} instrs`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(puzzle.optimalSteps)} optimal`)}, - ), - span( - list{Attrs.class_(testCls)}, - list{text(DlcWorkshopEngine.testStatusLabel(puzzle.testStatus))}, - ), - }, - ), - }, - ) -} - -/// Render puzzles list view. -let renderPuzzles = (state: dlcWorkshopState): Tea_Vdom.t => { - let filtered = DlcWorkshopEngine.filterPuzzles( - state.puzzles, - state.filterText, - state.filterDifficulty, - ) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter bar - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Filter puzzles..."), - Attrs.value(state.filterText), - Events.onInput(text => DlcWorkshop(SetDlcFilter(text))), - }, - list{}, - ), - // Difficulty filter chips - button( - list{ - Attrs.class_( - if state.filterDifficulty === None { - "px-2 py-1 text-xs bg-gray-600 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(DlcWorkshop(SetDifficultyFilter(None))), - }, - list{text("All")}, - ), - ...DlcWorkshopEngine.allDifficulties - ->Array.map(diff => { - let isActive = state.filterDifficulty === Some(diff) - let cls = if isActive { - `px-2 py-1 text-xs bg-gray-600 ${DlcWorkshopEngine.difficultyColour(diff)} rounded` - } else { - "px-2 py-1 text-xs bg-gray-800 text-gray-500 rounded cursor-pointer hover:text-gray-300" - } - button( - list{Attrs.class_(cls), Events.onClick(DlcWorkshop(SetDifficultyFilter(Some(diff))))}, - list{text(DlcWorkshopEngine.difficultyLabel(diff))}, - ) - }) - ->List.fromArray, - }, - ), - // Stats - div( - list{Attrs.class_("flex items-center gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text(`${Int.toString(Array.length(filtered))} puzzles`)}), - span( - list{}, - list{ - text(`${Int.toString(DlcWorkshopEngine.passedTests(state.puzzles))} tests passed`), - }, - ), - }, - ), - // Puzzle cards - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No puzzles found. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(DlcWorkshop(LoadPuzzles)), - KeyboardNav.onActivate(DlcWorkshop(LoadPuzzles)), - }, - list{text("Load puzzles")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-3")}, - filtered - ->Array.map(p => renderPuzzleCard(p, state.selectedPuzzleId === Some(p.id))) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the VM instruction composer. -let renderComposer = (state: dlcWorkshopState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text("VM Instruction Composer")}), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(AddInstruction)), - KeyboardNav.onActivate(DlcWorkshop(AddInstruction)), - }, - list{text("Add Instruction")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(ClearComposer)), - KeyboardNav.onActivate(DlcWorkshop(ClearComposer)), - }, - list{text("Clear")}, - ), - }, - ), - }, - ), - // Instructions list - if Array.length(state.composerInstructions) === 0 { - div( - list{ - Attrs.class_( - "text-center text-gray-500 text-sm py-8 border border-dashed border-gray-700 rounded", - ), - }, - list{text("No instructions — click 'Add Instruction' to start composing a puzzle")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 font-mono text-xs")}, - state.composerInstructions - ->Array.map(instr => - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded")}, - list{ - span( - list{Attrs.class_("text-gray-500 w-6")}, - list{text(Int.toString(instr.index))}, - ), - span(list{Attrs.class_("text-cyan-400 w-16")}, list{text(instr.opcode)}), - switch instr.operand { - | Some(op) => - span(list{Attrs.class_("text-amber-400 w-8")}, list{text(Int.toString(op))}) - | None => span(list{Attrs.class_("text-gray-600 w-8")}, list{text("-")}) - }, - if instr.comment !== "" { - span(list{Attrs.class_("text-gray-500 italic")}, list{text(`; ${instr.comment}`)}) - } else { - noNode - }, - button( - list{ - Attrs.class_("ml-auto text-red-400 hover:text-red-300 cursor-pointer"), - Events.onClick(DlcWorkshop(RemoveInstruction(instr.index))), - }, - list{text("x")}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render testing view. -let renderTesting = (state: dlcWorkshopState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(RunAllTests)), - KeyboardNav.onActivate(DlcWorkshop(RunAllTests)), - }, - list{text("Run All Tests")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${Int.toString(DlcWorkshopEngine.passedTests(state.puzzles))}/${Int.toString( - Array.length(state.puzzles), - )} passed`, - ), - }, - ), - }, - ), - // Test results per puzzle - div( - list{Attrs.class_("space-y-1")}, - state.puzzles - ->Array.map(puzzle => { - let testCls = DlcWorkshopEngine.testStatusColour(puzzle.testStatus) - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span(list{Attrs.class_("text-gray-200 w-40 truncate")}, list{text(puzzle.name)}), - span( - list{Attrs.class_(testCls)}, - list{text(DlcWorkshopEngine.testStatusLabel(puzzle.testStatus))}, - ), - button( - list{ - Attrs.class_( - "ml-auto px-2 py-0.5 bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(RunPuzzleTest(puzzle.id))), - }, - list{text("Run")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Render assets view. -let renderAssets = (state: dlcWorkshopState): Tea_Vdom.t => { - if Array.length(state.assets) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No assets loaded. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(DlcWorkshop(BrowseDlcAssets)), - KeyboardNav.onActivate(DlcWorkshop(BrowseDlcAssets)), - }, - list{text("Browse assets")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-3 lg:grid-cols-4 gap-2")}, - state.assets - ->Array.map(asset => - div( - list{Attrs.class_("p-2 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-100 font-medium truncate")}, - list{text(asset.name)}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(asset.assetType)}), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(asset.sizeBytes / 1024)}KB`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render packaging view. -let renderPackaging = (state: dlcWorkshopState): Tea_Vdom.t => { - let meta = state.packMeta - div( - list{Attrs.class_("space-y-4")}, - list{ - // Pack metadata - div( - list{Attrs.class_("p-4 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-200 mb-3")}, - list{text("Pack Metadata")}, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3 text-xs")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Name")}), - div(list{Attrs.class_("text-gray-200")}, list{text(meta.name)}), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Version")}), - div(list{Attrs.class_("text-gray-200")}, list{text(meta.version)}), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Author")}), - div(list{Attrs.class_("text-gray-200")}, list{text(meta.author)}), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Puzzles")}), - div( - list{Attrs.class_("text-gray-200")}, - list{text(Int.toString(Array.length(state.puzzles)))}, - ), - }, - ), - }, - ), - }, - ), - // Package button - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-purple-700 text-white rounded hover:bg-purple-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(PackageDlc)), - KeyboardNav.onActivate(DlcWorkshop(PackageDlc)), - }, - list{text("Package DLC")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${Int.toString(Array.length(state.puzzles))} puzzles, ${Int.toString( - Array.length(state.assets), - )} assets`, - ), - }, - ), - }, - ), - // Chains - if Array.length(state.chains) > 0 { - div( - list{Attrs.class_("space-y-2")}, - list{ - div(list{Attrs.class_("text-sm text-gray-300")}, list{text("Puzzle Chains")}), - ...state.chains - ->Array.map(chain => - div( - list{Attrs.class_("p-2 bg-gray-800 rounded text-xs")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-gray-200")}, list{text(chain.name)}), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(Array.length(chain.puzzleIds))} puzzles`)}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - }, - ) - } else { - noNode - }, - }, - ) -} - -/// Main view function. -let view = (state: dlcWorkshopState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("DLC Workshop panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("DLC Workshop")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(state.packMeta.name)}), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(ImportPuzzle)), - KeyboardNav.onActivate(DlcWorkshop(ImportPuzzle)), - }, - list{text("Import")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(DlcWorkshop(ExportPuzzle)), - KeyboardNav.onActivate(DlcWorkshop(ExportPuzzle)), - }, - list{text("Export")}, - ), - }, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Puzzles", WorkshopPuzzles, state.activeCategory), - renderTab("Composer", WorkshopComposer, state.activeCategory), - renderTab("Testing", WorkshopTesting, state.activeCategory), - renderTab("Assets", WorkshopAssets, state.activeCategory), - renderTab("Packaging", WorkshopPackaging, state.activeCategory), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(DlcWorkshop(DismissWorkshopError)), - KeyboardNav.onActivate(DlcWorkshop(DismissWorkshopError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Loading - if state.loading { - div( - list{Attrs.class_("px-4 py-2 text-xs text-cyan-400 animate-pulse")}, - list{text("Loading DLC data...")}, - ) - } else { - noNode - }, - // Main content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | WorkshopPuzzles => renderPuzzles(state) - | WorkshopComposer => renderComposer(state) - | WorkshopTesting => renderTesting(state) - | WorkshopAssets => renderAssets(state) - | WorkshopPackaging => renderPackaging(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/Echidna.affine b/src/components/Echidna.affine new file mode 100644 index 00000000..44b8962e --- /dev/null +++ b/src/components/Echidna.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Echidna; + +// TODO: Complete semantic implementation diff --git a/src/components/Echidna.res b/src/components/Echidna.res deleted file mode 100644 index 03eacf58..00000000 --- a/src/components/Echidna.res +++ /dev/null @@ -1,219 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ECHIDNA Component — multi-solver theorem prover dispatch panel. -/// -/// Two tabs: -/// - Proof Workbench: interactive proof sessions, tactic suggestions, dispatch -/// - Enterprise Model: MOF/OCL/ArchiMate model constraint checking - -open Model -open Msg -open Tea.Html - -/// Render a trust level badge. -let trustBadge = (level: echidnaTrustLevel): Tea_Vdom.t => { - let (color, label) = switch level { - | TrustLevel1 => ("text-red-400", "L1") - | TrustLevel2 => ("text-yellow-400", "L2") - | TrustLevel3 => ("text-blue-400", "L3") - | TrustLevel4 => ("text-green-400", "L4") - | TrustLevel5 => ("text-green-300 font-bold", "L5") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Main view function for the ECHIDNA panel. -let view = (state: echidnaState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("ECHIDNA — Multi-Solver Theorem Prover"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-bold text-purple-300")}, list{text("ECHIDNA")}), - span( - list{ - Attrs.class_( - "text-xs " ++ if state.connected { - "text-green-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if state.connected { - "Connected" - } else { - "Disconnected" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == EchidnaProofTab { - "bg-purple-700 text-white" - } else { - "bg-gray-800 text-gray-400" - }, - ), - Events.onClick(Echidna(SelectEchidnaTab(EchidnaProofTab))), - }, - list{text("Proof Workbench")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == EchidnaEnterpriseTab { - "bg-purple-700 text-white" - } else { - "bg-gray-800 text-gray-400" - }, - ), - Events.onClick(Echidna(SelectEchidnaTab(EchidnaEnterpriseTab))), - }, - list{text("Enterprise Model")}, - ), - }, - ), - }, - ), - // Prover catalog summary - div( - list{Attrs.class_("flex gap-4 px-4 py-2 text-xs text-gray-400 border-b border-gray-800")}, - list{ - span(list{}, list{text("Provers: " ++ Int.toString(Array.length(state.provers)))}), - switch state.version { - | Some(v) => span(list{}, list{text("Version: " ++ v)}) - | None => Tea_Html.noNode - }, - switch state.lastProofResult { - | Some(r) => - span( - list{Attrs.class_("flex items-center gap-1")}, - list{ - text("Last: "), - trustBadge(r.trustLevel), - text( - if r.verified { - " verified" - } else { - " unverified" - }, - ), - }, - ) - | None => span(list{}, list{text("No proofs yet")}) - }, - }, - ), - // Error banner - switch state.proofError { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => Tea_Html.noNode - }, - // Content placeholder - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | EchidnaProofTab => - div( - list{}, - list{ - // Proof input area - div( - list{Attrs.class_("mb-4")}, - list{ - label( - list{Attrs.class_("text-xs text-gray-400 block mb-1")}, - list{text("Proof Obligation")}, - ), - textarea( - list{ - Attrs.class_( - "w-full h-40 bg-gray-900 border border-gray-700 rounded p-2 text-sm text-gray-200 font-mono", - ), - Attrs.value(state.proofInput), - Attrs.placeholder("Enter proof obligation..."), - }, - list{}, - ), - }, - ), - // Tactic suggestions - if Array.length(state.tacticSuggestions) > 0 { - div( - list{Attrs.class_("mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm text-gray-300 mb-2")}, - list{text("Tactic Suggestions")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - state.tacticSuggestions - ->Array.map(s => - span( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-purple-900/50 border border-purple-700 rounded cursor-pointer hover:bg-purple-800", - ), - }, - list{ - text( - s.tactic ++ - " (" ++ - Float.toFixed(s.confidence *. 100.0, ~digits=0) ++ "%)", - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - Tea_Html.noNode - }, - }, - ) - | EchidnaEnterpriseTab => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{ - text( - "MOF/OCL enterprise model checking — load XMI models and verify OCL constraints.", - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/EditorBridge.affine b/src/components/EditorBridge.affine new file mode 100644 index 00000000..47e4d9ef --- /dev/null +++ b/src/components/EditorBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EditorBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/EditorBridge.res b/src/components/EditorBridge.res deleted file mode 100644 index be013d5e..00000000 --- a/src/components/EditorBridge.res +++ /dev/null @@ -1,612 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Editor Bridge Component — view for federating with external -/// code editors. Shows diagnostics, open files, symbols, and activity -/// from the connected editor without duplicating the editing surface. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: editorBridgeCategory, - active: editorBridgeCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(EditorBridge(SetBridgeCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render the overview — connection status, open files, diagnostic summary. -let renderOverview = (state: editorBridgeState): Tea_Vdom.t => { - let connCls = EditorBridgeEngine.connectionColour(state.connection) - div( - list{Attrs.class_("space-y-4")}, - list{ - // Connection card - div( - list{Attrs.class_("p-4 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(EditorBridgeEngine.editorLabel(state.editorKind))}, - ), - span( - list{Attrs.class_(`text-xs ${connCls}`)}, - list{text(EditorBridgeEngine.connectionLabel(state.connection))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(EditorBridge(DetectEditor)), - KeyboardNav.onActivate(EditorBridge(DetectEditor)), - }, - list{text("Detect")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(EditorBridge(ConnectLsp)), - KeyboardNav.onActivate(EditorBridge(ConnectLsp)), - }, - list{text("Connect LSP")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `LSP port: ${Int.toString(state.lspPort)} | Auto-sync: ${if state.autoSync { - "on" - } else { - "off" - }}`, - ), - }, - ), - }, - ), - // Stats row - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-100")}, - list{text(Int.toString(Array.length(state.openFiles)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Open Files")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-red-400")}, - list{ - text( - Int.toString(EditorBridgeEngine.countBySeverity(state.diagnostics, "error")), - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Errors")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-amber-400")}, - list{ - text( - Int.toString(EditorBridgeEngine.countBySeverity(state.diagnostics, "warning")), - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Warnings")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-100")}, - list{text(Int.toString(Array.length(state.symbols)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Symbols")}), - }, - ), - }, - ), - // Open files list - if Array.length(state.openFiles) > 0 { - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text("Open Files")}), - ...state.openFiles - ->Array.map(file => - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 rounded cursor-pointer hover:bg-gray-700/50", - ), - Events.onClick(EditorBridge(OpenFileInEditor(file.path, file.cursorLine))), - }, - list{ - span( - list{ - Attrs.class_( - if file.modified { - "text-amber-400 text-xs" - } else { - "text-gray-400 text-xs" - }, - ), - }, - list{ - text( - if file.modified { - "*" - } else { - " " - }, - ), - }, - ), - span( - list{Attrs.class_("text-sm text-gray-200 flex-1 truncate font-mono")}, - list{text(file.path)}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(file.language)}), - span( - list{Attrs.class_("text-xs text-gray-600 font-mono")}, - list{text(`L${Int.toString(file.cursorLine)}`)}, - ), - }, - ) - ) - ->List.fromArray, - }, - ) - } else { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No files open — connect to an editor to see open files")}, - ) - }, - }, - ) -} - -/// Render diagnostics list. -let renderDiagnostics = (state: editorBridgeState): Tea_Vdom.t => { - let filtered = EditorBridgeEngine.filterDiagnostics( - state.diagnostics, - state.showErrors, - state.showWarnings, - state.showInfo, - state.diagnosticFilter, - ) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter controls - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Filter diagnostics..."), - Attrs.value(state.diagnosticFilter), - Events.onInput(text => EditorBridge(SetDiagnosticFilter(text))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - if state.showErrors { - "px-2 py-1 text-xs bg-red-800 text-red-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(EditorBridge(ToggleShowErrors)), - KeyboardNav.onActivate(EditorBridge(ToggleShowErrors)), - }, - list{text("Errors")}, - ), - button( - list{ - Attrs.class_( - if state.showWarnings { - "px-2 py-1 text-xs bg-amber-800 text-amber-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(EditorBridge(ToggleShowWarnings)), - KeyboardNav.onActivate(EditorBridge(ToggleShowWarnings)), - }, - list{text("Warnings")}, - ), - button( - list{ - Attrs.class_( - if state.showInfo { - "px-2 py-1 text-xs bg-blue-800 text-blue-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(EditorBridge(ToggleShowInfo)), - KeyboardNav.onActivate(EditorBridge(ToggleShowInfo)), - }, - list{text("Info")}, - ), - }, - ), - // Diagnostics list - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-emerald-400 text-sm py-8")}, - list{text("No diagnostics — code is clean")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - filtered - ->Array.map(diag => { - let sevCls = EditorBridgeEngine.severityColour(diag.severity) - div( - list{ - Attrs.class_("p-2 bg-gray-800 rounded cursor-pointer hover:bg-gray-700"), - Events.onClick(EditorBridge(OpenFileInEditor(diag.filePath, diag.line))), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_(`text-xs font-bold ${sevCls} uppercase`)}, - list{text(diag.severity)}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 font-mono")}, - list{text(`${diag.filePath}:${Int.toString(diag.line)}`)}, - ), - if diag.source !== "" { - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`[${diag.source}]`)}, - ) - } else { - noNode - }, - }, - ), - div(list{Attrs.class_("text-xs text-gray-300")}, list{text(diag.message)}), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render symbols view. -let renderSymbols = (state: editorBridgeState): Tea_Vdom.t => { - let filtered = EditorBridgeEngine.filterSymbols(state.symbols, state.symbolFilter) - div( - list{Attrs.class_("space-y-3")}, - list{ - input( - list{ - Attrs.class_( - "w-full px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Search symbols..."), - Attrs.value(state.symbolFilter), - Events.onInput(text => EditorBridge(SetSymbolFilter(text))), - }, - list{}, - ), - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No symbols found")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - filtered - ->Array.map(sym => - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800 rounded cursor-pointer hover:bg-gray-700", - ), - Events.onClick(EditorBridge(OpenFileInEditor(sym.filePath, sym.line))), - }, - list{ - span(list{Attrs.class_("text-xs text-gray-500 w-16")}, list{text(sym.kind)}), - span(list{Attrs.class_("text-sm text-cyan-400 font-mono")}, list{text(sym.name)}), - if sym.containerName !== "" { - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`in ${sym.containerName}`)}, - ) - } else { - noNode - }, - span( - list{Attrs.class_("ml-auto text-xs text-gray-600 font-mono")}, - list{text(`${sym.filePath}:${Int.toString(sym.line)}`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render activity feed. -let renderActivity = (state: editorBridgeState): Tea_Vdom.t => { - if Array.length(state.activity) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No editor activity recorded yet")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - state.activity - ->Array.map(act => - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800/50 rounded text-xs")}, - list{ - span( - list{Attrs.class_("text-gray-500 font-mono w-16")}, - list{text(Float.toString(act.timestamp))}, - ), - span(list{Attrs.class_("text-gray-300")}, list{text(act.action)}), - span(list{Attrs.class_("text-gray-400 font-mono truncate")}, list{text(act.filePath)}), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render settings view. -let renderSettings = (state: editorBridgeState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - // Editor selection - div( - list{Attrs.class_("p-3 bg-gray-800 rounded")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text("Editor")}), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - EditorBridgeEngine.allEditors - ->Array.map(editor => { - let isActive = state.editorKind === editor - button( - list{ - Attrs.class_( - if isActive { - "px-2 py-1 text-xs bg-cyan-700 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer hover:bg-gray-600" - }, - ), - Events.onClick(EditorBridge(SetEditorKind(editor))), - }, - list{text(EditorBridgeEngine.editorLabel(editor))}, - ) - }) - ->List.fromArray, - ), - }, - ), - // Auto-sync toggle - div( - list{Attrs.class_("flex items-center justify-between p-3 bg-gray-800 rounded")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text("Auto-Sync")}), - button( - list{ - Attrs.class_( - if state.autoSync { - "px-3 py-1 text-xs bg-emerald-700 text-white rounded" - } else { - "px-3 py-1 text-xs bg-gray-700 text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(EditorBridge(ToggleAutoSync)), - KeyboardNav.onActivate(EditorBridge(ToggleAutoSync)), - }, - list{ - text( - if state.autoSync { - "Enabled" - } else { - "Disabled" - }, - ), - }, - ), - }, - ), - }, - ) -} - -/// Main view function. -let view = (state: editorBridgeState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Editor Bridge panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Editor Bridge")}, - ), - span( - list{ - Attrs.class_(`text-xs ${EditorBridgeEngine.connectionColour(state.connection)}`), - }, - list{text(EditorBridgeEngine.connectionLabel(state.connection))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if state.bojRouting { - "px-3 py-1.5 text-xs bg-blue-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600" - }, - ), - Attrs.ariaLabel( - if state.bojRouting { - "Disable BoJ routing" - } else { - "Enable BoJ routing" - }, - ), - Events.onClick(EditorBridge(ToggleBojRouting)), - KeyboardNav.onActivate(EditorBridge(ToggleBojRouting)), - }, - list{ - text( - if state.bojRouting { - "BoJ On" - } else { - "BoJ" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(EditorBridge(RefreshBridge)), - KeyboardNav.onActivate(EditorBridge(RefreshBridge)), - }, - list{text("Refresh")}, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Overview", BridgeOverview, state.activeCategory), - renderTab("Diagnostics", BridgeDiagnostics, state.activeCategory), - renderTab("Symbols", BridgeSymbols, state.activeCategory), - renderTab("Activity", BridgeActivity, state.activeCategory), - renderTab("Settings", BridgeSettings, state.activeCategory), - }, - ), - // Error - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(EditorBridge(DismissBridgeError)), - KeyboardNav.onActivate(EditorBridge(DismissBridgeError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | BridgeOverview => renderOverview(state) - | BridgeDiagnostics => renderDiagnostics(state) - | BridgeSymbols => renderSymbols(state) - | BridgeActivity => renderActivity(state) - | BridgeSettings => renderSettings(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/Evangeliser.affine b/src/components/Evangeliser.affine new file mode 100644 index 00000000..181e2c57 --- /dev/null +++ b/src/components/Evangeliser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Evangeliser; + +// TODO: Complete semantic implementation diff --git a/src/components/Evangeliser.res b/src/components/Evangeliser.res deleted file mode 100644 index 75d7d34b..00000000 --- a/src/components/Evangeliser.res +++ /dev/null @@ -1,868 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Evangeliser Component — JS-to-ReScript transformation teaching panel. -/// -/// Three-panel view integrated into PanLL's overlay system: -/// Panel-L section: Pattern constraints, category filters, confidence threshold -/// Panel-N section: Narrative reasoning, glyph annotations, scan progress -/// Panel-W section: JS→ReScript side-by-side with matched patterns -/// -/// Philosophy: "Celebrate good, minimize bad, show better" - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Tab Navigation -// ============================================================================ - -/// Render a tab button in the header. -let renderTab = (label: string, tab: evangeliserTab, active: evangeliserTab): Tea_Vdom.t => { - let isActive = tab === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Attrs.ariaLabel(label), Events.onClick(Evangeliser(SetTab(tab)))}, - list{text(label)}, - ) -} - -// ============================================================================ -// Panel-L: Constraints Sidebar -// ============================================================================ - -/// Render the constraint controls (left column in scan tab). -let renderConstraints = (state: evangeliserState): Tea_Vdom.t => { - div( - list{Attrs.class_("w-64 border-r border-gray-800 p-3 flex flex-col gap-3 overflow-y-auto")}, - list{ - // Section header - div( - list{Attrs.class_("text-[10px] uppercase tracking-wider text-gray-500 font-medium")}, - list{text("Constraints (Panel-L)")}, - ), - // Confidence threshold - div( - list{Attrs.class_("flex flex-col gap-1")}, - list{ - label( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{ - text( - "Min Confidence: " ++ - Float.toFixed(state.constraints.minConfidence *. 100.0, ~digits=0) ++ "%", - ), - }, - ), - input( - list{ - Attrs.type_("range"), - Attrs.class_("w-full accent-indigo-500"), - Attrs.value(Float.toString(state.constraints.minConfidence *. 100.0)), - Events.onInput(v => Evangeliser( - SetMinConfidence(Float.fromString(v)->Option.getOr(50.0) /. 100.0), - )), - Attrs.ariaLabel("Minimum confidence threshold"), - }, - list{}, - ), - }, - ), - // Difficulty filter - div( - list{Attrs.class_("flex flex-col gap-1")}, - list{ - span(list{Attrs.class_("text-[10px] text-gray-500")}, list{text("Difficulty")}), - div( - list{Attrs.class_("flex gap-1")}, - list{ - button( - list{ - Attrs.class_( - if state.constraints.difficultyFilter === None { - "px-2 py-0.5 text-[10px] bg-gray-700 text-white rounded" - } else { - "px-2 py-0.5 text-[10px] text-gray-500 hover:text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Evangeliser(SetDifficultyFilter(None))), - }, - list{text("All")}, - ), - button( - list{ - Attrs.class_( - if state.constraints.difficultyFilter === Some(Beginner) { - "px-2 py-0.5 text-[10px] bg-emerald-900 text-emerald-300 rounded" - } else { - "px-2 py-0.5 text-[10px] text-gray-500 hover:text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Evangeliser(SetDifficultyFilter(Some(Beginner)))), - }, - list{text("Beginner")}, - ), - button( - list{ - Attrs.class_( - if state.constraints.difficultyFilter === Some(Intermediate) { - "px-2 py-0.5 text-[10px] bg-amber-900 text-amber-300 rounded" - } else { - "px-2 py-0.5 text-[10px] text-gray-500 hover:text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Evangeliser(SetDifficultyFilter(Some(Intermediate)))), - }, - list{text("Intermediate")}, - ), - button( - list{ - Attrs.class_( - if state.constraints.difficultyFilter === Some(Advanced) { - "px-2 py-0.5 text-[10px] bg-red-900 text-red-300 rounded" - } else { - "px-2 py-0.5 text-[10px] text-gray-500 hover:text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Evangeliser(SetDifficultyFilter(Some(Advanced)))), - }, - list{text("Advanced")}, - ), - }, - ), - }, - ), - // Category toggles - div( - list{Attrs.class_("flex flex-col gap-1")}, - list{ - span( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{ - text( - "Categories (" ++ - Int.toString(Array.length(EvangeliserEngine.allCategories)) ++ ")", - ), - }, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - EvangeliserEngine.allCategories - ->Array.map(cat => { - let enabled = - state.constraints.enabledCategories->Array.length === 0 || - state.constraints.enabledCategories->Array.includes(cat) - let colour = if enabled { - EvangeliserEngine.categoryColour(cat) - } else { - "text-gray-600" - } - button( - list{ - Attrs.class_( - "px-1.5 py-0.5 text-[9px] rounded border border-gray-800 " ++ - colour ++ " hover:border-gray-600 cursor-pointer", - ), - Attrs.title(EvangeliserEngine.categoryLabel(cat)), - Events.onClick(Evangeliser(ToggleCategory(cat))), - }, - list{text(EvangeliserEngine.categoryCode(cat))}, - ) - }) - ->List.fromArray, - ), - }, - ), - // Pattern count - div( - list{Attrs.class_("mt-2 text-[10px] text-gray-600")}, - list{text(Int.toString(Array.length(state.patterns)) ++ " patterns loaded")}, - ), - }, - ) -} - -// ============================================================================ -// Panel-N: Narrative Display -// ============================================================================ - -/// Render the narrative for a single match. -let renderNarrative = (m: evangeliserMatch): Tea_Vdom.t => { - div( - list{Attrs.class_("p-3 bg-gray-900/60 rounded border border-gray-800 flex flex-col gap-2")}, - list{ - // Glyphs - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-sm")}, list{text(EvangeliserEngine.glyphSymbols(m.glyphs))}), - span( - list{ - Attrs.class_("text-xs font-medium " ++ EvangeliserEngine.categoryColour(m.category)), - }, - list{text(m.patternName)}, - ), - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{ - text( - "L" ++ - Int.toString(m.startLine) ++ - " \xc2\xb7 " ++ - Float.toFixed(m.confidence *. 100.0, ~digits=0) ++ "%", - ), - }, - ), - }, - ), - // Celebrate - div( - list{Attrs.class_("text-[11px] text-emerald-400")}, - list{ - span(list{Attrs.class_("font-medium")}, list{text("Celebrate: ")}), - text(m.narrative.celebrate), - }, - ), - // Minimize - div( - list{Attrs.class_("text-[11px] text-amber-400")}, - list{ - span(list{Attrs.class_("font-medium")}, list{text("Note: ")}), - text(m.narrative.minimize), - }, - ), - // Better - div( - list{Attrs.class_("text-[11px] text-cyan-400")}, - list{ - span(list{Attrs.class_("font-medium")}, list{text("Better: ")}), - text(m.narrative.better), - }, - ), - // Safety - div( - list{Attrs.class_("text-[11px] text-indigo-400")}, - list{ - span(list{Attrs.class_("font-medium")}, list{text("Safety: ")}), - text(m.narrative.safety), - }, - ), - }, - ) -} - -// ============================================================================ -// Panel-W: Results — JS→ReScript side-by-side -// ============================================================================ - -/// Render a single match result with JS→ReScript comparison. -let renderMatchResult = (m: evangeliserMatch, idx: int, selected: option): Tea_Vdom.t => { - let isSelected = selected === Some(idx) - let borderCls = isSelected ? "border-cyan-600" : "border-gray-800 hover:border-gray-700" - - div( - list{ - Attrs.class_("rounded border " ++ borderCls ++ " transition-colors cursor-pointer"), - Events.onClick(Evangeliser(SelectMatch(Some(idx)))), - Attrs.ariaLabel("Pattern match: " ++ m.patternName), - }, - list{ - // Header row - div( - list{ - Attrs.class_( - "flex items-center justify-between px-3 py-1.5 bg-gray-900/40 border-b border-gray-800", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm")}, - list{text(EvangeliserEngine.glyphSymbols(m.glyphs))}, - ), - span( - list{ - Attrs.class_( - "text-xs font-medium " ++ EvangeliserEngine.categoryColour(m.category), - ), - }, - list{text(m.patternName)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("line " ++ Int.toString(m.startLine))}, - ), - span( - list{Attrs.class_("text-[10px] px-1.5 py-0.5 rounded bg-gray-800 text-gray-400")}, - list{text(Float.toFixed(m.confidence *. 100.0, ~digits=0) ++ "%")}, - ), - }, - ), - }, - ), - // Code comparison - div( - list{Attrs.class_("grid grid-cols-2 divide-x divide-gray-800")}, - list{ - // JS side - div( - list{Attrs.class_("p-3")}, - list{ - div( - list{Attrs.class_("text-[9px] uppercase text-gray-600 mb-1")}, - list{text("JavaScript")}, - ), - pre( - list{ - Attrs.class_( - "text-[11px] text-gray-300 whitespace-pre-wrap font-mono bg-gray-900/40 p-2 rounded", - ), - }, - list{code(list{}, list{text(m.jsExample)})}, - ), - }, - ), - // ReScript side - div( - list{Attrs.class_("p-3")}, - list{ - div( - list{Attrs.class_("text-[9px] uppercase text-emerald-600 mb-1")}, - list{text("ReScript")}, - ), - pre( - list{ - Attrs.class_( - "text-[11px] text-emerald-300 whitespace-pre-wrap font-mono bg-gray-900/40 p-2 rounded", - ), - }, - list{code(list{}, list{text(m.rescriptExample)})}, - ), - }, - ), - }, - ), - // Narrative (expanded when selected) - if isSelected { - div(list{Attrs.class_("border-t border-gray-800 p-3")}, list{renderNarrative(m)}) - } else { - noNode - }, - }, - ) -} - -// ============================================================================ -// Tab: Scan (main input + results view) -// ============================================================================ - -/// Render the scan tab — JS input on left, results on right. -let renderScanTab = (state: evangeliserState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Left: Constraints + Input - div( - list{Attrs.class_("flex flex-col flex-1")}, - list{ - // JS code input - div( - list{Attrs.class_("flex-1 flex flex-col p-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-[10px] uppercase text-gray-500")}, - list{text("Paste JavaScript Code")}, - ), - button( - list{ - Attrs.class_( - if state.scanning { - "px-3 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-not-allowed" - } else { - "px-3 py-1 text-xs bg-indigo-600 hover:bg-indigo-500 text-white rounded cursor-pointer" - }, - ), - Attrs.disabled(state.scanning), - Events.onClick(Evangeliser(RunScan)), - KeyboardNav.onActivate(Evangeliser(RunScan)), - Attrs.ariaLabel("Scan JavaScript code for patterns"), - }, - list{ - text( - if state.scanning { - "Scanning..." - } else { - "Scan" - }, - ), - }, - ), - }, - ), - textarea( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-800 rounded p-3 text-xs text-gray-200 font-mono resize-none focus:border-indigo-500 outline-none", - ), - Attrs.placeholder( - "// Paste your JavaScript code here...\n// The evangeliser will detect patterns and show\n// how ReScript makes them safer and cleaner.", - ), - Attrs.value(state.jsInput), - Events.onInput(v => Evangeliser(SetJsInput(v))), - Attrs.ariaLabel("JavaScript code input"), - }, - list{}, - ), - // Error display - switch state.scanError { - | Some(err) => - div( - list{ - Attrs.class_( - "mt-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - }, - ), - }, - ), - // Right: Results - div( - list{Attrs.class_("flex-1 flex flex-col border-l border-gray-800 overflow-y-auto")}, - list{ - switch state.analysis { - | None => - div( - list{Attrs.class_("flex-1 flex items-center justify-center text-gray-600 text-sm")}, - list{text("Paste JS code and click Scan to see patterns")}, - ) - | Some(analysis) => - div( - list{Attrs.class_("flex flex-col gap-2 p-3")}, - list{ - // Summary bar - div( - list{Attrs.class_("flex items-center gap-3 mb-2 pb-2 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(Int.toString(Array.length(analysis.matches)) ++ " matches")}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text(Float.toFixed(analysis.coveragePercentage, ~digits=1) ++ "% coverage"), - }, - ), - span( - list{ - Attrs.class_( - "text-[10px] px-1.5 py-0.5 rounded " ++ - EvangeliserEngine.difficultyColour(analysis.difficulty), - ), - }, - list{text(EvangeliserEngine.difficultyLabel(analysis.difficulty))}, - ), - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text(Float.toFixed(analysis.analysisTime, ~digits=1) ++ "ms")}, - ), - }, - ), - // Category breakdown - div( - list{Attrs.class_("flex flex-wrap gap-1 mb-2")}, - EvangeliserEngine.matchCategoryStats(analysis.matches) - ->Array.map(((cat, count)) => { - span( - list{ - Attrs.class_( - "text-[9px] px-1.5 py-0.5 rounded bg-gray-900 " ++ - EvangeliserEngine.categoryColour(cat), - ), - }, - list{text(EvangeliserEngine.categoryCode(cat) ++ ":" ++ Int.toString(count))}, - ) - }) - ->List.fromArray, - ), - // Match list - div( - list{Attrs.class_("flex flex-col gap-2")}, - analysis.matches - ->Array.mapWithIndex((m, idx) => { - renderMatchResult(m, idx, state.selectedMatchIndex) - }) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} - -// ============================================================================ -// Tab: Pattern Library Browser -// ============================================================================ - -/// Render the pattern library browser. -let renderPatternsTab = (state: evangeliserState): Tea_Vdom.t => { - let filtered = state.patterns->EvangeliserEngine.filterBySearch(state.filterText) - - div( - list{Attrs.class_("flex-1 flex flex-col overflow-hidden")}, - list{ - // Search bar - div( - list{Attrs.class_("px-3 py-2 border-b border-gray-800")}, - list{ - input( - list{ - Attrs.class_( - "w-full bg-gray-900 border border-gray-800 rounded px-3 py-1.5 text-xs text-gray-200 focus:border-indigo-500 outline-none", - ), - Attrs.placeholder("Search patterns by name or tag..."), - Attrs.value(state.filterText), - Events.onInput(v => Evangeliser(SetFilterText(v))), - Attrs.ariaLabel("Filter patterns"), - }, - list{}, - ), - }, - ), - // Pattern grid - div( - list{Attrs.class_("flex-1 overflow-y-auto p-3")}, - list{ - div( - list{Attrs.class_("grid grid-cols-1 gap-2")}, - filtered - ->Array.map(p => { - div( - list{ - Attrs.class_( - "p-3 bg-gray-900/40 rounded border border-gray-800 hover:border-gray-700 transition-colors", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm")}, - list{text(EvangeliserEngine.glyphSymbols(p.glyphs))}, - ), - span( - list{ - Attrs.class_( - "text-xs font-medium " ++ - EvangeliserEngine.categoryColour(p.category), - ), - }, - list{text(p.name)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span( - list{ - Attrs.class_( - "text-[10px] px-1.5 py-0.5 rounded " ++ - EvangeliserEngine.difficultyColour(p.difficulty), - ), - }, - list{text(EvangeliserEngine.difficultyLabel(p.difficulty))}, - ), - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text(Float.toFixed(p.confidence *. 100.0, ~digits=0) ++ "%")}, - ), - }, - ), - }, - ), - // JS→ReScript comparison - div( - list{Attrs.class_("grid grid-cols-2 gap-2")}, - list{ - div( - list{Attrs.class_("bg-gray-950 p-2 rounded")}, - list{ - div( - list{Attrs.class_("text-[8px] uppercase text-gray-600 mb-1")}, - list{text("JS")}, - ), - pre( - list{ - Attrs.class_( - "text-[10px] text-gray-400 whitespace-pre-wrap font-mono", - ), - }, - list{code(list{}, list{text(p.jsExample)})}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-950 p-2 rounded")}, - list{ - div( - list{Attrs.class_("text-[8px] uppercase text-emerald-700 mb-1")}, - list{text("ReScript")}, - ), - pre( - list{ - Attrs.class_( - "text-[10px] text-emerald-400 whitespace-pre-wrap font-mono", - ), - }, - list{code(list{}, list{text(p.rescriptExample)})}, - ), - }, - ), - }, - ), - // Tags - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-2")}, - p.tags - ->Array.map(t => { - span( - list{ - Attrs.class_("text-[9px] px-1 py-0.5 bg-gray-800 text-gray-500 rounded"), - }, - list{text(t)}, - ) - }) - ->List.fromArray, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Footer with count - div( - list{Attrs.class_("px-3 py-1.5 border-t border-gray-800 text-[10px] text-gray-600")}, - list{ - text( - Int.toString(Array.length(filtered)) ++ - " of " ++ - Int.toString(Array.length(state.patterns)) ++ " patterns", - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Tab: Glyph Legend -// ============================================================================ - -/// Render the Makaton glyph legend. -let renderLegendTab = (state: evangeliserState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{ - div( - list{Attrs.class_("mb-4")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text("Makaton-Inspired Glyph System")}, - ), - div( - list{Attrs.class_("text-[11px] text-gray-500 mt-1")}, - list{ - text( - "Glyphs transcend syntax to show semantic meaning. Each glyph represents a programming concept that maps from JavaScript patterns to ReScript equivalents.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-1 md:grid-cols-2 gap-2")}, - state.glyphs - ->Array.map(g => { - let catColour = switch g.semanticCategory { - | Transformation => "text-emerald-400" - | Safety => "text-red-400" - | Flow => "text-cyan-400" - | Structure => "text-violet-400" - | State => "text-amber-400" - | Data => "text-pink-400" - } - div( - list{ - Attrs.class_( - "flex items-start gap-3 p-2 bg-gray-900/40 rounded border border-gray-800", - ), - }, - list{ - span(list{Attrs.class_("text-xl")}, list{text(g.symbol)}), - div( - list{Attrs.class_("flex flex-col")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs font-medium text-gray-200")}, - list{text(g.name)}, - ), - span( - list{Attrs.class_("text-[9px] " ++ catColour)}, - list{ - text( - switch g.semanticCategory { - | Transformation => "transformation" - | Safety => "safety" - | Flow => "flow" - | Structure => "structure" - | State => "state" - | Data => "data" - }, - ), - }, - ), - }, - ), - span(list{Attrs.class_("text-[10px] text-gray-500")}, list{text(g.meaning)}), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main view function for the Evangeliser panel. -let view = (state: evangeliserState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("ReScript Evangeliser — JS to ReScript transformation panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text("Evangeliser")}, - ), - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("Celebrate good, minimize bad, show better")}, - ), - span( - list{Attrs.class_("text-[10px] px-1.5 py-0.5 rounded bg-gray-800 text-gray-500")}, - list{text(Int.toString(Array.length(state.patterns)) ++ " patterns")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - renderTab("Scan", TabScan, state.activeTab), - renderTab("Patterns", TabPatterns, state.activeTab), - renderTab("Results", TabResults, state.activeTab), - renderTab("Legend", TabLegend, state.activeTab), - // View layer selector - div( - list{Attrs.class_("ml-2 flex items-center gap-1 border-l border-gray-800 pl-2")}, - list{ - span(list{Attrs.class_("text-[9px] text-gray-600")}, list{text("View:")}), - button( - list{ - Attrs.class_( - if state.viewLayer === ViewRaw { - "text-[9px] px-1.5 py-0.5 bg-gray-700 text-white rounded" - } else { - "text-[9px] px-1.5 py-0.5 text-gray-500 hover:text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Evangeliser(SetViewLayer(ViewRaw))), - }, - list{text("RAW")}, - ), - button( - list{ - Attrs.class_( - if state.viewLayer === ViewGlyphed { - "text-[9px] px-1.5 py-0.5 bg-gray-700 text-white rounded" - } else { - "text-[9px] px-1.5 py-0.5 text-gray-500 hover:text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(Evangeliser(SetViewLayer(ViewGlyphed))), - }, - list{text("GLYPHED")}, - ), - }, - ), - }, - ), - }, - ), - // Tab content - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Constraints sidebar (visible in Scan and Results tabs) - if state.activeTab === TabScan || state.activeTab === TabResults { - renderConstraints(state) - } else { - noNode - }, - // Main content area - switch state.activeTab { - | TabScan => renderScanTab(state) - | TabPatterns => renderPatternsTab(state) - | TabResults => renderScanTab(state) // Results shown in scan view - | TabLegend => renderLegendTab(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/ExploratoryWorkbench.affine b/src/components/ExploratoryWorkbench.affine new file mode 100644 index 00000000..59cf0976 --- /dev/null +++ b/src/components/ExploratoryWorkbench.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ExploratoryWorkbench; + +// TODO: Complete semantic implementation diff --git a/src/components/ExploratoryWorkbench.res b/src/components/ExploratoryWorkbench.res deleted file mode 100644 index 0d0f4115..00000000 --- a/src/components/ExploratoryWorkbench.res +++ /dev/null @@ -1,364 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ExploratoryWorkbench — freeform play session recording and anomaly -/// detection for QA testers and designers. -/// -/// Four tabs: Session (current recording with quick-flag button), Anomalies -/// (severity-badged anomaly list), Notes (session notes textarea), and History -/// (previous session summaries). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for exploratoryTab variants. -let tabLabel = (tab: exploratoryTab): string => - switch tab { - | TabSession => "Session" - | TabAnomalies => "Anomalies" - | TabNotes => "Notes" - | TabHistory => "History" - } - -/// Render the tab bar. -let renderTabs = (active: exploratoryTab): Tea_Vdom.t => { - let tabs: array = [TabSession, TabAnomalies, TabNotes, TabHistory] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(ExploratoryWorkbench(SetEwTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Anomaly severity badge with colour coding. -let severityBadge = (severity: anomalySeverity): Tea_Vdom.t => { - let (colour, lbl) = switch severity { - | AnomalyLow => ("bg-blue-600 text-white", "LOW") - | AnomalyMedium => ("bg-amber-500 text-white", "MED") - | AnomalyHigh => ("bg-orange-600 text-white", "HIGH") - | AnomalyCritical => ("bg-red-600 text-white", "CRIT") - } - span(list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded font-mono ${colour}`)}, list{text(lbl)}) -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Session tab: current recording state, quick-flag button, and session vitals. -let renderSessionTab = (state: exploratoryWorkbenchState): Tea_Vdom.t => { - switch state.currentSession { - | None => - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No active session. Click Start Recording to begin an exploratory play session.")}, - ) - | Some(session) => - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // Session vitals - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-cyan-400")}, - list{text(`${Float.toFixed(session.durationMinutes, ~digits=1)}`)}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Minutes")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(session.playerActions))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Actions")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-amber-400")}, - list{text(Int.toString(Array.length(session.anomalies)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Anomalies")}), - }, - ), - }, - ), - // Quick-flag button — the "That felt weird" button - div( - list{Attrs.class_("flex justify-center")}, - list{ - button( - list{ - Attrs.class_( - "px-6 py-3 bg-amber-600 text-white rounded-lg hover:bg-amber-500 cursor-pointer text-sm font-medium shadow-lg", - ), - Events.onClick(ExploratoryWorkbench(QuickFlag("anomaly"))), - }, - list{text("That felt weird")}, - ), - }, - ), - // Anomaly detection toggle - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium cursor-pointer ${state.anomalyDetectionEnabled - ? "bg-cyan-700 text-white" - : "bg-gray-700 text-gray-400"}`, - ), - Events.onClick(ExploratoryWorkbench(ToggleAnomalyDetection)), - KeyboardNav.onActivate(ExploratoryWorkbench(ToggleAnomalyDetection)), - }, - list{ - text(state.anomalyDetectionEnabled ? "Auto-Detection: ON" : "Auto-Detection: OFF"), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Automatically detect gameplay anomalies")}, - ), - }, - ), - }, - ) - } -} - -/// Anomalies tab: severity-badged list of detected anomalies. -let renderAnomaliesTab = (state: exploratoryWorkbenchState): Tea_Vdom.t => { - if Array.length(state.anomalies) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No anomalies flagged yet. Use the quick-flag button or enable auto-detection.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.anomalies))} anomaly(ies) flagged`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - state.anomalies - ->Array.map(anomaly => { - let autoTag = anomaly.autoDetected - ? span( - list{ - Attrs.class_("px-1 py-0.5 text-xs rounded bg-gray-600 text-gray-300 font-mono"), - }, - list{text("AUTO")}, - ) - : noNode - div( - list{Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-800 rounded text-sm")}, - list{ - severityBadge(anomaly.severity), - autoTag, - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(anomaly.description)}), - span(list{Attrs.class_("text-gray-600 text-xs")}, list{text(anomaly.category)}), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Notes tab: session notes textarea for recording observations. -let renderNotesTab = (state: exploratoryWorkbenchState): Tea_Vdom.t => { - let currentNotes = switch state.currentSession { - | Some(session) => session.notes - | None => "" - } - div( - list{Attrs.class_("flex flex-col gap-3 p-4")}, - list{ - h3(list{Attrs.class_("text-sm font-medium text-gray-300")}, list{text("Session Notes")}), - textarea( - list{ - Attrs.class_( - "w-full h-48 bg-gray-800 text-gray-200 text-sm rounded p-3 border border-gray-700 focus:border-cyan-600 focus:outline-none resize-y font-mono", - ), - Attrs.placeholder("Record observations, hunches, and test ideas..."), - Attrs.value(currentNotes), - Events.onInput(text => ExploratoryWorkbench(UpdateNotes(text))), - }, - list{}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(String.length(currentNotes))} character(s)`)}, - ), - }, - ) -} - -/// History tab: previous exploratory session summaries. -let renderHistoryTab = (state: exploratoryWorkbenchState): Tea_Vdom.t => { - if Array.length(state.sessions) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No previous sessions recorded.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - state.sessions - ->Array.map(session => { - let anomalyCount = Array.length(session.anomalies) - let borderCls = anomalyCount > 0 ? "border-amber-700" : "border-gray-700" - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${borderCls}`)}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(session.name)}, - ), - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(session.id)}), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - text(`${Float.toFixed(session.durationMinutes, ~digits=1)} min`), - text(`${Int.toString(session.playerActions)} actions`), - text(`${Int.toString(anomalyCount)} anomalies`), - }, - ), - if session.notes !== "" { - div( - list{Attrs.class_("text-xs text-gray-500 mt-1 truncate")}, - list{text(session.notes)}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ) - } -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: exploratoryWorkbenchState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabSession => renderSessionTab(state) - | TabAnomalies => renderAnomaliesTab(state) - | TabNotes => renderNotesTab(state) - | TabHistory => renderHistoryTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with Start/Stop Recording - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Exploratory Workbench")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(ExploratoryWorkbench(StartRecording)), - KeyboardNav.onActivate(ExploratoryWorkbench(StartRecording)), - }, - list{text("Start Recording")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-red-700 text-white rounded hover:bg-red-600 cursor-pointer font-medium", - ), - Events.onClick(ExploratoryWorkbench(StopRecording)), - KeyboardNav.onActivate(ExploratoryWorkbench(StopRecording)), - }, - list{text("Stop Recording")}, - ), - }, - ), - }, - ), - // Recording indicator - if state.recording { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-red-500 rounded-full animate-pulse")}, list{}), - span(list{Attrs.class_("text-sm text-red-300")}, list{text("Recording...")}), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/Farm.affine b/src/components/Farm.affine new file mode 100644 index 00000000..01d8b30c --- /dev/null +++ b/src/components/Farm.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Farm; + +// TODO: Complete semantic implementation diff --git a/src/components/Farm.res b/src/components/Farm.res deleted file mode 100644 index 1bc35eef..00000000 --- a/src/components/Farm.res +++ /dev/null @@ -1,632 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Farm Component — view layer for the Git-Private-Farm panel. -/// -/// Renders a full-screen overlay with the repo inventory from -/// farm-manifest.json. Layout follows the CloudGuard pattern: -/// - Header with title, stats, and close button -/// - Category tab bar (All | By Group | By Language | By Forge | Enrollment | Health) -/// - Filter/sort controls -/// - Main inventory table/grid -/// -/// The panel reads local JSON via the Gossamer backend — no HTTP service required. - -open Model -open Msg -open Tea.Html - -/// Render a single category tab button. -let renderCategoryTab = (cat: farmCategory, isActive: bool): Tea_Vdom.t => { - let activeClass = isActive - ? "border-indigo-500 text-indigo-300 bg-gray-800/50" - : "border-transparent text-gray-500 hover:text-gray-300 hover:border-gray-600" - - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium border-b-2 cursor-pointer transition-colors ${activeClass}`, - ), - Attrs.role("tab"), - Events.onClick(Farm(SetFarmCategory(cat))), - }, - list{text(FarmEngine.categoryLabel(cat))}, - ) -} - -/// Render the category tab bar. -let renderCategoryTabBar = (activeCategory: farmCategory): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex border-b border-gray-800 overflow-x-auto"), - Attrs.role("tablist"), - Attrs.ariaLabel("Farm view categories"), - }, - FarmEngine.allCategories - ->Array.map(cat => renderCategoryTab(cat, cat === activeCategory)) - ->List.fromArray, - ) -} - -/// Render a priority badge with colour coding. -let renderPriorityBadge = (priority: farmPriority): Tea_Vdom.t => { - let (label, bgClass) = switch priority { - | High => ("HIGH", "bg-red-900/50 text-red-300 border-red-700") - | Medium => ("MED", "bg-amber-900/50 text-amber-300 border-amber-700") - | Low => ("LOW", "bg-gray-800/50 text-gray-400 border-gray-700") - } - span(list{Attrs.class_(`text-xs px-1.5 py-0.5 rounded border ${bgClass}`)}, list{text(label)}) -} - -/// Render forge badges for a repo (small coloured pills). -let renderForgeBadges = (forges: array): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - forges - ->Array.map(f => { - let colour = switch f.name { - | "github" => "bg-gray-700 text-gray-200" - | "gitlab" => "bg-orange-900/50 text-orange-300" - | "sourcehut" => "bg-blue-900/50 text-blue-300" - | "codeberg" => "bg-green-900/50 text-green-300" - | "bitbucket" => "bg-blue-800/50 text-blue-200" - | "radicle" => "bg-purple-900/50 text-purple-300" - | _ => "bg-gray-800 text-gray-400" - } - span(list{Attrs.class_(`text-xs px-1.5 py-0.5 rounded ${colour}`)}, list{text(f.name)}) - }) - ->List.fromArray, - ) -} - -/// Render a single repo row in the inventory table. -let renderRepoRow = (repo: farmRepo): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-4 py-2 hover:bg-gray-800/30 border-b border-gray-800/50 transition-colors", - ), - }, - list{ - // Name + description - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-200 truncate")}, - list{text(repo.name)}, - ), - div(list{Attrs.class_("text-xs text-gray-500 truncate")}, list{text(repo.description)}), - }, - ), - // Language - div( - list{Attrs.class_("w-20 text-xs text-gray-400 text-center")}, - list{text(repo.language === "" ? "-" : repo.language)}, - ), - // Priority - div(list{Attrs.class_("w-16 flex justify-center")}, list{renderPriorityBadge(repo.priority)}), - // Forges - div(list{Attrs.class_("w-48")}, list{renderForgeBadges(repo.forges)}), - // Auto-propagate indicator - div( - list{Attrs.class_("w-8 text-center")}, - list{ - span( - list{ - Attrs.class_(repo.autoPropagation ? "text-emerald-400" : "text-gray-600"), - Attrs.title(repo.autoPropagation ? "Auto-propagation enabled" : "Manual sync"), - }, - list{text(repo.autoPropagation ? "A" : "-")}, - ), - }, - ), - }, - ) -} - -/// Render the table header row. -let renderTableHeader = (): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-4 py-2 border-b border-gray-700 text-xs font-medium text-gray-500 uppercase tracking-wider", - ), - }, - list{ - div(list{Attrs.class_("flex-1")}, list{text("Repository")}), - div(list{Attrs.class_("w-20 text-center")}, list{text("Lang")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Priority")}), - div(list{Attrs.class_("w-48")}, list{text("Forges")}), - div(list{Attrs.class_("w-8 text-center")}, list{text("Auto")}), - }, - ) -} - -/// Render grouped repos (for By Group / By Language views). -let renderGroupedRepos = (groups: array<(string, array)>): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - groups - ->Array.map(((groupName, repos)) => { - div( - list{Attrs.class_("border border-gray-800 rounded-lg overflow-hidden")}, - list{ - // Group header - div( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800/50 text-sm font-medium text-gray-300 flex items-center justify-between", - ), - }, - list{ - text(groupName), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(repos))} repos`)}, - ), - }, - ), - // Repo rows - div(list{Attrs.class_("")}, repos->Array.map(renderRepoRow)->List.fromArray), - }, - ) - }) - ->List.fromArray, - ) -} - -/// Render the forge coverage summary (for By Forge view). -let renderForgeCoverage = (repos: array): Tea_Vdom.t => { - let forgeCounts = FarmEngine.countByForge(repos) - let total = Array.length(repos) - div( - list{Attrs.class_("space-y-3 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text(`Forge coverage across ${Int.toString(total)} repos`)}, - ), - div( - list{Attrs.class_("space-y-2")}, - forgeCounts - ->Array.map(((forgeName, count)) => { - let pct = - total > 0 - ? Float.toFixed(Int.toFloat(count) /. Int.toFloat(total) *. 100.0, ~digits=0) - : "0" - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div(list{Attrs.class_("w-24 text-sm text-gray-300")}, list{text(forgeName)}), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500 rounded-full transition-all"), - Attrs.style("width", `${pct}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-16 text-xs text-gray-500 text-right")}, - list{text(`${Int.toString(count)}/${Int.toString(total)}`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Render the main content area based on active category. -let renderContent = (farm: farmState): Tea_Vdom.t => { - let filtered = FarmEngine.filterRepos(farm.repos, farm.filterText) - let sorted = FarmEngine.sortRepos(filtered, farm.sortBy) - - switch farm.activeCategory { - | AllRepos => - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - renderTableHeader(), - div(list{Attrs.class_("")}, sorted->Array.map(renderRepoRow)->List.fromArray), - }, - ) - | ByGroup => - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{renderGroupedRepos(FarmEngine.groupByGroup(filtered))}, - ) - | ByLanguage => - div( - list{Attrs.class_("flex-1 overflow-y-auto p-4")}, - list{renderGroupedRepos(FarmEngine.groupByLanguage(filtered))}, - ) - | ByForge => - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{renderForgeCoverage(filtered)}) - | Enrollment => { - let farmCount = filtered->Array.filter(r => r.enrollment.farm)->Array.length - let hypatiaCount = filtered->Array.filter(r => r.enrollment.hypatia)->Array.length - let fleetCount = filtered->Array.filter(r => r.enrollment.fleet)->Array.length - let totalCount = Array.length(filtered) - let pctBar = (count: int): string => - if totalCount > 0 { - Float.toFixed(Int.toFloat(count) /. Int.toFloat(totalCount) *. 100.0, ~digits=0) - } else { - "0" - } - div( - list{ - Attrs.class_("flex-1 overflow-y-auto p-4 space-y-6"), - Attrs.role("region"), - Attrs.ariaLabel("Three-tier enrollment status"), - }, - list{ - // Summary heading - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-2")}, - list{text("Three-Tier Enrollment Pipeline")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{ - text( - "Each tier builds on the previous: Farm (admin registry) > Hypatia (scanning) > Fleet (execution)", - ), - }, - ), - // Tier bars - div( - list{Attrs.class_("space-y-3")}, - list{ - // Tier 1: Farm - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-36 text-sm text-emerald-400 font-medium")}, - list{text("git-private-farm")}, - ), - div( - list{Attrs.class_("flex-1 h-4 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-emerald-600 rounded-full transition-all"), - Attrs.prop("style", `width: ${pctBar(farmCount)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-20 text-xs text-gray-400 text-right")}, - list{text(`${Int.toString(farmCount)}/${Int.toString(totalCount)}`)}, - ), - }, - ), - // Tier 2: Hypatia - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-36 text-sm text-indigo-400 font-medium")}, - list{text("hypatia")}, - ), - div( - list{Attrs.class_("flex-1 h-4 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-600 rounded-full transition-all"), - Attrs.prop("style", `width: ${pctBar(hypatiaCount)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-20 text-xs text-gray-400 text-right")}, - list{text(`${Int.toString(hypatiaCount)}/${Int.toString(totalCount)}`)}, - ), - }, - ), - // Tier 3: Fleet - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-36 text-sm text-purple-400 font-medium")}, - list{text("gitbot-fleet")}, - ), - div( - list{Attrs.class_("flex-1 h-4 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-purple-600 rounded-full transition-all"), - Attrs.prop("style", `width: ${pctBar(fleetCount)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-20 text-xs text-gray-400 text-right")}, - list{text(`${Int.toString(fleetCount)}/${Int.toString(totalCount)}`)}, - ), - }, - ), - }, - ), - // Per-repo enrollment table - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden mt-4")}, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-4 py-2 border-b border-gray-700 text-xs font-medium text-gray-500 uppercase tracking-wider", - ), - }, - list{ - div(list{Attrs.class_("flex-1")}, list{text("Repository")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Farm")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Hypatia")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Fleet")}), - }, - ), - // Rows - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - sorted - ->Array.map(repo => { - let tierDot = (enrolled: bool): Tea_Vdom.t => - span( - list{ - Attrs.class_(enrolled ? "text-emerald-400" : "text-gray-700"), - Attrs.ariaLabel(enrolled ? "Enrolled" : "Not enrolled"), - }, - list{text(enrolled ? "Y" : "-")}, - ) - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-4 py-2 border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors", - ), - Attrs.ariaLabel(`${repo.name} enrollment status`), - }, - list{ - div( - list{Attrs.class_("flex-1 text-sm text-gray-300 truncate")}, - list{text(repo.name)}, - ), - div( - list{Attrs.class_("w-16 text-center")}, - list{tierDot(repo.enrollment.farm)}, - ), - div( - list{Attrs.class_("w-16 text-center")}, - list{tierDot(repo.enrollment.hypatia)}, - ), - div( - list{Attrs.class_("w-16 text-center")}, - list{tierDot(repo.enrollment.fleet)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ) - } - | Health => { - let unhealthy = - filtered - ->Array.filter(r => - switch r.healthScore { - | Some(s) => s < 0.5 - | None => false - } - ) - ->Array.length - let unassessed = filtered->Array.filter(r => Option.isNone(r.healthScore))->Array.length - let withAlerts = filtered->Array.filter(r => r.hasDependabotAlerts)->Array.length - div( - list{ - Attrs.class_("flex-1 overflow-y-auto p-4 space-y-6"), - Attrs.role("region"), - Attrs.ariaLabel("Farm health dashboard"), - }, - list{ - // Quick stats - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_(unhealthy > 0 ? "text-red-400" : "text-gray-400")}, - list{text(`Unhealthy (score < 0.5): ${Int.toString(unhealthy)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Unassessed: ${Int.toString(unassessed)}`)}, - ), - div( - list{Attrs.class_(withAlerts > 0 ? "text-amber-400" : "text-gray-400")}, - list{text(`Dependabot alerts: ${Int.toString(withAlerts)}`)}, - ), - }, - ), - // Hypatia integration prompt - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-6 text-center")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-3")}, - list{text("Health scores are populated by Hypatia neurosymbolic scanning.")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{ - text( - `${Int.toString(unassessed)} of ${Int.toString( - Array.length(filtered), - )} repos have not been assessed yet.`, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-indigo-600 text-white text-sm rounded hover:bg-indigo-500 transition-colors", - ), - Attrs.ariaLabel("Open Hypatia panel to run health scans"), - Events.onClick(PanelSwitcher(TogglePanel(PanelHypatia))), - }, - list{text("Open Hypatia")}, - ), - }, - ), - }, - ) - } - } -} - -/// Render the header bar with title, stats summary, and controls. -let renderHeader = (farm: farmState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center justify-between px-6 py-4 border-b border-gray-800")}, - list{ - // Title and stats - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Git-Private-Farm")}, - ), - if farm.loaded { - div( - list{Attrs.class_("flex items-center gap-3 text-xs text-gray-500")}, - list{ - span(list{}, list{text(`${Int.toString(farm.totalRepos)} repos`)}), - span(list{Attrs.class_("text-gray-700")}, list{text("|")}), - span(list{}, list{text(`${Int.toString(Array.length(farm.repos))} loaded`)}), - }, - ) - } else { - noNode - }, - }, - ), - // Controls: filter, sort, close - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Filter input - input( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-700 rounded px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-indigo-500 focus:outline-none w-48", - ), - Attrs.placeholder("Filter repos..."), - Attrs.value(farm.filterText), - Events.onInput(text => Farm(SetFarmFilter(text))), - }, - list{}, - ), - // Close button - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ) -} - -/// Render a loading state. -let renderLoading = (): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-gray-500 animate-pulse")}, - list{text("Loading farm manifest...")}, - ), - }, - ) -} - -/// Render an error state. -let renderError = (error: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div(list{Attrs.class_("text-red-400 mb-2")}, list{text("Failed to load farm manifest")}), - div(list{Attrs.class_("text-sm text-gray-500 mb-4")}, list{text(error)}), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(Farm(LoadRepos)), - KeyboardNav.onActivate(Farm(LoadRepos)), - }, - list{text("Retry")}, - ), - }, - ), - }, - ) -} - -/// Main Farm panel view — full-screen overlay. -let view = (farm: farmState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Git-Private-Farm panel"), - }, - list{ - // Header - renderHeader(farm), - // Category tabs - renderCategoryTabBar(farm.activeCategory), - // Content area - if farm.loading { - renderLoading() - } else { - switch farm.error { - | Some(e) => renderError(e) - | None => - if !farm.loaded { - renderLoading() - } else { - renderContent(farm) - } - } - }, - }, - ) -} diff --git a/src/components/FeedbackOTron.affine b/src/components/FeedbackOTron.affine new file mode 100644 index 00000000..b40ef774 --- /dev/null +++ b/src/components/FeedbackOTron.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FeedbackOTron; + +// TODO: Complete semantic implementation diff --git a/src/components/FeedbackOTron.res b/src/components/FeedbackOTron.res deleted file mode 100644 index 5d6923ee..00000000 --- a/src/components/FeedbackOTron.res +++ /dev/null @@ -1,348 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Feedback-O-Tron Component -/// -/// The "Voice of the Arena" module for collective governance. -/// Captures context-aware reports on Agent performance and -/// enables crowdsourced constraint suggestions. - -open Msg -open Tea.Html - -/// Report types for Orbital Decay -type reportType = - | Hallucination - | ConstraintViolation - | PerformanceIssue - | UXFriction - | FeatureRequest - -/// Get report type label -let getReportLabel = (rt: reportType): string => { - switch rt { - | Hallucination => "Hallucination" - | ConstraintViolation => "Constraint Violation" - | PerformanceIssue => "Performance Issue" - | UXFriction => "UX Friction" - | FeatureRequest => "Feature Request" - } -} - -/// Map report type to string value -let reportTypeToString = (rt: reportType): string => { - switch rt { - | Hallucination => "Hallucination" - | ConstraintViolation => "ConstraintViolation" - | PerformanceIssue => "PerformanceIssue" - | UXFriction => "UXFriction" - | FeatureRequest => "FeatureRequest" - } -} - -/// Get report type colour -let getReportColour = (rt: reportType): string => { - switch rt { - | Hallucination => "bg-red-600" - | ConstraintViolation => "bg-amber-600" - | PerformanceIssue => "bg-orange-600" - | UXFriction => "bg-yellow-600" - | FeatureRequest => "bg-blue-600" - } -} - -/// Render a report type button -let renderReportTypeButton = (rt: reportType, selectedType: option): Tea_Vdom.t => { - let baseClass = "px-3 py-1 rounded text-xs transition-all" - let colour = getReportColour(rt) - let isSelected = selectedType === Some(reportTypeToString(rt)) - let selectedClass = isSelected - ? `${colour} text-white` - : "bg-gray-800 text-gray-400 hover:bg-gray-700" - - button( - list{ - Attrs.class_(`${baseClass} ${selectedClass}`), - Events.onClick(Feedback(SetReportType(reportTypeToString(rt)))), - }, - list{text(getReportLabel(rt))}, - ) -} - -/// Render the BoJ context snapshot section for feedback reports. -/// Captures cartridge server state so maintainers can correlate -/// user-reported issues with backend conditions. -let renderBojContext = (boj: BojModel.bojState): Tea_Vdom.t => { - let connectedLabel = boj.connected ? "Connected" : "Disconnected" - let connectedColour = boj.connected ? "text-emerald-400" : "text-red-400" - let cartridgeCount = Array.length(boj.cartridges) - let loadedCount = boj.cartridges->Array.filter(c => c.loaded)->Array.length - let federationLabel = boj.umoja.active ? "Active" : "Inactive" - let federationColour = boj.umoja.active ? "text-emerald-400" : "text-gray-500" - let peerCount = Array.length(boj.umoja.peers) - - let lastResultView = switch boj.invokeResult { - | Some(result) => - let statusLabel = result.success ? "OK" : "FAIL" - let statusColour = result.success ? "text-emerald-400" : "text-red-400" - div( - list{Attrs.class_("flex justify-between")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text("Last Invoke")}), - span( - list{Attrs.class_(statusColour)}, - list{text(`${statusLabel} (${Int.toString(result.durationMs)}ms)`)}, - ), - }, - ) - | None => - div( - list{Attrs.class_("flex justify-between")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text("Last Invoke")}), - span(list{Attrs.class_("text-gray-600")}, list{text("None")}), - }, - ) - } - - let errorView = switch boj.error { - | Some(err) => - div( - list{Attrs.class_("flex justify-between")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text("Error")}), - span( - list{Attrs.class_("text-red-400 truncate ml-2 max-w-[200px]"), Attrs.title(err)}, - list{text(err)}, - ), - }, - ) - | None => noNode - } - - div( - list{Attrs.class_("bg-sky-900/20 p-2 rounded")}, - list{ - div(list{Attrs.class_("text-sky-400 mb-1")}, list{text("BoJ Server")}), - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("flex justify-between")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text("Status")}), - span(list{Attrs.class_(connectedColour)}, list{text(connectedLabel)}), - }, - ), - div( - list{Attrs.class_("flex justify-between")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text("Cartridges")}), - span( - list{Attrs.class_("text-gray-300")}, - list{text(`${Int.toString(loadedCount)}/${Int.toString(cartridgeCount)} loaded`)}, - ), - }, - ), - lastResultView, - errorView, - div( - list{Attrs.class_("flex justify-between")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text("Umoja")}), - span( - list{Attrs.class_(federationColour)}, - list{text(`${federationLabel} (${Int.toString(peerCount)} peers)`)}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render the feedback form -let renderFeedbackForm = ( - pendingReport: option, - feedbackError: option, - selectedType: option, - boj: BojModel.bojState, -): Tea_Vdom.t => { - let reportTypes = [ - Hallucination, - ConstraintViolation, - PerformanceIssue, - UXFriction, - FeatureRequest, - ] - let errorView = switch feedbackError { - | Some(err) => div(list{Attrs.class_("mt-2 text-xs text-red-400")}, list{text(err)}) - | None => noNode - } - - div( - list{Attrs.class_("fixed inset-0 bg-black/80 flex items-center justify-center z-50")}, - list{ - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg w-[500px] max-h-[80vh] overflow-auto", - ), - Attrs.role("dialog"), - Attrs.ariaLabel("Feedback Form"), - }, - list{ - // Header - div( - list{Attrs.class_("p-4 border-b border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("text-lg font-semibold text-gray-200")}, - list{text("Feedback-O-Tron")}, - ), - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300"), - Attrs.ariaLabel("Close feedback form"), - Events.onClick(Feedback(CancelFeedback)), - KeyboardNav.onActivate(Feedback(CancelFeedback)), - }, - list{text("×")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text("Report Orbital Decay to the Community")}, - ), - }, - ), - // Report type selection - div( - list{Attrs.class_("p-4 border-b border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("REPORT TYPE")}), - div( - list{Attrs.class_("flex flex-wrap gap-2"), Attrs.role("radiogroup")}, - reportTypes - ->Array.map(rt => renderReportTypeButton(rt, selectedType)) - ->List.fromArray, - ), - }, - ), - // Description - div( - list{Attrs.class_("p-4 border-b border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("DESCRIPTION")}), - textarea( - list{ - Attrs.class_( - "w-full h-24 bg-gray-800 border border-gray-700 rounded p-3 text-sm text-gray-300 resize-none focus:border-gray-500 focus:outline-none", - ), - Attrs.placeholder("Describe the issue..."), - Attrs.value(Option.getOr(pendingReport, "")), - Events.onInput(value => Feedback(SubmitFeedback(value))), - }, - list{}, - ), - errorView, - }, - ), - // Context snapshot info - div( - list{Attrs.class_("p-4 border-b border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("CONTEXT SNAPSHOT")}), - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_("bg-indigo-900/30 p-2 rounded")}, - list{ - div(list{Attrs.class_("text-indigo-400")}, list{text("Panel-L")}), - div(list{Attrs.class_("text-gray-500")}, list{text("Captured")}), - }, - ), - div( - list{Attrs.class_("bg-emerald-900/30 p-2 rounded")}, - list{ - div(list{Attrs.class_("text-emerald-400")}, list{text("Panel-N")}), - div(list{Attrs.class_("text-gray-500")}, list{text("Captured")}), - }, - ), - div( - list{Attrs.class_("bg-gray-800/50 p-2 rounded")}, - list{ - div(list{Attrs.class_("text-gray-400")}, list{text("Panel-W")}), - div(list{Attrs.class_("text-gray-500")}, list{text("Captured")}), - }, - ), - renderBojContext(boj), - }, - ), - }, - ), - // Actions - div( - list{Attrs.class_("p-4 flex justify-end gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded text-sm text-gray-400 transition-colors", - ), - Events.onClick(Feedback(CancelFeedback)), - KeyboardNav.onActivate(Feedback(CancelFeedback)), - }, - list{text("Cancel")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-emerald-600 hover:bg-emerald-500 rounded text-sm text-white transition-colors", - ), - Events.onClick(Feedback(FeedbackSubmitted)), - KeyboardNav.onActivate(Feedback(FeedbackSubmitted)), - }, - list{text("Submit Report")}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render the feedback trigger button -let renderTriggerButton = (): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - "fixed bottom-4 left-4 px-3 py-2 bg-gray-800 hover:bg-gray-700 border border-gray-700 rounded text-xs text-gray-400 transition-colors", - ), - Attrs.title("Open the Feedback-O-Tron to report an issue or suggest an improvement"), - Attrs.ariaLabel("Report Issue"), - Events.onClick(Feedback(OpenFeedback)), - KeyboardNav.onActivate(Feedback(OpenFeedback)), - }, - list{text("Report Issue")}, - ) -} - -/// Main Feedback-O-Tron view -let view = ( - feedbackPending: option, - feedbackError: option, - selectedType: option, - boj: BojModel.bojState, -): Tea_Vdom.t => { - switch feedbackPending { - | Some(_) => renderFeedbackForm(feedbackPending, feedbackError, selectedType, boj) - | None => renderTriggerButton() - } -} diff --git a/src/components/FeedbackRouting.affine b/src/components/FeedbackRouting.affine new file mode 100644 index 00000000..dccfb171 --- /dev/null +++ b/src/components/FeedbackRouting.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FeedbackRouting; + +// TODO: Complete semantic implementation diff --git a/src/components/FeedbackRouting.res b/src/components/FeedbackRouting.res deleted file mode 100644 index 765543d7..00000000 --- a/src/components/FeedbackRouting.res +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Feedback Routing Component — upstream bug report status and integration map. -/// -/// Two-column layout: left sidebar with report list, right content with -/// detail view showing report status, platform, and external links. - -open Model -open Msg -open Tea.Html - -/// Render a report status badge. -let statusBadge = (status: reportStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | ReportFiled => ("text-blue-400", "Filed") - | ReportAcknowledged => ("text-cyan-400", "Ack") - | ReportInProgress => ("text-amber-400", "In Progress") - | ReportResolved => ("text-green-400", "Resolved") - | ReportClosed => ("text-gray-400", "Closed") - | ReportWontFix => ("text-red-400", "Won't Fix") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a report row in the sidebar. -let reportRow = (report: feedbackReport, selected: bool): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-2 border-b border-gray-800 hover:bg-gray-800/60 transition-colors " ++ if ( - selected - ) { - "bg-gray-800/80 border-l-2 border-l-blue-500" - } else { - "" - }, - ), - Events.onClick(FeedbackRouting(SelectReport(report.reportId))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(report.title)}), - statusBadge(report.status), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{ - text(`${FeedbackRoutingEngine.platformLabel(report.platform)} | ${report.targetRepo}`), - }, - ), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: feedbackRoutingTab, target: feedbackRoutingTab, label: string): Tea_Vdom.t< - msg, -> => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(FeedbackRouting(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Feedback Routing panel. -let view = (state: feedbackRoutingState): Tea_Vdom.t => { - let openCount = FeedbackRoutingEngine.openReportCount(state.reports) - let total = Array.length(state.reports) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Feedback Routing — Upstream Bug Report Status"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-pink-300")}, - list{text("Feedback Routing")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(openCount)} open / ${Int.toString(total)} total`)}, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600"), - Events.onClick(FeedbackRouting(RefreshReports)), - KeyboardNav.onActivate(FeedbackRouting(RefreshReports)), - }, - list{ - text( - if state.refreshing { - "Refreshing..." - } else { - "Refresh" - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - FeedbackRoutingEngine.allTabs - ->Array.map(t => tabBtn(state.activeTab, t, FeedbackRoutingEngine.tabLabel(t))) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar - div( - list{Attrs.class_("w-72 border-r border-gray-800 overflow-y-auto")}, - state.reports - ->Array.map(r => reportRow(r, state.selectedReport == Some(r.reportId))) - ->List.fromArray, - ), - // Right content - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.selectedReport { - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a report to view details")}, - ) - | Some(reportId) => - switch state.reports->Array.find(r => r.reportId == reportId) { - | None => div(list{}, list{text("Report not found")}) - | Some(report) => - div( - list{}, - list{ - h3( - list{Attrs.class_("text-md font-semibold text-gray-200 mb-3")}, - list{text(report.title)}, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Status:")}, - ), - statusBadge(report.status), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Platform:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300")}, - list{text(FeedbackRoutingEngine.platformLabel(report.platform))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Target:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300")}, - list{text(report.targetRepo)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Filed:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(report.dateFiled)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Last Updated:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(report.lastUpdated)}, - ), - }, - ), - switch report.externalUrl { - | Some(url) => - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("URL:")}, - ), - span( - list{Attrs.class_("text-xs text-blue-400 font-mono truncate")}, - list{text(url)}, - ), - }, - ) - | None => noNode - }, - }, - ), - }, - ) - } - }, - }, - ), - }, - ), - // Footer - div( - list{Attrs.class_("px-4 py-2 border-t border-gray-800 text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.platformStats))} platforms tracked`)}, - ), - }, - ) -} diff --git a/src/components/Fleet.affine b/src/components/Fleet.affine new file mode 100644 index 00000000..37c5c032 --- /dev/null +++ b/src/components/Fleet.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Fleet; + +// TODO: Complete semantic implementation diff --git a/src/components/Fleet.res b/src/components/Fleet.res deleted file mode 100644 index 5769e8e2..00000000 --- a/src/components/Fleet.res +++ /dev/null @@ -1,633 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Fleet Component — Gitbot-Fleet orchestration dashboard. -/// -/// Renders the 6-bot status grid, safety triangle visualisation, -/// findings queue with filtering, and dispatch controls. Every -/// interactive element has ARIA labels and keyboard navigation. - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Bot Status Card -// ============================================================================ - -/// Render a single bot status card in the fleet grid. -let renderBotCard = (bot: botState): Tea_Vdom.t => { - let dotColor = FleetEngine.statusColor(bot.status) - let label = FleetEngine.botLabel(bot.id) - let desc = FleetEngine.botDescription(bot.id) - let statusText = FleetEngine.statusLabel(bot.status) - let icon = FleetEngine.botIcon(bot.id) - - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-4 hover:border-gray-500 transition-colors", - ), - Attrs.role("article"), - Attrs.ariaLabel(`${label} — ${statusText}`), - }, - list{ - // Header: icon + bot name + status dot - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-1.5")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500"), Attrs.prop("aria-hidden", "true")}, - list{text(icon)}, - ), - span(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(label)}), - }, - ), - span( - list{ - Attrs.class_(`w-2.5 h-2.5 rounded-full ${dotColor}`), - Attrs.ariaLabel(statusText), - Attrs.role("status"), - }, - list{}, - ), - }, - ), - // Description - div(list{Attrs.class_("text-xs text-gray-500 mb-3")}, list{text(desc)}), - // Metrics row - div( - list{Attrs.class_("flex justify-between text-xs")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Queued: ${Int.toString(bot.queuedFindings)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Done: ${Int.toString(bot.processedFindings)}`)}, - ), - }, - ), - // Confidence threshold - div( - list{Attrs.class_("mt-2 text-xs text-gray-500")}, - list{text(`Threshold: ${Float.toFixed(bot.confidenceThreshold, ~digits=0)}%`)}, - ), - }, - ) -} - -// ============================================================================ -// Safety Triangle -// ============================================================================ - -/// Render the safety triangle (Eliminate / Substitute / Control) with counts. -let renderSafetyTriangle = (health: fleetHealth): Tea_Vdom.t => { - let (elim, sub, ctrl) = health.triangleCounts - let total = elim + sub + ctrl - let pct = (count: int): string => { - if total <= 0 { - "0" - } else { - Float.toFixed(Int.toFloat(count) /. Int.toFloat(total) *. 100.0, ~digits=0) - } - } - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-6"), - Attrs.role("figure"), - Attrs.ariaLabel("Safety triangle — hierarchy of controls"), - }, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-4")}, - list{text("Safety Triangle")}, - ), - // Triangle tiers stacked vertically (inverted pyramid) - div( - list{Attrs.class_("space-y-2")}, - list{ - // Eliminate (top, narrowest = highest priority) - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-20 text-right text-xs text-red-400 font-medium")}, - list{text("Eliminate")}, - ), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-4 overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("bg-red-600 h-full rounded-full transition-all"), - Attrs.prop("style", `width: ${pct(elim)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-xs text-gray-400 text-right")}, - list{text(Int.toString(elim))}, - ), - }, - ), - // Substitute (middle) - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-20 text-right text-xs text-amber-400 font-medium")}, - list{text("Substitute")}, - ), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-4 overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("bg-amber-600 h-full rounded-full transition-all"), - Attrs.prop("style", `width: ${pct(sub)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-xs text-gray-400 text-right")}, - list{text(Int.toString(sub))}, - ), - }, - ), - // Control (bottom, widest = most common) - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-20 text-right text-xs text-blue-400 font-medium")}, - list{text("Control")}, - ), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-4 overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("bg-blue-600 h-full rounded-full transition-all"), - Attrs.prop("style", `width: ${pct(ctrl)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-xs text-gray-400 text-right")}, - list{text(Int.toString(ctrl))}, - ), - }, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Finding Row -// ============================================================================ - -/// Render a single finding in the findings list. -let renderFinding = (finding: fleetFinding): Tea_Vdom.t => { - let tierBadge = FleetEngine.tierColor(finding.tier) - let tierText = FleetEngine.tierLabel(finding.tier) - let assignedText = switch finding.assignedBot { - | Some(bot) => FleetEngine.botLabel(bot) - | None => "Unassigned" - } - - div( - list{ - Attrs.class_( - `flex items-center gap-4 p-3 border-b border-gray-800 ${finding.resolved - ? "opacity-50" - : ""}`, - ), - Attrs.role("row"), - Attrs.ariaLabel(`${finding.summary} — ${tierText} — ${assignedText}`), - }, - list{ - // Tier badge - span(list{Attrs.class_(`text-xs px-2 py-0.5 rounded ${tierBadge}`)}, list{text(tierText)}), - // Repo name - span(list{Attrs.class_("text-xs text-gray-500 w-32 truncate")}, list{text(finding.repoName)}), - // Summary - span( - list{Attrs.class_("flex-1 text-sm text-gray-300 truncate")}, - list{text(finding.summary)}, - ), - // Confidence - span( - list{Attrs.class_("text-xs text-gray-400 w-16 text-right")}, - list{text(`${Float.toFixed(finding.confidence *. 100.0, ~digits=0)}%`)}, - ), - // Assigned bot - span(list{Attrs.class_("text-xs text-gray-500 w-24 text-right")}, list{text(assignedText)}), - }, - ) -} - -// ============================================================================ -// Dashboard View -// ============================================================================ - -/// Render the fleet dashboard — bot grid + safety triangle + health summary. -let renderDashboard = (fleet: fleetState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-6")}, - list{ - // Health summary bar - switch fleet.health { - | Some(health) => - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Active: ${Int.toString(health.activeBots)}/6 bots`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Queued: ${Int.toString(health.totalQueued)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Processed: ${Int.toString(health.totalProcessed)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{ - text(`Avg confidence: ${Float.toFixed(health.avgConfidence *. 100.0, ~digits=0)}%`), - }, - ), - }, - ) - | None => - div(list{Attrs.class_("text-sm text-gray-500")}, list{text("No health data available")}) - }, - // Two-column layout: bot grid + safety triangle - div( - list{Attrs.class_("flex gap-6")}, - list{ - // Bot grid (2x3) - div( - list{ - Attrs.class_("flex-1 grid grid-cols-3 gap-3"), - Attrs.role("list"), - Attrs.ariaLabel("Fleet bot status"), - }, - fleet.bots->Array.map(bot => renderBotCard(bot))->List.fromArray, - ), - // Safety triangle (right column) - switch fleet.health { - | Some(health) => div(list{Attrs.class_("w-80")}, list{renderSafetyTriangle(health)}) - | None => noNode - }, - }, - ), - }, - ) -} - -// ============================================================================ -// Findings View -// ============================================================================ - -/// Render the findings queue with filtering. -let renderFindings = (fleet: fleetState): Tea_Vdom.t => { - let filtered = FleetEngine.filterFindings(fleet.findings, fleet.filterText) - div( - list{Attrs.class_("space-y-4")}, - list{ - // Filter input - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600", - ), - Attrs.placeholder("Filter findings by repo or summary..."), - Attrs.ariaLabel("Filter findings"), - Attrs.value(fleet.filterText), - Events.onInput(v => Fleet(SetFleetFilter(v))), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(filtered))} findings`)}, - ), - }, - ), - // Findings list - div( - list{ - Attrs.class_("border border-gray-700 rounded-lg overflow-hidden"), - Attrs.role("table"), - Attrs.ariaLabel("Findings queue"), - }, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center gap-4 p-3 bg-gray-900 border-b border-gray-700 text-xs text-gray-500", - ), - Attrs.role("row"), - }, - list{ - span(list{Attrs.class_("w-20")}, list{text("Tier")}), - span(list{Attrs.class_("w-32")}, list{text("Repo")}), - span(list{Attrs.class_("flex-1")}, list{text("Summary")}), - span(list{Attrs.class_("w-16 text-right")}, list{text("Conf.")}), - span(list{Attrs.class_("w-24 text-right")}, list{text("Assigned")}), - }, - ), - // Rows - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered->Array.map(f => renderFinding(f))->List.fromArray, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Dispatch View -// ============================================================================ - -/// Render the dispatch summary — stats derived from bot and findings state. -/// Shows per-bot dispatch counts, resolution rates, and assignment coverage. -let renderDispatch = (fleet: fleetState): Tea_Vdom.t => { - let totalResolved = fleet.findings->Array.filter(f => f.resolved)->Array.length - let totalFindings = Array.length(fleet.findings) - let assigned = fleet.findings->Array.filter(f => Option.isSome(f.assignedBot))->Array.length - let unassigned = totalFindings - assigned - div( - list{ - Attrs.class_("space-y-6"), - Attrs.role("region"), - Attrs.ariaLabel("Dispatch manifest summary"), - }, - list{ - // Summary stats row - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Total findings: ${Int.toString(totalFindings)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Resolved: ${Int.toString(totalResolved)}`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Assigned: ${Int.toString(assigned)}`)}, - ), - div( - list{Attrs.class_(unassigned > 0 ? "text-amber-400" : "text-gray-400")}, - list{text(`Unassigned: ${Int.toString(unassigned)}`)}, - ), - }, - ), - // Per-bot dispatch breakdown - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("Per-Bot Dispatch")}, - ), - div( - list{Attrs.class_("space-y-2")}, - fleet.bots - ->Array.map(bot => { - let assignedToBot = - fleet.findings - ->Array.filter(f => f.assignedBot === Some(bot.id)) - ->Array.length - let resolvedByBot = - fleet.findings - ->Array.filter(f => f.assignedBot === Some(bot.id) && f.resolved) - ->Array.length - let botName = FleetEngine.botLabel(bot.id) - div( - list{ - Attrs.class_("flex items-center gap-3"), - Attrs.ariaLabel( - `${botName}: ${Int.toString(assignedToBot)} assigned, ${Int.toString( - resolvedByBot, - )} resolved`, - ), - }, - list{ - div(list{Attrs.class_("w-28 text-sm text-gray-300")}, list{text(botName)}), - div( - list{Attrs.class_("flex-1 h-3 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500 rounded-full transition-all"), - Attrs.prop( - "style", - `width: ${if totalFindings > 0 { - Float.toFixed( - Int.toFloat(assignedToBot) /. Int.toFloat(totalFindings) *. 100.0, - ~digits=0, - ) - } else { - "0" - }}%`, - ), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-24 text-xs text-gray-500 text-right")}, - list{text(`${Int.toString(resolvedByBot)}/${Int.toString(assignedToBot)}`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Note about live dispatch history - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("Dispatch audit log requires a live connection to the fleet backend.")}, - ), - }, - ) -} - -// ============================================================================ -// Category Tabs -// ============================================================================ - -/// Render the category tab bar. -let renderTabs = (active: fleetCategory): Tea_Vdom.t => { - let tabs: array = [FleetDashboard, FleetFindings, FleetDispatch] - div( - list{ - Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), - Attrs.role("tablist"), - Attrs.ariaLabel("Fleet panel sections"), - }, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-indigo-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Fleet(SetFleetCategory(tab))), - }, - list{text(FleetEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main view for the Fleet panel — full-screen overlay. -let view = (fleet: fleetState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Gitbot Fleet orchestration panel"), - }, - list{ - // Header bar - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Gitbot Fleet")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("6-bot orchestration")}), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-500", - ), - Attrs.ariaLabel("Refresh fleet status"), - Events.onClick(Fleet(LoadFleet)), - KeyboardNav.onActivate(Fleet(LoadFleet)), - }, - list{text("Refresh")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Attrs.ariaLabel("Close Fleet panel"), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - if fleet.loading { - div( - list{Attrs.class_("text-gray-400"), Attrs.role("status")}, - list{text("Loading fleet status...")}, - ) - } else if !fleet.loaded { - div( - list{Attrs.class_("text-center text-gray-500 mt-12")}, - list{ - div(list{Attrs.class_("text-4xl mb-4")}, list{text("Fleet")}), - div( - list{Attrs.class_("text-sm mb-6")}, - list{text("6-bot gitbot-fleet orchestration")}, - ), - button( - list{ - Attrs.class_("px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-500"), - Events.onClick(Fleet(LoadFleet)), - KeyboardNav.onActivate(Fleet(LoadFleet)), - }, - list{text("Connect to Fleet")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderTabs(fleet.activeCategory), - switch fleet.activeCategory { - | FleetDashboard => renderDashboard(fleet) - | FleetFindings => renderFindings(fleet) - | FleetDispatch => renderDispatch(fleet) - }, - }, - ) - }, - // Error display - switch fleet.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/FleetAgenticBridge.affine b/src/components/FleetAgenticBridge.affine new file mode 100644 index 00000000..26b16790 --- /dev/null +++ b/src/components/FleetAgenticBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FleetAgenticBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/FleetAgenticBridge.res b/src/components/FleetAgenticBridge.res deleted file mode 100644 index 1af8cee6..00000000 --- a/src/components/FleetAgenticBridge.res +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Fleet-AgenticBridge Wiring — dispatches Fleet bot actions to -/// AgenticBridge OODA phases and updates agent status on bot task completion. -/// -/// When a Fleet bot completes a finding (e.g., echidnabot resolves a security -/// finding), this module maps the bot action into the AgenticBridge's OODA -/// loop so automated playtesting agents can react to fleet-driven changes. -/// -/// The mapping is: -/// - Bot finding assigned -> AgenticBridge agent enters Observe phase -/// - Bot processing finding -> AgenticBridge agent enters Orient phase -/// - Bot applying fix -> AgenticBridge agent enters Decide phase -/// - Bot resolving finding -> AgenticBridge agent enters Act + Completed - -open Model - -/// Map a Fleet bot ID to an AgenticBridge OODA phase based on the bot's -/// current processing stage. Returns the OODA phase and a descriptive action. -let botActionToOodaPhase = (botId: FleetModel.botId, resolved: bool): (oodaPhase, string) => { - let botName = switch botId { - | Rhodibot => "rhodibot" - | Echidnabot => "echidnabot" - | Sustainabot => "sustainabot" - | Glambot => "glambot" - | Seambot => "seambot" - | Finishbot => "finishbot" - } - if resolved { - (Act, botName ++ " resolved finding — applying changes") - } else { - (Observe, botName ++ " assigned finding — observing impact") - } -} - -/// Derive an updated AgenticBridge agent status from a Fleet bot's state. -/// When a bot completes all queued findings, its corresponding agent is -/// marked as Completed. Active bots map to Running agents. -let botStatusToAgentStatus = (botStatus: FleetModel.botStatus): agentStatus => { - switch botStatus { - | BotActive => AgentRunning - | BotIdle => AgentIdle - | BotOffline => AgentPaused - | BotError(_) => AgentFailed - } -} - -/// Build a bridge action record from a Fleet finding resolution event. -/// This creates an AgenticBridge action entry that appears in the Execution -/// tab, linking Fleet findings to OODA-phase tracking. -let buildBridgeAction = (finding: FleetModel.fleetFinding, phase: oodaPhase): agentAction => { - { - phase, - description: "Fleet: " ++ finding.summary, - targetPath: finding.repoName, - timestampMs: 0.0, // Caller should set actual timestamp - } -} diff --git a/src/components/FloorRaise.affine b/src/components/FloorRaise.affine new file mode 100644 index 00000000..3693035d --- /dev/null +++ b/src/components/FloorRaise.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FloorRaise; + +// TODO: Complete semantic implementation diff --git a/src/components/FloorRaise.res b/src/components/FloorRaise.res deleted file mode 100644 index 4f0c650d..00000000 --- a/src/components/FloorRaise.res +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Floor Raise Component — foundational tool adoption dashboard. -/// -/// Master dashboard for the Floor Raise campaign. Shows adoption metrics -/// for all foundational tools, active dispatch campaigns, recent fix -/// outcomes, and gap analysis. - -open Model -open Msg -open Tea.Html - -/// Render a progress bar for a tool adoption metric. -let progressBar = (adoption: toolAdoption): Tea_Vdom.t => { - let pctStr = Float.toFixed(adoption.percentage, ~digits=1) - let barColor = if adoption.percentage > 80.0 { - "bg-green-500" - } else if adoption.percentage > 50.0 { - "bg-amber-500" - } else { - "bg-red-500" - } - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 font-medium")}, list{text(adoption.name)}), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text(`${Int.toString(adoption.adoptedCount)}/${Int.toString(adoption.targetCount)}`), - }, - ), - }, - ), - div( - list{Attrs.class_("w-full bg-gray-800 rounded-full h-2")}, - list{ - div( - list{ - Attrs.class_(`${barColor} h-2 rounded-full transition-all duration-300`), - Attrs.style("width", `${pctStr}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center justify-between mt-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(`${pctStr}%`)}), - if adoption.campaignActive { - span( - list{Attrs.class_("text-xs text-blue-400 animate-pulse")}, - list{text("Campaign Active")}, - ) - } else { - noNode - }, - }, - ), - }, - ) -} - -/// Render a dispatch outcome row. -let outcomeRow = (outcome: dispatchOutcome): Tea_Vdom.t => { - let statusColor = if outcome.success { - "text-green-400" - } else { - "text-red-400" - } - let statusLabel = if outcome.success { - "OK" - } else { - "FAIL" - } - div( - list{Attrs.class_("flex items-center gap-3 py-2 px-2 border-b border-gray-800 text-xs")}, - list{ - span(list{Attrs.class_("text-gray-500 w-36 shrink-0")}, list{text(outcome.timestamp)}), - span(list{Attrs.class_("text-gray-300 w-40 shrink-0 truncate")}, list{text(outcome.repo)}), - span( - list{Attrs.class_("text-gray-400 w-32 shrink-0 truncate")}, - list{text(outcome.fixScript)}, - ), - span(list{Attrs.class_("text-gray-400 w-32 shrink-0")}, list{text(outcome.category)}), - span(list{Attrs.class_(`${statusColor} w-12 font-mono`)}, list{text(statusLabel)}), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: floorRaiseTab, target: floorRaiseTab, label: string): Tea_Vdom.t => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(FloorRaise(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Floor Raise panel. -let view = (state: floorRaiseState): Tea_Vdom.t => { - let progress = FloorRaiseEngine.overallProgress(state) - let progressStr = Float.toFixed(progress, ~digits=1) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Floor Raise — Foundational Tool Adoption Dashboard"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-bold text-blue-300")}, list{text("Floor Raise")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Overall: ${progressStr}% | ${Int.toString(state.totalRepos)} repos`)}, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600", - ), - Events.onClick(FloorRaise(ScanAdoption)), - KeyboardNav.onActivate(FloorRaise(ScanAdoption)), - }, - list{ - text( - if state.scanning { - "Scanning..." - } else { - "Scan Adoption" - }, - ), - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - list{ - tabBtn(state.activeTab, TabOverview, "Overview"), - tabBtn(state.activeTab, TabCampaigns, "Campaigns"), - tabBtn(state.activeTab, TabOutcomes, "Outcomes"), - tabBtn(state.activeTab, TabGaps, "Gaps"), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - Events.onClick(FloorRaise(ClearError)), - KeyboardNav.onActivate(FloorRaise(ClearError)), - }, - list{text(err)}, - ) - | None => noNode - }, - // Content - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.activeTab { - | TabOverview => - div( - list{Attrs.class_("grid grid-cols-2 gap-3")}, - state.adoptions->Array.map(a => progressBar(a))->List.fromArray, - ) - | TabCampaigns => - div( - list{}, - state.adoptions - ->Array.filter(a => a.campaignActive) - ->Array.map(a => - div( - list{ - Attrs.class_( - "flex items-center justify-between py-2 px-3 border-b border-gray-800", - ), - }, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(a.name)}), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Float.toFixed(a.percentage, ~digits=1)}% adopted`)}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs rounded bg-blue-700 text-white hover:bg-blue-600", - ), - Events.onClick(FloorRaise(RunCampaign(a.name))), - }, - list{text("Dispatch")}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - | TabOutcomes => - div( - list{Attrs.role("table"), Attrs.ariaLabel("Dispatch outcomes")}, - state.outcomes->Array.map(o => outcomeRow(o))->List.fromArray, - ) - | TabGaps => - div( - list{}, - state.adoptions - ->Array.filter(a => a.percentage < 100.0) - ->Array.map(a => - div( - list{ - Attrs.class_( - "flex items-center justify-between py-2 px-3 border-b border-gray-800", - ), - }, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(a.name)}), - span( - list{Attrs.class_("text-xs text-red-400")}, - list{text(`${Int.toString(a.targetCount - a.adoptedCount)} repos missing`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - // Footer - div( - list{ - Attrs.class_( - "px-4 py-2 border-t border-gray-800 text-xs text-gray-500 flex justify-between", - ), - }, - list{ - span( - list{}, - list{ - text(`${Int.toString(FloorRaiseEngine.activeCampaignCount(state))} active campaigns`), - }, - ), - span( - list{}, - list{ - text( - `${Int.toString(FloorRaiseEngine.successCount(state.outcomes))}/${Int.toString( - Array.length(state.outcomes), - )} dispatches succeeded`, - ), - }, - ), - }, - ), - }, - ) -} diff --git a/src/components/FunctionalTester.affine b/src/components/FunctionalTester.affine new file mode 100644 index 00000000..8dd656e1 --- /dev/null +++ b/src/components/FunctionalTester.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FunctionalTester; + +// TODO: Complete semantic implementation diff --git a/src/components/FunctionalTester.res b/src/components/FunctionalTester.res deleted file mode 100644 index dda71475..00000000 --- a/src/components/FunctionalTester.res +++ /dev/null @@ -1,378 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL FunctionalTester — end-to-end game workflow simulation and validation. -/// -/// Provides four tabs: a workflow list with progress indicators, a step-by-step -/// editor for composing workflow sequences, a results view showing pass/fail -/// per step, and a templates library for common game-testing patterns. - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for functionalTestTab variants. -let tabLabel = (tab: functionalTestTab): string => - switch tab { - | TabWorkflows => "Workflows" - | TabEditor => "Editor" - | TabResults => "Results" - | TabTemplates => "Templates" - } - -/// Render the tab bar. -let renderTabs = (active: functionalTestTab): Tea_Vdom.t => { - let tabs: array = [TabWorkflows, TabEditor, TabResults, TabTemplates] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(FunctionalTester(SetFtTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Workflow status badge with colour and animation for running workflows. -let workflowStatusBadge = (status: workflowStatus): Tea_Vdom.t => { - let (colour, lbl) = switch status { - | WorkflowDraft => ("bg-gray-600 text-gray-200", "DRAFT") - | WorkflowReady => ("bg-blue-600 text-white", "READY") - | WorkflowRunning(step) => ( - "bg-amber-500 text-white animate-pulse", - `STEP ${Int.toString(step + 1)}`, - ) - | WorkflowPassed(ms) => ("bg-emerald-600 text-white", `PASS ${Float.toFixed(ms, ~digits=0)}ms`) - | WorkflowFailed(_, _) => ("bg-red-600 text-white", "FAIL") - } - span(list{Attrs.class_(`px-2 py-0.5 text-xs rounded font-mono ${colour}`)}, list{text(lbl)}) -} - -/// Progress bar for a workflow based on completed steps vs total. -let workflowProgress = (workflow: testWorkflow): Tea_Vdom.t => { - let total = Array.length(workflow.steps) - if total === 0 { - noNode - } else { - let completed = workflow.steps->Array.filter(s => Option.isSome(s.passed))->Array.length - let pct = Int.toString(Int.fromFloat(Int.toFloat(completed) /. Int.toFloat(total) *. 100.0)) - div( - list{Attrs.class_("w-full h-1.5 bg-gray-700 rounded overflow-hidden mt-1")}, - list{ - div( - list{Attrs.class_(`h-full bg-cyan-500 transition-all duration-300 w-[${pct}%]`)}, - list{}, - ), - }, - ) - } -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Workflows tab: list of all workflows with status badges and progress. -let renderWorkflowsTab = (state: functionalTesterState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.workflows))} workflow(s)`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-96 overflow-y-auto")}, - state.workflows - ->Array.map(wf => { - let isSelected = state.selectedWorkflow === Some(wf.id) - let borderCls = isSelected ? "border-cyan-600" : "border-gray-700" - div( - list{ - Attrs.class_(`bg-gray-800 rounded p-3 border ${borderCls} cursor-pointer`), - Events.onClick(FunctionalTester(SelectWorkflow(wf.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(wf.name)}, - ), - workflowStatusBadge(wf.status), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text(`${Int.toString(Array.length(wf.steps))} step(s)`)}, - ), - workflowProgress(wf), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Editor tab: step-by-step view for the selected workflow. -let renderEditorTab = (state: functionalTesterState): Tea_Vdom.t => { - let selectedWf = state.workflows->Array.find(wf => Some(wf.id) === state.selectedWorkflow) - switch selectedWf { - | None => - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("Select a workflow from the Workflows tab to edit its steps.")}, - ) - | Some(wf) => - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text(`Editing: ${wf.name}`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-96 overflow-y-auto")}, - wf.steps - ->Array.mapWithIndex((step, idx) => { - let stepColour = switch step.passed { - | Some(true) => "border-emerald-700" - | Some(false) => "border-red-700" - | None => "border-gray-700" - } - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${stepColour}`)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(`#${Int.toString(idx + 1)}`)}, - ), - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(step.action)}), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Expected: ${step.expectedOutcome}`)}, - ), - switch step.actualOutcome { - | Some(actual) => - div( - list{Attrs.class_("text-xs text-gray-400 mt-1")}, - list{text(`Actual: ${actual}`)}, - ) - | None => noNode - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Results tab: aggregated pass/fail results for all workflows. -let renderResultsTab = (state: functionalTesterState): Tea_Vdom.t => { - let passedCount = - state.workflows - ->Array.filter(wf => - switch wf.status { - | WorkflowPassed(_) => true - | _ => false - } - ) - ->Array.length - let failedCount = - state.workflows - ->Array.filter(wf => - switch wf.status { - | WorkflowFailed(_, _) => true - | _ => false - } - ) - ->Array.length - - div( - list{Attrs.class_("flex flex-col gap-3 p-4")}, - list{ - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(Array.length(state.workflows)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Total")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-emerald-400")}, - list{text(Int.toString(passedCount))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Passed")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-red-400")}, - list{text(Int.toString(failedCount))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Failed")}), - }, - ), - }, - ), - }, - ) -} - -/// Templates tab: library of reusable workflow templates. -let renderTemplatesTab = (_state: functionalTesterState): Tea_Vdom.t => { - let templates = [ - ("Login Flow", "Complete user authentication cycle"), - ("Tutorial Walkthrough", "First-time user experience path"), - ("Combat Loop", "Engage guard, evade, complete level"), - ("Inventory CRUD", "Create, equip, drop, destroy items"), - ("Multiplayer Join", "Connect, sync, verify game state"), - ] - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - templates - ->Array.map(((name, desc)) => { - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(name)}), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer", - ), - Events.onClick(FunctionalTester(LoadTemplate(name))), - }, - list{text("Use")}, - ), - }, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(desc)}), - }, - ) - }) - ->List.fromArray, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: functionalTesterState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabWorkflows => renderWorkflowsTab(state) - | TabEditor => renderEditorTab(state) - | TabResults => renderResultsTab(state) - | TabTemplates => renderTemplatesTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Functional Tester")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-blue-700 text-white rounded hover:bg-blue-600 cursor-pointer font-medium", - ), - Events.onClick(FunctionalTester(NewWorkflow)), - KeyboardNav.onActivate(FunctionalTester(NewWorkflow)), - }, - list{text("New Workflow")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(FunctionalTester(RunWorkflow(""))), - }, - list{text("Run")}, - ), - }, - ), - }, - ), - // Running indicator - if state.running { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-amber-400 rounded-full animate-pulse")}, list{}), - span(list{Attrs.class_("text-sm text-amber-300")}, list{text("Workflow running...")}), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/GamePreview.affine b/src/components/GamePreview.affine new file mode 100644 index 00000000..9def8fbe --- /dev/null +++ b/src/components/GamePreview.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GamePreview; + +// TODO: Complete semantic implementation diff --git a/src/components/GamePreview.res b/src/components/GamePreview.res deleted file mode 100644 index ec8b1a30..00000000 --- a/src/components/GamePreview.res +++ /dev/null @@ -1,620 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Game Preview Component — renders the live IDApTIK game preview panel. -/// -/// The Live Preview tab embeds the Vite dev server output via an iframe. -/// When the Gossamer multi-webview system is wired (Phase 2), the iframe -/// will be replaced by a dedicated webview for tighter integration. -/// -/// Additional tabs show the device interaction log, saved gameplay clips, -/// and render performance statistics. - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Render the category tab bar. -let renderTabs = (active: gamePreviewCategory): Tea_Vdom.t => { - let tabs: array = [ - PreviewLive, - PreviewDeviceLog, - PreviewClips, - PreviewPerformance, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - let label = GamePreviewEngine.categoryLabel(tab) - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900"}`, - ), - Events.onClick(GamePreview(SetPreviewCategory(tab))), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the game loop execution controls. -let renderExecutionControls = (state: gamePreviewState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Pause/Resume button - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium ${switch state.execution { - | GameRunning => "bg-amber-800 text-amber-200 hover:bg-amber-700" - | GamePaused | GameStepping => "bg-emerald-800 text-emerald-200 hover:bg-emerald-700" - }}`, - ), - Events.onClick( - GamePreview( - switch state.execution { - | GameRunning => PauseGame - | GamePaused | GameStepping => ResumeGame - }, - ), - ), - }, - list{ - text( - switch state.execution { - | GameRunning => "Pause" - | GamePaused | GameStepping => "Resume" - }, - ), - }, - ), - // Step button (only when paused) - switch state.execution { - | GamePaused | GameStepping => - button( - list{ - Attrs.class_("px-3 py-1.5 text-xs rounded bg-blue-800 text-blue-200 hover:bg-blue-700"), - Events.onClick(GamePreview(StepFrame)), - KeyboardNav.onActivate(GamePreview(StepFrame)), - }, - list{text("Step Frame")}, - ) - | GameRunning => noNode - }, - // Execution state indicator - div( - list{Attrs.class_("flex items-center gap-1 ml-2")}, - list{ - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${switch state.execution { - | GameRunning => "bg-emerald-400 animate-pulse" - | GamePaused => "bg-amber-400" - | GameStepping => "bg-blue-400" - }}`, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(GamePreviewEngine.executionLabel(state.execution))}, - ), - }, - ), - }, - ) -} - -/// Render the overlay toggle buttons. -let renderOverlayToggles = (activeOverlays: array): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - GamePreviewEngine.allOverlays - ->Array.map(overlay => { - let isActive = GamePreviewEngine.isOverlayActive(activeOverlays, overlay) - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded transition-colors ${isActive - ? "bg-cyan-800 text-cyan-200" - : "bg-gray-800 text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(GamePreview(ToggleOverlay(overlay))), - }, - list{text(GamePreviewEngine.overlayLabel(overlay))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the live preview tab — embedded game + toolbar. -let renderLivePreview = (state: gamePreviewState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex flex-col")}, - list{ - // Toolbar - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-2 bg-gray-900/50 border-b border-gray-800", - ), - }, - list{ - renderExecutionControls(state), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Recording controls - switch state.gameRecording { - | GameRecordingActive(_) => - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div( - list{Attrs.class_("w-2 h-2 rounded-full bg-red-500 animate-pulse")}, - list{}, - ), - span(list{Attrs.class_("text-xs text-red-400")}, list{text("REC")}), - button( - list{ - Attrs.class_("text-xs text-gray-400 hover:text-gray-200 px-2 py-1"), - Events.onClick(GamePreview(StopGameRecording)), - KeyboardNav.onActivate(GamePreview(StopGameRecording)), - }, - list{text("Stop")}, - ), - }, - ) - | GameRecordingPaused(_) => - span(list{Attrs.class_("text-xs text-yellow-400")}, list{text("REC PAUSED")}) - | GameRecordingIdle => - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300 px-2 py-1 rounded bg-gray-800", - ), - Events.onClick(GamePreview(StartGameRecording)), - KeyboardNav.onActivate(GamePreview(StartGameRecording)), - }, - list{text("Record")}, - ) - }, - // Screenshot button - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300 px-2 py-1 rounded bg-gray-800", - ), - Events.onClick(GamePreview(ScreenshotGame)), - KeyboardNav.onActivate(GamePreview(ScreenshotGame)), - }, - list{text("Screenshot")}, - ), - // Zoom controls - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-300 px-1"), - Events.onClick(GamePreview(SetZoom(state.zoomLevel -. 0.25))), - }, - list{text("-")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 w-10 text-center")}, - list{text(`${Float.toFixed(state.zoomLevel *. 100.0, ~digits=0)}%`)}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-300 px-1"), - Events.onClick(GamePreview(SetZoom(state.zoomLevel +. 0.25))), - }, - list{text("+")}, - ), - }, - ), - // Multiplayer view toggle - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded ${state.multiplayerView - ? "bg-purple-800 text-purple-200" - : "bg-gray-800 text-gray-500"}`, - ), - Events.onClick(GamePreview(ToggleMultiplayerView)), - KeyboardNav.onActivate(GamePreview(ToggleMultiplayerView)), - }, - list{text("Co-op View")}, - ), - }, - ), - }, - ), - // Overlay toggles - div( - list{Attrs.class_("px-4 py-2 border-b border-gray-800 bg-gray-900/30")}, - list{renderOverlayToggles(state.activeOverlays)}, - ), - // Game iframe - if state.devServerConnected { - div( - list{ - Attrs.class_("flex-1 relative bg-black"), - Attrs.style("transform", `scale(${Float.toString(state.zoomLevel)})`), - Attrs.style("transform-origin", "center center"), - }, - list{ - node( - "iframe", - list{ - Attrs.src(state.devServerUrl), - Attrs.class_("w-full h-full border-0"), - Attrs.title("IDApTIK Game Preview"), - }, - list{}, - ), - }, - ) - } else { - div( - list{Attrs.class_("flex-1 flex items-center justify-center bg-gray-950")}, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div(list{Attrs.class_("text-gray-600 text-lg mb-2")}, list{text("Game Preview")}), - div( - list{Attrs.class_("text-gray-700 text-sm mb-4")}, - list{text(`Dev server not detected at ${state.devServerUrl}`)}, - ), - div( - list{Attrs.class_("text-gray-700 text-xs mb-4")}, - list{text("Run `deno task dev` or `./start-game-only.sh` to start the game.")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-cyan-900 text-cyan-200 hover:bg-cyan-800", - ), - Events.onClick(GamePreview(CheckDevServer)), - KeyboardNav.onActivate(GamePreview(CheckDevServer)), - }, - list{text("Retry Connection")}, - ), - }, - ), - }, - ) - }, - }, - ) -} - -/// Render the device interaction log tab. -let renderDeviceLog = (state: gamePreviewState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text(`Device Interactions (${Int.toString(Array.length(state.deviceLog))})`)}, - ), - button( - list{ - Attrs.class_( - "text-xs text-gray-400 hover:text-gray-200 px-3 py-1 rounded bg-gray-800", - ), - Events.onClick(GamePreview(ClearDeviceLog)), - KeyboardNav.onActivate(GamePreview(ClearDeviceLog)), - }, - list{text("Clear")}, - ), - }, - ), - if Array.length(state.deviceLog) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No device interactions recorded. Play the game to see interactions here.")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.deviceLog - ->Array.map(entry => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 font-mono text-sm px-3 py-1.5 rounded bg-gray-900/50", - ), - }, - list{ - span( - list{Attrs.class_("text-cyan-400 w-32 truncate")}, - list{text(entry.deviceType)}, - ), - span(list{Attrs.class_("text-gray-500 w-20")}, list{text(entry.deviceId)}), - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(entry.interaction)}), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the gameplay clips tab. -let renderClips = (state: gamePreviewState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - h3(list{Attrs.class_("text-sm font-medium text-gray-300")}, list{text("Gameplay Clips")}), - button( - list{ - Attrs.class_( - "text-xs text-gray-400 hover:text-gray-200 px-3 py-1 rounded bg-gray-800", - ), - Events.onClick(GamePreview(LoadClips)), - KeyboardNav.onActivate(GamePreview(LoadClips)), - }, - list{text("Refresh")}, - ), - }, - ), - if Array.length(state.clips) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No clips yet. Record gameplay from the Live Preview tab.")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.clips - ->Array.map(clip => { - div( - list{ - Attrs.class_( - "flex items-center justify-between bg-gray-900 rounded-lg px-4 py-3 border border-gray-800", - ), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text(clip.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{ - text( - `${Float.toString(clip.durationSecs)}s | ${Int.toString( - clip.sizeBytes, - )} bytes`, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "text-xs text-red-400 hover:text-red-300 px-2 py-1 bg-gray-800 rounded", - ), - Events.onClick(GamePreview(DeleteClip(clip.id))), - }, - list{text("Delete")}, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the performance/render stats tab. -let renderPerformance = (state: gamePreviewState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Render Performance")}, - ), - button( - list{ - Attrs.class_( - "text-xs text-gray-400 hover:text-gray-200 px-3 py-1 rounded bg-gray-800", - ), - Events.onClick(GamePreview(RefreshStats)), - KeyboardNav.onActivate(GamePreview(RefreshStats)), - }, - list{text("Refresh")}, - ), - }, - ), - switch state.stats { - | None => - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No render stats available. Connect to the running game.")}, - ) - | Some(stats) => - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - // FPS card - div( - list{Attrs.class_("bg-gray-900 rounded-lg p-4 border border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("FPS")}), - div( - list{ - Attrs.class_( - `text-2xl font-bold ${stats.fps >= 55.0 - ? "text-emerald-400" - : stats.fps >= 30.0 - ? "text-amber-400" - : "text-red-400"}`, - ), - }, - list{text(Float.toFixed(stats.fps, ~digits=1))}, - ), - }, - ), - // Draw calls card - div( - list{Attrs.class_("bg-gray-900 rounded-lg p-4 border border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Draw Calls")}), - div( - list{Attrs.class_("text-2xl font-bold text-gray-200")}, - list{text(Int.toString(stats.drawCalls))}, - ), - }, - ), - // Texture memory card - div( - list{Attrs.class_("bg-gray-900 rounded-lg p-4 border border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Texture Memory")}), - div( - list{Attrs.class_("text-2xl font-bold text-gray-200")}, - list{ - text( - `${Float.toFixed( - Int.toFloat(stats.textureMemory) /. 1048576.0, - ~digits=1, - )} MB`, - ), - }, - ), - }, - ), - // Sprite count card - div( - list{Attrs.class_("bg-gray-900 rounded-lg p-4 border border-gray-800")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Active Sprites")}), - div( - list{Attrs.class_("text-2xl font-bold text-gray-200")}, - list{text(Int.toString(stats.spriteCount))}, - ), - }, - ), - }, - ) - }, - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Render the Game Preview panel as a full-screen overlay. -let view = (state: gamePreviewState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/98 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Game Preview — live IDApTIK game preview with hot-reload"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("w-6 h-6 rounded bg-cyan-900 flex items-center justify-center")}, - list{span(list{Attrs.class_("text-cyan-400 text-xs font-bold")}, list{text("GP")})}, - ), - div( - list{}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Game Preview")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - if state.devServerConnected { - `Connected to ${state.devServerUrl}` - } else { - "Dev server not connected" - }, - ), - }, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "text-gray-500 hover:text-gray-300 px-3 py-1.5 text-sm rounded bg-gray-800 hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{Attrs.class_("px-4 py-2 bg-red-950 border-b border-red-900")}, - list{span(list{Attrs.class_("text-red-400 text-sm")}, list{text(err)})}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeCategory), - // Content - switch state.activeCategory { - | PreviewLive => renderLivePreview(state) - | PreviewDeviceLog => renderDeviceLog(state) - | PreviewClips => renderClips(state) - | PreviewPerformance => renderPerformance(state) - }, - }, - ) -} diff --git a/src/components/GeneratorMode.affine b/src/components/GeneratorMode.affine new file mode 100644 index 00000000..5e56a9af --- /dev/null +++ b/src/components/GeneratorMode.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GeneratorMode; + +// TODO: Complete semantic implementation diff --git a/src/components/GeneratorMode.res b/src/components/GeneratorMode.res deleted file mode 100644 index a8d69f91..00000000 --- a/src/components/GeneratorMode.res +++ /dev/null @@ -1,433 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Generator Mode Component — parametric procedural world builder. -/// Displays district list builder, slider controls for global parameters, -/// preview area, and export button. - -open Model -open Msg -open Tea.Html - -/// Render a district type label. -let districtTypeLabel = (dt: districtType): string => { - switch dt { - | Residential => "Residential" - | Commercial => "Commercial" - | Military => "Military" - | Industrial => "Industrial" - | Government => "Government" - | Transport => "Transport" - | Historic => "Historic" - } -} - -/// Render a weather condition label. -let weatherLabel = (w: weatherCondition): string => { - switch w { - | Clear => "Clear" - | Rain => "Rain" - | Snow => "Snow" - | Fog => "Fog" - | Storm => "Storm" - | NightRain => "Night Rain" - } -} - -/// Render a time-of-day label. -let timeLabel = (t: timeOfDay): string => { - switch t { - | Dawn => "Dawn" - | Morning => "Morning" - | Afternoon => "Afternoon" - | Evening => "Evening" - | Night => "Night" - | Midnight => "Midnight" - } -} - -/// Render a slider-style read-only parameter display. -let paramRow = (label: string, value: float, maxVal: float): Tea_Vdom.t => { - let pct = value /. maxVal *. 100.0 - div( - list{Attrs.class_("flex items-center gap-3 py-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400 w-28")}, list{text(label)}), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500 transition-all"), - Attrs.style("width", Float.toFixed(pct, ~digits=1) ++ "%"), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 w-12 text-right font-mono")}, - list{text(Float.toFixed(value, ~digits=2))}, - ), - }, - ) -} - -/// Main view function for the Generator Mode panel. -let view = (state: generatorModeState): Tea_Vdom.t => { - let districtCount = switch state.currentSpec { - | Some(spec) => Array.length(spec.districts) - | None => 0 - } - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Generator Mode — Parametric World Builder"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-indigo-300")}, - list{text("Generator Mode")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(Int.toString(districtCount) ++ " districts")}, - ), - if state.generating { - span( - list{Attrs.class_("text-xs text-yellow-400 animate-pulse")}, - list{text("Generating...")}, - ) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-800 hover:bg-indigo-700 text-white rounded", - ), - Events.onClick(GeneratorMode(GenStarted)), - KeyboardNav.onActivate(GeneratorMode(GenStarted)), - }, - list{text("Generate")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Design { - "bg-indigo-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GeneratorMode(SetGenCategory(Design))), - }, - list{text("Design")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Parameters { - "bg-indigo-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GeneratorMode(SetGenCategory(Parameters))), - }, - list{text("Parameters")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Preview { - "bg-indigo-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GeneratorMode(SetGenCategory(Preview))), - }, - list{text("Preview")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Export { - "bg-indigo-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GeneratorMode(SetGenCategory(Export))), - }, - list{text("Export")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(GeneratorMode(DismissGenError)), - KeyboardNav.onActivate(GeneratorMode(DismissGenError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Design => - switch state.currentSpec { - | Some(spec) => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("text-sm text-indigo-300 font-bold mb-2")}, - list{text(spec.name)}, - ), - div( - list{}, - spec.districts - ->Array.map(d => - div( - list{ - Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded mb-2"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(districtTypeLabel(d.districtType))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Size: " ++ d.sizeHint)}, - ), - }, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - d.facilities - ->Array.map(f => - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-gray-300 rounded", - ), - }, - list{text(f.facilityType ++ " x" ++ Int.toString(f.count))}, - ) - ) - ->List.fromArray, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("No world specification loaded. Create a new design to begin.")}, - ) - } - | Parameters => - div( - list{Attrs.class_("space-y-2")}, - list{ - h3( - list{Attrs.class_("text-sm text-indigo-300 mb-2")}, - list{text("Global Parameters")}, - ), - paramRow("Security", state.params.securityLevel, 1.0), - paramRow("Tech Level", state.params.techLevel, 1.0), - paramRow("Trap Density", state.params.trapDensity, 1.0), - paramRow("Difficulty", state.params.difficultyTarget, 1.0), - div( - list{Attrs.class_("flex gap-4 pt-2 text-xs text-gray-400")}, - list{ - span( - list{}, - list{text("Weather: " ++ weatherLabel(state.params.weatherCondition))}, - ), - span(list{}, list{text("Time: " ++ timeLabel(state.params.timeOfDay))}), - span( - list{}, - list{text("Civilians: " ++ Int.toString(state.params.civilianPopulation))}, - ), - }, - ), - }, - ) - | Preview => - switch state.previewResult { - | Some(result) => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text("Entities: " ++ Int.toString(result.entityCount))}), - span( - list{ - Attrs.class_( - if result.validationPassed { - "text-green-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if result.validationPassed { - "Validation passed" - } else { - "Validation failed" - }, - ), - }, - ), - span(list{}, list{text("Generated: " ++ result.generatedAt)}), - }, - ), - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-800 rounded p-3 min-h-[200px]"), - }, - list{ - pre( - list{ - Attrs.class_( - "text-xs text-gray-300 font-mono whitespace-pre-wrap overflow-auto max-h-96", - ), - }, - list{text(result.levelConfigJson)}, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Generate a world to see the preview.")}, - ) - } - | Export => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text("Export the generated LevelConfig JSON for use in IDApTIK.")}, - ), - switch state.previewResult { - | Some(result) => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - span( - list{}, - list{text("Entities: " ++ Int.toString(result.entityCount))}, - ), - span(list{}, list{text("World: " ++ result.worldSpec.name)}), - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-indigo-700 hover:bg-indigo-600 text-white rounded text-sm", - ), - }, - list{text("Export LevelConfig JSON")}, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("No generation result to export. Run the generator first.")}, - ) - }, - // Saved templates - if Array.length(state.templates) > 0 { - div( - list{Attrs.class_("pt-4 border-t border-gray-800")}, - list{ - h3( - list{Attrs.class_("text-sm text-gray-300 mb-2")}, - list{text("Saved Templates")}, - ), - div( - list{Attrs.class_("space-y-1")}, - state.templates - ->Array.map(t => - div( - list{Attrs.class_("flex items-center gap-3 py-1 text-xs")}, - list{ - span(list{Attrs.class_("text-gray-300")}, list{text(t.name)}), - span( - list{Attrs.class_("text-gray-500")}, - list{text(Int.toString(Array.length(t.districts)) ++ " districts")}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - Tea_Html.noNode - }, - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/GuardAiTuner.affine b/src/components/GuardAiTuner.affine new file mode 100644 index 00000000..1f02a010 --- /dev/null +++ b/src/components/GuardAiTuner.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GuardAiTuner; + +// TODO: Complete semantic implementation diff --git a/src/components/GuardAiTuner.res b/src/components/GuardAiTuner.res deleted file mode 100644 index 3bcb25ce..00000000 --- a/src/components/GuardAiTuner.res +++ /dev/null @@ -1,378 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Guard AI Tuner Component — guard patrol, alert threshold, and spawn -/// rate tuning. Displays guard profile cards with patrol patterns, slider -/// controls, patrol route editor, and presets dropdown. - -open Model -open Msg -open Tea.Html - -/// Render a slider-style parameter row for guard profiles. -let guardParamRow = (label: string, value: float, maxVal: float, unit: string): Tea_Vdom.t => { - let pct = Math.min(value /. maxVal *. 100.0, 100.0) - div( - list{Attrs.class_("flex items-center gap-3 py-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400 w-28")}, list{text(label)}), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-orange-500 transition-all"), - Attrs.style("width", Float.toFixed(pct, ~digits=1) ++ "%"), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 w-16 text-right font-mono")}, - list{text(Float.toFixed(value, ~digits=2) ++ unit)}, - ), - }, - ) -} - -/// Main view function for the Guard AI Tuner panel. -let view = (state: guardAiTunerState): Tea_Vdom.t => { - let guardCount = Array.length(state.guards) - let routeCount = Array.length(state.routes) - let presetCount = Array.length(state.presets) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Guard AI Tuner — Patrol and Alert Threshold Tuning"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-orange-300")}, - list{text("Guard AI Tuner")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(guardCount) ++ - " guards, " ++ - Int.toString(routeCount) ++ " routes", - ), - }, - ), - if state.editing { - span(list{Attrs.class_("text-xs text-yellow-400")}, list{text("Editing...")}) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-orange-800 hover:bg-orange-700 text-white rounded", - ), - Events.onClick(GuardAiTuner(GatStarted)), - KeyboardNav.onActivate(GuardAiTuner(GatStarted)), - }, - list{text("Apply Tuning")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Profiles { - "bg-orange-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GuardAiTuner(SetGatCategory(Profiles))), - }, - list{text("Profiles")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == PatrolEditor { - "bg-orange-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GuardAiTuner(SetGatCategory(PatrolEditor))), - }, - list{text("Patrol Editor")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Thresholds { - "bg-orange-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GuardAiTuner(SetGatCategory(Thresholds))), - }, - list{text("Thresholds")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Presets { - "bg-orange-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(GuardAiTuner(SetGatCategory(Presets))), - }, - list{text("Presets (" ++ Int.toString(presetCount) ++ ")")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(GuardAiTuner(DismissGatError)), - KeyboardNav.onActivate(GuardAiTuner(DismissGatError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Profiles => - div( - list{Attrs.class_("space-y-3")}, - state.guards - ->Array.map(g => { - let isSelected = state.selectedGuard == Some(g.id) - div( - list{ - Attrs.class_( - "px-3 py-2 border rounded " ++ if isSelected { - "bg-orange-900/20 border-orange-700" - } else { - "bg-gray-900 border-gray-800" - }, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-orange-300")}, - list{text(g.name)}, - ), - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-gray-300 rounded font-mono", - ), - }, - list{text(g.patrolPattern)}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-gray-400")}, - list{ - span( - list{}, - list{text("Alert: " ++ Float.toFixed(g.alertThreshold, ~digits=2))}, - ), - span( - list{}, - list{text("Spawn: " ++ Float.toFixed(g.spawnRate, ~digits=1) ++ "x")}, - ), - span( - list{}, - list{text("Speed: " ++ Float.toFixed(g.speed, ~digits=1) ++ " u/s")}, - ), - span( - list{}, - list{ - text( - "Detection: " ++ Float.toFixed(g.detectionRange, ~digits=0) ++ " u", - ), - }, - ), - span( - list{}, - list{ - text("Response: " ++ Float.toFixed(g.responseTime, ~digits=1) ++ "s"), - }, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - | PatrolEditor => - div( - list{Attrs.class_("space-y-3")}, - state.routes - ->Array.map(route => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-200 font-mono")}, - list{text("Route: " ++ route.id)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - if route.looping { - "Looping" - } else { - "Ping-pong" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - route.points - ->Array.mapWithIndex((pt, idx) => - div( - list{Attrs.class_("flex items-center gap-2 text-xs text-gray-400")}, - list{ - span( - list{Attrs.class_("w-6 text-gray-600")}, - list{text(Int.toString(idx + 1))}, - ), - span( - list{Attrs.class_("font-mono")}, - list{ - text( - "(" ++ - Float.toFixed(pt.x, ~digits=0) ++ - ", " ++ - Float.toFixed(pt.y, ~digits=0) ++ ")", - ), - }, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text("wait " ++ Float.toFixed(pt.waitTime, ~digits=1) ++ "s")}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - ) - ->List.fromArray, - ) - | Thresholds => - switch state.selectedGuard { - | Some(gid) => - switch state.guards->Array.find(g => g.id == gid) { - | Some(g) => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-sm text-orange-300 font-bold mb-3")}, - list{text(g.name ++ " — Thresholds")}, - ), - guardParamRow("Alert Threshold", g.alertThreshold, 1.0, ""), - guardParamRow("Spawn Rate", g.spawnRate, 5.0, "x"), - guardParamRow("Speed", g.speed, 20.0, " u/s"), - guardParamRow("Detection", g.detectionRange, 100.0, " u"), - guardParamRow("Response Time", g.responseTime, 10.0, "s"), - }, - ) - | None => - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Guard not found.")}) - } - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Select a guard profile to tune thresholds.")}, - ) - } - | Presets => - div( - list{Attrs.class_("space-y-2")}, - state.presets - ->Array.map(p => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div(list{Attrs.class_("text-sm font-bold text-gray-200")}, list{text(p.name)}), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text(p.description)}, - ), - div( - list{Attrs.class_("flex gap-2 mt-2")}, - p.profiles - ->Array.map(prof => - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-orange-900/30 text-orange-300 rounded", - ), - }, - list{text(prof.name)}, - ) - ) - ->List.fromArray, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Help.affine b/src/components/Help.affine new file mode 100644 index 00000000..6cd3ae05 --- /dev/null +++ b/src/components/Help.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Help; + +// TODO: Complete semantic implementation diff --git a/src/components/Help.res b/src/components/Help.res deleted file mode 100644 index 09ba9e03..00000000 --- a/src/components/Help.res +++ /dev/null @@ -1,781 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Help Component — In-application help, glossary, and onboarding. -/// -/// Renders a full-screen overlay containing the PanLL help system with -/// six content categories (Getting Started, Glossary, Panel Guides, -/// Shortcuts, FAQ, Architecture), a searchable entry list, detailed -/// article views, a glossary browser with related-term navigation, -/// and an 8-step onboarding walkthrough for new users. -/// -/// All interactive elements carry ARIA attributes and keyboard semantics -/// so the help system itself meets the accessibility standard PanLL -/// enforces on every panel it mints. -/// -/// This component is purely presentational — all state transformations -/// live in HelpEngine and are dispatched through Help(...) messages. - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Constants -// ============================================================================ - -/// Ordered list of all help categories for tab rendering. -/// Kept local because HelpEngine does not export an enumeration array. -let allCategories: array = [ - GettingStarted, - Glossary, - PanelGuide, - Shortcuts, - Faq, - Architecture, -] - -// ============================================================================ -// Header -// ============================================================================ - -/// Renders the help panel header bar containing the title, a search input -/// for filtering entries and glossary terms, and a close button. -/// -/// The search input dispatches `Help(SetHelpSearch(...))` on every keystroke -/// so the engine can recompute filtered results in real time. -/// -/// The close button dispatches `Help(CloseHelp)` to dismiss the overlay. -/// -/// @param state The current help system state (for pre-filling the search box) -/// @returns A virtual DOM node representing the header bar -let renderHeader = (state: helpState): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center justify-between px-6 py-4 border-b border-gray-800 bg-gray-950 shrink-0", - ), - }, - list{ - // Title - h2( - list{Attrs.class_("text-lg font-semibold text-gray-100 tracking-tight")}, - list{text("Help & Documentation")}, - ), - // Search + close cluster - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Search input - div( - list{Attrs.class_("relative")}, - list{ - span( - list{ - Attrs.class_( - "absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 text-sm pointer-events-none", - ), - Attrs.prop("aria-hidden", "true"), - }, - list{text("search")}, - ), - input( - list{ - Attrs.class_( - "w-72 bg-gray-900 border border-gray-700 rounded-lg pl-10 pr-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500/30", - ), - Attrs.type_("text"), - Attrs.placeholder("Search help topics..."), - Attrs.value(state.searchQuery), - Attrs.ariaLabel("Search help topics"), - Events.onInput(v => Help(SetHelpSearch(v))), - }, - list{}, - ), - }, - ), - // Close button - button( - list{ - Attrs.class_( - "p-2 rounded-lg text-gray-400 hover:text-gray-200 hover:bg-gray-800 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50", - ), - Attrs.ariaLabel("Close help panel"), - Events.onClick(Help(CloseHelp)), - KeyboardNav.onActivate(Help(CloseHelp)), - }, - list{text("close")}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Category Tabs -// ============================================================================ - -/// Renders the category tab bar for switching between help sections. -/// -/// Each tab dispatches `Help(SetHelpCategory(...))` when clicked. The -/// currently active tab is highlighted with an indigo accent and carries -/// `ariaSelected="true"` for screen reader users. -/// -/// The tab bar uses `role="tablist"` and each tab uses `role="tab"`. -/// -/// @param active The currently selected help category -/// @returns A virtual DOM node representing the tab bar -let renderCategoryTabs = (active: helpCategory): Tea_Vdom.t => { - let renderTab = (cat: helpCategory): Tea_Vdom.t => { - let isActive = cat === active - let baseClass = "px-4 py-2 text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50 whitespace-nowrap" - let stateClass = if isActive { - "bg-indigo-600 text-gray-100" - } else { - "text-gray-400 hover:text-gray-200 hover:bg-gray-800" - } - button( - list{ - Attrs.class_(`${baseClass} ${stateClass}`), - Attrs.role("tab"), - Attrs.prop("aria-selected", isActive ? "true" : "false"), - Attrs.ariaLabel(HelpEngine.categoryLabel(cat)), - Events.onClick(Help(SetHelpCategory(cat))), - }, - list{text(HelpEngine.categoryLabel(cat))}, - ) - } - - div( - list{ - Attrs.class_( - "flex items-center gap-1 px-6 py-3 border-b border-gray-800 bg-gray-950/80 overflow-x-auto shrink-0", - ), - Attrs.role("tablist"), - Attrs.ariaLabel("Help category tabs"), - }, - allCategories->Array.map(renderTab)->List.fromArray, - ) -} - -// ============================================================================ -// Entry List (Card Grid) -// ============================================================================ - -/// Renders a single help entry as a clickable card in the entry list. -/// -/// Clicking the card dispatches `Help(SelectEntry(entry.id))` to open -/// the full article view for that entry. -/// -/// @param entry The help entry to render as a card -/// @returns A virtual DOM node for the entry card -let renderEntryCard = (entry: helpEntry): Tea_Vdom.t => { - let truncatedBody = if String.length(entry.body) > 160 { - String.slice(entry.body, ~start=0, ~end=157) ++ "..." - } else { - entry.body - } - - button( - list{ - Attrs.class_( - "w-full text-left bg-gray-900 border border-gray-700 rounded-lg p-4 hover:border-indigo-500/50 hover:bg-gray-900/80 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50 group", - ), - Attrs.ariaLabel(`Read: ${entry.title}`), - Events.onClick(Help(SelectEntry(entry.id))), - }, - list{ - // Title - h3( - list{ - Attrs.class_( - "text-sm font-medium text-gray-200 group-hover:text-indigo-400 transition-colors mb-1", - ), - }, - list{text(entry.title)}, - ), - // Category badge - span( - list{ - Attrs.class_("inline-block text-xs text-gray-500 bg-gray-800 rounded px-2 py-0.5 mb-2"), - }, - list{text(HelpEngine.categoryLabel(entry.category))}, - ), - // Truncated body preview - p( - list{Attrs.class_("text-xs text-gray-400 leading-relaxed line-clamp-3")}, - list{text(truncatedBody)}, - ), - // Keywords - switch Array.length(entry.keywords) > 0 { - | true => - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-2")}, - entry.keywords - ->Array.slice(~start=0, ~end=4) - ->Array.map(kw => - span( - list{Attrs.class_("text-xs text-gray-600 bg-gray-800/50 rounded px-1.5 py-0.5")}, - list{text(kw)}, - ) - ) - ->List.fromArray, - ) - | false => noNode - }, - }, - ) -} - -/// Renders the entry list view — a grid of clickable help entry cards. -/// -/// When no entries match the current filter/search, a "no results" message -/// is displayed. Otherwise entries are laid out in a responsive card grid. -/// -/// @param entries The filtered array of help entries to display -/// @param _activeEntry The currently selected entry ID (unused here but kept for signature consistency) -/// @returns A virtual DOM node for the entry list grid -let renderEntryList = (entries: array, _activeEntry: option): Tea_Vdom.t< - msg, -> => { - if Array.length(entries) === 0 { - div( - list{Attrs.class_("flex flex-col items-center justify-center py-16 text-center")}, - list{ - div(list{Attrs.class_("text-4xl mb-4 text-gray-600")}, list{text("?")}), - p( - list{Attrs.class_("text-gray-400 text-sm")}, - list{text("No help entries found matching your search.")}, - ), - p( - list{Attrs.class_("text-gray-500 text-xs mt-1")}, - list{text("Try a different search term or switch categories.")}, - ), - }, - ) - } else { - div( - list{ - Attrs.class_("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-6"), - Attrs.role("list"), - Attrs.ariaLabel("Help entries"), - }, - entries - ->Array.map(entry => div(list{Attrs.role("listitem")}, list{renderEntryCard(entry)})) - ->List.fromArray, - ) - } -} - -// ============================================================================ -// Entry Detail -// ============================================================================ - -/// Renders the full article view for a single help entry. -/// -/// Includes a back button to return to the entry list, the entry title, -/// category badge, full body text, and keyword tags. The back button -/// dispatches `Help(SelectEntry(""))` with an empty string to clear the -/// active entry selection (the engine treats empty-string SelectEntry -/// as deselection, or alternatively we dispatch SetHelpSearch to reset). -/// -/// @param entry The help entry to render in full detail -/// @returns A virtual DOM node for the article view -let renderEntryDetail = (entry: helpEntry): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full")}, - list{ - // Back button bar - div( - list{Attrs.class_("px-6 py-3 border-b border-gray-800 shrink-0")}, - list{ - button( - list{ - Attrs.class_( - "flex items-center gap-2 text-sm text-gray-400 hover:text-indigo-400 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50 rounded px-2 py-1", - ), - Attrs.ariaLabel("Back to entry list"), - Events.onClick(Help(SelectEntry(""))), - }, - list{ - span(list{Attrs.prop("aria-hidden", "true")}, list{text("back")}), - text("Back to entries"), - }, - ), - }, - ), - // Article content - div( - list{Attrs.class_("flex-1 overflow-y-auto px-6 py-6")}, - list{ - // Title - h2( - list{Attrs.class_("text-xl font-semibold text-gray-100 mb-2")}, - list{text(entry.title)}, - ), - // Category badge - span( - list{ - Attrs.class_( - "inline-block text-xs text-indigo-400 bg-indigo-900/30 border border-indigo-800/50 rounded px-2 py-0.5 mb-6", - ), - }, - list{text(HelpEngine.categoryLabel(entry.category))}, - ), - // Body text — rendered as paragraphs split on double newlines - div( - list{Attrs.class_("space-y-4")}, - entry.body - ->String.split("\n\n") - ->Array.map(paragraph => - p(list{Attrs.class_("text-sm text-gray-300 leading-relaxed")}, list{text(paragraph)}) - ) - ->List.fromArray, - ), - // Panel link (if panel-specific) - switch entry.panelId { - | Some(_pid) => - div( - list{Attrs.class_("mt-6 p-3 bg-gray-900 border border-gray-700 rounded-lg")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "This entry is associated with a specific panel. Use the Panel Switcher to navigate there.", - ), - }, - ), - }, - ) - | None => noNode - }, - // Keywords footer - switch Array.length(entry.keywords) > 0 { - | true => - div( - list{Attrs.class_("mt-6 pt-4 border-t border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text("Related keywords")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - entry.keywords - ->Array.map(kw => - span( - list{ - Attrs.class_("text-xs text-gray-400 bg-gray-800 rounded-full px-3 py-1"), - }, - list{text(kw)}, - ) - ) - ->List.fromArray, - ), - }, - ) - | false => noNode - }, - }, - ), - }, - ) -} - -// ============================================================================ -// Glossary -// ============================================================================ - -/// Renders a single glossary term card with its definition and related terms. -/// -/// Related terms are rendered as clickable links that dispatch -/// `Help(SearchGlossary(relatedTerm))` to navigate the glossary graph. -/// -/// @param term The glossary term to render -/// @returns A virtual DOM node for the glossary term card -let renderGlossaryTerm = (term: glossaryTerm): Tea_Vdom.t => { - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4"), - Attrs.role("article"), - Attrs.ariaLabel(`Glossary term: ${term.term}`), - }, - list{ - // Term heading - h3(list{Attrs.class_("text-sm font-semibold text-indigo-400 mb-2")}, list{text(term.term)}), - // Definition - p( - list{Attrs.class_("text-sm text-gray-300 leading-relaxed mb-3")}, - list{text(term.definition)}, - ), - // Extended description (if available) - switch term.extendedDescription { - | Some(ext) => - p(list{Attrs.class_("text-xs text-gray-400 leading-relaxed mb-3 italic")}, list{text(ext)}) - | None => noNode - }, - // Related terms - switch Array.length(term.relatedTerms) > 0 { - | true => - div( - list{Attrs.class_("flex flex-wrap gap-1.5 pt-2 border-t border-gray-800")}, - [ - span( - list{Attrs.class_("text-xs text-gray-500 mr-1 self-center")}, - list{text("See also:")}, - ), - ] - ->Array.concat( - term.relatedTerms->Array.map(rt => - button( - list{ - Attrs.class_( - "text-xs text-indigo-400 hover:text-indigo-300 bg-indigo-900/20 hover:bg-indigo-900/40 rounded px-2 py-0.5 transition-colors focus:outline-none focus:ring-1 focus:ring-indigo-500/50", - ), - Attrs.ariaLabel(`Look up related term: ${rt}`), - Events.onClick(Help(SearchGlossary(rt))), - }, - list{text(rt)}, - ) - ), - ) - ->List.fromArray, - ) - | false => noNode - }, - }, - ) -} - -/// Renders the glossary view — an alphabetical list of neurosymbolic terms -/// with definitions and cross-reference links. -/// -/// The glossary is filtered by the current search query. Terms are sorted -/// alphabetically by their `term` field. When no terms match the search, -/// a "no results" message is shown. -/// -/// @param glossary The array of glossary terms (pre-filtered by the engine or raw) -/// @param searchQuery The current search string for additional client-side filtering -/// @returns A virtual DOM node for the glossary list -let renderGlossary = (glossary: array, searchQuery: string): Tea_Vdom.t => { - let filtered = HelpEngine.searchGlossary(searchQuery, glossary) - let sorted = filtered->Array.toSorted((a, b) => { - let la = String.toLowerCase(a.term) - let lb = String.toLowerCase(b.term) - if la < lb { - -1.0 - } else if la > lb { - 1.0 - } else { - 0.0 - } - }) - - if Array.length(sorted) === 0 { - div( - list{Attrs.class_("flex flex-col items-center justify-center py-16 text-center")}, - list{ - div(list{Attrs.class_("text-4xl mb-4 text-gray-600")}, list{text("A-Z")}), - p( - list{Attrs.class_("text-gray-400 text-sm")}, - list{text("No glossary terms match your search.")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("p-6")}, - list{ - // Term count - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{ - text( - `${Int.toString(Array.length(sorted))} term${Array.length(sorted) === 1 ? "" : "s"}`, - ), - }, - ), - // Term list - div( - list{ - Attrs.class_("grid grid-cols-1 md:grid-cols-2 gap-4"), - Attrs.role("list"), - Attrs.ariaLabel("Glossary terms"), - }, - sorted - ->Array.map(term => div(list{Attrs.role("listitem")}, list{renderGlossaryTerm(term)})) - ->List.fromArray, - ), - }, - ) - } -} - -// ============================================================================ -// Onboarding Overlay -// ============================================================================ - -/// Renders the onboarding walkthrough overlay — a step-by-step card -/// sequence introducing new users to PanLL's core concepts. -/// -/// The overlay floats above the help panel content with a semi-transparent -/// backdrop. Navigation buttons dispatch `Help(PrevOnboardingStep)`, -/// `Help(NextOnboardingStep)`, and `Help(SkipOnboarding)` messages. -/// -/// On the final step, the "Next" button becomes "Finish" and dispatches -/// `Help(CompleteOnboarding)`. -/// -/// @param state The current onboarding walkthrough state -/// @returns A virtual DOM node for the onboarding overlay (or noNode if inactive) -let renderOnboarding = (state: onboardingState): Tea_Vdom.t => { - if !state.active { - noNode - } else { - let totalSteps = Array.length(state.steps) - let currentIdx = state.currentStep - let isLast = currentIdx >= totalSteps - 1 - let isFirst = currentIdx <= 0 - - let currentStep = state.steps->Array.get(currentIdx) - - switch currentStep { - | None => noNode - | Some(step) => - div( - list{ - Attrs.class_( - "fixed inset-0 z-50 flex items-center justify-center bg-gray-950/80 backdrop-blur-sm", - ), - Attrs.role("dialog"), - Attrs.ariaLabel("Onboarding walkthrough"), - }, - list{ - // Walkthrough card - div( - list{ - Attrs.class_( - "w-full max-w-lg bg-gray-900 border border-gray-700 rounded-xl shadow-2xl overflow-hidden", - ), - }, - list{ - // Progress bar - div( - list{Attrs.class_("h-1 bg-gray-800")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500 transition-all duration-300"), - Attrs.style( - "width", - `${Float.toFixed( - Int.toFloat(currentIdx + 1) /. Int.toFloat(totalSteps) *. 100.0, - ~digits=0, - )}%`, - ), - }, - list{}, - ), - }, - ), - // Card body - div( - list{Attrs.class_("p-8")}, - list{ - // Step counter - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{ - text(`Step ${Int.toString(currentIdx + 1)} of ${Int.toString(totalSteps)}`), - }, - ), - // Title - h3( - list{Attrs.class_("text-lg font-semibold text-gray-100 mb-3")}, - list{text(step.title)}, - ), - // Description - p( - list{Attrs.class_("text-sm text-gray-300 leading-relaxed")}, - list{text(step.description)}, - ), - }, - ), - // Navigation footer - div( - list{ - Attrs.class_( - "flex items-center justify-between px-8 py-4 border-t border-gray-800 bg-gray-950/50", - ), - }, - list{ - // Skip button - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50 rounded px-2 py-1", - ), - Attrs.ariaLabel("Skip onboarding walkthrough"), - Events.onClick(Help(SkipOnboarding)), - KeyboardNav.onActivate(Help(SkipOnboarding)), - }, - list{text("Skip walkthrough")}, - ), - // Prev / Next buttons - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Previous - if isFirst { - noNode - } else { - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50", - ), - Attrs.ariaLabel("Previous onboarding step"), - Events.onClick(Help(PrevOnboardingStep)), - KeyboardNav.onActivate(Help(PrevOnboardingStep)), - }, - list{text("Previous")}, - ) - }, - // Next / Finish - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-500 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50", - ), - Attrs.ariaLabel(isLast ? "Finish onboarding" : "Next onboarding step"), - Events.onClick( - isLast ? Help(CompleteOnboarding) : Help(NextOnboardingStep), - ), - }, - list{text(isLast ? "Finish" : "Next")}, - ), - }, - ), - }, - ), - }, - ), - }, - ) - } - } -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main entry point for the Help panel. -/// -/// Renders a full-screen overlay containing the header bar, category tabs, -/// and the appropriate content area based on the current state: -/// -/// - If a specific entry is selected (`activeEntry = Some(id)`), shows -/// the full article view via `renderEntryDetail`. -/// - If the Glossary tab is active, shows the glossary browser via -/// `renderGlossary`. -/// - Otherwise, shows the entry list grid via `renderEntryList`. -/// -/// The onboarding overlay is conditionally rendered on top of everything -/// when the walkthrough is active. -/// -/// The outermost div carries `role="dialog"` and an ARIA label for -/// assistive technology. -/// -/// @param state The complete help system state -/// @returns A virtual DOM tree for the entire Help panel overlay -let view = (state: helpState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col h-screen"), - Attrs.role("dialog"), - Attrs.ariaLabel("Help and documentation"), - }, - list{ - // Header with search and close - renderHeader(state), - // Category tabs - renderCategoryTabs(state.activeCategory), - // Content area (scrollable) - div( - list{ - Attrs.class_("flex-1 overflow-y-auto"), - Attrs.role("tabpanel"), - Attrs.ariaLabel(`${HelpEngine.categoryLabel(state.activeCategory)} content`), - }, - list{ - // Determine which view to render based on state - switch state.activeEntry { - | Some(entryId) if entryId !== "" => - // Entry detail view — find the entry and render it - switch HelpEngine.findEntry(entryId, state.filteredEntries) { - | Some(entry) => renderEntryDetail(entry) - | None => - // Entry not found in filtered set — show a fallback message - div( - list{Attrs.class_("flex flex-col items-center justify-center py-16 text-center")}, - list{ - p( - list{Attrs.class_("text-gray-400 text-sm")}, - list{text("Entry not found. It may have been filtered out.")}, - ), - button( - list{ - Attrs.class_( - "mt-4 px-4 py-2 text-sm text-indigo-400 hover:text-indigo-300 bg-gray-900 border border-gray-700 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500/50", - ), - Events.onClick(Help(SelectEntry(""))), - }, - list{text("Back to entries")}, - ), - }, - ) - } - | _ => - // List / glossary view depending on active category - switch state.activeCategory { - | Glossary => renderGlossary(state.glossary, state.searchQuery) - | _ => renderEntryList(state.filteredEntries, state.activeEntry) - } - }, - }, - ), - // Context panel indicator (when opened from a specific panel via F1) - switch state.contextPanelId { - | Some(_pid) => - div( - list{Attrs.class_("px-6 py-2 border-t border-gray-800 bg-gray-950 shrink-0")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Showing context-sensitive help. ")}, - ), - button( - list{ - Attrs.class_( - "text-xs text-indigo-400 hover:text-indigo-300 transition-colors focus:outline-none focus:ring-1 focus:ring-indigo-500/50 rounded px-1", - ), - Attrs.ariaLabel("Show all help entries, clear panel filter"), - Events.onClick(Help(OpenContextHelp(None))), - }, - list{text("Show all")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Onboarding overlay (floats above everything when active) - renderOnboarding(state.onboarding), - }, - ) -} diff --git a/src/components/Hypatia.affine b/src/components/Hypatia.affine new file mode 100644 index 00000000..537748f0 --- /dev/null +++ b/src/components/Hypatia.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Hypatia; + +// TODO: Complete semantic implementation diff --git a/src/components/Hypatia.res b/src/components/Hypatia.res deleted file mode 100644 index 04c8f8c1..00000000 --- a/src/components/Hypatia.res +++ /dev/null @@ -1,1120 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Hypatia Component — Neurosymbolic scanner dashboard. -/// -/// Renders the 5 neural network confidence gauges, scan results table, -/// quarantine status, pipeline health, and learning cycle progress. -/// The brain of the entire ecosystem. - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Neural Network Gauge -// ============================================================================ - -/// Render a single neural network confidence gauge. -let renderNetGauge = (net: neuralNetState): Tea_Vdom.t => { - let label = HypatiaEngine.netLabel(net.id) - let desc = HypatiaEngine.netDescription(net.id) - let dotColor = HypatiaEngine.netStatusColor(net.status) - let confPct = Float.toFixed(net.confidence *. 100.0, ~digits=0) - // Gauge bar width as percentage - let barWidth = Float.toFixed(net.confidence *. 100.0, ~digits=0) - let barColor = if net.confidence > 0.8 { - "bg-green-500" - } else if net.confidence > 0.5 { - "bg-amber-500" - } else { - "bg-red-500" - } - - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4"), - Attrs.role("meter"), - Attrs.ariaLabel(`${label} confidence: ${confPct}%`), - Attrs.prop("aria-valuenow", confPct), - Attrs.prop("aria-valuemin", "0"), - Attrs.prop("aria-valuemax", "100"), - }, - list{ - // Header: name + status dot - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(label)}), - span( - list{Attrs.class_(`w-2 h-2 rounded-full ${dotColor}`), Attrs.role("status")}, - list{}, - ), - }, - ), - // Description - div(list{Attrs.class_("text-xs text-gray-500 mb-3")}, list{text(desc)}), - // Confidence bar - div( - list{Attrs.class_("w-full bg-gray-800 rounded-full h-2 mb-2")}, - list{ - div( - list{ - Attrs.class_(`${barColor} h-full rounded-full transition-all duration-500`), - Attrs.prop("style", `width: ${barWidth}%`), - }, - list{}, - ), - }, - ), - // Metrics - div( - list{Attrs.class_("flex justify-between text-xs text-gray-400")}, - list{ - span(list{}, list{text(`${confPct}% conf`)}), - span(list{}, list{text(`${Int.toString(net.inferenceCount)} inferences`)}), - }, - ), - }, - ) -} - -// ============================================================================ -// Scan Result Row -// ============================================================================ - -/// Render a single scan result row. -let renderScanRow = (scan: scanResult): Tea_Vdom.t => { - let riskClass = if scan.riskScore > 0.7 { - "text-red-400" - } else if scan.riskScore > 0.3 { - "text-amber-400" - } else { - "text-green-400" - } - let statusIcon = scan.passed ? "text-green-400" : "text-red-400" - let statusText = scan.passed ? "PASS" : "FAIL" - - div( - list{ - Attrs.class_("flex items-center gap-4 p-3 border-b border-gray-800 hover:bg-gray-900/50"), - Attrs.role("row"), - }, - list{ - // Pass/fail indicator - span(list{Attrs.class_(`text-xs font-bold ${statusIcon} w-10`)}, list{text(statusText)}), - // Repo name - span(list{Attrs.class_("text-sm text-gray-300 w-48 truncate")}, list{text(scan.repoName)}), - // Risk score - span( - list{Attrs.class_(`text-xs ${riskClass} w-16 text-right`)}, - list{text(`${Float.toFixed(scan.riskScore *. 100.0, ~digits=0)}%`)}, - ), - // Finding count - span( - list{Attrs.class_("text-xs text-gray-400 w-20 text-right")}, - list{text(`${Int.toString(scan.findingCount)} findings`)}, - ), - // Quarantine count - span( - list{Attrs.class_("text-xs text-gray-500 w-20 text-right")}, - list{text(`${Int.toString(scan.quarantineCount)} quarantined`)}, - ), - // Last scanned - span( - list{Attrs.class_("text-xs text-gray-600 flex-1 text-right truncate")}, - list{text(scan.lastScanned)}, - ), - }, - ) -} - -// ============================================================================ -// Learning Cycle Status -// ============================================================================ - -/// Render the learning cycle progress indicator. -let renderLearningCycle = (cycle: learningCycle): Tea_Vdom.t => { - let progress = if cycle.reposTotal > 0 { - Int.toFloat(cycle.reposScanned) /. Int.toFloat(cycle.reposTotal) *. 100.0 - } else { - 0.0 - } - let pctStr = Float.toFixed(progress, ~digits=0) - - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4"), - Attrs.role("status"), - Attrs.ariaLabel(`Learning cycle: ${pctStr}% complete`), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Learning Cycle")}, - ), - span( - list{Attrs.class_("text-xs text-indigo-400")}, - list{text(HypatiaEngine.stageLabel(cycle.stage))}, - ), - }, - ), - // Progress bar - div( - list{Attrs.class_("w-full bg-gray-800 rounded-full h-2 mb-2")}, - list{ - div( - list{ - Attrs.class_("bg-indigo-500 h-full rounded-full transition-all"), - Attrs.prop("style", `width: ${pctStr}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("flex justify-between text-xs text-gray-500")}, - list{ - span( - list{}, - list{ - text(`${Int.toString(cycle.reposScanned)}/${Int.toString(cycle.reposTotal)} repos`), - }, - ), - if cycle.noveltyTriggered { - span(list{Attrs.class_("text-amber-400")}, list{text("Novelty detected")}) - } else { - noNode - }, - }, - ), - }, - ) -} - -// ============================================================================ -// Category Tabs -// ============================================================================ - -/// Render the category tab bar (Dashboard, Scans, Quarantine, Neural, Recipes). -let renderTabs = (active: hypatiaCategory): Tea_Vdom.t => { - let tabs: array = [ - HypatiaDashboard, - HypatiaScans, - HypatiaQuarantine, - HypatiaNeural, - HypatiaRecipes, - ] - div( - list{ - Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), - Attrs.role("tablist"), - Attrs.ariaLabel("Hypatia panel sections"), - }, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-emerald-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Hypatia(SetHypatiaCategory(tab))), - }, - list{text(HypatiaEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main Hypatia panel view — full-screen overlay with neural gauges, scan results, and recipes. -let view = (hypatia: hypatiaState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Hypatia neurosymbolic scanner panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("Hypatia")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("neurosymbolic CI/CD intelligence")}, - ), - span( - list{Attrs.class_("text-xs text-emerald-400 ml-2")}, - list{ - text( - `${Float.toFixed( - HypatiaEngine.avgConfidence(hypatia.networks) *. 100.0, - ~digits=0, - )}% ensemble confidence`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-emerald-600 text-white rounded hover:bg-emerald-500", - ), - Events.onClick(Hypatia(LoadHypatia)), - KeyboardNav.onActivate(Hypatia(LoadHypatia)), - }, - list{text("Refresh")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - if hypatia.loading { - div( - list{Attrs.class_("text-gray-400"), Attrs.role("status")}, - list{text("Scanning...")}, - ) - } else if !hypatia.loaded { - div( - list{Attrs.class_("text-center text-gray-500 mt-12")}, - list{ - div(list{Attrs.class_("text-4xl mb-2")}, list{text("Hypatia")}), - div( - list{Attrs.class_("text-sm mb-1")}, - list{text("Neurosymbolic CI/CD Intelligence")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mb-6")}, - list{ - text( - `5 neural networks. ${Int.toString( - hypatia.totalRepos, - )}+ repos. Safety triangle routing.`, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-emerald-600 text-white rounded hover:bg-emerald-500", - ), - Events.onClick(Hypatia(LoadHypatia)), - KeyboardNav.onActivate(Hypatia(LoadHypatia)), - }, - list{text("Connect to Hypatia")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderTabs(hypatia.activeCategory), - switch hypatia.activeCategory { - | HypatiaDashboard => - div( - list{Attrs.class_("space-y-6")}, - list{ - // Summary bar - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(hypatia.totalRepos)} repos`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(Array.length(hypatia.scans))} scanned`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(hypatia.quarantinedCount)} quarantined`)}, - ), - }, - ), - // Neural network gauges (5 in a row) - div( - list{ - Attrs.class_("grid grid-cols-5 gap-3"), - Attrs.role("list"), - Attrs.ariaLabel("Neural network ensemble"), - }, - hypatia.networks->Array.map(net => renderNetGauge(net))->List.fromArray, - ), - // Safety Triangle summary - switch hypatia.triangleCounts { - | Some((elim, sub, ctrl)) => - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4"), - Attrs.role("figure"), - Attrs.ariaLabel("Safety triangle — hierarchy of controls"), - }, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("Safety Triangle")}, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - // Eliminate tier - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_( - "w-20 text-right text-xs text-red-400 font-medium", - ), - }, - list{text("Eliminate")}, - ), - div( - list{ - Attrs.class_( - "flex-1 bg-gray-800 rounded-full h-3 overflow-hidden", - ), - }, - list{ - div( - list{ - Attrs.class_( - "bg-red-600 h-full rounded-full transition-all", - ), - Attrs.prop( - "style", - `width: ${Int.toString(elim * 10)}%`, - ), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-xs text-gray-400 text-right")}, - list{text(Int.toString(elim))}, - ), - }, - ), - // Substitute tier - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_( - "w-20 text-right text-xs text-amber-400 font-medium", - ), - }, - list{text("Substitute")}, - ), - div( - list{ - Attrs.class_( - "flex-1 bg-gray-800 rounded-full h-3 overflow-hidden", - ), - }, - list{ - div( - list{ - Attrs.class_( - "bg-amber-600 h-full rounded-full transition-all", - ), - Attrs.prop( - "style", - `width: ${Int.toString(sub * 10)}%`, - ), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-xs text-gray-400 text-right")}, - list{text(Int.toString(sub))}, - ), - }, - ), - // Control tier - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_( - "w-20 text-right text-xs text-blue-400 font-medium", - ), - }, - list{text("Control")}, - ), - div( - list{ - Attrs.class_( - "flex-1 bg-gray-800 rounded-full h-3 overflow-hidden", - ), - }, - list{ - div( - list{ - Attrs.class_( - "bg-blue-600 h-full rounded-full transition-all", - ), - Attrs.prop( - "style", - `width: ${Int.toString(ctrl * 10)}%`, - ), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-xs text-gray-400 text-right")}, - list{text(Int.toString(ctrl))}, - ), - }, - ), - }, - ), - }, - ) - | None => noNode - }, - // Outcomes summary - switch hypatia.outcomes { - | Some(out) => - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4"), - Attrs.role("region"), - Attrs.ariaLabel("Dispatch outcomes summary"), - }, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("Outcomes")}, - ), - div( - list{Attrs.class_("flex gap-6 text-xs")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(out.totalOutcomes)} total`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(out.successCount)} succeeded`)}, - ), - div( - list{ - Attrs.class_( - if out.successRate >= 90.0 { - "text-green-400" - } else if out.successRate >= 70.0 { - "text-amber-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text(`${Float.toFixed(out.successRate, ~digits=1)}% success`), - }, - ), - div( - list{ - Attrs.class_( - if out.mismatchFixApplied { - "text-green-400" - } else { - "text-amber-400" - }, - ), - }, - list{ - text( - if out.mismatchFixApplied { - "Mismatch fix: applied" - } else { - "Mismatch fix: pending" - }, - ), - }, - ), - }, - ), - }, - ) - | None => noNode - }, - // Learning cycle - switch hypatia.learningCycle { - | Some(cycle) => renderLearningCycle(cycle) - | None => noNode - }, - }, - ) - | HypatiaScans => { - let filtered = HypatiaEngine.filterScans(hypatia.scans, hypatia.filterText) - div( - list{Attrs.class_("space-y-4")}, - list{ - // Filter - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600", - ), - Attrs.placeholder("Filter by repo name..."), - Attrs.ariaLabel("Filter scan results"), - Attrs.value(hypatia.filterText), - Events.onInput(v => Hypatia(SetHypatiaFilter(v))), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(filtered))} results`)}, - ), - }, - ), - // Results table - div( - list{ - Attrs.class_("border border-gray-700 rounded-lg overflow-hidden"), - Attrs.role("table"), - }, - list{ - div( - list{ - Attrs.class_( - "flex items-center gap-4 p-3 bg-gray-900 border-b border-gray-700 text-xs text-gray-500", - ), - }, - list{ - span(list{Attrs.class_("w-10")}, list{text("Status")}), - span(list{Attrs.class_("w-48")}, list{text("Repository")}), - span(list{Attrs.class_("w-16 text-right")}, list{text("Risk")}), - span(list{Attrs.class_("w-20 text-right")}, list{text("Findings")}), - span( - list{Attrs.class_("w-20 text-right")}, - list{text("Quarantine")}, - ), - span( - list{Attrs.class_("flex-1 text-right")}, - list{text("Last Scan")}, - ), - }, - ), - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered->Array.map(s => renderScanRow(s))->List.fromArray, - ), - }, - ), - }, - ) - } - | HypatiaQuarantine => - div( - list{ - Attrs.class_("space-y-4"), - Attrs.role("region"), - Attrs.ariaLabel("Quarantine status"), - }, - list{ - if hypatia.quarantinedCount > 0 { - div( - list{ - Attrs.class_("bg-amber-900/20 border border-amber-700 rounded-lg p-4"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3 mb-2")}, - list{ - span( - list{Attrs.class_("text-amber-400 text-sm font-medium")}, - list{ - text( - `${Int.toString(hypatia.quarantinedCount)} items quarantined`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - "These items have been held for human review. Connect to the Hypatia backend to inspect and resolve individual quarantine entries.", - ), - }, - ), - }, - ) - } else { - div( - list{Attrs.class_("text-center py-12")}, - list{ - div( - list{Attrs.class_("text-3xl text-gray-600 mb-2")}, - list{text("Clear")}, - ), - div( - list{Attrs.class_("text-sm text-gray-500")}, - list{ - text( - "No items in quarantine. All findings have been routed through the safety triangle.", - ), - }, - ), - }, - ) - }, - }, - ) - | HypatiaNeural => - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-4")}, - hypatia.networks->Array.map(net => renderNetGauge(net))->List.fromArray, - ) - | HypatiaRecipes => - div( - list{ - Attrs.class_("space-y-4"), - Attrs.role("region"), - Attrs.ariaLabel("Recipe inventory"), - }, - list{ - { - let total = Array.length(hypatia.recipeEntries) - let withFix = - hypatia.recipeEntries->Array.filter(r => r.hasFixScript)->Array.length - let elimCount = - hypatia.recipeEntries - ->Array.filter(r => r.tier === Eliminate) - ->Array.length - let subCount = - hypatia.recipeEntries - ->Array.filter(r => r.tier === Substitute) - ->Array.length - let ctrlCount = - hypatia.recipeEntries->Array.filter(r => r.tier === Control)->Array.length - div( - list{Attrs.class_("flex gap-4 text-xs flex-wrap")}, - list{ - div( - list{Attrs.class_("text-gray-300 font-medium")}, - list{text(`${Int.toString(total)} recipes`)}, - ), - div( - list{Attrs.class_("text-green-400")}, - list{text(`${Int.toString(withFix)} with fix_script`)}, - ), - div( - list{Attrs.class_("text-red-400")}, - list{text(`${Int.toString(elimCount)} eliminate`)}, - ), - div( - list{Attrs.class_("text-amber-400")}, - list{text(`${Int.toString(subCount)} substitute`)}, - ), - div( - list{Attrs.class_("text-blue-400")}, - list{text(`${Int.toString(ctrlCount)} control`)}, - ), - }, - ) - }, - // Filter input - input( - list{ - Attrs.class_( - "w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600", - ), - Attrs.placeholder("Filter recipes by name, description, or language..."), - Attrs.ariaLabel("Filter recipes"), - Attrs.value(hypatia.recipeFilter), - Events.onInput(v => Hypatia(SetRecipeFilter(v))), - }, - list{}, - ), - { - let filtered = HypatiaEngine.filterRecipes( - hypatia.recipeEntries, - hypatia.recipeFilter, - ) - div( - list{Attrs.class_("flex gap-4")}, - list{ - // Recipe list (left) - div( - list{ - Attrs.class_( - "flex-1 border border-gray-700 rounded-lg overflow-hidden", - ), - Attrs.role("list"), - }, - list{ - // Column headers - div( - list{ - Attrs.class_( - "flex items-center gap-2 p-2 bg-gray-900 border-b border-gray-700 text-xs text-gray-500", - ), - }, - list{ - span(list{Attrs.class_("w-6")}, list{text("")}), - span(list{Attrs.class_("flex-1")}, list{text("Recipe")}), - span(list{Attrs.class_("w-12 text-right")}, list{text("Conf")}), - span(list{Attrs.class_("w-16 text-right")}, list{text("Tier")}), - span( - list{Attrs.class_("w-14 text-right")}, - list{text("Fired")}, - ), - }, - ), - div( - list{Attrs.class_("max-h-[32rem] overflow-y-auto")}, - filtered - ->Array.map(recipe => { - let isSelected = hypatia.selectedRecipe === Some(recipe.id) - let confPct = Float.toFixed( - recipe.confidence *. 100.0, - ~digits=0, - ) - let tierCol = HypatiaEngine.tierColor(recipe.tier) - div( - list{ - Attrs.class_( - `flex items-center gap-2 p-2 cursor-pointer transition-colors ${isSelected - ? "bg-indigo-900/40 border-l-2 border-indigo-500" - : "hover:bg-gray-900/50 border-l-2 border-transparent"}`, - ), - Attrs.role("listitem"), - Events.onClick(Hypatia(SelectRecipe(Some(recipe.id)))), - }, - list{ - // Fix script indicator - span( - list{ - Attrs.class_( - `w-6 text-center text-xs ${recipe.hasFixScript - ? "text-green-500" - : "text-gray-700"}`, - ), - }, - list{text(recipe.hasFixScript ? "F" : "-")}, - ), - // Name - span( - list{ - Attrs.class_("flex-1 text-sm text-gray-300 truncate"), - }, - list{text(recipe.name)}, - ), - // Confidence - span( - list{ - Attrs.class_( - `w-12 text-right text-xs ${recipe.confidence >= 0.97 - ? "text-emerald-400" - : recipe.confidence >= 0.93 - ? "text-amber-400" - : "text-red-400"}`, - ), - }, - list{text(`${confPct}%`)}, - ), - // Tier - span( - list{Attrs.class_(`w-16 text-right text-xs ${tierCol}`)}, - list{text(HypatiaEngine.tierLabel(recipe.tier))}, - ), - // Times triggered - span( - list{ - Attrs.class_( - "w-14 text-right text-xs text-gray-500 font-mono", - ), - }, - list{text(Int.toString(recipe.timesTriggered))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Detail panel (right) — shown when a recipe is selected - switch hypatia.selectedRecipe { - | Some(recipeId) => - switch HypatiaEngine.findRecipe(hypatia.recipeEntries, recipeId) { - | Some(recipe) => - div( - list{ - Attrs.class_( - `w-80 border rounded-lg p-4 space-y-4 ${HypatiaEngine.tierBg( - recipe.tier, - )}`, - ), - Attrs.role("complementary"), - Attrs.ariaLabel(`Recipe detail: ${recipe.name}`), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-start justify-between")}, - list{ - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_("text-sm font-medium text-gray-200"), - }, - list{text(recipe.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(recipe.id)}, - ), - }, - ), - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300", - ), - Events.onClick(Hypatia(SelectRecipe(None))), - Attrs.ariaLabel("Close detail"), - }, - list{text("x")}, - ), - }, - ), - // Description - div( - list{Attrs.class_("text-xs text-gray-400 leading-relaxed")}, - list{text(recipe.description)}, - ), - // Properties grid - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_("text-gray-500")}, - list{text("Confidence")}, - ), - div( - list{ - Attrs.class_( - `font-mono ${recipe.confidence >= 0.97 - ? "text-emerald-400" - : "text-amber-400"}`, - ), - }, - list{ - text( - `${Float.toFixed( - recipe.confidence *. 100.0, - ~digits=1, - )}%`, - ), - }, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text("Tier")}, - ), - div( - list{Attrs.class_(HypatiaEngine.tierColor(recipe.tier))}, - list{text(HypatiaEngine.tierLabel(recipe.tier))}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text("Fix script")}, - ), - div( - list{ - Attrs.class_( - recipe.hasFixScript - ? "text-green-400" - : "text-gray-600", - ), - }, - list{text(recipe.hasFixScript ? "Available" : "None")}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text("Times fired")}, - ), - div( - list{Attrs.class_("text-gray-300 font-mono")}, - list{text(Int.toString(recipe.timesTriggered))}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text("Last triggered")}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(recipe.lastTriggered)}, - ), - }, - ), - // Languages - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Languages")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - recipe.languages - ->Array.map(lang => - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800/80 text-gray-300 rounded border border-gray-700", - ), - }, - list{text(lang)}, - ) - ) - ->List.fromArray, - ), - }, - ), - // Confidence gauge - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Confidence gauge")}, - ), - div( - list{ - Attrs.class_("w-full bg-gray-800/60 rounded-full h-2"), - }, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full transition-all ${recipe.confidence >= 0.97 - ? "bg-emerald-500" - : recipe.confidence >= 0.93 - ? "bg-amber-500" - : "bg-red-500"}`, - ), - Attrs.prop( - "style", - `width: ${Float.toFixed( - recipe.confidence *. 100.0, - ~digits=0, - )}%`, - ), - }, - list{}, - ), - }, - ), - }, - ), - }, - ) - | None => - div( - list{ - Attrs.class_( - "w-80 border border-gray-700 rounded-lg p-4 text-xs text-gray-500", - ), - }, - list{text("Recipe not found")}, - ) - } - | None => - div( - list{ - Attrs.class_( - "w-80 border border-gray-700 rounded-lg p-6 flex items-center justify-center", - ), - }, - list{ - div( - list{Attrs.class_("text-center text-gray-600 text-xs")}, - list{text("Select a recipe to view details")}, - ), - }, - ) - }, - }, - ) - }, - }, - ) - }, - }, - ) - }, - switch hypatia.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/Interfaces.affine b/src/components/Interfaces.affine new file mode 100644 index 00000000..c5d99bb7 --- /dev/null +++ b/src/components/Interfaces.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Interfaces; + +// TODO: Complete semantic implementation diff --git a/src/components/Interfaces.res b/src/components/Interfaces.res deleted file mode 100644 index 760d1462..00000000 --- a/src/components/Interfaces.res +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Interfaces Component — ABI/FFI inventory dashboard. -/// -/// Idris2 ABI definitions, Zig FFI implementations, per-language -/// binding coverage matrix, believe_me audit. - -open Model -open Msg -open Tea.Html - -let renderAbiRow = (def: abiDefinition): Tea_Vdom.t => { - let verifiedIcon = def.verified ? "text-green-400" : "text-red-400" - let verifiedText = def.verified ? "Verified" : "Unverified" - div( - list{Attrs.class_("flex items-center gap-4 p-2 border-b border-gray-800"), Attrs.role("row")}, - list{ - span(list{Attrs.class_(`text-xs ${verifiedIcon} w-16`)}, list{text(verifiedText)}), - span(list{Attrs.class_("text-sm text-gray-300 w-32")}, list{text(def.moduleName)}), - span(list{Attrs.class_("text-xs text-gray-500 flex-1 truncate")}, list{text(def.path)}), - span( - list{Attrs.class_("text-xs text-gray-400 w-20 text-right")}, - list{text(`${Int.toString(def.exportCount)} exports`)}, - ), - if def.believeMeCount > 0 { - span( - list{Attrs.class_("text-xs text-red-400 w-24 text-right font-bold")}, - list{text(`${Int.toString(def.believeMeCount)} believe_me`)}, - ) - } else { - span( - list{Attrs.class_("text-xs text-green-400 w-24 text-right")}, - list{text("0 believe_me")}, - ) - }, - }, - ) -} - -let renderBindingRow = (binding: bindingCoverage): Tea_Vdom.t => { - let covColor = if binding.coverage > 80.0 { - "bg-green-500" - } else if binding.coverage > 40.0 { - "bg-amber-500" - } else { - "bg-red-500" - } - div( - list{Attrs.class_("flex items-center gap-3 mb-2")}, - list{ - div( - list{Attrs.class_("w-24 text-sm text-gray-300 text-right")}, - list{text(binding.language)}, - ), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-3")}, - list{ - div( - list{ - Attrs.class_(`${covColor} h-full rounded-full transition-all`), - Attrs.prop("style", `width: ${Float.toFixed(binding.coverage, ~digits=0)}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-20 text-xs text-gray-400 text-right")}, - list{text(`${Int.toString(binding.boundCount)}/${Int.toString(binding.totalCount)}`)}, - ), - div( - list{Attrs.class_("w-14 text-xs text-gray-500")}, - list{text(`${Float.toFixed(binding.coverage, ~digits=0)}%`)}, - ), - }, - ) -} - -let renderTabs = (active: interfacesCategory): Tea_Vdom.t => { - let tabs: array = [IfaceDashboard, IfaceAbi, IfaceFfi, IfaceBindings] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-orange-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Interfaces(SetIfaceCategory(tab))), - }, - list{text(InterfacesEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -let view = (iface: interfacesState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Interfaces ABI/FFI panel"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("Interfaces")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Idris2 ABI + Zig FFI inventory")}, - ), - if iface.totalBelieveMe > 0 { - span( - list{Attrs.class_("text-xs text-red-400 ml-2 font-bold")}, - list{text(`${Int.toString(iface.totalBelieveMe)} believe_me VIOLATIONS`)}, - ) - } else { - span(list{Attrs.class_("text-xs text-green-400 ml-2")}, list{text("0 believe_me")}) - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-orange-600 text-white rounded hover:bg-orange-500", - ), - Events.onClick(Interfaces(ScanInterfaces)), - KeyboardNav.onActivate(Interfaces(ScanInterfaces)), - }, - list{text("Scan")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - if !iface.loaded { - div( - list{Attrs.class_("text-center text-gray-500 mt-12")}, - list{ - div(list{Attrs.class_("text-4xl mb-2")}, list{text("Interfaces")}), - div( - list{Attrs.class_("text-sm mb-6")}, - list{ - text("ABI definitions (Idris2) + FFI implementations (Zig) + binding coverage"), - }, - ), - button( - list{ - Attrs.class_("px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-500"), - Events.onClick(Interfaces(ScanInterfaces)), - KeyboardNav.onActivate(Interfaces(ScanInterfaces)), - }, - list{text("Scan ABI/FFI")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderTabs(iface.activeCategory), - switch iface.activeCategory { - | IfaceDashboard => - div( - list{Attrs.class_("space-y-6")}, - list{ - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(Array.length(iface.abiDefs))} ABI modules`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Int.toString( - InterfacesEngine.totalAbiExports(iface.abiDefs), - )} exports`, - ), - }, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Float.toFixed( - InterfacesEngine.verificationRate(iface.abiDefs) *. 100.0, - ~digits=0, - )}% verified`, - ), - }, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Float.toFixed( - InterfacesEngine.avgCoverage(iface.bindings), - ~digits=0, - )}% avg binding coverage`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("Binding Coverage by Language")}, - ), - div( - list{}, - iface.bindings->Array.map(b => renderBindingRow(b))->List.fromArray, - ), - }, - ), - }, - ) - | IfaceAbi => - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - iface.abiDefs->Array.map(d => renderAbiRow(d))->List.fromArray, - ), - }, - ) - | IfaceFfi => - div( - list{Attrs.class_("text-gray-500 text-sm")}, - list{ - text(`${Int.toString(Array.length(iface.ffiImpls))} Zig FFI implementations`), - }, - ) - | IfaceBindings => - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{}, - iface.bindings->Array.map(b => renderBindingRow(b))->List.fromArray, - ), - }, - ) - }, - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/K9Manager.affine b/src/components/K9Manager.affine new file mode 100644 index 00000000..bc813bcb --- /dev/null +++ b/src/components/K9Manager.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module K9Manager; + +// TODO: Complete semantic implementation diff --git a/src/components/K9Manager.res b/src/components/K9Manager.res deleted file mode 100644 index 707be3ca..00000000 --- a/src/components/K9Manager.res +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL K9 Manager Component — self-validating K9 contractile management panel. -/// -/// Displays K9 security levels (Kennel/Yard/Hunt) with colour-coded badges, -/// lists loaded contractile files with validation status, and provides -/// action buttons for loading, validating, and applying K9 layouts. -/// -/// Wires to K9Cmd.res functions via the K9 message channel in the TEA loop. - -open Msg -open Tea.Html - -/// Render a K9 security level badge with colour coding. -/// Kennel=green (safe), Yard=amber (validated), Hunt=red (full execution). -let securityLevelBadge = (level: K9Engine.k9SecurityLevel): Tea_Vdom.t => { - let (color, label) = switch level { - | Kennel => ("bg-green-700 text-green-100", "Kennel") - | Yard => ("bg-amber-700 text-amber-100", "Yard") - | Hunt => ("bg-red-700 text-red-100", "Hunt") - } - span( - list{ - Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color), - Attrs.ariaLabel("Security level: " ++ label), - }, - list{text(label)}, - ) -} - -/// Render a validation status indicator for a loaded file. -let validationBadge = (entry: K9Model.k9FileEntry): Tea_Vdom.t => { - if entry.validating { - span( - list{Attrs.class_("text-xs text-blue-400 animate-pulse font-mono")}, - list{text("Validating...")}, - ) - } else { - switch entry.contractile { - | Some(c) if c.isValid => - span(list{Attrs.class_("text-xs text-green-400 font-mono")}, list{text("Valid")}) - | Some(c) => - span( - list{ - Attrs.class_("text-xs text-red-400 font-mono"), - Attrs.ariaLabel("Invalid: " ++ c.errors->Array.join(", ")), - }, - list{text("Invalid (" ++ Int.toString(Array.length(c.errors)) ++ " errors)")}, - ) - | None => - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text("Not validated")}) - } - } -} - -/// Render a single loaded file row. -let renderFileEntry = (entry: K9Model.k9FileEntry): Tea_Vdom.t => { - let levelBadge = switch entry.contractile { - | Some(c) => securityLevelBadge(c.securityLevel) - | None => span(list{Attrs.class_("text-xs text-gray-600")}, list{text("--")}) - } - div( - list{ - Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-900 border border-gray-800 rounded"), - Attrs.role("listitem"), - Attrs.ariaLabel("K9 file: " ++ entry.path), - }, - list{ - levelBadge, - span( - list{Attrs.class_("flex-1 text-sm text-gray-300 truncate font-mono")}, - list{text(entry.path)}, - ), - validationBadge(entry), - button( - list{ - Attrs.class_("px-2 py-1 text-xs bg-gray-800 hover:bg-gray-700 text-gray-300 rounded"), - Attrs.ariaLabel("Validate " ++ entry.path), - Events.onClick(K9(ValidateContractile(entry.path))), - }, - list{text("Validate")}, - ), - }, - ) -} - -/// Main view function for the K9 Manager panel. -let view = (state: K9Model.k9ManagerState): Tea_Vdom.t => { - let fileCount = Array.length(state.loadedFiles) - let validCount = - state.loadedFiles - ->Array.filter(e => - switch e.contractile { - | Some(c) => c.isValid - | None => false - } - ) - ->Array.length - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("K9 Manager — Self-validating contractile management"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-bold text-green-300")}, list{text("K9 Manager")}), - securityLevelBadge(state.currentLevel), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(fileCount) ++ " files, " ++ Int.toString(validCount) ++ " valid", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-green-800 hover:bg-green-700 text-white rounded", - ), - Attrs.ariaLabel("Load a K9 contractile file"), - Events.onClick(K9(LoadContractile(""))), - }, - list{text("Load Contractile")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-amber-800 hover:bg-amber-700 text-white rounded", - ), - Attrs.ariaLabel("Apply a K9 layout preset"), - Events.onClick(K9(ApplyLayout(""))), - }, - list{text("Apply Layout")}, - ), - }, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - Attrs.role("alert"), - }, - list{text(err)}, - ) - | None => Tea_Html.noNode - }, - // File list - div( - list{ - Attrs.class_("flex-1 overflow-y-auto px-4 py-4 space-y-2"), - Attrs.role("list"), - Attrs.ariaLabel("Loaded K9 contractile files"), - }, - if fileCount == 0 { - list{ - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{ - div(list{Attrs.class_("text-sm mb-2")}, list{text("No K9 files loaded")}), - div( - list{Attrs.class_("text-xs")}, - list{text("Click \"Load Contractile\" to add a .k9.ncl file")}, - ), - }, - ), - } - } else { - state.loadedFiles->Array.map(entry => renderFileEntry(entry))->List.fromArray - }, - ), - }, - ) -} diff --git a/src/components/LanguageForge.affine b/src/components/LanguageForge.affine new file mode 100644 index 00000000..ca82a051 --- /dev/null +++ b/src/components/LanguageForge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LanguageForge; + +// TODO: Complete semantic implementation diff --git a/src/components/LanguageForge.res b/src/components/LanguageForge.res deleted file mode 100644 index 268da0c5..00000000 --- a/src/components/LanguageForge.res +++ /dev/null @@ -1,550 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Language Forge Component — view layer for the nextgen-languages panel. -/// -/// Renders a full-screen overlay with the 14 nextgen-languages portfolio. -/// Layout follows the Farm panel pattern: -/// - Header with title, stats, and close button -/// - Category tab bar (All | Production Ready | In Progress | Needs Work) -/// - Filter/sort controls -/// - Language cards with score bars, phase badges, component indicators -/// - Selected language detail view with MoSCoW breakdown - -open Model -open Msg -open Tea.Html - -/// Render a single category tab button. -let renderCategoryTab = (cat: forgeCategory, isActive: bool): Tea_Vdom.t => { - let activeClass = isActive - ? "border-indigo-500 text-indigo-300 bg-gray-800/50" - : "border-transparent text-gray-500 hover:text-gray-300 hover:border-gray-600" - - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium border-b-2 cursor-pointer transition-colors ${activeClass}`, - ), - Attrs.role("tab"), - Attrs.ariaLabel(`Filter by ${LanguageForgeEngine.categoryLabel(cat)}`), - Events.onClick(LanguageForge(SetForgeCategory(cat))), - }, - list{text(LanguageForgeEngine.categoryLabel(cat))}, - ) -} - -/// Render the category tab bar. -let renderCategoryTabBar = (activeCategory: forgeCategory): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex border-b border-gray-800 overflow-x-auto"), - Attrs.role("tablist"), - Attrs.ariaLabel("Language portfolio categories"), - }, - LanguageForgeEngine.allCategories - ->Array.map(cat => renderCategoryTab(cat, cat === activeCategory)) - ->List.fromArray, - ) -} - -/// Render a phase badge with colour coding. -let renderPhaseBadge = (phase: languagePhase): Tea_Vdom.t => { - span( - list{ - Attrs.class_( - `text-xs px-1.5 py-0.5 rounded border ${LanguageForgeEngine.phaseBadgeClass(phase)}`, - ), - }, - list{text(LanguageForgeEngine.phaseLabel(phase))}, - ) -} - -/// Render a score bar (horizontal progress indicator). -let renderScoreBar = (score: int): Tea_Vdom.t => { - let pct = Int.toString(score) - let barColour = if score >= 90 { - "bg-emerald-500" - } else if score >= 70 { - "bg-green-500" - } else if score >= 40 { - "bg-amber-500" - } else if score > 0 { - "bg-red-500" - } else { - "bg-gray-700" - } - div( - list{Attrs.class_("flex items-center gap-2"), Attrs.ariaLabel(`Score: ${pct} percent`)}, - list{ - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_(`h-full ${barColour} rounded-full transition-all`), - Attrs.style("width", `${pct}%`), - }, - list{}, - ), - }, - ), - span(list{Attrs.class_("text-xs text-gray-400 w-8 text-right")}, list{text(`${pct}%`)}), - }, - ) -} - -/// Render component pipeline indicators (lexer/parser/typechecker/wasm). -let renderPipelineIndicators = (lang: languageEntry): Tea_Vdom.t => { - let indicator = (label: string, complete: bool): Tea_Vdom.t => - span( - list{ - Attrs.class_(complete ? "text-emerald-400" : "text-gray-600"), - Attrs.title(`${label}: ${complete ? "complete" : "incomplete"}`), - Attrs.ariaLabel(`${label} ${complete ? "complete" : "incomplete"}`), - }, - list{text(complete ? "Y" : "-")}, - ) - div( - list{Attrs.class_("flex gap-3 text-xs")}, - list{ - indicator("Lexer", lang.lexerComplete), - indicator("Parser", lang.parserComplete), - indicator("Types", lang.typeCheckerComplete), - indicator("WASM", lang.hasWasmBackend), - }, - ) -} - -/// Render a single language card/row. -let renderLanguageRow = (lang: languageEntry, isSelected: bool): Tea_Vdom.t => { - let selectedClass = isSelected - ? "bg-gray-800/60 border-l-2 border-l-indigo-500" - : "hover:bg-gray-800/30 border-l-2 border-l-transparent" - - div( - list{ - Attrs.class_( - `flex items-center gap-3 px-4 py-3 ${selectedClass} border-b border-gray-800/50 transition-colors cursor-pointer`, - ), - Attrs.role("button"), - Attrs.tabIndex(0), - Attrs.ariaLabel( - `${lang.name} — ${LanguageForgeEngine.phaseLabel(lang.phase)}, score ${Int.toString( - lang.score, - )} percent`, - ), - Events.onClick(LanguageForge(SelectLanguage(Some(lang.name)))), - KeyboardUtil.onEnterOrSpace(LanguageForge(SelectLanguage(Some(lang.name)))), - }, - list{ - // Name + impl language - div( - list{Attrs.class_("w-36 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-200 truncate")}, - list{text(lang.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 truncate")}, - list{text(lang.implLang === "" ? "unspecified" : lang.implLang)}, - ), - }, - ), - // Score bar - div(list{Attrs.class_("flex-1 min-w-0")}, list{renderScoreBar(lang.score)}), - // Phase badge - div(list{Attrs.class_("w-28 flex justify-center")}, list{renderPhaseBadge(lang.phase)}), - // Pipeline indicators - div(list{Attrs.class_("w-28")}, list{renderPipelineIndicators(lang)}), - // WASM readiness - div( - list{Attrs.class_("w-12 text-center")}, - list{ - span( - list{ - Attrs.class_( - lang.hasWasmBackend - ? "text-emerald-400 text-xs font-medium" - : "text-gray-600 text-xs", - ), - Attrs.title(lang.hasWasmBackend ? "WASM backend available" : "No WASM backend"), - }, - list{text(lang.hasWasmBackend ? "WASM" : "-")}, - ), - }, - ), - }, - ) -} - -/// Render the table header row. -let renderTableHeader = (): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-4 py-2 border-b border-gray-700 text-xs font-medium text-gray-500 uppercase tracking-wider", - ), - }, - list{ - div(list{Attrs.class_("w-36")}, list{text("Language")}), - div(list{Attrs.class_("flex-1")}, list{text("Score")}), - div(list{Attrs.class_("w-28 text-center")}, list{text("Phase")}), - div(list{Attrs.class_("w-28")}, list{text("L / P / T / W")}), - div(list{Attrs.class_("w-12 text-center")}, list{text("WASM")}), - }, - ) -} - -/// Render component detail table for a selected language. -let renderComponentDetails = (components: array): Tea_Vdom.t => { - div( - list{ - Attrs.class_("space-y-2"), - Attrs.role("list"), - Attrs.ariaLabel("Component completion details"), - }, - components - ->Array.map(comp => { - let pct = Int.toString(comp.completion) - div( - list{Attrs.class_("flex items-center gap-3"), Attrs.role("listitem")}, - list{ - div(list{Attrs.class_("w-32 text-sm text-gray-300")}, list{text(comp.name)}), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500 rounded-full transition-all"), - Attrs.style("width", `${pct}%`), - }, - list{}, - ), - }, - ), - div(list{Attrs.class_("w-12 text-xs text-gray-500 text-right")}, list{text(`${pct}%`)}), - span( - list{ - Attrs.class_(comp.hasTests ? "text-emerald-400 text-xs" : "text-gray-600 text-xs"), - Attrs.title(comp.hasTests ? "Has tests" : "No tests"), - }, - list{text(comp.hasTests ? "tested" : "untested")}, - ), - }, - ) - }) - ->List.fromArray, - ) -} - -/// Render the selected language detail panel. -let renderDetailView = (lang: languageEntry, showMoscow: bool): Tea_Vdom.t => { - div( - list{ - Attrs.class_("border-l border-gray-700 w-80 flex-shrink-0 overflow-y-auto p-4 space-y-4"), - Attrs.role("complementary"), - Attrs.ariaLabel(`${lang.name} detail view`), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - h3(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text(lang.name)}), - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300 text-sm"), - Attrs.ariaLabel("Close detail view"), - Events.onClick(LanguageForge(SelectLanguage(None))), - }, - list{text("X")}, - ), - }, - ), - // Summary stats - div( - list{Attrs.class_("space-y-1 text-sm")}, - list{ - div( - list{Attrs.class_("flex justify-between text-gray-400")}, - list{ - text("Implementation"), - span( - list{Attrs.class_("text-gray-200")}, - list{text(lang.implLang === "" ? "None" : lang.implLang)}, - ), - }, - ), - div( - list{Attrs.class_("flex justify-between text-gray-400")}, - list{text("Phase"), renderPhaseBadge(lang.phase)}, - ), - div( - list{Attrs.class_("flex justify-between text-gray-400")}, - list{ - text("Score"), - span(list{Attrs.class_("text-gray-200")}, list{text(`${Int.toString(lang.score)}%`)}), - }, - ), - div( - list{Attrs.class_("flex justify-between text-gray-400")}, - list{ - text("LOC"), - span(list{Attrs.class_("text-gray-200")}, list{text(Int.toString(lang.locCount))}), - }, - ), - div( - list{Attrs.class_("flex justify-between text-gray-400")}, - list{ - text("TODOs"), - span( - list{Attrs.class_(lang.todoCount > 20 ? "text-amber-400" : "text-gray-200")}, - list{text(Int.toString(lang.todoCount))}, - ), - }, - ), - }, - ), - // WASM readiness - div( - list{Attrs.class_("flex items-center gap-2 text-sm")}, - list{ - span( - list{Attrs.class_(lang.hasWasmBackend ? "text-emerald-400" : "text-red-400")}, - list{text(lang.hasWasmBackend ? "WASM Ready" : "No WASM")}, - ), - }, - ), - // Component breakdown - div( - list{Attrs.class_("border-t border-gray-700 pt-3")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-2")}, - list{text("Components")}, - ), - if Array.length(lang.components) > 0 { - renderComponentDetails(lang.components) - } else { - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("No components defined yet.")}, - ) - }, - }, - ), - // MoSCoW toggle - div( - list{Attrs.class_("border-t border-gray-700 pt-3")}, - list{ - button( - list{ - Attrs.class_("text-sm text-indigo-400 hover:text-indigo-300 transition-colors"), - Attrs.ariaLabel(showMoscow ? "Hide MoSCoW breakdown" : "Show MoSCoW breakdown"), - Events.onClick(LanguageForge(ToggleMoscow)), - KeyboardNav.onActivate(LanguageForge(ToggleMoscow)), - }, - list{text(showMoscow ? "Hide MoSCoW" : "Show MoSCoW")}, - ), - if showMoscow { - div( - list{Attrs.class_("mt-2 space-y-1 text-xs text-gray-400")}, - list{ - div(list{}, list{text("Must Have: Lexer, Parser")}), - div(list{}, list{text("Should Have: Type Checker, Tests")}), - div(list{}, list{text("Could Have: WASM Backend, LSP")}), - div(list{}, list{text("Won't Have (now): IDE Plugin, Package Manager")}), - }, - ) - } else { - noNode - }, - }, - ), - }, - ) -} - -/// Render summary statistics in the header. -let renderStats = (languages: array): Tea_Vdom.t => { - let total = Array.length(languages) - let production = languages->Array.filter(l => l.phase === Production)->Array.length - let withWasm = languages->Array.filter(l => l.hasWasmBackend)->Array.length - div( - list{Attrs.class_("flex items-center gap-3 text-xs text-gray-500")}, - list{ - span(list{}, list{text(`${Int.toString(total)} languages`)}), - span(list{Attrs.class_("text-gray-700")}, list{text("|")}), - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`${Int.toString(production)} production`)}, - ), - span(list{Attrs.class_("text-gray-700")}, list{text("|")}), - span(list{}, list{text(`${Int.toString(withWasm)} WASM`)}), - }, - ) -} - -/// Render the header bar with title, stats, and controls. -let renderHeader = (forge: languageForgeState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center justify-between px-6 py-4 border-b border-gray-800")}, - list{ - // Title and stats - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Language Forge")}, - ), - if forge.loaded { - renderStats(forge.languages) - } else { - noNode - }, - }, - ), - // Controls: filter, close - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Filter input - input( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-700 rounded px-3 py-1.5 text-sm text-gray-200 placeholder-gray-500 focus:border-indigo-500 focus:outline-none w-48", - ), - Attrs.placeholder("Filter languages..."), - Attrs.value(forge.filterText), - Attrs.ariaLabel("Filter languages by name or implementation"), - Events.onInput(text => LanguageForge(SetForgeFilter(text))), - }, - list{}, - ), - // Close button - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Attrs.ariaLabel("Close Language Forge panel"), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ) -} - -/// Render a loading state. -let renderLoading = (): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-gray-500 animate-pulse")}, - list{text("Loading language portfolio...")}, - ), - }, - ) -} - -/// Render an error state. -let renderError = (error: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div(list{Attrs.class_("text-red-400 mb-2")}, list{text("Failed to load language data")}), - div(list{Attrs.class_("text-sm text-gray-500 mb-4")}, list{text(error)}), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors", - ), - Attrs.ariaLabel("Retry loading language data"), - Events.onClick(LanguageForge(LoadLanguages)), - KeyboardNav.onActivate(LanguageForge(LoadLanguages)), - }, - list{text("Retry")}, - ), - }, - ), - }, - ) -} - -/// Main Language Forge panel view — full-screen overlay. -let view = (forge: languageForgeState): Tea_Vdom.t => { - let filtered = LanguageForgeEngine.filterLanguages( - forge.languages, - forge.activeCategory, - forge.filterText, - ) - let sorted = LanguageForgeEngine.sortLanguages(filtered, forge.sortBy) - - // Find selected language entry for detail view - let selectedEntry = switch forge.selectedLanguage { - | None => None - | Some(name) => forge.languages->Array.find(l => l.name === name) - } - - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Language Forge panel"), - }, - list{ - // Header - renderHeader(forge), - // Category tabs - renderCategoryTabBar(forge.activeCategory), - // Content area with optional detail sidebar - if forge.loading { - renderLoading() - } else { - switch forge.error { - | Some(e) => renderError(e) - | None => - if !forge.loaded { - renderLoading() - } else { - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Main language list - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{ - renderTableHeader(), - div( - list{Attrs.role("list"), Attrs.ariaLabel("Language portfolio")}, - sorted - ->Array.map(lang => { - let isSelected = forge.selectedLanguage === Some(lang.name) - renderLanguageRow(lang, isSelected) - }) - ->List.fromArray, - ), - }, - ), - // Detail sidebar (if a language is selected) - switch selectedEntry { - | Some(lang) => renderDetailView(lang, forge.showMoscow) - | None => noNode - }, - }, - ) - } - } - }, - }, - ) -} diff --git a/src/components/LevelArchitect.affine b/src/components/LevelArchitect.affine new file mode 100644 index 00000000..3115068c --- /dev/null +++ b/src/components/LevelArchitect.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LevelArchitect; + +// TODO: Complete semantic implementation diff --git a/src/components/LevelArchitect.res b/src/components/LevelArchitect.res deleted file mode 100644 index 23503fc8..00000000 --- a/src/components/LevelArchitect.res +++ /dev/null @@ -1,582 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Level Architect Component — view for the IDApTIK visual level -/// design tool. Grid editor, asset browser, patrol editor, and level -/// validation with undo/redo. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: levelArchitectCategory, - active: levelArchitectCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(LevelArchitect(SetArchitectCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render tool selector buttons. -let renderToolbar = (state: levelArchitectState): Tea_Vdom.t => { - let tools: array = [ - ToolSelect, - ToolPlace(EntityDevice), - ToolPlace(EntityGuard), - ToolPlace(EntitySpawnPoint), - ToolErase, - ToolPatrol, - ToolDefenceFlag, - ] - div( - list{Attrs.class_("flex items-center gap-1 flex-wrap")}, - tools - ->Array.map(tool => { - let isActive = state.selectedTool === tool - let cls = isActive - ? "px-2 py-1 text-xs bg-cyan-700 text-white rounded" - : "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(LevelArchitect(SelectTool(tool)))}, - list{text(LevelArchitectEngine.toolLabel(tool))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a single grid cell. -let renderGridCell = (state: levelArchitectState, x: int, y: int): Tea_Vdom.t => { - let entity = state.entities->Array.find(e => e.gridX === x && e.gridY === y) - let isSelected = switch entity { - | Some(e) => state.selectedEntityId === Some(e.id) - | None => false - } - let bgCls = switch entity { - | Some(e) => - switch e.kind { - | EntityDevice => "bg-cyan-900/50" - | EntityGuard => "bg-red-900/50" - | EntitySpawnPoint => "bg-emerald-900/50" - | EntityCompanion => "bg-purple-900/50" - | EntityCollectable => "bg-amber-900/50" - | EntityTrigger => "bg-orange-900/50" - | EntityDecoration => "bg-gray-700/50" - } - | None => "bg-gray-900/30" - } - let borderCls = if isSelected { - "border-cyan-400" - } else { - "border-gray-700" - } - div( - list{ - Attrs.class_( - `w-8 h-8 border ${borderCls} ${bgCls} flex items-center justify-center cursor-pointer hover:border-gray-500`, - ), - Events.onClick(LevelArchitect(ClickGrid(x, y))), - }, - list{ - switch entity { - | Some(e) => - span( - list{Attrs.class_("text-xs text-gray-300")}, - list{ - text( - switch e.kind { - | EntityDevice => "D" - | EntityGuard => "G" - | EntitySpawnPoint => "S" - | EntityCompanion => "C" - | EntityCollectable => "$" - | EntityTrigger => "!" - | EntityDecoration => "." - }, - ), - }, - ) - | None => noNode - }, - }, - ) -} - -/// Render the grid editor. -let renderGrid = (state: levelArchitectState): Tea_Vdom.t => { - let rows = Array.fromInitializer(~length=state.gridHeight, i => i) - let cols = Array.fromInitializer(~length=state.gridWidth, i => i) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Toolbar - renderToolbar(state), - // Grid - div( - list{Attrs.class_("overflow-auto border border-gray-700 rounded p-2 bg-gray-900")}, - list{ - div( - list{Attrs.class_("inline-block")}, - rows - ->Array.map(y => - div( - list{Attrs.class_("flex")}, - cols->Array.map(x => renderGridCell(state, x, y))->List.fromArray, - ) - ) - ->List.fromArray, - ), - }, - ), - // Entity count summary - div( - list{Attrs.class_("flex items-center gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text(`Entities: ${Int.toString(Array.length(state.entities))}`)}), - span( - list{}, - list{ - text( - `Guards: ${Int.toString( - LevelArchitectEngine.countByKind(state.entities, EntityGuard), - )}`, - ), - }, - ), - span( - list{}, - list{ - text( - `Devices: ${Int.toString( - LevelArchitectEngine.countByKind(state.entities, EntityDevice), - )}`, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render the asset browser. -let renderAssets = (state: levelArchitectState): Tea_Vdom.t => { - if Array.length(state.assets) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No assets loaded. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(LevelArchitect(BrowseAssets)), - KeyboardNav.onActivate(LevelArchitect(BrowseAssets)), - }, - list{text("Browse assets")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-3 gap-2")}, - state.assets - ->Array.map(asset => - div( - list{ - Attrs.class_( - "p-2 bg-gray-800 rounded border border-gray-700 cursor-pointer hover:border-gray-500", - ), - Events.onClick(LevelArchitect(SelectTool(ToolPlace(asset.entityKind)))), - }, - list{ - div(list{Attrs.class_("text-xs text-gray-100 font-medium")}, list{text(asset.name)}), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(asset.category)}), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render patrol paths view. -let renderPatrols = (state: levelArchitectState): Tea_Vdom.t => { - if Array.length(state.patrols) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text( - "No guard patrols defined. Select the Patrol tool and click guard entities to add waypoints.", - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.patrols - ->Array.map(patrol => - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-100")}, - list{text(`Guard: ${patrol.guardId}`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(Array.length(patrol.waypoints))} waypoints`)}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - if patrol.looping { - `Speed: ${Float.toString(patrol.speed)} | Looping` - } else { - `Speed: ${Float.toString(patrol.speed)} | One-way` - }, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render validation issues. -/// Render a single ABI proof badge (pass/fail indicator). -let renderAbiProofBadge = (name: string, passed: bool): Tea_Vdom.t => { - let (dotCls, textCls) = if passed { - ("bg-emerald-400", "text-emerald-400") - } else { - ("bg-red-400", "text-red-400") - } - div( - list{Attrs.class_("flex items-center gap-1.5")}, - list{ - div(list{Attrs.class_(`w-2 h-2 rounded-full ${dotCls}`)}, list{}), - span(list{Attrs.class_(`text-xs ${textCls}`)}, list{text(name)}), - }, - ) -} - -let renderValidation = (state: levelArchitectState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Action bar - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-amber-700 text-white rounded hover:bg-amber-600 cursor-pointer", - ), - Events.onClick(LevelArchitect(ValidateLevel)), - KeyboardNav.onActivate(LevelArchitect(ValidateLevel)), - }, - list{text("Validate Level")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-cyan-400 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelLevelArchitect))), - }, - list{text("Open in UMS")}, - ), - }, - ), - // UMS ABI Validation (5 Idris2 proofs) - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-300 mb-2")}, - list{text("Idris2 ABI Proofs (5 cross-domain invariants)")}, - ), - switch state.umsValidation { - | Some(v) => - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-2")}, - list{ - renderAbiProofBadge("Guards in Zones", v.guardsInZones), - renderAbiProofBadge("Defence Targets Valid", v.defenceTargetsValid), - renderAbiProofBadge("Zones Ordered", v.zonesOrdered), - renderAbiProofBadge("PBX Consistent", v.pbxConsistent), - renderAbiProofBadge("Devices Exist", v.devicesExist), - div( - list{Attrs.class_("flex items-center gap-1.5")}, - list{ - div( - list{ - Attrs.class_( - if v.allPassed { - "w-2 h-2 rounded-full bg-emerald-400" - } else { - "w-2 h-2 rounded-full bg-red-400" - }, - ), - }, - list{}, - ), - span( - list{ - Attrs.class_( - if v.allPassed { - "text-xs font-bold text-emerald-400" - } else { - "text-xs font-bold text-red-400" - }, - ), - }, - list{ - text( - if v.allPassed { - "ALL PASSED" - } else { - "HAS FAILURES" - }, - ), - }, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Not yet validated. Click 'Validate Level' to run ABI proof checks.")}, - ) - }, - }, - ), - // Classic validation issues - if Array.length(state.validationIssues) === 0 { - div( - list{Attrs.class_("text-center text-emerald-400 text-sm py-4")}, - list{text("No validation issues found")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.validationIssues - ->Array.map(issue => { - let severityCls = switch issue.severity { - | "error" => "border-red-700 bg-red-900/30" - | "warning" => "border-amber-700 bg-amber-900/30" - | _ => "border-blue-700 bg-blue-900/30" - } - div( - list{Attrs.class_(`p-2 rounded border ${severityCls} text-xs`)}, - list{ - span(list{Attrs.class_("text-gray-200")}, list{text(issue.message)}), - switch issue.entityId { - | Some(eid) => - span(list{Attrs.class_("text-gray-500 ml-2")}, list{text(`(${eid})`)}) - | None => noNode - }, - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render defence flags toggles. -let renderDefenceFlags = (state: levelArchitectState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-wrap gap-2 mt-2")}, - LevelArchitectEngine.allDefenceFlags - ->Array.map(flag => { - let isActive = state.defenceFlags->Array.includes(flag) - let cls = if isActive { - "px-2 py-1 text-xs bg-emerald-700 text-emerald-100 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer hover:bg-gray-600" - } - button( - list{Attrs.class_(cls), Events.onClick(LevelArchitect(ToggleDefenceFlag(flag)))}, - list{text(LevelArchitectEngine.defenceFlagLabel(flag))}, - ) - }) - ->List.fromArray, - ) -} - -/// Main view function. -let view = (state: levelArchitectState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Level Architect panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Level Architect")}, - ), - span(list{Attrs.class_("text-sm text-gray-400")}, list{text(state.levelName)}), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Undo / Redo - button( - list{ - Attrs.class_( - if state.historyIndex > 0 { - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - } else { - "px-2 py-1 text-xs bg-gray-800 text-gray-600 rounded cursor-not-allowed" - }, - ), - Events.onClick(LevelArchitect(UndoAction)), - KeyboardNav.onActivate(LevelArchitect(UndoAction)), - }, - list{text("Undo")}, - ), - button( - list{ - Attrs.class_( - if state.historyIndex < Array.length(state.history) - 1 { - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - } else { - "px-2 py-1 text-xs bg-gray-800 text-gray-600 rounded cursor-not-allowed" - }, - ), - Events.onClick(LevelArchitect(RedoAction)), - KeyboardNav.onActivate(LevelArchitect(RedoAction)), - }, - list{text("Redo")}, - ), - // Toggle grid lines - button( - list{ - Attrs.class_( - if state.showGrid { - "px-2 py-1 text-xs bg-cyan-800 text-cyan-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(LevelArchitect(ToggleGrid)), - KeyboardNav.onActivate(LevelArchitect(ToggleGrid)), - }, - list{text("Grid")}, - ), - // Toggle patrol paths - button( - list{ - Attrs.class_( - if state.showPatrolPaths { - "px-2 py-1 text-xs bg-purple-800 text-purple-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(LevelArchitect(TogglePatrolPaths)), - KeyboardNav.onActivate(LevelArchitect(TogglePatrolPaths)), - }, - list{text("Patrols")}, - ), - }, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Grid", ArchitectGrid, state.activeCategory), - renderTab("Assets", ArchitectAssets, state.activeCategory), - renderTab("Patrols", ArchitectPatrols, state.activeCategory), - renderTab("Validation", ArchitectValidation, state.activeCategory), - }, - ), - // Defence flags bar - div( - list{Attrs.class_("px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400")}, list{text("Defence Flags:")}), - renderDefenceFlags(state), - }, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(LevelArchitect(DismissArchitectError)), - KeyboardNav.onActivate(LevelArchitect(DismissArchitectError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Main content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | ArchitectGrid => renderGrid(state) - | ArchitectAssets => renderAssets(state) - | ArchitectPatrols => renderPatrols(state) - | ArchitectValidation => renderValidation(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/LlmCoding.affine b/src/components/LlmCoding.affine new file mode 100644 index 00000000..f783860f --- /dev/null +++ b/src/components/LlmCoding.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LlmCoding; + +// TODO: Complete semantic implementation diff --git a/src/components/LlmCoding.res b/src/components/LlmCoding.res deleted file mode 100644 index 2040d5c1..00000000 --- a/src/components/LlmCoding.res +++ /dev/null @@ -1,464 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL LLM Coding Panel View — headed supervisor for parallel Claude/LLM -/// coding sessions. Spawns, monitors, freezes, and coordinates multiple -/// sessions from a single control panel. -/// -/// Layout: -/// Top bar: system resources + daemon status + spawn button -/// Left column: session cards with state/resource indicators -/// Right column: selected session detail (tasks, locks, messages) -/// Bottom bar: pending actions requiring approval - -open Msg -open LlmCodingModel -open LlmCodingEngine -open Tea.Html - -// ============================================================================ -// System Status Bar -// ============================================================================ - -/// System resource summary and daemon connection status. -let systemBar = (state: llmCodingState): Tea_Vdom.t => { - let memHealth = systemMemoryHealth(state.systemMemoryAvailableMb, state.systemMemoryTotalMb) - let memColor = resourceHealthColor(memHealth) - div( - list{ - Attrs.class_("flex items-center justify-between p-2 bg-gray-900 border-b border-gray-800"), - }, - list{ - // Left: daemon status - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${if state.daemonConnected { - "bg-emerald-400" - } else { - "bg-red-400" - }}`, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - if state.daemonConnected { - "Daemon connected" - } else { - "Daemon offline" - }, - ), - }, - ), - }, - ), - // Centre: system resources - div( - list{Attrs.class_("flex items-center gap-4 text-xs")}, - list{ - span( - list{Attrs.class_(memColor)}, - list{ - text( - `RAM: ${Int.toString(state.systemMemoryAvailableMb)}/${Int.toString( - state.systemMemoryTotalMb, - )}MB`, - ), - }, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text(`CPU: ${Float.toFixed(state.systemCpuPercent, ~digits=0)}%`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(Array.length(state.sessions))} sessions`)}, - ), - }, - ), - // Right: spawn button - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs font-semibold rounded bg-emerald-600 hover:bg-emerald-500 text-white", - ), - }, - list{text("+ Spawn Session")}, - ), - }, - ) -} - -// ============================================================================ -// Session Card -// ============================================================================ - -/// Resource usage bar for a session. -let resourceBar = (usage: resourceUsage, limits: resourceLimits): Tea_Vdom.t => { - let health = resourceHealth(usage, limits) - let color = resourceHealthColor(health) - div( - list{Attrs.class_("flex items-center gap-2 text-xs mt-2")}, - list{ - span(list{Attrs.class_(color)}, list{text(`${Int.toString(usage.memoryMb)}MB`)}), - span( - list{Attrs.class_("text-gray-600")}, - list{text(`CPU ${Float.toFixed(usage.cpuPercent, ~digits=0)}%`)}, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text(`${Int.toString(usage.subagentCount)} agents`)}, - ), - span( - list{Attrs.class_(`ml-auto font-mono ${color}`)}, - list{text(resourceHealthLabel(health))}, - ), - }, - ) -} - -/// Progress bar for a session's task list. -let taskProgress = (tasks: array): Tea_Vdom.t => { - let total = Array.length(tasks) - if total == 0 { - noNode - } else { - let pct = progressPercent(tasks) - let done = completedCount(tasks) - div( - list{Attrs.class_("mt-2")}, - list{ - div( - list{Attrs.class_("flex justify-between text-xs text-gray-500 mb-1")}, - list{ - span(list{}, list{text(`${Int.toString(done)}/${Int.toString(total)} tasks`)}), - span(list{}, list{text(`${Int.toString(pct)}%`)}), - }, - ), - div( - list{Attrs.class_("w-full h-1 bg-gray-800 rounded")}, - list{ - div( - list{ - Attrs.class_("h-1 bg-emerald-500 rounded"), - Attrs.style("width", `${Int.toString(pct)}%`), - }, - list{}, - ), - }, - ), - }, - ) - } -} - -/// A single session card in the sidebar. -let sessionCard = (session: llmSession, isSelected: bool): Tea_Vdom.t => { - let borderClass = stateBorderColor(session.state) - let selectedRing = if isSelected { - " ring-2 ring-emerald-500/30" - } else { - "" - } - div( - list{ - Attrs.class_( - `flex flex-col p-3 rounded-lg border ${borderClass}${selectedRing} bg-gray-900/50 cursor-pointer hover:brightness-110 transition-all`, - ), - }, - list{ - // Header: name + state - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("font-semibold text-sm text-gray-100")}, list{text(session.name)}), - span( - list{Attrs.class_(`text-xs font-mono ${stateColor(session.state)}`)}, - list{text(stateLabel(session.state))}, - ), - }, - ), - // Provider + PID - div( - list{Attrs.class_("flex items-center gap-2 mt-1 text-xs text-gray-500")}, - list{ - span(list{}, list{text(providerName(session.provider))}), - switch session.pid { - | Some(pid) => - span(list{Attrs.class_("font-mono")}, list{text(`PID ${Int.toString(pid)}`)}) - | None => noNode - }, - }, - ), - // Allowed repos - div( - list{Attrs.class_("flex gap-1 flex-wrap mt-1")}, - session.allowedRepos - ->Array.map(repo => - span( - list{Attrs.class_("px-1 py-0.5 text-xs rounded bg-gray-800 text-gray-400 font-mono")}, - list{text(repo)}, - ) - ) - ->List.fromArray, - ), - // Resource usage - resourceBar(session.resources, session.limits), - // Task progress - taskProgress(session.tasks), - // Action buttons (only for alive sessions) - if isAlive(session.state) { - div( - list{Attrs.class_("flex gap-1 mt-2")}, - list{ - if canFreeze(session.state) { - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs rounded bg-amber-700 hover:bg-amber-600 text-white", - ), - }, - list{text("Freeze")}, - ) - } else if canThaw(session.state) { - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs rounded bg-blue-700 hover:bg-blue-600 text-white", - ), - }, - list{text("Thaw")}, - ) - } else { - noNode - }, - button( - list{ - Attrs.class_("px-2 py-0.5 text-xs rounded bg-red-800 hover:bg-red-700 text-white"), - }, - list{text("Kill")}, - ), - }, - ) - } else { - noNode - }, - }, - ) -} - -// ============================================================================ -// Pending Actions Bar -// ============================================================================ - -/// A single pending action requiring approval. -let pendingActionView = (action: pendingAction): Tea_Vdom.t => { - let danger = actionDanger(action.category) - let color = dangerColor(danger) - div( - list{ - Attrs.class_( - "flex items-center justify-between p-2 bg-gray-900 border border-gray-700 rounded", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_(`text-xs font-mono ${color}`)}, - list{text(actionCategoryName(action.category))}, - ), - span(list{Attrs.class_("text-xs text-gray-300")}, list{text(action.description)}), - span(list{Attrs.class_("text-xs text-gray-600")}, list{text(`from ${action.sessionId}`)}), - }, - ), - div( - list{Attrs.class_("flex gap-1")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs rounded bg-emerald-700 hover:bg-emerald-600 text-white", - ), - }, - list{text("Approve")}, - ), - button( - list{ - Attrs.class_("px-2 py-0.5 text-xs rounded bg-red-800 hover:bg-red-700 text-white"), - }, - list{text("Deny")}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Messages View -// ============================================================================ - -/// Cross-session message in the log. -let messageView = (msg_: sessionMessage): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-2 text-xs py-1 border-b border-gray-800/50")}, - list{ - span(list{Attrs.class_("text-gray-600 font-mono shrink-0")}, list{text(msg_.sentAt)}), - span(list{Attrs.class_("text-emerald-400 font-mono shrink-0")}, list{text(msg_.fromSession)}), - span(list{Attrs.class_("text-gray-300")}, list{text(msg_.content)}), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Top-level view for the LLM Coding panel. -let view = (state: llmCodingState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100")}, - list{ - // System status bar - systemBar(state), - // Main content area - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left: session cards - div( - list{ - Attrs.class_("w-80 border-r border-gray-800 overflow-y-auto p-2 flex flex-col gap-2"), - }, - if Array.length(state.sessions) == 0 { - list{ - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-600 text-sm")}, - list{text("No sessions. Click '+ Spawn Session' to begin.")}, - ), - } - } else { - state.sessions - ->Array.map(s => sessionCard(s, state.selectedSession == Some(s.id))) - ->List.fromArray - }, - ), - // Right: detail / messages - div( - list{Attrs.class_("flex-1 overflow-y-auto p-3")}, - list{ - // Cross-session messages - div( - list{Attrs.class_("mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-semibold text-gray-400 mb-2")}, - list{text("Cross-Session Messages")}, - ), - if Array.length(state.messages) == 0 { - div(list{Attrs.class_("text-xs text-gray-600")}, list{text("No messages yet")}) - } else { - div( - list{Attrs.class_("flex flex-col")}, - state.messages->Array.map(messageView)->List.fromArray, - ) - }, - }, - ), - // Workspace locks - div( - list{Attrs.class_("mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-semibold text-gray-400 mb-2")}, - list{text(`Workspace Locks (${Int.toString(Array.length(state.locks))})`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - state.locks - ->Array.map(lock => - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{ - Attrs.class_( - if lock.exclusive { - "text-red-400" - } else { - "text-blue-400" - }, - ), - }, - list{ - text( - if lock.exclusive { - "EXCL" - } else { - "READ" - }, - ), - }, - ), - span( - list{Attrs.class_("text-gray-300 font-mono")}, - list{text(lock.path)}, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text(`held by ${lock.heldBy}`)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ), - }, - ), - }, - ), - // Bottom: pending actions - if Array.length(state.pendingActions) > 0 { - div( - list{Attrs.class_("border-t border-amber-800 bg-amber-950/30 p-2 flex flex-col gap-1")}, - list{ - h3( - list{Attrs.class_("text-xs font-semibold text-amber-400 mb-1")}, - list{ - text( - `${Int.toString(Array.length(state.pendingActions))} action(s) awaiting approval`, - ), - }, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - state.pendingActions->Array.map(pendingActionView)->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Error display - switch state.lastError { - | Some(err) => - div( - list{Attrs.class_("p-2 bg-red-950 border-t border-red-800 text-xs text-red-300")}, - list{text(err)}, - ) - | None => noNode - }, - }, - ) -} diff --git a/src/components/LoadTester.affine b/src/components/LoadTester.affine new file mode 100644 index 00000000..6d4ecdcb --- /dev/null +++ b/src/components/LoadTester.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module LoadTester; + +// TODO: Complete semantic implementation diff --git a/src/components/LoadTester.res b/src/components/LoadTester.res deleted file mode 100644 index e4cf5911..00000000 --- a/src/components/LoadTester.res +++ /dev/null @@ -1,411 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL LoadTester — Phoenix channel stress testing and concurrency simulation -/// for IDApTIK multiplayer infrastructure. -/// -/// Four tabs: Scenarios (editor with player count/ramp-up/duration), Live Test -/// (connected player count, latency, throughput), Results (table of completed -/// runs), and Saturation Curve (placeholder for latency-vs-concurrency chart). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for loadTestTab variants. -let tabLabel = (tab: loadTestTab): string => - switch tab { - | TabScenarios => "Scenarios" - | TabLiveTest => "Live Test" - | TabResults => "Results" - | TabSaturationCurve => "Saturation Curve" - } - -/// Render the tab bar. -let renderTabs = (active: loadTestTab): Tea_Vdom.t => { - let tabs: array = [TabScenarios, TabLiveTest, TabResults, TabSaturationCurve] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(LoadTester(SetLtTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Player status badge. -let playerStatusBadge = (status: simulatedPlayerStatus): Tea_Vdom.t => - switch status { - | PlayerConnecting => - span( - list{ - Attrs.class_( - "px-1.5 py-0.5 text-xs rounded bg-amber-500 text-white font-mono animate-pulse", - ), - }, - list{text("CONNECTING")}, - ) - | PlayerConnected => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-emerald-600 text-white font-mono")}, - list{text("CONNECTED")}, - ) - | PlayerDisconnected(_) => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-gray-600 text-gray-200 font-mono")}, - list{text("DISCONNECTED")}, - ) - | PlayerError(_) => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-red-600 text-white font-mono")}, - list{text("ERROR")}, - ) - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Scenarios tab: list of load test scenarios with configuration details. -let renderScenariosTab = (state: loadTesterState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.scenarios))} scenario(s)`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-96 overflow-y-auto")}, - state.scenarios - ->Array.map(scenario => { - let isSelected = state.selectedScenario === Some(scenario.name) - let borderCls = isSelected ? "border-cyan-600" : "border-gray-700" - div( - list{ - Attrs.class_(`bg-gray-800 rounded p-3 border ${borderCls} cursor-pointer`), - Events.onClick(LoadTester(SelectScenario(scenario.name))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(scenario.name)}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(LoadTester(RunScenario(scenario.name))), - }, - list{text("Run")}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_("text-gray-500")}, - list{text(`Players: ${Int.toString(scenario.concurrentPlayers)}`)}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text(`Ramp-up: ${Int.toString(scenario.rampUpSeconds)}s`)}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text(`Duration: ${Int.toString(scenario.durationSeconds)}s`)}, - ), - div( - list{Attrs.class_("text-gray-500")}, - list{text(`Msg/s: ${Int.toString(scenario.messagesPerSecond)}`)}, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{text(`Channel: ${scenario.channelName}`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Live Test tab: connected player count, latency, and throughput overview. -let renderLiveTestTab = (state: loadTesterState): Tea_Vdom.t => { - let connectedCount = - state.players - ->Array.filter(p => - switch p.status { - | PlayerConnected => true - | _ => false - } - ) - ->Array.length - let totalPlayers = Array.length(state.players) - let avgLatency = if totalPlayers > 0 { - state.players->Array.reduce(0.0, (acc, p) => acc +. p.latencyMs) /. Int.toFloat(totalPlayers) - } else { - 0.0 - } - let totalMessages = - state.players->Array.reduce(0, (acc, p) => acc + p.messagesSent + p.messagesReceived) - - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // Key metrics row - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-emerald-400")}, - list{text(Int.toString(connectedCount))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Connected")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(totalPlayers))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Total")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-cyan-400")}, - list{text(`${Float.toFixed(avgLatency, ~digits=1)}ms`)}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Avg Latency")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(totalMessages))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Messages")}), - }, - ), - }, - ), - // Player list (capped to most recent 20) - div( - list{Attrs.class_("flex flex-col gap-1 max-h-64 overflow-y-auto")}, - state.players - ->Array.sliceToEnd(~start=max(0, totalPlayers - 20)) - ->Array.map(player => { - div( - list{ - Attrs.class_( - "flex items-center justify-between px-3 py-1.5 bg-gray-800 rounded text-xs", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-500 font-mono")}, - list{text(`P-${Int.toString(player.id)}`)}, - ), - playerStatusBadge(player.status), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(`${Float.toFixed(player.latencyMs, ~digits=1)}ms`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Results tab: table of completed load test results. -let renderResultsTab = (state: loadTesterState): Tea_Vdom.t => { - if Array.length(state.results) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No load test results yet. Run a scenario to see results here.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - state.results - ->Array.map(result => { - let errRate = if result.messagesTotal > 0 { - Int.toFloat(result.errorsTotal) /. Int.toFloat(result.messagesTotal) *. 100.0 - } else { - 0.0 - } - let errColour = if errRate > 5.0 { - "text-red-400" - } else if errRate > 1.0 { - "text-amber-400" - } else { - "text-emerald-400" - } - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(result.scenario.name)}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(result.timestamp)}), - }, - ), - div( - list{Attrs.class_("grid grid-cols-3 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Peak: ${Int.toString(result.peakPlayers)} players`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Avg: ${Float.toFixed(result.avgLatencyMs, ~digits=1)}ms`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`P99: ${Float.toFixed(result.p99LatencyMs, ~digits=1)}ms`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Throughput: ${Float.toFixed(result.throughputPerSec, ~digits=0)}/s`)}, - ), - div( - list{Attrs.class_(errColour)}, - list{text(`Errors: ${Float.toFixed(errRate, ~digits=1)}%`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{text(`Duration: ${Float.toFixed(result.durationMs, ~digits=0)}ms`)}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Saturation Curve tab: placeholder for latency-vs-concurrency chart. -let renderSaturationCurveTab = (_state: loadTesterState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4 flex items-center justify-center h-48")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Saturation curve chart (latency vs. concurrency) — Phase 2")}, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: loadTesterState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabScenarios => renderScenariosTab(state) - | TabLiveTest => renderLiveTestTab(state) - | TabResults => renderResultsTab(state) - | TabSaturationCurve => renderSaturationCurveTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold text-cyan-300")}, list{text("Load Tester")}), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(LoadTester(RunSelectedScenario)), - KeyboardNav.onActivate(LoadTester(RunSelectedScenario)), - }, - list{text("Run Scenario")}, - ), - }, - ), - // Running indicator - if state.running { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-amber-400 rounded-full animate-pulse")}, list{}), - span( - list{Attrs.class_("text-sm text-amber-300")}, - list{text("Load test in progress...")}, - ), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/ManifestCoverage.affine b/src/components/ManifestCoverage.affine new file mode 100644 index 00000000..80da54ed --- /dev/null +++ b/src/components/ManifestCoverage.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ManifestCoverage; + +// TODO: Complete semantic implementation diff --git a/src/components/ManifestCoverage.res b/src/components/ManifestCoverage.res deleted file mode 100644 index 22babbcf..00000000 --- a/src/components/ManifestCoverage.res +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Manifest Coverage Component — AI manifest presence across all repos. -/// -/// Two-column layout: left sidebar with repo list, right content with -/// detail view showing manifest presence, validity, and errors. - -open Model -open Msg -open Tea.Html - -/// Render a repo row in the sidebar. -let repoRow = (repo: repoManifestStatus, selected: bool): Tea_Vdom.t => { - let statusColor = if repo.hasManifest && repo.isValid { - "text-green-400" - } else if repo.hasManifest { - "text-amber-400" - } else { - "text-red-400" - } - let statusLabel = if repo.hasManifest && repo.isValid { - "Valid" - } else if repo.hasManifest { - "Invalid" - } else { - "Missing" - } - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-2 border-b border-gray-800 hover:bg-gray-800/60 transition-colors " ++ if ( - selected - ) { - "bg-gray-800/80 border-l-2 border-l-blue-500" - } else { - "" - }, - ), - Events.onClick(ManifestCoverage(SelectRepo(repo.repoName))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(repo.repoName)}), - span(list{Attrs.class_("text-xs font-mono " ++ statusColor)}, list{text(statusLabel)}), - }, - ), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: manifestCoverageTab, target: manifestCoverageTab, label: string): Tea_Vdom.t< - msg, -> => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(ManifestCoverage(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Manifest Coverage panel. -let view = (state: manifestCoverageState): Tea_Vdom.t => { - let valid = ManifestCoverageEngine.validManifestCount(state.repos) - let total = Array.length(state.repos) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Manifest Coverage — AI Manifest Presence"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-purple-300")}, - list{text("Manifest Coverage")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(valid)}/${Int.toString(total)} valid manifests`)}, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600"), - Events.onClick(ManifestCoverage(ScanRepos)), - KeyboardNav.onActivate(ManifestCoverage(ScanRepos)), - }, - list{ - text( - if state.scanning { - "Scanning..." - } else { - "Scan" - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - ManifestCoverageEngine.allTabs - ->Array.map(t => tabBtn(state.activeTab, t, ManifestCoverageEngine.tabLabel(t))) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar - div( - list{Attrs.class_("w-64 border-r border-gray-800 overflow-y-auto")}, - state.repos - ->Array.map(r => repoRow(r, state.selectedRepo == Some(r.repoName))) - ->List.fromArray, - ), - // Right content - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.selectedRepo { - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a repo to view manifest details")}, - ) - | Some(name) => - switch state.repos->Array.find(r => r.repoName == name) { - | None => div(list{}, list{text("Repo not found")}) - | Some(repo) => - div( - list{}, - list{ - h3( - list{Attrs.class_("text-md font-semibold text-gray-200 mb-3")}, - list{text(repo.repoName)}, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Has Manifest:")}, - ), - span( - list{ - Attrs.class_( - if repo.hasManifest { - "text-xs text-green-400" - } else { - "text-xs text-red-400" - }, - ), - }, - list{ - text( - if repo.hasManifest { - "Yes" - } else { - "No" - }, - ), - }, - ), - }, - ), - switch repo.manifestFile { - | Some(file) => - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("File:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(file)}, - ), - }, - ) - | None => noNode - }, - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Valid:")}, - ), - span( - list{ - Attrs.class_( - if repo.isValid { - "text-xs text-green-400" - } else { - "text-xs text-red-400" - }, - ), - }, - list{ - text( - if repo.isValid { - "Yes" - } else { - "No" - }, - ), - }, - ), - }, - ), - if Array.length(repo.validationErrors) > 0 { - div( - list{Attrs.class_("mt-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 block mb-1")}, - list{text("Errors:")}, - ), - div( - list{Attrs.class_("space-y-1")}, - repo.validationErrors - ->Array.map(e => - div( - list{Attrs.class_("text-xs text-red-300 font-mono pl-2")}, - list{text(e)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ), - }, - ) - } - }, - }, - ), - }, - ), - // Footer - div( - list{Attrs.class_("px-4 py-2 border-t border-gray-800 text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - ManifestCoverageEngine.missingManifestCount(state.repos), - )} repos missing manifest`, - ), - }, - ), - }, - ) -} diff --git a/src/components/MassPanic.affine b/src/components/MassPanic.affine new file mode 100644 index 00000000..c6d69c79 --- /dev/null +++ b/src/components/MassPanic.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MassPanic; + +// TODO: Complete semantic implementation diff --git a/src/components/MassPanic.res b/src/components/MassPanic.res deleted file mode 100644 index a2d05884..00000000 --- a/src/components/MassPanic.res +++ /dev/null @@ -1,1222 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Mass Panic Panel — organisation-scale batch scanning GUI. -/// -/// Provides a visual interface for panic-attack's mass-panic deployment mode: -/// repo discovery, select-all/checkbox batch controls, assemblyline scanning -/// with progress tracking, incremental BLAKE3 delta, verisim persistence, -/// result sorting/filtering, delta comparison, and notification generation. -/// -/// Replaces the complex CLI orchestration of: -/// panic-attack assemblyline /path --incremental --store ./data --cache ... - -open Msg -open MassPanicModel -open Tea.Html - -/// Status badge for a repo scan result. -let statusBadge = (status: repoScanStatus): Tea_Vdom.t => { - let (colour, lbl) = switch status { - | Queued => ("bg-gray-600 text-gray-200", "QUEUED") - | Scanning => ("bg-amber-500 text-white animate-pulse", "SCANNING") - | Complete => ("bg-emerald-600 text-white", "DONE") - | Skipped => ("bg-blue-600 text-white", "SKIPPED") - | Failed(_) => ("bg-red-600 text-white", "FAILED") - } - span(list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded font-mono ${colour}`)}, list{text(lbl)}) -} - -/// Severity count pills for a repo row. -let severityPills = (repo: repoResult): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1")}, - list{ - if repo.critical > 0 { - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-red-600 text-white font-mono")}, - list{text(`${Int.toString(repo.critical)}C`)}, - ) - } else { - noNode - }, - if repo.high > 0 { - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-orange-500 text-white font-mono")}, - list{text(`${Int.toString(repo.high)}H`)}, - ) - } else { - noNode - }, - if repo.medium > 0 { - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-amber-400 text-gray-900 font-mono")}, - list{text(`${Int.toString(repo.medium)}M`)}, - ) - } else { - noNode - }, - if repo.low > 0 { - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-blue-400 text-white font-mono")}, - list{text(`${Int.toString(repo.low)}L`)}, - ) - } else { - noNode - }, - }, - ) -} - -/// Progress bar (thin horizontal bar showing scan completion). -let progressBar = (progress: float, scanning: bool): Tea_Vdom.t => { - if !scanning { - noNode - } else { - let pct = Int.toString(Int.fromFloat(progress *. 100.0)) - div( - list{Attrs.class_("w-full h-1.5 bg-gray-700 rounded overflow-hidden")}, - list{ - div( - list{Attrs.class_(`h-full bg-amber-500 transition-all duration-300 w-[${pct}%]`)}, - list{}, - ), - }, - ) - } -} - -/// Aggregate summary bar. -let summaryView = (summary: option): Tea_Vdom.t => { - switch summary { - | None => - div( - list{Attrs.class_("text-gray-500 text-sm italic py-2")}, - list{text("No scan results. Set a repos directory and run assemblyline.")}, - ) - | Some(s) => - div( - list{Attrs.class_("flex flex-wrap gap-4 items-center py-2 text-sm")}, - list{ - span( - list{Attrs.class_("text-gray-300 font-mono")}, - list{text(`${Int.toString(s.scannedRepos)}/${Int.toString(s.totalRepos)} repos scanned`)}, - ), - if s.skippedRepos > 0 { - span( - list{Attrs.class_("text-blue-400 font-mono")}, - list{text(`${Int.toString(s.skippedRepos)} skipped (unchanged)`)}, - ) - } else { - noNode - }, - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(`${Int.toString(s.totalFindings)} findings`)}, - ), - if s.totalCritical > 0 { - span( - list{Attrs.class_("text-red-400 font-mono font-bold")}, - list{text(`${Int.toString(s.totalCritical)} critical`)}, - ) - } else { - noNode - }, - if s.totalHigh > 0 { - span( - list{Attrs.class_("text-orange-400 font-mono")}, - list{text(`${Int.toString(s.totalHigh)} high`)}, - ) - } else { - noNode - }, - span( - list{Attrs.class_("text-gray-500 font-mono")}, - list{text(`${Float.toString(s.scanDuration)}s`)}, - ), - }, - ) - } -} - -/// Filter and sort repo results. -let filterAndSort = ( - repos: array, - filterMode: repoFilterMode, - sortMode: repoSortMode, - searchText: string, -): array => { - repos - ->Array.filter(r => { - let filterMatch = switch filterMode { - | AllRepos => true - | FindingsOnly => r.totalFindings > 0 - | CriticalOnly => r.critical > 0 - | FailedOnly => - switch r.status { - | Failed(_) => true - | _ => false - } - } - let textMatch = - searchText == "" || - String.includes(String.toLowerCase(r.repoName), String.toLowerCase(searchText)) || - String.includes(String.toLowerCase(r.repoPath), String.toLowerCase(searchText)) - filterMatch && textMatch - }) - ->Array.toSorted((a, b) => - switch sortMode { - | ByRisk => - Float.fromInt(b.critical * 100 + b.high * 10 + b.totalFindings) -. - Float.fromInt(a.critical * 100 + a.high * 10 + a.totalFindings) - | ByName => String.localeCompare(a.repoName, b.repoName) - | ByFindings => Float.fromInt(b.totalFindings - a.totalFindings) - | ByDuration => - switch (b.scanDuration, a.scanDuration) { - | (Some(bd), Some(ad)) => bd -. ad - | (Some(_), None) => 1.0 - | (None, Some(_)) => -1.0 - | (None, None) => 0.0 - } - } - ) -} - -/// Single repo row in the results table. -let repoRow = (repo: repoResult, index: int, isSelected: bool): Tea_Vdom.t => { - let selectedClass = isSelected ? "bg-gray-800/30" : "" - div( - list{ - Attrs.class_( - `flex items-center gap-3 py-2 px-3 border-b border-gray-700 hover:bg-gray-800/50 ${selectedClass}`, - ), - }, - list{ - // Checkbox - input( - list{ - Attrs.type_("checkbox"), - Attrs.checked(isSelected), - Attrs.class_("w-4 h-4 accent-amber-500"), - Events.onClick(MassPanic(ToggleRepoSelection(index))), - }, - list{}, - ), - // Status badge - statusBadge(repo.status), - // Repo name - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-mono truncate")}, - list{text(repo.repoName)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono truncate")}, - list{text(repo.repoPath)}, - ), - }, - ), - // Findings count - span( - list{Attrs.class_("text-sm text-gray-300 font-mono min-w-[60px] text-right")}, - list{text(Int.toString(repo.totalFindings))}, - ), - // Severity pills - severityPills(repo), - // Files scanned - span( - list{Attrs.class_("text-xs text-gray-500 font-mono min-w-[40px] text-right")}, - list{text(`${Int.toString(repo.filesScanned)}f`)}, - ), - // Duration - span( - list{Attrs.class_("text-xs text-gray-500 font-mono min-w-[50px] text-right")}, - list{ - text( - switch repo.scanDuration { - | Some(d) => `${Float.toFixed(d, ~digits=1)}s` - | None => "-" - }, - ), - }, - ), - // BLAKE3 hash indicator - switch repo.blake3Hash { - | Some(_) => span(list{Attrs.class_("text-xs text-emerald-600")}, list{text("#")}) - | None => noNode - }, - }, - ) -} - -/// Delta comparison row. -let deltaRow = (entry: deltaEntry): Tea_Vdom.t => { - let dirColour = switch entry.changeDirection { - | "improved" => "text-emerald-400" - | "regressed" => "text-red-400" - | "new" => "text-amber-400" - | _ => "text-gray-400" - } - div( - list{Attrs.class_("flex items-center gap-3 py-1.5 px-3 border-b border-gray-700 text-sm")}, - list{ - span( - list{Attrs.class_(`font-mono font-bold ${dirColour} min-w-[80px]`)}, - list{text(String.toUpperCase(entry.changeDirection))}, - ), - span( - list{Attrs.class_("flex-1 text-gray-200 font-mono truncate")}, - list{text(entry.repoName)}, - ), - if entry.newFindings > 0 { - span( - list{Attrs.class_("text-red-400 font-mono")}, - list{text(`+${Int.toString(entry.newFindings)}`)}, - ) - } else { - noNode - }, - if entry.fixedFindings > 0 { - span( - list{Attrs.class_("text-emerald-400 font-mono")}, - list{text(`-${Int.toString(entry.fixedFindings)}`)}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render a filter button. -let filterBtn = (mode: repoFilterMode, lbl: string, activeMode: repoFilterMode): Tea_Vdom.t< - msg, -> => { - let active = activeMode == mode - button( - list{ - Attrs.class_( - `px-2 py-0.5 rounded font-mono text-xs ${active - ? "bg-amber-600 text-white" - : "bg-gray-700 text-gray-400 hover:bg-gray-600"}`, - ), - Events.onClick(MassPanic(SetFilterMode(mode))), - }, - list{text(lbl)}, - ) -} - -/// Render a sort button. -let sortBtn = (mode: repoSortMode, lbl: string, activeMode: repoSortMode): Tea_Vdom.t => { - let active = activeMode == mode - button( - list{ - Attrs.class_( - `px-2 py-0.5 rounded font-mono text-xs ${active - ? "bg-gray-600 text-white" - : "bg-gray-750 text-gray-500 hover:bg-gray-600"}`, - ), - Events.onClick(MassPanic(SetSortMode(mode))), - }, - list{text(lbl)}, - ) -} - -/// Sub-view tab button. -let viewTab = (targetView: massPanicView, lbl: string, activeView: massPanicView): Tea_Vdom.t< - msg, -> => { - let active = activeView == targetView - button( - list{ - Attrs.class_( - `px-3 py-1.5 rounded-t font-mono text-xs border-b-2 ${active - ? "bg-gray-800 text-white border-amber-500" - : "bg-gray-900 text-gray-500 border-transparent hover:text-gray-300"}`, - ), - Events.onClick(MassPanic(SwitchView(targetView))), - }, - list{text(lbl)}, - ) -} - -/// Risk intensity bar (horizontal coloured bar for a node's risk). -let riskBar = (intensity: float): Tea_Vdom.t => { - let pct = Int.toString(Int.fromFloat(intensity *. 100.0)) - let colour = if intensity > 0.7 { - "bg-red-500" - } else if intensity > 0.4 { - "bg-orange-500" - } else if intensity > 0.2 { - "bg-amber-400" - } else { - "bg-emerald-500" - } - div( - list{Attrs.class_("w-20 h-2 bg-gray-700 rounded overflow-hidden")}, - list{div(list{Attrs.class_(`h-full ${colour} w-[${pct}%]`)}, list{})}, - ) -} - -/// Imaging sub-view — fNIRS-style spatial health map. -let renderImagingView = (state: massPanicState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full overflow-hidden")}, - list{ - // Toolbar - div( - list{Attrs.class_("flex items-center gap-3 px-4 py-2 border-b border-gray-700")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-cyan-700 hover:bg-cyan-600 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled(state.imagingLoading || state.reposDirectory == ""), - Events.onClick(MassPanic(BuildImage)), - KeyboardNav.onActivate(MassPanic(BuildImage)), - }, - list{ - text( - if state.imagingLoading { - "building..." - } else { - "build image" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-gray-200 font-mono", - ), - Events.onClick(MassPanic(ImportImageFile)), - KeyboardNav.onActivate(MassPanic(ImportImageFile)), - }, - list{text("import JSON")}, - ), - switch state.currentImage { - | Some(img) => - div( - list{Attrs.class_("flex gap-3 ml-auto text-xs font-mono text-gray-400")}, - list{ - span(list{}, list{text(`${Int.toString(img.nodeCount)} nodes`)}), - span(list{}, list{text(`${Int.toString(img.edgeCount)} edges`)}), - span( - list{ - Attrs.class_( - if img.globalHealth > 0.7 { - "text-emerald-400" - } else if img.globalHealth > 0.4 { - "text-amber-400" - } else { - "text-red-400" - }, - ), - }, - list{text(`health: ${Float.toFixed(img.globalHealth *. 100.0, ~digits=1)}%`)}, - ), - span( - list{}, - list{text(`risk: ${Float.toFixed(img.globalRisk *. 100.0, ~digits=1)}%`)}, - ), - span(list{}, list{text(`${Int.toString(img.totalCritical)} critical`)}), - }, - ) - | None => noNode - }, - }, - ), - // Risk distribution bar - switch state.currentImage { - | Some(img) => - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-700 text-xs")}, - list{ - span(list{Attrs.class_("text-gray-500 mr-2")}, list{text("Distribution:")}), - span( - list{Attrs.class_("text-emerald-400 font-mono")}, - list{text(`${Int.toString(img.riskDistribution.healthy)} healthy`)}, - ), - span( - list{Attrs.class_("text-blue-400 font-mono")}, - list{text(`${Int.toString(img.riskDistribution.low)} low`)}, - ), - span( - list{Attrs.class_("text-amber-400 font-mono")}, - list{text(`${Int.toString(img.riskDistribution.moderate)} mod`)}, - ), - span( - list{Attrs.class_("text-orange-400 font-mono")}, - list{text(`${Int.toString(img.riskDistribution.high)} high`)}, - ), - span( - list{Attrs.class_("text-red-400 font-mono")}, - list{text(`${Int.toString(img.riskDistribution.critical)} crit`)}, - ), - }, - ) - | None => noNode - }, - // Node grid - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - switch state.currentImage { - | None => - list{ - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-500 text-sm")}, - list{text("No system image. Click 'build image' or 'import JSON' to generate one.")}, - ), - } - | Some(img) => - img.nodes - ->Array.map(node => - div( - list{ - Attrs.class_( - "flex items-center gap-3 py-2 px-4 border-b border-gray-700 hover:bg-gray-800/50", - ), - }, - list{ - // Health indicator - span( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${if node.healthScore > 0.7 { - "bg-emerald-500" - } else if node.healthScore > 0.4 { - "bg-amber-500" - } else { - "bg-red-500" - }}`, - ), - }, - list{}, - ), - // Name - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-mono truncate")}, - list{text(node.name)}, - ), - if Array.length(node.topCategories) > 0 { - div( - list{Attrs.class_("text-xs text-gray-500 font-mono truncate")}, - list{text(Array.join(node.topCategories, ", "))}, - ) - } else { - noNode - }, - }, - ), - // Risk bar - riskBar(node.riskIntensity), - // Metrics - span( - list{Attrs.class_("text-xs text-gray-400 font-mono min-w-[60px] text-right")}, - list{text(`${Float.toFixed(node.healthScore *. 100.0, ~digits=0)}%`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono min-w-[40px] text-right")}, - list{text(`${Int.toString(node.weakPointCount)}wp`)}, - ), - if node.criticalCount > 0 { - span( - list{Attrs.class_("text-xs text-red-400 font-mono")}, - list{text(`${Int.toString(node.criticalCount)}C`)}, - ) - } else { - noNode - }, - if node.skipped { - span(list{Attrs.class_("text-xs text-blue-500")}, list{text("skipped")}) - } else { - noNode - }, - }, - ) - ) - ->List.fromArray - }, - ), - }, - ) -} - -/// Temporal sub-view — time-series snapshot navigation. -let renderTemporalView = (state: massPanicState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full overflow-hidden")}, - list{ - // Toolbar - div( - list{Attrs.class_("flex items-center gap-3 px-4 py-2 border-b border-gray-700")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-violet-700 hover:bg-violet-600 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled(state.temporalLoading), - Events.onClick(MassPanic(ListSnapshots)), - KeyboardNav.onActivate(MassPanic(ListSnapshots)), - }, - list{ - text( - if state.temporalLoading { - "loading..." - } else { - "list snapshots" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-gray-200 font-mono disabled:opacity-50", - ), - Attrs.disabled( - switch state.selectedSnapshots { - | (Some(_), Some(_)) => false - | _ => true - }, - ), - Events.onClick(MassPanic(DiffSnapshots)), - KeyboardNav.onActivate(MassPanic(DiffSnapshots)), - }, - list{text("diff selected")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-emerald-700 hover:bg-emerald-600 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled( - switch state.currentImage { - | Some(_) => false - | None => true - }, - ), - Events.onClick(MassPanic(TakeSnapshot("manual"))), - }, - list{text("take snapshot")}, - ), - }, - ), - // Diff summary (when active) - switch state.currentDiff { - | Some(diff) => - div( - list{Attrs.class_("px-4 py-2 border-b border-gray-700 bg-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-4 text-xs font-mono")}, - list{ - span( - list{Attrs.class_("text-gray-400")}, - list{text(`${diff.fromLabel} → ${diff.toLabel}`)}, - ), - span( - list{ - Attrs.class_( - if diff.healthDelta > 0.0 { - "text-emerald-400" - } else if diff.healthDelta < 0.0 { - "text-red-400" - } else { - "text-gray-400" - }, - ), - }, - list{ - text( - `health: ${if diff.healthDelta > 0.0 { - "+" - } else { - "" - }}${Float.toFixed(diff.healthDelta *. 100.0, ~digits=1)}%`, - ), - }, - ), - span( - list{ - Attrs.class_( - if diff.weakPointDelta < 0 { - "text-emerald-400" - } else if diff.weakPointDelta > 0 { - "text-red-400" - } else { - "text-gray-400" - }, - ), - }, - list{ - text( - `wp: ${if diff.weakPointDelta > 0 { - "+" - } else { - "" - }}${Int.toString(diff.weakPointDelta)}`, - ), - }, - ), - span( - list{ - Attrs.class_( - switch diff.trend { - | "improving" => "text-emerald-400 font-bold" - | "degrading" => "text-red-400 font-bold" - | _ => "text-gray-400" - }, - ), - }, - list{text(String.toUpperCase(diff.trend))}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{ - text( - `${Int.toString(Array.length(diff.improvedNodes))} improved, ${Int.toString( - Array.length(diff.degradedNodes), - )} degraded, ${Int.toString(diff.unchangedCount)} stable`, - ), - }, - ), - }, - ), - // Changed nodes - if Array.length(diff.degradedNodes) > 0 { - div( - list{Attrs.class_("mt-2")}, - list{ - div( - list{Attrs.class_("text-xs text-red-400 font-bold mb-1")}, - list{text("Degraded:")}, - ), - div( - list{Attrs.class_("max-h-24 overflow-y-auto")}, - diff.degradedNodes - ->Array.map(nd => - div( - list{Attrs.class_("flex items-center gap-2 text-xs font-mono py-0.5")}, - list{ - span(list{Attrs.class_("text-gray-200")}, list{text(nd.name)}), - span( - list{Attrs.class_("text-red-400")}, - list{ - text(`${Float.toFixed(nd.healthDelta *. 100.0, ~digits=1)}% health`), - }, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{ - text( - `${if nd.weakPointDelta > 0 { - "+" - } else { - "" - }}${Int.toString(nd.weakPointDelta)} wp`, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - if Array.length(diff.improvedNodes) > 0 { - div( - list{Attrs.class_("mt-2")}, - list{ - div( - list{Attrs.class_("text-xs text-emerald-400 font-bold mb-1")}, - list{text("Improved:")}, - ), - div( - list{Attrs.class_("max-h-24 overflow-y-auto")}, - diff.improvedNodes - ->Array.map(nd => - div( - list{Attrs.class_("flex items-center gap-2 text-xs font-mono py-0.5")}, - list{ - span(list{Attrs.class_("text-gray-200")}, list{text(nd.name)}), - span( - list{Attrs.class_("text-emerald-400")}, - list{ - text(`+${Float.toFixed(nd.healthDelta *. 100.0, ~digits=1)}% health`), - }, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ) - | None => noNode - }, - // Snapshot timeline list - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - if Array.length(state.snapshots) == 0 { - list{ - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-500 text-sm")}, - list{ - text( - "No snapshots. Run an image scan and click 'take snapshot', or 'list snapshots' to load existing ones.", - ), - }, - ), - } - } else { - state.snapshots - ->Array.map(snap => { - let (selA, selB) = state.selectedSnapshots - let isSelected = selA == Some(snap.sequence) || selB == Some(snap.sequence) - div( - list{ - Attrs.class_( - `flex items-center gap-3 py-2 px-4 border-b border-gray-700 cursor-pointer hover:bg-gray-800/50 ${isSelected - ? "bg-violet-900/20 border-l-2 border-l-violet-500" - : ""}`, - ), - Events.onClick(MassPanic(SelectSnapshot(snap.sequence, 0))), - }, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 font-mono min-w-[30px]")}, - list{text(`#${Int.toString(snap.sequence)}`)}, - ), - span(list{Attrs.class_("text-sm text-gray-200 font-mono")}, list{text(snap.label)}), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(snap.timestamp)}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 font-mono ml-auto")}, - list{text(`${Int.toString(snap.nodeCount)} nodes`)}, - ), - span( - list{ - Attrs.class_( - `text-xs font-mono ${if snap.globalHealth > 0.7 { - "text-emerald-400" - } else if snap.globalHealth > 0.4 { - "text-amber-400" - } else { - "text-red-400" - }}`, - ), - }, - list{text(`${Float.toFixed(snap.globalHealth *. 100.0, ~digits=0)}%`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(`${Int.toString(snap.totalWeakPoints)}wp`)}, - ), - }, - ) - }) - ->List.fromArray - }, - ), - }, - ) -} - -/// Main panel view. -let view = (state: massPanicState): Tea_Vdom.t => { - let filtered = filterAndSort( - state.repoResults, - state.filterMode, - state.sortMode, - state.searchText, - ) - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100 overflow-hidden")}, - list{ - // Header bar with tab navigation - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-3 bg-gray-800 border-b border-gray-700", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span(list{Attrs.class_("text-lg font-bold text-red-400")}, list{text("mass-panic")}), - // View tabs - div( - list{Attrs.class_("flex items-center gap-0 ml-4")}, - list{ - viewTab(ScanView, "scan", state.activeView), - viewTab(ImagingView, "imaging", state.activeView), - viewTab(TemporalView, "temporal", state.activeView), - }, - ), - if state.incremental { - span( - list{ - Attrs.class_("text-xs text-blue-400 font-mono px-2 py-0.5 rounded bg-gray-700"), - }, - list{text("BLAKE3 incremental")}, - ) - } else { - noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - if state.scanning { - span( - list{Attrs.class_("text-xs text-amber-400 animate-pulse")}, - list{ - text( - switch state.currentRepo { - | Some(name) => `Scanning ${name}...` - | None => "Scanning..." - }, - ), - }, - ) - } else { - noNode - }, - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-red-700 hover:bg-red-600 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled(state.scanning || state.reposDirectory == ""), - Events.onClick(MassPanic(RunAssemblyline)), - KeyboardNav.onActivate(MassPanic(RunAssemblyline)), - }, - list{text("scan all")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-amber-700 hover:bg-amber-600 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled(state.scanning || Array.length(state.selectedRepos) == 0), - Events.onClick(MassPanic(RunSelected)), - KeyboardNav.onActivate(MassPanic(RunSelected)), - }, - list{text(`scan ${Int.toString(Array.length(state.selectedRepos))} selected`)}, - ), - }, - ), - }, - ), - // Error display (shared across all views) - switch state.lastError { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 border-b border-red-700 text-red-300 text-sm"), - }, - list{ - text(err), - button( - list{ - Attrs.class_("ml-2 text-red-400 hover:text-red-200 text-xs"), - Events.onClick(MassPanic(DismissMassPanicError)), - KeyboardNav.onActivate(MassPanic(DismissMassPanicError)), - }, - list{text("[dismiss]")}, - ), - }, - ) - | None => noNode - }, - // Sub-view content - switch state.activeView { - | ImagingView => renderImagingView(state) - | TemporalView => renderTemporalView(state) - | ScanView => - // Scan view — assemblyline batch scanning - div( - list{Attrs.class_("flex flex-col flex-1 overflow-hidden")}, - list{ - // Directory input + controls bar - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-2 border-b border-gray-700")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Repos:")}), - input( - list{ - Attrs.type_("text"), - Attrs.class_( - "flex-1 bg-gray-800 text-sm text-gray-200 px-2 py-1 rounded border border-gray-600 font-mono", - ), - Attrs.placeholder("/path/to/repos/"), - Attrs.value(state.reposDirectory), - Events.onInput(v => MassPanic(SetReposDirectory(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs rounded bg-gray-700 hover:bg-gray-600 text-gray-200 font-mono", - ), - Attrs.disabled(state.reposDirectory == ""), - Events.onClick(MassPanic(DiscoverRepos)), - KeyboardNav.onActivate(MassPanic(DiscoverRepos)), - }, - list{text("discover")}, - ), - }, - ), - // Options bar: incremental, storage, filter, sort - div( - list{ - Attrs.class_("flex items-center gap-4 px-4 py-2 border-b border-gray-700 text-xs"), - }, - list{ - label( - list{Attrs.class_("flex items-center gap-1 text-gray-400 cursor-pointer")}, - list{ - input( - list{ - Attrs.type_("checkbox"), - Attrs.checked(state.incremental), - Attrs.class_("w-3.5 h-3.5 accent-blue-500"), - Events.onClick(MassPanic(ToggleIncremental)), - KeyboardNav.onActivate(MassPanic(ToggleIncremental)), - }, - list{}, - ), - text("Incremental"), - }, - ), - label( - list{Attrs.class_("flex items-center gap-1 text-gray-400 cursor-pointer")}, - list{ - input( - list{ - Attrs.type_("checkbox"), - Attrs.checked(state.notifyEnabled), - Attrs.class_("w-3.5 h-3.5 accent-amber-500"), - Events.onClick(MassPanic(ToggleNotify)), - KeyboardNav.onActivate(MassPanic(ToggleNotify)), - }, - list{}, - ), - text("Notify"), - }, - ), - // Filter buttons - div( - list{Attrs.class_("flex gap-1 ml-auto")}, - list{ - filterBtn(AllRepos, "All", state.filterMode), - filterBtn(FindingsOnly, "Findings", state.filterMode), - filterBtn(CriticalOnly, "Critical", state.filterMode), - filterBtn(FailedOnly, "Failed", state.filterMode), - }, - ), - // Search - input( - list{ - Attrs.type_("text"), - Attrs.class_( - "w-40 bg-gray-800 text-sm text-gray-200 px-2 py-0.5 rounded border border-gray-600 font-mono", - ), - Attrs.placeholder("Search repos..."), - Attrs.value(state.searchText), - Events.onInput(v => MassPanic(SetSearchText(v))), - }, - list{}, - ), - }, - ), - // Progress bar - progressBar(state.progress, state.scanning), - // Summary bar - div( - list{Attrs.class_("px-4 border-b border-gray-700")}, - list{summaryView(state.summary)}, - ), - // Select-all bar - if Array.length(state.repoResults) > 0 { - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-4 py-1.5 border-b border-gray-700 bg-gray-850 text-xs", - ), - }, - list{ - label( - list{Attrs.class_("flex items-center gap-1 text-gray-400 cursor-pointer")}, - list{ - input( - list{ - Attrs.type_("checkbox"), - Attrs.checked(state.selectAll), - Attrs.class_("w-3.5 h-3.5 accent-amber-500"), - Events.onClick(MassPanic(ToggleSelectAll)), - KeyboardNav.onActivate(MassPanic(ToggleSelectAll)), - }, - list{}, - ), - text("Select all"), - }, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{ - text( - `${Int.toString(Array.length(state.selectedRepos))} of ${Int.toString( - Array.length(state.repoResults), - )} selected`, - ), - }, - ), - // Sort controls - div( - list{Attrs.class_("flex gap-1 ml-auto")}, - list{ - sortBtn(ByRisk, "Risk", state.sortMode), - sortBtn(ByName, "Name", state.sortMode), - sortBtn(ByFindings, "Findings", state.sortMode), - sortBtn(ByDuration, "Time", state.sortMode), - }, - ), - }, - ) - } else { - noNode - }, - // Delta comparison view (when active) - if state.showDelta && Array.length(state.delta) > 0 { - div( - list{Attrs.class_("border-b border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-1.5 bg-gray-800 text-xs")}, - list{ - span(list{Attrs.class_("text-gray-400 font-bold")}, list{text("DELTA")}), - span( - list{Attrs.class_("text-gray-500")}, - list{text("Changes since previous run")}, - ), - button( - list{ - Attrs.class_("ml-auto text-gray-500 hover:text-gray-300"), - Events.onClick(MassPanic(ToggleDelta)), - KeyboardNav.onActivate(MassPanic(ToggleDelta)), - }, - list{text("[close]")}, - ), - }, - ), - div( - list{Attrs.class_("max-h-40 overflow-y-auto")}, - state.delta->Array.map(deltaRow)->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Repo results list - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - if Array.length(filtered) == 0 && !state.scanning { - list{ - div( - list{ - Attrs.class_("flex items-center justify-center h-32 text-gray-500 text-sm"), - }, - list{ - text( - if Array.length(state.repoResults) == 0 { - "No repos discovered. Enter a directory path and click 'discover'." - } else { - "No repos match the current filter." - }, - ), - }, - ), - } - } else { - filtered - ->Array.mapWithIndex((repo, index) => { - let isSelected = state.selectedRepos->Array.includes(index) - repoRow(repo, index, isSelected) - }) - ->List.fromArray - }, - ), - }, - ) - }, - // Footer - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-2 bg-gray-800 border-t border-gray-700 text-xs text-gray-500", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span(list{}, list{text(`${Int.toString(Array.length(state.repoResults))} repos`)}), - switch state.storage { - | NoStorage => noNode - | Filesystem(path) => - span(list{Attrs.class_("text-emerald-600")}, list{text(`store: ${path}`)}) - | VerisimDB(path) => - span(list{Attrs.class_("text-cyan-500")}, list{text(`verisim: ${path}`)}) - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - if state.showDelta { - noNode - } else { - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300"), - Attrs.disabled(state.scanning), - Events.onClick(MassPanic(ToggleDelta)), - KeyboardNav.onActivate(MassPanic(ToggleDelta)), - }, - list{text("show delta")}, - ) - }, - span(list{}, list{text("panic-attack 2.1.0 — mass-panic mode")}), - }, - ), - }, - ), - }, - ) -} diff --git a/src/components/MenuBar.affine b/src/components/MenuBar.affine new file mode 100644 index 00000000..1106f282 --- /dev/null +++ b/src/components/MenuBar.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MenuBar; + +// TODO: Complete semantic implementation diff --git a/src/components/MenuBar.res b/src/components/MenuBar.res deleted file mode 100644 index 214b51a3..00000000 --- a/src/components/MenuBar.res +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL MenuBar — Standard application menu bar. -/// -/// Provides File / Edit / View / Panel / Tools / Help menus following -/// conventional desktop patterns (Notepad++, Visual Paradigm, VS Code). -/// -/// Menu items dispatch to the unified msg type via `MenuBar(MenuAction(id))`. -/// The Update module routes these action IDs to the appropriate sub-updaters. -/// -/// Interoperability: Menu structure mirrors standard IDE conventions so that -/// users of Visual Paradigm, VS Code, Notepad++, etc. find familiar -/// entry points. Items map to PanLL's unique features behind standard names. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// Menu item definitions -// =========================================================================== - -/// A single menu item: label, action ID, keyboard shortcut hint (optional). -type rec menuItem = - | Action(string, string, option) // (label, actionId, shortcut) - | Separator - | SubMenu(string, array) // (label, children) - -/// File menu items — workspace, import/export, sessions. -let fileMenuItems: array = [ - Action("New Workspace", "file:new-workspace", Some("Ctrl+N")), - Action("Open Repository...", "file:open-repo", Some("Ctrl+O")), - Separator, - Action("Save State", "file:save-state", Some("Ctrl+S")), - Action("Export ENSAID Config...", "file:export-ensaid", None), - Action("Export Event Chain...", "file:export-chain", None), - Separator, - Action("Import Event Chain...", "file:import-chain", None), - Action("Import Panic Report...", "file:import-panic", None), - Separator, - Action("Print...", "file:print", Some("Ctrl+P")), - Separator, - Action("Preferences...", "file:preferences", None), -] - -/// Edit menu items — undo/redo, search, clipboard. -let editMenuItems: array = [ - Action("Undo", "edit:undo", Some("Ctrl+Z")), - Action("Redo", "edit:redo", Some("Ctrl+Shift+Z")), - Separator, - Action("Find in Panel...", "edit:find", Some("Ctrl+F")), - Action("Replace...", "edit:replace", Some("Ctrl+H")), - Separator, - Action("Clear Event Chain", "edit:clear-chain", None), - Action("Reset Panel State", "edit:reset-panel", Some("Ctrl+Shift+R")), -] - -/// View menu items — panel visibility, layout, themes. -let viewMenuItems: array = [ - Action("Toggle Panel-L (Symbolic)", "view:toggle-pane-l", Some("Ctrl+Shift+L")), - Action("Toggle Panel-N (Neural)", "view:toggle-pane-n", Some("Ctrl+Shift+N")), - Action("Toggle Panel-W (Barycentre)", "view:toggle-pane-w", Some("Ctrl+Shift+B")), - Separator, - Action("Toggle Panel Bar", "view:toggle-panel-bar", Some("Ctrl+`")), - Action("Toggle Topology View", "view:toggle-topology", None), - Separator, - Action("Fullscreen", "view:fullscreen", Some("F11")), - Action("Light Mode", "view:light-mode", None), - Action("Zen Mode", "view:zen", None), - Action("Dark Start", "view:dark-start", None), - Separator, - Action("Accessibility Settings...", "view:accessibility", None), -] - -/// Panel menu items — quick access to key overlay panels. -let panelMenuItems: array = [ - Action("AI Neural Interface", "panel:ai", None), - Action("VeriSimDB (VAB)", "panel:vab", None), - Action("CloudGuard", "panel:cloudguard", None), - Action("Hypatia Scanner", "panel:hypatia", None), - Action("Repository System", "panel:reposystem", None), - Separator, - Action("Editor Bridge (LSP + Modeling)", "panel:editor-bridge", None), - Action("Build Dashboard", "panel:build-dashboard", None), - Action("Release Manager", "panel:release-manager", None), - Separator, - Action("ECHIDNA (Prover + MOF/OCL)", "panel:echidna", None), - Action("TypeLL Verifier", "panel:typell", None), - Action("Interfaces (ABI/FFI)", "panel:interfaces", None), - Action("Protocol Squisher (XMI/Formats)", "panel:protocol-squisher", None), - Separator, - Action("Workspace Settings", "panel:workspace", Some("Ctrl+Shift+K")), - Action("Capture Tools", "panel:capture", Some("Ctrl+Shift+C")), - Action("Security", "panel:security", None), - Action("Bundle of Joy (BoJ)", "panel:boj", None), - Action("Provenance Map", "panel:provenance", None), -] - -/// Tools menu items — analysis, diagnostics, automation, enterprise architecture. -let toolsMenuItems: array = [ - Action("ECHIDNA Theorem Prover", "tools:echidna", None), - Action("MOF/OCL Model Checker", "tools:mof-ocl", None), - Action("TSDM Directive", "tools:tsdm", None), - Separator, - Action("Panic Attacker", "tools:panic-attack", None), - Action("Mass Panic (Batch)", "tools:mass-panic", None), - Separator, - Action("Clade Browser", "tools:clade-browser", None), - Action("Protocol Squisher", "tools:protocol-squisher", None), - Action("7-Tentacles Compiler", "tools:tentacles", None), - Separator, - Action("Network Topology", "tools:network-topology", None), - Action("VM Inspector", "tools:vm-inspector", None), - Action("Coprocessor Monitor", "tools:coprocessors", None), - Action("Automation Router", "tools:automation", None), - Separator, - Action("Keyboard Shortcuts...", "tools:keybindings", None), -] - -/// Help menu items. -let helpMenuItems: array = [ - Action("Welcome Tour", "help:tour", None), - Action("Glossary", "help:glossary", None), - Action("Barycentre Tour", "help:barycentre-tour", None), - Separator, - Action("About PanLL", "help:about", None), -] - -// =========================================================================== -// Rendering -// =========================================================================== - -/// Render a single menu item. -let renderMenuItem = (item: menuItem): Tea_Vdom.t => { - switch item { - | Action(label, actionId, shortcut) => - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-1.5 text-xs text-gray-300 hover:bg-gray-700 hover:text-white flex items-center justify-between gap-4 whitespace-nowrap", - ), - Events.onClick(MenuBar(MenuAction(actionId))), - }, - list{ - span(list{}, list{text(label)}), - switch shortcut { - | Some(sc) => - span(list{Attrs.class_("text-gray-600 text-[10px] font-mono")}, list{text(sc)}) - | None => noNode - }, - }, - ) - | Separator => div(list{Attrs.class_("border-t border-gray-700 my-1")}, list{}) - | SubMenu(label, _children) => - // Sub-menus rendered as expandable items (simplified — flat for now) - div( - list{Attrs.class_("px-3 py-1.5 text-xs text-gray-500 cursor-default")}, - list{text(label ++ " >")}, - ) - } -} - -/// Render a dropdown menu panel. -let renderDropdown = (items: array): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "absolute top-full left-0 mt-0.5 min-w-[220px] bg-gray-900 border border-gray-700 rounded-md shadow-xl shadow-black/40 py-1 z-[9990]", - ), - }, - items - ->Array.map(renderMenuItem) - ->List.fromArray, - ) -} - -/// Render a top-level menu button. -let renderMenuButton = ( - label: string, - menu: openMenu, - items: array, - activeMenu: option, -): Tea_Vdom.t => { - let isOpen = activeMenu === Some(menu) - div( - list{Attrs.class_("relative")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs transition-colors ${isOpen - ? "bg-gray-700 text-white" - : "text-gray-400 hover:bg-gray-800 hover:text-gray-200"}`, - ), - Events.onClick( - if isOpen { - MenuBar(CloseMenus) - } else { - MenuBar(OpenMenu(menu)) - }, - ), - }, - list{text(label)}, - ), - if isOpen { - renderDropdown(items) - } else { - noNode - }, - }, - ) -} - -/// Main menu bar view — renders as a horizontal bar above the three-panel layout. -let view = (state: menuBarState): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center bg-gray-900/90 border-b border-gray-800 h-7 px-1 z-30 relative", - ), - Attrs.role("menubar"), - Attrs.ariaLabel("Application menu"), - }, - list{ - renderMenuButton("File", MenuFile, fileMenuItems, state.activeMenu), - renderMenuButton("Edit", MenuEdit, editMenuItems, state.activeMenu), - renderMenuButton("View", MenuView, viewMenuItems, state.activeMenu), - renderMenuButton("Panel", MenuPanel, panelMenuItems, state.activeMenu), - renderMenuButton("Tools", MenuTools, toolsMenuItems, state.activeMenu), - renderMenuButton("Help", MenuHelp, helpMenuItems, state.activeMenu), - }, - ) -} diff --git a/src/components/MergeCoordinator.affine b/src/components/MergeCoordinator.affine new file mode 100644 index 00000000..cedb8e2a --- /dev/null +++ b/src/components/MergeCoordinator.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MergeCoordinator; + +// TODO: Complete semantic implementation diff --git a/src/components/MergeCoordinator.res b/src/components/MergeCoordinator.res deleted file mode 100644 index e7059198..00000000 --- a/src/components/MergeCoordinator.res +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL MergeCoordinator — branch management, conflict resolution, and merge queue. -/// Directive clade panel for coordinated merge workflows. -/// -/// Four tabs: Branches (list with ahead/behind indicators), Conflicts (diff viewer), -/// Merge Queue (ordered queue), and History (merged branch log). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Human-readable label for a branch status. -let statusLabel = (status: branchStatus): string => - switch status { - | BrActive => "Active" - | BrMerging => "Merging" - | BrConflicted => "Conflicted" - | BrMerged => "Merged" - | BrStale => "Stale" - } - -/// Tailwind colour class for a branch status badge. -let statusColour = (status: branchStatus): string => - switch status { - | BrActive => "text-blue-400" - | BrMerging => "text-amber-400 animate-pulse" - | BrConflicted => "text-red-400" - | BrMerged => "text-emerald-400" - | BrStale => "text-gray-500" - } - -/// Tab bar rendering. -let renderTabs = (active: mergeCoordinatorTab): Tea_Vdom.t => { - let tabs = MergeCoordinatorEngine.allTabs - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(MergeCoordinator(SetMcTab(tab))), - }, - list{text(MergeCoordinatorEngine.tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Branches tab: list with ahead/behind indicators and status badges. -let renderBranchesTab = (state: mergeCoordinatorState): Tea_Vdom.t => { - if Array.length(state.branches) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No tracked branches. Branches appear here when merge coordination is active.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - state.branches - ->Array.map(branch => { - let isSelected = state.selectedBranch === Some(branch.name) - let bgCls = isSelected ? "bg-gray-750 border border-cyan-700" : "bg-gray-800" - div( - list{ - Attrs.class_( - `p-3 rounded border border-gray-700 cursor-pointer hover:bg-gray-750 ${bgCls}`, - ), - Events.onClick(MergeCoordinator(SelectBranch(branch.name))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-200")}, - list{text(branch.name)}, - ), - span( - list{Attrs.class_(`text-xs ${statusColour(branch.status)}`)}, - list{text(statusLabel(branch.status))}, - ), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500")}, - list{ - span(list{}, list{text(`base: ${branch.baseBranch}`)}), - span(list{}, list{text(branch.author)}), - span( - list{Attrs.class_("text-emerald-500")}, - list{text(`+${Int.toString(branch.aheadBy)}`)}, - ), - span( - list{Attrs.class_("text-red-400")}, - list{text(`-${Int.toString(branch.behindBy)}`)}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Conflicts tab: file-level conflict viewer with resolution controls. -let renderConflictsTab = (state: mergeCoordinatorState): Tea_Vdom.t => { - let unresolvedCount = MergeCoordinatorEngine.countConflicts(state.conflicts) - if Array.length(state.conflicts) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No merge conflicts detected.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{ - text( - `${Int.toString(Array.length(state.conflicts))} conflict(s), ${Int.toString( - unresolvedCount, - )} unresolved`, - ), - }, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-80 overflow-y-auto")}, - state.conflicts - ->Array.map(conflict => { - let resolvedCls = conflict.resolved ? "border-emerald-800 opacity-60" : "border-red-800" - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${resolvedCls}`)}, - list{ - div( - list{Attrs.class_("text-xs font-mono text-gray-300 mb-2")}, - list{text(conflict.filePath)}, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-2")}, - list{ - div( - list{ - Attrs.class_( - "bg-gray-900 rounded p-2 text-xs font-mono text-emerald-400 max-h-24 overflow-auto", - ), - }, - list{text(`ours: ${conflict.ours}`)}, - ), - div( - list{ - Attrs.class_( - "bg-gray-900 rounded p-2 text-xs font-mono text-blue-400 max-h-24 overflow-auto", - ), - }, - list{text(`theirs: ${conflict.theirs}`)}, - ), - }, - ), - if !conflict.resolved { - div( - list{Attrs.class_("flex gap-2 mt-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick( - MergeCoordinator(ResolveConflict(conflict.filePath, "ours")), - ), - }, - list{text("Accept Ours")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-blue-700 text-white rounded hover:bg-blue-600 cursor-pointer", - ), - Events.onClick( - MergeCoordinator(ResolveConflict(conflict.filePath, "theirs")), - ), - }, - list{text("Accept Theirs")}, - ), - }, - ) - } else { - div(list{Attrs.class_("text-xs text-emerald-500 mt-1")}, list{text("Resolved")}) - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Merge Queue tab: ordered list of branches awaiting merge. -let renderMergeQueueTab = (state: mergeCoordinatorState): Tea_Vdom.t => { - if Array.length(state.mergeQueue) === 0 { - div(list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, list{text("Merge queue is empty.")}) - } else { - div( - list{Attrs.class_("flex flex-col gap-1 p-4 max-h-80 overflow-y-auto")}, - state.mergeQueue - ->Array.mapWithIndex((entry, idx) => { - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-6 text-right")}, - list{text(`#${Int.toString(idx + 1)}`)}, - ), - span( - list{Attrs.class_("text-sm font-mono text-gray-300 flex-1")}, - list{text(entry.branchName)}, - ), - if entry.checksPassed { - span(list{Attrs.class_("text-xs text-green-400")}, list{text("Checks passed")}) - } else { - span( - list{Attrs.class_("text-xs text-yellow-400")}, - list{text(`${Int.toString(Array.length(entry.pendingChecks))} checks pending`)}, - ) - }, - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// History tab: placeholder for merged branch history. -let renderHistoryTab = (_state: mergeCoordinatorState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-48 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Merge history will appear here as branches are merged.")}, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function for the Merge Coordinator panel. -let view = (state: mergeCoordinatorState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabBranches => renderBranchesTab(state) - | TabConflicts => renderConflictsTab(state) - | TabMergeQueue => renderMergeQueueTab(state) - | TabHistory => renderHistoryTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Merge Coordinator")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(MergeCoordinatorEngine.queueLength(state.mergeQueue))} in queue`, - ), - }, - ), - }, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/Migration.affine b/src/components/Migration.affine new file mode 100644 index 00000000..92c222a2 --- /dev/null +++ b/src/components/Migration.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Migration; + +// TODO: Complete semantic implementation diff --git a/src/components/Migration.res b/src/components/Migration.res deleted file mode 100644 index e89516ed..00000000 --- a/src/components/Migration.res +++ /dev/null @@ -1,1017 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Migration Component — ReScript Migration Observatory dashboard. -/// -/// Three-panel mapping visualised as a tabbed full-screen overlay: -/// Dashboard — Panel-W health scores, version brackets, aggregate metrics -/// Timeline — Panel-W session history, before/after snapshots -/// Reports — Panel-W generated reports (per-repo, cross-repo, v13 trial) -/// Submissions— Panel-W review queue with approve/reject for ReScript team -/// Merge — Panel-N/W merge conflict resolution timeline + rollback -/// -/// Panel-L constraints and Panel-N reasoning are woven into each tab -/// as inline indicators rather than separate views. - -open Model -open Msg -open Tea.Html - -/// Render a health bar — a thin coloured bar proportional to 0.0–1.0. -let renderHealthBar = (score: float): Tea_Vdom.t => { - let pct = Float.toFixed(score *. 100.0, ~digits=0) - let barColor = if score >= 0.8 { - "bg-green-500" - } else if score >= 0.5 { - "bg-amber-500" - } else { - "bg-red-500" - } - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-2")}, - list{ - div( - list{ - Attrs.class_(`${barColor} h-full rounded-full transition-all`), - Attrs.prop("style", `width: ${pct}%`), - }, - list{}, - ), - }, - ) -} - -/// Render a single repo row in the dashboard table. -let renderRepoRow = (repo: migrationRepoSummary): Tea_Vdom.t => { - let trendClass = MigrationEngine.trendIndicator(repo.trend) - let versionColor = MigrationEngine.versionBracketColor(repo.versionBracket) - div( - list{ - Attrs.class_("flex items-center gap-3 p-2 border-b border-gray-800 hover:bg-gray-900/50"), - Attrs.role("row"), - }, - list{ - // Blocked indicator - if repo.blocked { - span(list{Attrs.class_("text-xs text-red-400 w-8")}, list{text("BLK")}) - } else { - span(list{Attrs.class_("text-xs text-green-400 w-8")}, list{text("OK")}) - }, - // Repo name - span( - list{Attrs.class_("text-sm text-gray-200 w-40 truncate font-medium")}, - list{text(repo.name)}, - ), - // Version bracket badge - span( - list{Attrs.class_(`text-xs text-gray-900 px-2 py-0.5 rounded ${versionColor}`)}, - list{text(MigrationEngine.versionBracketLabel(repo.versionBracket))}, - ), - // Health bar - div( - list{Attrs.class_("flex-1 flex items-center gap-2")}, - list{ - renderHealthBar(repo.healthScore), - span( - list{ - Attrs.class_( - `text-xs w-10 text-right ${MigrationEngine.healthColor(repo.healthScore)}`, - ), - }, - list{text(MigrationEngine.healthPercent(repo.healthScore))}, - ), - }, - ), - // Trend - span( - list{Attrs.class_(`text-xs w-12 ${trendClass}`)}, - list{text(MigrationEngine.trendLabel(repo.trend))}, - ), - // Deprecated / modern counts - span( - list{Attrs.class_("text-xs text-red-400 w-16 text-right")}, - list{text(`${Int.toString(repo.deprecatedCount)} dep`)}, - ), - span( - list{Attrs.class_("text-xs text-green-400 w-16 text-right")}, - list{text(`${Int.toString(repo.modernCount)} mod`)}, - ), - // Config format - span( - list{Attrs.class_("text-xs text-gray-500 w-24 text-right")}, - list{text(MigrationEngine.configFormatLabel(repo.configFormat))}, - ), - }, - ) -} - -/// Render the version bracket distribution as horizontal stacked segments. -let renderVersionDistribution = (repos: array): Tea_Vdom.t => { - let groups = MigrationEngine.groupByVersion(repos) - let total = Array.length(repos) - if total === 0 { - div(list{Attrs.class_("text-gray-500 text-sm")}, list{text("No repos loaded")}) - } else { - div( - list{Attrs.class_("space-y-2")}, - list{ - div(list{Attrs.class_("text-sm text-gray-400 mb-2")}, list{text("Version Distribution")}), - div( - list{Attrs.class_("flex h-6 rounded overflow-hidden")}, - groups - ->Array.map(((bracket, rs)) => { - let count = Array.length(rs) - let pct = Float.toFixed(Int.toFloat(count) /. Int.toFloat(total) *. 100.0, ~digits=0) - let color = MigrationEngine.versionBracketColor(bracket) - div( - list{ - Attrs.class_(`${color} flex items-center justify-center`), - Attrs.prop("style", `width: ${pct}%`), - Attrs.title( - `${MigrationEngine.versionBracketLabel(bracket)}: ${Int.toString(count)} repos`, - ), - }, - list{ - if count > 2 { - span( - list{Attrs.class_("text-xs text-gray-900 font-medium")}, - list{text(Int.toString(count))}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ), - // Legend - div( - list{Attrs.class_("flex flex-wrap gap-3 mt-2")}, - groups - ->Array.map(((bracket, rs)) => { - let color = MigrationEngine.versionBracketColor(bracket) - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div(list{Attrs.class_(`w-3 h-3 rounded ${color}`)}, list{}), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${MigrationEngine.versionBracketLabel(bracket)} (${Int.toString( - Array.length(rs), - )})`, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render the constraint summary strip (Panel-L inline). -let renderConstraintStrip = (constraints: array): Tea_Vdom.t => { - let unsatisfied = constraints->Array.filter(c => !c.satisfied) - let count = Array.length(unsatisfied) - if count === 0 { - div( - list{ - Attrs.class_( - "text-xs text-green-400 px-3 py-1 bg-green-900/20 border border-green-800 rounded", - ), - }, - list{text("All migration constraints satisfied")}, - ) - } else { - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-red-400 font-medium")}, - list{text(`${Int.toString(count)} constraints remaining:`)}, - ), - ...unsatisfied - ->Array.map(c => - span( - list{Attrs.class_("text-xs px-2 py-0.5 bg-red-900/30 border border-red-800 rounded")}, - list{ - text( - `${c.pattern} (${Int.toString(c.totalCount)} in ${Int.toString( - c.repoCount, - )} repos)`, - ), - }, - ) - ) - ->List.fromArray, - }, - ) - } -} - -/// Render proof obligation indicators (Panel-L inline). -let renderObligations = (obligations: array): Tea_Vdom.t => { - if Array.length(obligations) === 0 { - noNode - } else { - let met = obligations->Array.filter(o => o.met)->Array.length - let total = Array.length(obligations) - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-3")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Proof Obligations")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(met)}/${Int.toString(total)} met`)}, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - obligations - ->Array.map(o => { - let icon = o.met ? "text-green-400" : "text-red-400" - let mark = o.met ? "[OK]" : "[!!]" - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span(list{Attrs.class_(icon)}, list{text(mark)}), - span(list{Attrs.class_("text-gray-400")}, list{text(o.repo)}), - span(list{Attrs.class_("text-gray-500")}, list{text(o.property)}), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render a migration session row. -let renderSessionRow = (session: migrationSession): Tea_Vdom.t => { - let statusClass = session.active ? "text-amber-400" : "text-gray-400" - let statusText = session.active ? "In Progress" : "Complete" - div( - list{Attrs.class_("flex items-center gap-3 p-3 border-b border-gray-800")}, - list{ - span(list{Attrs.class_(`text-xs ${statusClass} w-16`)}, list{text(statusText)}), - span(list{Attrs.class_("text-sm text-gray-200 w-32 truncate")}, list{text(session.label)}), - span(list{Attrs.class_("text-xs text-gray-500 w-40 truncate")}, list{text(session.repoPath)}), - div( - list{Attrs.class_("flex items-center gap-2 flex-1")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`Before: ${MigrationEngine.healthPercent(session.beforeHealth)}`)}, - ), - switch session.afterHealth { - | Some(after) => - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`After: ${MigrationEngine.healthPercent(after)}`)}, - ) - | None => noNode - }, - switch session.healthDelta { - | Some(delta) => - let deltaClass = delta > 0.0 ? "text-green-400" : "text-red-400" - let sign = delta > 0.0 ? "+" : "" - span( - list{Attrs.class_(`text-xs font-medium ${deltaClass}`)}, - list{text(`${sign}${Float.toFixed(delta *. 100.0, ~digits=1)}%`)}, - ) - | None => noNode - }, - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 w-20 text-right")}, - list{text(`${Int.toString(session.issueCount)} issues`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600 w-28 text-right")}, - list{text(session.startedAt)}, - ), - }, - ) -} - -/// Render a submission row with approve/reject controls. -let renderSubmissionRow = (sub: migrationSubmission): Tea_Vdom.t => { - let statusColor = MigrationEngine.submissionStatusColor(sub.status) - div( - list{Attrs.class_("flex items-center gap-3 p-3 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_(`text-xs ${statusColor} w-16`)}, - list{text(MigrationEngine.submissionStatusLabel(sub.status))}, - ), - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text(sub.title)}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${sub.repo} | ${sub.severity}`)}, - ), - }, - ), - if sub.status == SubmissionPending { - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-green-700 text-white rounded hover:bg-green-600", - ), - Events.onClick(Migration(ApproveSubmission(sub.id))), - }, - list{text("Approve")}, - ), - button( - list{ - Attrs.class_("px-2 py-1 text-xs bg-red-700 text-white rounded hover:bg-red-600"), - Events.onClick(Migration(RejectSubmission(sub.id))), - }, - list{text("Reject")}, - ), - }, - ) - } else { - noNode - }, - }, - ) -} - -/// Render a merge resolution row. -let renderMergeRow = (merge: mergeResolution): Tea_Vdom.t => { - let statusColor = switch merge.status { - | "in_progress" => "text-amber-400" - | "accepted" => "text-green-400" - | "rolled_back" => "text-red-400" - | _ => "text-gray-400" - } - let confColor = if merge.avgConfidence >= 0.9 { - "text-green-400" - } else if merge.avgConfidence >= 0.7 { - "text-amber-400" - } else { - "text-red-400" - } - div( - list{Attrs.class_("flex items-center gap-3 p-3 border-b border-gray-800")}, - list{ - span(list{Attrs.class_(`text-xs ${statusColor} w-16`)}, list{text(merge.status)}), - span(list{Attrs.class_("text-sm text-gray-200 w-32 truncate")}, list{text(merge.repo)}), - span( - list{Attrs.class_("text-xs text-gray-500 w-40")}, - list{text(`${merge.sourceBranch} -> ${merge.targetBranch}`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 w-24")}, - list{ - text( - `${Int.toString(merge.resolvedCount)}/${Int.toString(merge.conflictCount)} resolved`, - ), - }, - ), - span( - list{Attrs.class_(`text-xs w-16 ${confColor}`)}, - list{text(`${Float.toFixed(merge.avgConfidence *. 100.0, ~digits=0)}% conf`)}, - ), - if merge.status === "in_progress" { - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-green-700 text-white rounded hover:bg-green-600", - ), - Events.onClick(Migration(AcceptMerge(merge.sessionId))), - }, - list{text("Accept")}, - ), - button( - list{ - Attrs.class_("px-2 py-1 text-xs bg-red-700 text-white rounded hover:bg-red-600"), - Events.onClick(Migration(RollbackMerge(merge.sessionId))), - }, - list{text("Rollback")}, - ), - }, - ) - } else { - noNode - }, - span( - list{Attrs.class_("text-xs text-gray-600 w-28 text-right")}, - list{text(merge.timestamp)}, - ), - }, - ) -} - -/// Render the category tabs. -let renderTabs = (active: migrationCategory): Tea_Vdom.t => { - let tabs: array = [ - MigrationDashboard, - MigrationTimeline, - MigrationReports, - MigrationSubmissions, - MigrationMergeResolver, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-indigo-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Migration(SetMigrationCategory(tab))), - }, - list{text(MigrationEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the report type selector pills. -let renderReportTypeSelector = (active: migrationReportType): Tea_Vdom.t => { - let types: array = [PerRepoReport, CrossRepoReport, V13TrialReport] - div( - list{Attrs.class_("flex gap-2 mb-4")}, - types - ->Array.map(rt => { - let isActive = rt === active - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded transition-colors ${isActive - ? "bg-indigo-600 text-white" - : "bg-gray-800 text-gray-400 hover:text-gray-200"}`, - ), - Events.onClick(Migration(SetMigrationReportType(rt))), - }, - list{text(MigrationEngine.reportTypeLabel(rt))}, - ) - }) - ->List.fromArray, - ) -} - -/// Dashboard tab — aggregate stats + version distribution + repo table. -let renderDashboard = (mig: migrationState): Tea_Vdom.t => { - let filtered = MigrationEngine.filterRepos(mig.repos, mig.filterText) - div( - list{Attrs.class_("space-y-6")}, - list{ - // Stat cards - div( - list{Attrs.class_("flex gap-4 text-sm")}, - list{ - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3 flex-1")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs uppercase tracking-wider mb-1")}, - list{text("Repos")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-gray-200")}, - list{text(Int.toString(mig.totalRepos))}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3 flex-1")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs uppercase tracking-wider mb-1")}, - list{text("Avg Health")}, - ), - div( - list{ - Attrs.class_(`text-2xl font-light ${MigrationEngine.healthColor(mig.avgHealth)}`), - }, - list{text(MigrationEngine.healthPercent(mig.avgHealth))}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3 flex-1")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs uppercase tracking-wider mb-1")}, - list{text("Ready")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-green-400")}, - list{text(Int.toString(mig.readyCount))}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3 flex-1")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs uppercase tracking-wider mb-1")}, - list{text("Blocked")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-red-400")}, - list{text(Int.toString(mig.blockedCount))}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3 flex-1")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs uppercase tracking-wider mb-1")}, - list{text("Velocity")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-indigo-400")}, - list{text(`${Float.toFixed(mig.velocity *. 100.0, ~digits=1)}%/sess`)}, - ), - }, - ), - }, - ), - // Constraints strip (Panel-L inline) - renderConstraintStrip(mig.constraints), - // Version distribution - renderVersionDistribution(mig.repos), - // Proof obligations - renderObligations(mig.obligations), - // Repo table - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 border-b border-gray-700 text-xs text-gray-500 uppercase tracking-wider", - ), - }, - list{ - span(list{Attrs.class_("w-8")}, list{text("St")}), - span(list{Attrs.class_("w-40")}, list{text("Repository")}), - span(list{Attrs.class_("w-20")}, list{text("Version")}), - span(list{Attrs.class_("flex-1")}, list{text("Health")}), - span(list{Attrs.class_("w-12")}, list{text("Trend")}), - span(list{Attrs.class_("w-16 text-right")}, list{text("Depr.")}), - span(list{Attrs.class_("w-16 text-right")}, list{text("Modern")}), - span(list{Attrs.class_("w-24 text-right")}, list{text("Config")}), - }, - ), - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered->Array.map(r => renderRepoRow(r))->List.fromArray, - ), - }, - ), - }, - ) -} - -/// Timeline tab — session history. -let renderTimeline = (mig: migrationState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{ - text(`${Int.toString(MigrationEngine.activeSessions(mig.sessions))} active sessions`), - }, - ), - }, - ), - if Array.length(mig.sessions) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{ - div(list{Attrs.class_("text-lg mb-2")}, list{text("No observation sessions")}), - div( - list{Attrs.class_("text-sm")}, - list{text("Use feedback-o-tron to begin a migration observation")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - mig.sessions->Array.map(s => renderSessionRow(s))->List.fromArray, - ), - }, - ) - }, - }, - ) -} - -/// Reports tab — report type selector + placeholder for generated content. -let renderReports = (mig: migrationState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderReportTypeSelector(mig.activeReportType), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-6")}, - list{ - switch mig.activeReportType { - | PerRepoReport => - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-2")}, - list{text("Per-Repository Reports")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{text("Before/after migration tables, issues, recommendations per repo")}, - ), - if Array.length(mig.repos) > 0 { - div( - list{Attrs.class_("space-y-2")}, - mig.repos - ->Array.map(r => - div( - list{ - Attrs.class_("flex items-center gap-3 p-2 hover:bg-gray-800/50 rounded"), - }, - list{ - span( - list{Attrs.class_("text-sm text-gray-300 w-40")}, - list{text(r.name)}, - ), - renderHealthBar(r.healthScore), - span( - list{ - Attrs.class_(`text-xs ${MigrationEngine.healthColor(r.healthScore)}`), - }, - list{text(MigrationEngine.healthPercent(r.healthScore))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(r.deprecatedCount)} deprecated`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-gray-500 text-sm")}, - list{ - text("No repo data available. Run panic-attack migration-snapshot first."), - }, - ) - }, - }, - ) - | CrossRepoReport => - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-2")}, - list{text("Cross-Repository Aggregation")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{text("Common pain points, average health improvement, migration velocity")}, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text(`${Int.toString(mig.totalRepos)} repos tracked`)}), - span( - list{}, - list{text(`Avg health: ${MigrationEngine.healthPercent(mig.avgHealth)}`)}, - ), - span( - list{}, - list{ - text( - `Velocity: ${Float.toFixed(mig.velocity *. 100.0, ~digits=1)}%/session`, - ), - }, - ), - }, - ), - }, - ) - | V13TrialReport => - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-2")}, - list{text("v13 Pre-Release Trial Report")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{text("Performance data, regressions, missing features for ReScript team")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Generate report after completing v13 migration sessions")}, - ), - }, - ) - }, - }, - ), - }, - ) -} - -/// Submissions tab — review queue. -let renderSubmissions = (mig: migrationState): Tea_Vdom.t => { - let pending = MigrationEngine.pendingSubmissions(mig.submissions) - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`${Int.toString(pending)} pending review`)}, - ), - if pending > 0 { - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-500", - ), - Events.onClick(Migration(SubmitApproved)), - KeyboardNav.onActivate(Migration(SubmitApproved)), - }, - list{text("Submit Approved")}, - ) - } else { - noNode - }, - }, - ), - if Array.length(mig.submissions) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{ - div(list{Attrs.class_("text-lg mb-2")}, list{text("No submissions")}), - div( - list{Attrs.class_("text-sm")}, - list{text("Issues discovered during migration observations will appear here")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - mig.submissions->Array.map(s => renderSubmissionRow(s))->List.fromArray, - ), - }, - ) - }, - }, - ) -} - -/// Merge Resolver tab — conflict resolution timeline. -let renderMergeResolver = (mig: migrationState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`${Int.toString(Array.length(mig.mergeResolutions))} merge sessions`)}, - ), - }, - ), - if Array.length(mig.mergeResolutions) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{ - div(list{Attrs.class_("text-lg mb-2")}, list{text("No merge resolutions")}), - div( - list{Attrs.class_("text-sm mb-4")}, - list{text("Use merge-resolver to begin resolving conflicts with rollback support")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("merge-resolver begin ")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - mig.mergeResolutions->Array.map(m => renderMergeRow(m))->List.fromArray, - ), - }, - ) - }, - }, - ) -} - -/// Main view for the Migration Observatory panel. -let view = (mig: migrationState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Migration Observatory panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Migration Observatory")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("ReScript migration health, sessions, submissions")}, - ), - if mig.loaded { - span( - list{Attrs.class_("text-xs text-indigo-400 ml-2")}, - list{ - text( - `${Int.toString(mig.totalRepos)} repos | ${MigrationEngine.healthPercent( - mig.avgHealth, - )} avg`, - ), - }, - ) - } else { - noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Filter input - if mig.loaded { - input( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-gray-800 border border-gray-700 rounded text-gray-300 w-48 placeholder-gray-600", - ), - Attrs.placeholder("Filter repos..."), - Attrs.value(mig.filterText), - Events.onInput(text => Migration(SetMigrationFilter(text))), - }, - list{}, - ) - } else { - noNode - }, - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-500", - ), - Events.onClick(Migration(RefreshMigrationHealth)), - KeyboardNav.onActivate(Migration(RefreshMigrationHealth)), - }, - list{text(mig.loaded ? "Refresh" : "Load Data")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - if !mig.loaded { - div( - list{Attrs.class_("text-center text-gray-500 mt-12")}, - list{ - div(list{Attrs.class_("text-4xl mb-2")}, list{text("Migration Observatory")}), - div( - list{Attrs.class_("text-sm mb-6")}, - list{ - text( - "Track ReScript migration health across 54+ repos with before/after snapshots, session observation, issue submission, and merge conflict resolution", - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mb-4")}, - list{ - text( - "Data from panic-attack + feedback-o-tron + merge-resolver + Hypatia + VeriSimDB", - ), - }, - ), - button( - list{ - Attrs.class_("px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-500"), - Events.onClick(Migration(LoadMigrationData)), - KeyboardNav.onActivate(Migration(LoadMigrationData)), - }, - list{text("Load Migration Data")}, - ), - }, - ) - } else if mig.loading { - div( - list{Attrs.class_("text-center text-gray-400 mt-12"), Attrs.role("status")}, - list{ - div( - list{Attrs.class_("text-sm animate-pulse")}, - list{text("Loading migration data...")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderTabs(mig.activeCategory), - switch mig.activeCategory { - | MigrationDashboard => renderDashboard(mig) - | MigrationTimeline => renderTimeline(mig) - | MigrationReports => renderReports(mig) - | MigrationSubmissions => renderSubmissions(mig) - | MigrationMergeResolver => renderMergeResolver(mig) - }, - }, - ) - }, - }, - ), - // Error display - switch mig.error { - | Some(e) => - div( - list{Attrs.class_("p-3 bg-red-900/30 border-t border-red-700"), Attrs.role("alert")}, - list{span(list{Attrs.class_("text-xs text-red-400")}, list{text(e)})}, - ) - | None => noNode - }, - }, - ) -} diff --git a/src/components/Minter.affine b/src/components/Minter.affine new file mode 100644 index 00000000..7589f1aa --- /dev/null +++ b/src/components/Minter.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Minter; + +// TODO: Complete semantic implementation diff --git a/src/components/Minter.res b/src/components/Minter.res deleted file mode 100644 index ef976e28..00000000 --- a/src/components/Minter.res +++ /dev/null @@ -1,650 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Minter Component — Panel creation wizard. -/// -/// A multi-step wizard for minting new panel modules. Every generated panel -/// includes accessibility, ARIA semantics, and keyboard navigation by default. -/// The wizard itself follows the same accessibility standards. -/// -/// Steps: -/// 0. Name & Identity — panel name, short name, description, icon -/// 1. Backend & Config — backend type, endpoint, accessibility level -/// 2. Capabilities — declare what the panel can do -/// 3. Review & Mint — preview generated files, confirm, generate - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Step 0: Name & Identity -// ============================================================================ - -/// Render the name and identity step. -let renderStep0 = (form: minterForm): Tea_Vdom.t => { - let validationClass = switch form.nameValidation { - | NameValid => "text-green-400" - | NameConflict(_) => "text-red-400" - | NameInvalid(_) => "text-amber-400" - } - let validationText = switch form.nameValidation { - | NameValid => "Name is available" - | NameConflict(msg) => msg - | NameInvalid(msg) => msg - } - - div( - list{Attrs.class_("space-y-6")}, - list{ - // Panel name - div( - list{}, - list{ - label( - list{Attrs.class_("block text-sm text-gray-400 mb-1")}, - list{text("Panel Name (PascalCase)")}, - ), - input( - list{ - Attrs.class_( - "w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-200 focus:border-indigo-500 focus:outline-none", - ), - Attrs.value(form.panelName), - Attrs.placeholder("e.g. Wharf, Statistease, Fleet"), - Attrs.ariaLabel("Panel name in PascalCase"), - Events.onInput(v => Minter(SetPanelName(v))), - }, - list{}, - ), - div(list{Attrs.class_(`text-xs mt-1 ${validationClass}`)}, list{text(validationText)}), - }, - ), - // Short name - div( - list{}, - list{ - label( - list{Attrs.class_("block text-sm text-gray-400 mb-1")}, - list{text("Short Name (for panel bar)")}, - ), - input( - list{ - Attrs.class_( - "w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-200 focus:border-indigo-500 focus:outline-none", - ), - Attrs.value(form.shortName), - Attrs.placeholder("e.g. Wharf, Stats"), - Attrs.ariaLabel("Short name for panel bar"), - Events.onInput(v => Minter(SetShortName(v))), - }, - list{}, - ), - }, - ), - // Description - div( - list{}, - list{ - label(list{Attrs.class_("block text-sm text-gray-400 mb-1")}, list{text("Description")}), - input( - list{ - Attrs.class_( - "w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-200 focus:border-indigo-500 focus:outline-none", - ), - Attrs.value(form.description), - Attrs.placeholder("One-line description of what this panel does"), - Attrs.ariaLabel("Panel description"), - Events.onInput(v => Minter(SetDescription(v))), - }, - list{}, - ), - }, - ), - // Icon - div( - list{}, - list{ - label( - list{Attrs.class_("block text-sm text-gray-400 mb-1")}, - list{text("Icon identifier")}, - ), - input( - list{ - Attrs.class_( - "w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-200 focus:border-indigo-500 focus:outline-none", - ), - Attrs.value(form.icon), - Attrs.placeholder("e.g. shield, barn, database, globe"), - Attrs.ariaLabel("Icon identifier"), - Events.onInput(v => Minter(SetIcon(v))), - }, - list{}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Step 1: Backend & Config -// ============================================================================ - -/// Render a backend kind option as a selectable card. -let renderBackendOption = (kind: panelBackendKind, isSelected: bool): Tea_Vdom.t => { - let selectedClass = isSelected - ? "border-indigo-500 bg-indigo-950/30" - : "border-gray-700 hover:border-gray-600" - button( - list{ - Attrs.class_( - `w-full text-left p-3 rounded border ${selectedClass} cursor-pointer transition-colors`, - ), - Attrs.role("radio"), - Attrs.ariaChecked(isSelected), - Events.onClick(Minter(SetBackendKind(kind))), - }, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text(MinterEngine.backendKindLabel(kind))}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text(MinterEngine.backendKindDescription(kind))}, - ), - }, - ) -} - -/// Render the backend and configuration step. -let renderStep1 = (form: minterForm): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-6")}, - list{ - // Backend kind - div( - list{}, - list{ - div(list{Attrs.class_("text-sm text-gray-400 mb-2")}, list{text("Backend Type")}), - div( - list{ - Attrs.class_("space-y-2"), - Attrs.role("radiogroup"), - Attrs.ariaLabel("Backend type selection"), - }, - MinterEngine.allBackendKinds - ->Array.map(kind => renderBackendOption(kind, kind === form.backendKind)) - ->List.fromArray, - ), - }, - ), - // Accessibility level - div( - list{}, - list{ - div(list{Attrs.class_("text-sm text-gray-400 mb-2")}, list{text("Accessibility Level")}), - div( - list{Attrs.class_("space-y-2")}, - list{ - button( - list{ - Attrs.class_( - `w-full text-left p-3 rounded border cursor-pointer transition-colors ${form.accessibility === - StandardAccessibility - ? "border-indigo-500 bg-indigo-950/30" - : "border-gray-700 hover:border-gray-600"}`, - ), - Events.onClick(Minter(SetAccessibility(StandardAccessibility))), - }, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text("Standard")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text(MinterEngine.accessibilityDescription(StandardAccessibility))}, - ), - }, - ), - button( - list{ - Attrs.class_( - `w-full text-left p-3 rounded border cursor-pointer transition-colors ${form.accessibility === - EnhancedAccessibility - ? "border-indigo-500 bg-indigo-950/30" - : "border-gray-700 hover:border-gray-600"}`, - ), - Events.onClick(Minter(SetAccessibility(EnhancedAccessibility))), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text("Enhanced (Recommended)")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text(MinterEngine.accessibilityDescription(EnhancedAccessibility))}, - ), - }, - ), - }, - ), - }, - ), - // Endpoint (if HTTP backend) - if form.backendKind === HttpBackend { - div( - list{}, - list{ - label( - list{Attrs.class_("block text-sm text-gray-400 mb-1")}, - list{text("API Endpoint")}, - ), - input( - list{ - Attrs.class_( - "w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-gray-200 focus:border-indigo-500 focus:outline-none", - ), - Attrs.value(form.endpoint), - Attrs.placeholder("e.g. http://localhost:4000/api/v1"), - Attrs.ariaLabel("API endpoint URL"), - Events.onInput(v => Minter(SetEndpoint(v))), - }, - list{}, - ), - }, - ) - } else { - noNode - }, - }, - ) -} - -// ============================================================================ -// Step 2: Capabilities -// ============================================================================ - -/// Render the capabilities step. -let renderStep2 = (form: minterForm): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{ - text( - "Declare what your panel can do. Capabilities control which UI elements are rendered.", - ), - }, - ), - // Existing capabilities - div( - list{Attrs.class_("space-y-2")}, - form.capabilities - ->Array.mapWithIndex((cap, i) => - div( - list{Attrs.class_("flex items-center gap-2 p-2 bg-gray-800/50 rounded")}, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 font-mono")}, list{text(cap.id)}), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(cap.label)}), - }, - ), - button( - list{ - Attrs.class_("text-xs text-red-400 hover:text-red-300 px-2 py-1"), - Attrs.ariaLabel(`Remove capability ${cap.id}`), - Events.onClick(Minter(RemoveCapability(i))), - }, - list{text("Remove")}, - ), - }, - ) - ) - ->List.fromArray, - ), - // Add capability button - button( - list{ - Attrs.class_( - "px-3 py-2 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors", - ), - Attrs.ariaLabel("Add a new capability"), - Events.onClick(Minter(AddCapability)), - }, - list{text("+ Add Capability")}, - ), - if Array.length(form.capabilities) === 0 { - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("No capabilities declared yet. A default base capability will be generated.")}, - ) - } else { - noNode - }, - }, - ) -} - -// ============================================================================ -// Step 3: Review & Mint -// ============================================================================ - -/// Render the review and mint step. -let renderStep3 = (form: minterForm, minting: bool, lastResult: option): Tea_Vdom.t< - msg, -> => { - let files = MinterEngine.fileSummary(form) - - div( - list{Attrs.class_("space-y-6")}, - list{ - // Panel summary - div( - list{Attrs.class_("p-4 bg-gray-800/50 rounded-lg")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200 mb-2")}, - list{text(form.panelName)}, - ), - div(list{Attrs.class_("text-sm text-gray-400 mb-3")}, list{text(form.description)}), - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Backend:")}), - div( - list{Attrs.class_("text-gray-300")}, - list{text(MinterEngine.backendKindLabel(form.backendKind))}, - ), - div(list{Attrs.class_("text-gray-500")}, list{text("Accessibility:")}), - div( - list{Attrs.class_("text-gray-300")}, - list{text(MinterEngine.accessibilityLabel(form.accessibility))}, - ), - div(list{Attrs.class_("text-gray-500")}, list{text("Capabilities:")}), - div( - list{Attrs.class_("text-gray-300")}, - list{text(Int.toString(Array.length(form.capabilities)))}, - ), - }, - ), - }, - ), - // File list - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text(`${Int.toString(Array.length(files))} files will be created or patched:`)}, - ), - div( - list{Attrs.class_("space-y-1 max-h-48 overflow-y-auto")}, - files - ->Array.map(((path, desc)) => - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span(list{Attrs.class_("text-indigo-400 font-mono flex-1")}, list{text(path)}), - span(list{Attrs.class_("text-gray-600")}, list{text(desc)}), - }, - ) - ) - ->List.fromArray, - ), - }, - ), - // Mint button - if minting { - div( - list{Attrs.class_("text-sm text-indigo-400 animate-pulse"), Attrs.ariaLive("polite")}, - list{text("Minting panel...")}, - ) - } else { - button( - list{ - Attrs.class_( - "w-full px-4 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 transition-colors font-medium", - ), - Attrs.ariaLabel(`Mint the ${form.panelName} panel`), - Events.onClick(Minter(ExecuteMint)), - }, - list{text(`Mint ${form.panelName} Panel`)}, - ) - }, - // Result - switch lastResult { - | None => noNode - | Some(result) => - div( - list{ - Attrs.class_( - if result.success { - "p-3 bg-green-950/30 border border-green-800/50 rounded" - } else { - "p-3 bg-red-950/30 border border-red-800/50 rounded" - }, - ), - Attrs.role("alert"), - }, - list{ - if result.success { - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm text-green-400 font-medium mb-1")}, - list{text("Panel minted successfully!")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - Array.length(result.filesCreated), - )} files created, ${Int.toString( - Array.length(result.filesPatched), - )} files patched`, - ), - }, - ), - }, - ) - } else { - div( - list{Attrs.class_("text-sm text-red-400")}, - list{ - text( - switch result.error { - | Some(err) => err - | None => "Unknown error" - }, - ), - }, - ) - }, - }, - ) - }, - }, - ) -} - -// ============================================================================ -// Wizard navigation -// ============================================================================ - -/// Render the wizard step indicators. -let renderStepIndicators = (currentStep: int): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex items-center gap-2 px-6 py-3 border-b border-gray-800"), - Attrs.role("navigation"), - Attrs.ariaLabel("Minting wizard steps"), - }, - Array.make(~length=MinterEngine.totalSteps, 0) - ->Array.mapWithIndex((_, i) => { - let stepClass = if i === currentStep { - "bg-indigo-600 text-white" - } else if i < currentStep { - "bg-green-900/50 text-green-400" - } else { - "bg-gray-800 text-gray-600" - } - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{ - Attrs.class_( - `w-6 h-6 rounded-full flex items-center justify-center text-xs ${stepClass}`, - ), - }, - list{text(Int.toString(i + 1))}, - ), - div( - list{ - Attrs.class_( - if i === currentStep { - "text-xs text-gray-300" - } else { - "text-xs text-gray-600" - }, - ), - }, - list{text(MinterEngine.stepLabel(i))}, - ), - if i < MinterEngine.totalSteps - 1 { - div(list{Attrs.class_("w-8 border-t border-gray-700")}, list{}) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ) -} - -/// Render the wizard navigation buttons (Back / Next). -let renderNavigation = (form: minterForm, step: int): Tea_Vdom.t => { - let canProceed = MinterEngine.canProceedFromStep(form, step) - div( - list{Attrs.class_("flex items-center justify-between px-6 py-3 border-t border-gray-800")}, - list{ - if step > 0 { - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Attrs.ariaLabel("Go to previous step"), - Events.onClick(Minter(PrevStep)), - }, - list{text("Back")}, - ) - } else { - div(list{}, list{}) - }, - if step < MinterEngine.totalSteps - 1 { - button( - list{ - Attrs.class_( - if canProceed { - "px-4 py-2 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-500 transition-colors" - } else { - "px-4 py-2 text-sm bg-gray-700 text-gray-500 rounded cursor-not-allowed" - }, - ), - Attrs.ariaLabel("Go to next step"), - if canProceed { - Events.onClick(Minter(NextStep)) - } else { - Attrs.noProp - }, - }, - list{text("Next")}, - ) - } else { - div(list{}, list{}) - }, - }, - ) -} - -// ============================================================================ -// Main view -// ============================================================================ - -/// Main view function for the Panel Minter. -let view = (state: minterState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("main"), - Attrs.ariaLabel("Panel Minter wizard"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-6 py-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Panel Minter")}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("Create a new eNSAID-compliant panel module")}, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Attrs.ariaLabel("Close Panel Minter"), - Events.onClick(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Step indicators - renderStepIndicators(state.wizardStep), - // Step content - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6 max-w-2xl mx-auto w-full")}, - list{ - switch state.wizardStep { - | 0 => renderStep0(state.form) - | 1 => renderStep1(state.form) - | 2 => renderStep2(state.form) - | 3 => renderStep3(state.form, state.minting, state.lastResult) - | _ => noNode - }, - }, - ), - // Navigation - if state.wizardStep < MinterEngine.totalSteps - 1 || state.wizardStep === 0 { - renderNavigation(state.form, state.wizardStep) - } else { - noNode - }, - }, - ) -} diff --git a/src/components/MultiplayerMonitor.affine b/src/components/MultiplayerMonitor.affine new file mode 100644 index 00000000..d8f77fdd --- /dev/null +++ b/src/components/MultiplayerMonitor.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MultiplayerMonitor; + +// TODO: Complete semantic implementation diff --git a/src/components/MultiplayerMonitor.res b/src/components/MultiplayerMonitor.res deleted file mode 100644 index 3f6525a5..00000000 --- a/src/components/MultiplayerMonitor.res +++ /dev/null @@ -1,583 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Multiplayer Monitor Component — view for monitoring the IDApTIK -/// Phoenix sync server. Dashboard, channels, state diffs, latency, locks. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = (label: string, cat: multiplayerCategory, active: multiplayerCategory): Tea_Vdom.t< - msg, -> => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(MultiplayerMonitor(SetMultiplayerCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render a player card. -let renderPlayerCard = (player: connectedPlayer, isSelected: bool): Tea_Vdom.t => { - let borderCls = if isSelected { - "border-cyan-400" - } else { - "border-gray-700" - } - let latencyCls = if player.latencyMs < 50 { - "text-emerald-400" - } else if player.latencyMs < 100 { - "text-amber-400" - } else { - "text-red-400" - } - div( - list{ - Attrs.class_( - `p-3 bg-gray-800 rounded border ${borderCls} cursor-pointer hover:border-gray-500`, - ), - Events.onClick(MultiplayerMonitor(SelectPlayer(player.playerId))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-100")}, - list{text(player.displayName)}, - ), - if player.isHost { - span( - list{Attrs.class_("text-xs bg-amber-700 text-amber-100 px-1.5 py-0.5 rounded")}, - list{text("HOST")}, - ) - } else if player.isSpectator { - span( - list{Attrs.class_("text-xs bg-gray-600 text-gray-300 px-1.5 py-0.5 rounded")}, - list{text("SPEC")}, - ) - } else { - noNode - }, - }, - ), - span( - list{Attrs.class_(`text-xs font-mono ${latencyCls}`)}, - list{text(`${Int.toString(player.latencyMs)}ms`)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3 text-xs text-gray-500")}, - list{ - span(list{}, list{text(`Device: ${player.deviceId}`)}), - span(list{}, list{text(`Clock: ${Int.toString(player.lamportClock)}`)}), - }, - ), - }, - ) -} - -/// Render dashboard view. -let renderDashboard = (state: multiplayerMonitorState): Tea_Vdom.t => { - let connCls = MultiplayerMonitorEngine.connectionColour(state.wsConnection) - let players = MultiplayerMonitorEngine.filterPlayers(state.players, state.showSpectators) - div( - list{Attrs.class_("space-y-4")}, - list{ - // Connection status card - div( - list{Attrs.class_("p-4 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text("Sync Server")}, - ), - span( - list{Attrs.class_(`text-xs ${connCls}`)}, - list{text(MultiplayerMonitorEngine.connectionLabel(state.wsConnection))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - switch state.wsConnection { - | WsConnected => - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-red-700 text-white rounded hover:bg-red-600 cursor-pointer", - ), - Events.onClick(MultiplayerMonitor(DisconnectServer)), - KeyboardNav.onActivate(MultiplayerMonitor(DisconnectServer)), - }, - list{text("Disconnect")}, - ) - | WsDisconnected | WsError(_) => - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(MultiplayerMonitor(ConnectServer)), - KeyboardNav.onActivate(MultiplayerMonitor(ConnectServer)), - }, - list{text("Connect")}, - ) - | WsConnecting | WsReconnecting => noNode - }, - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(MultiplayerMonitor(RefreshState)), - KeyboardNav.onActivate(MultiplayerMonitor(RefreshState)), - }, - list{text("Refresh")}, - ), - }, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(state.serverUrl)}), - }, - ), - // Stats row - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-100")}, - list{text(Int.toString(Array.length(players)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Players")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-100")}, - list{text(Int.toString(Array.length(state.channels)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Channels")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-100")}, - list{text(Int.toString(MultiplayerMonitorEngine.averageLatency(state.players)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Avg Latency (ms)")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{ - Attrs.class_( - if MultiplayerMonitorEngine.contestedLocks(state.deviceLocks) > 0 { - "text-2xl font-light text-red-400" - } else { - "text-2xl font-light text-gray-100" - }, - ), - }, - list{ - text(Int.toString(MultiplayerMonitorEngine.contestedLocks(state.deviceLocks))), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Contested Locks")}), - }, - ), - }, - ), - // Player list - if Array.length(players) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No players connected")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - players - ->Array.map(p => renderPlayerCard(p, state.selectedPlayerId === Some(p.playerId))) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render channels view. -let renderChannels = (state: multiplayerMonitorState): Tea_Vdom.t => { - if Array.length(state.channels) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No channel subscriptions — connect to the sync server first")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.channels - ->Array.map(ch => - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-mono text-cyan-400")}, list{text(ch.topic)}), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(ch.messageCount)} messages`)}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render state diffs view. -let renderStateDiffs = (state: multiplayerMonitorState): Tea_Vdom.t => { - let unresolved = MultiplayerMonitorEngine.unresolvedDiffs(state.stateDiffs) - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(Array.length(unresolved))} unresolved diffs`)}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(MultiplayerMonitor(RefreshDiffs)), - KeyboardNav.onActivate(MultiplayerMonitor(RefreshDiffs)), - }, - list{text("Refresh")}, - ), - }, - ), - if Array.length(state.stateDiffs) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No state diffs detected — game state is in sync")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.stateDiffs - ->Array.map(diff => - div( - list{ - Attrs.class_( - `p-2 rounded text-xs ${if diff.resolved { - "bg-gray-800 opacity-50" - } else { - "bg-red-900/20 border border-red-800" - }}`, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span(list{Attrs.class_("text-gray-400")}, list{text(diff.playerId)}), - span(list{Attrs.class_("text-gray-200 font-mono")}, list{text(diff.field)}), - span(list{Attrs.class_("text-red-400 font-mono")}, list{text(diff.localValue)}), - span(list{Attrs.class_("text-gray-500")}, list{text("vs")}), - span( - list{Attrs.class_("text-emerald-400 font-mono")}, - list{text(diff.remoteValue)}, - ), - if diff.resolved { - span(list{Attrs.class_("text-emerald-500")}, list{text("Resolved")}) - } else { - span(list{Attrs.class_("text-red-400")}, list{text("Unresolved")}) - }, - }, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render latency view. -let renderLatency = (state: multiplayerMonitorState): Tea_Vdom.t => { - if Array.length(state.latencySamples) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No latency data — connect players to see latency graph")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{text(`${Int.toString(Array.length(state.latencySamples))} samples`)}, - ), - ...// Simple latency bars per player - state.players - ->Array.map(player => { - let latencyCls = if player.latencyMs < 50 { - "bg-emerald-600" - } else if player.latencyMs < 100 { - "bg-amber-600" - } else { - "bg-red-600" - } - let widthPct = Int.toString( - if player.latencyMs > 200 { - 100 - } else { - player.latencyMs * 100 / 200 - }, - ) - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("w-24 text-xs text-gray-300 truncate")}, - list{text(player.displayName)}, - ), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded h-4 overflow-hidden")}, - list{ - div( - list{ - Attrs.class_(`h-full ${latencyCls} rounded`), - Attrs.style("width", `${widthPct}%`), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("w-12 text-xs text-gray-400 text-right font-mono")}, - list{text(`${Int.toString(player.latencyMs)}ms`)}, - ), - }, - ) - }) - ->List.fromArray, - }, - ) - } -} - -/// Render device locks view. -let renderDeviceLocks = (state: multiplayerMonitorState): Tea_Vdom.t => { - if Array.length(state.deviceLocks) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No device locks active")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.deviceLocks - ->Array.map(lock => { - let isContested = Array.length(lock.contestedBy) > 0 - let borderCls = if isContested { - "border-red-700" - } else { - "border-gray-700" - } - div( - list{Attrs.class_(`p-3 bg-gray-800 rounded border ${borderCls}`)}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-200")}, - list{text(lock.deviceId)}, - ), - switch lock.lockedBy { - | Some(player) => - span( - list{Attrs.class_("text-xs text-cyan-400")}, - list{text(`Locked by ${player}`)}, - ) - | None => span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Unlocked")}) - }, - }, - ), - if isContested { - div( - list{Attrs.class_("text-xs text-red-400 mt-1")}, - list{text(`Contested by: ${lock.contestedBy->Array.join(", ")}`)}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Main view function. -let view = (state: multiplayerMonitorState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Multiplayer Monitor panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Multiplayer Monitor")}, - ), - span( - list{ - Attrs.class_( - `text-xs ${MultiplayerMonitorEngine.connectionColour(state.wsConnection)}`, - ), - }, - list{text(MultiplayerMonitorEngine.connectionLabel(state.wsConnection))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - if state.showSpectators { - "px-2 py-1 text-xs bg-gray-600 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(MultiplayerMonitor(ToggleSpectators)), - KeyboardNav.onActivate(MultiplayerMonitor(ToggleSpectators)), - }, - list{text("Spectators")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-amber-700 text-white rounded hover:bg-amber-600 cursor-pointer", - ), - Events.onClick(MultiplayerMonitor(ReconnectionTest)), - KeyboardNav.onActivate(MultiplayerMonitor(ReconnectionTest)), - }, - list{text("Reconnection Test")}, - ), - }, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Dashboard", MultiplayerDashboard, state.activeCategory), - renderTab("Channels", MultiplayerChannels, state.activeCategory), - renderTab("State Diff", MultiplayerStateDiff, state.activeCategory), - renderTab("Latency", MultiplayerLatency, state.activeCategory), - renderTab("Device Locks", MultiplayerDeviceLocks, state.activeCategory), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(MultiplayerMonitor(DismissMultiplayerError)), - KeyboardNav.onActivate(MultiplayerMonitor(DismissMultiplayerError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Loading - if state.loading { - div( - list{Attrs.class_("px-4 py-2 text-xs text-cyan-400 animate-pulse")}, - list{text("Loading multiplayer state...")}, - ) - } else { - noNode - }, - // Main content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | MultiplayerDashboard => renderDashboard(state) - | MultiplayerChannels => renderChannels(state) - | MultiplayerStateDiff => renderStateDiffs(state) - | MultiplayerLatency => renderLatency(state) - | MultiplayerDeviceLocks => renderDeviceLocks(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/MyLang.affine b/src/components/MyLang.affine new file mode 100644 index 00000000..21be6f32 --- /dev/null +++ b/src/components/MyLang.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module MyLang; + +// TODO: Complete semantic implementation diff --git a/src/components/MyLang.res b/src/components/MyLang.res deleted file mode 100644 index 40aac476..00000000 --- a/src/components/MyLang.res +++ /dev/null @@ -1,457 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL My-Lang Component — AI-native language development panel. -/// -/// Provides a code editor, REPL, compilation output, and dialect reference -/// for the 4 my-lang dialects: Solo, Duet, Ensemble, Me. - -open Model -open Msg -open Tea.Html - -/// Render a dialect selector button. -let renderDialectButton = (d: myLangDialect, active: myLangDialect): Tea_Vdom.t => { - let isActive = d === active - let colour = MyLangEngine.dialectColour(d) - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded transition-colors ${isActive - ? colour ++ " ring-1 ring-gray-600" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(MyLang(SetDialect(d))), - }, - list{text(MyLangEngine.dialectLabel(d))}, - ) -} - -/// Render a REPL entry. -let renderReplEntry = (entry: replEntry): Tea_Vdom.t => { - div( - list{Attrs.class_("font-mono text-xs space-y-0.5")}, - list{ - div(list{Attrs.class_("text-cyan-400")}, list{text(`> ${entry.input}`)}), - div( - list{ - Attrs.class_( - if entry.isError { - "text-red-400" - } else { - "text-gray-300" - }, - ), - }, - list{text(entry.output)}, - ), - }, - ) -} - -/// Render category tabs. -let renderTabs = (active: myLangCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - MyLangEngine.allCategories - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-cyan-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(MyLang(SetMlCategory(tab))), - }, - list{text(MyLangEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render TypeLL cross-panel type intelligence result (if available). -/// Parses the raw JSON via TypeLLEngine.parseCheckResult and displays an -/// evangeliser-style narrative with proof obligations and linearity notes. -let viewTypeCheckResult = (lastTypeCheck: option): Tea_Vdom.t => { - switch lastTypeCheck { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let narrative = TypeLLEngine.generateNarrative(result) - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - let statusText = if result.valid { - "Type-safe" - } else { - "Type issues detected" - } - div( - list{Attrs.class_("mt-4 p-3 rounded-lg border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL")}, - ), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono mb-1")}, - list{text(result.typeSignature)}, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(narrative.celebrate)}), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -/// Main view for the My-Lang panel. -let view = (ml: myLangState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("My-Lang AI-native language panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("My-Lang")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("AI-native language workbench")}, - ), - if ml.cliAvailable { - span(list{Attrs.class_("text-xs text-emerald-500")}, list{text("CLI ready")}) - } else { - span(list{Attrs.class_("text-xs text-amber-500")}, list{text("CLI not found")}) - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Dialect selector - div( - list{Attrs.class_("flex gap-1")}, - MyLangEngine.allDialects - ->Array.map(d => renderDialectButton(d, ml.activeDialect)) - ->List.fromArray, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(ml.activeCategory), - switch ml.activeCategory { - | MlEditor => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{ - text(`Editing in ${MyLangEngine.dialectLabel(ml.activeDialect)} dialect`), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-emerald-600 text-white rounded hover:bg-emerald-500", - ), - Events.onClick(MyLang(Compile)), - KeyboardNav.onActivate(MyLang(Compile)), - }, - list{text("Compile")}, - ), - }, - ), - textarea( - list{ - Attrs.class_( - "w-full h-96 bg-gray-900 border border-gray-700 rounded-lg p-4 font-mono text-sm text-gray-200 resize-none focus:border-cyan-500 focus:outline-none", - ), - Attrs.value(ml.editorContent), - Attrs.placeholder("Write your code here..."), - Events.onInput(v => MyLang(UpdateEditor(v))), - }, - list{}, - ), - }, - ) - | MlRepl => - div( - list{Attrs.class_("space-y-4")}, - list{ - // REPL history - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-4 h-80 overflow-y-auto space-y-2", - ), - }, - if ml.replHistory->Array.length === 0 { - list{ - div( - list{Attrs.class_("text-gray-600 text-sm")}, - list{ - text( - `${MyLangEngine.dialectLabel( - ml.activeDialect, - )} REPL — type an expression`, - ), - }, - ), - } - } else { - ml.replHistory->Array.map(e => renderReplEntry(e))->List.fromArray - }, - ), - // REPL input - div( - list{Attrs.class_("flex gap-2")}, - list{ - span( - list{Attrs.class_("text-cyan-400 font-mono text-sm pt-2")}, - list{text(">")}, - ), - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono placeholder-gray-600", - ), - Attrs.placeholder("Enter expression..."), - Attrs.value(ml.replInput), - Events.onInput(v => MyLang(UpdateReplInput(v))), - Events.onKeyDown(key => - if key === "Enter" { - Some(MyLang(EvalRepl)) - } else { - None - } - ), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-3 py-2 text-sm bg-cyan-600 text-white rounded hover:bg-cyan-500", - ), - Events.onClick(MyLang(EvalRepl)), - KeyboardNav.onActivate(MyLang(EvalRepl)), - }, - list{text("Eval")}, - ), - }, - ), - }, - ) - | MlCompile => - switch ml.lastCompilation { - | Some(result) => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{ - Attrs.class_( - if result.success { - "text-emerald-400 text-sm font-medium" - } else { - "text-red-400 text-sm font-medium" - }, - ), - }, - list{ - text( - if result.success { - "Compilation succeeded" - } else { - "Compilation failed" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(result.compileTimeMs)}ms`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(result.errorCount)} errors, ${Int.toString( - result.warningCount, - )} warnings`, - ), - }, - ), - }, - ), - if result.output !== "" { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("Output")}), - pre( - list{Attrs.class_("font-mono text-sm text-gray-300 whitespace-pre-wrap")}, - list{text(result.output)}, - ), - }, - ) - } else { - noNode - }, - if result.diagnostics !== "" { - div( - list{Attrs.class_("bg-gray-900 border border-red-700/50 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-red-400 mb-2")}, - list{text("Diagnostics")}, - ), - pre( - list{Attrs.class_("font-mono text-sm text-red-300 whitespace-pre-wrap")}, - list{text(result.diagnostics)}, - ), - }, - ) - } else { - noNode - }, - viewTypeCheckResult(ml.lastTypeCheck), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{ - text("No compilation results. Write code in the Editor tab and click Compile."), - }, - ) - } - | MlDialects => - div( - list{Attrs.class_("space-y-6 max-w-2xl")}, - list{ - h3( - list{Attrs.class_("text-base font-medium text-gray-200")}, - list{text("My-Lang Dialects")}, - ), - div( - list{Attrs.class_("space-y-4")}, - MyLangEngine.allDialects - ->Array.map(d => { - let colour = MyLangEngine.dialectColour(d) - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3 mb-2")}, - list{ - span( - list{ - Attrs.class_(`px-2 py-0.5 text-xs font-medium rounded ${colour}`), - }, - list{text(MyLangEngine.dialectLabel(d))}, - ), - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(MyLangEngine.dialectDescription(d))}, - ), - }, - ), - pre( - list{ - Attrs.class_( - "mt-2 font-mono text-xs text-gray-500 bg-gray-950 rounded p-3 whitespace-pre-wrap", - ), - }, - list{text(MyLangEngine.dialectExample(d))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - }, - switch ml.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/NesyDrift.affine b/src/components/NesyDrift.affine new file mode 100644 index 00000000..7111d42e --- /dev/null +++ b/src/components/NesyDrift.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NesyDrift; + +// TODO: Complete semantic implementation diff --git a/src/components/NesyDrift.res b/src/components/NesyDrift.res deleted file mode 100644 index 285a4034..00000000 --- a/src/components/NesyDrift.res +++ /dev/null @@ -1,226 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL NeSy Drift Dashboard Panel — model drift detection and alerting -/// display. -/// -/// Layout: Current drift status (large indicator) at top, action -/// recommendation beneath, scrollable alert timeline below. Model status -/// cards show per-model drift state. - -open Msg -open NesyDriftModel -open NesyDriftEngine -open Tea.Html - -// ============================================================================ -// Model Status Card -// ============================================================================ - -/// A single model status card showing current drift state. -let modelStatusCard = (status: modelDriftStatus): Tea_Vdom.t => { - let borderColor = driftBorderColor(status.isDrifting) - div( - list{Attrs.class_(`flex flex-col p-3 rounded border ${borderColor} bg-gray-900/50`)}, - list{ - // Model name - div( - list{Attrs.class_("font-semibold text-sm text-gray-100 mb-1")}, - list{text(status.modelName)}, - ), - // Drift indicator - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{ - Attrs.class_( - if status.isDrifting { - "w-2 h-2 rounded-full bg-red-500 animate-pulse" - } else { - "w-2 h-2 rounded-full bg-emerald-500" - }, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - if status.isDrifting { - "DRIFTING" - } else { - "STABLE" - }, - ), - }, - ), - }, - ), - // Last drift kind - switch status.lastDriftKind { - | Some(kind) => - span(list{Attrs.class_("text-xs text-amber-400")}, list{text(driftKindLabel(kind))}) - | None => span(list{Attrs.class_("text-xs text-gray-600")}, list{text("No drift detected")}) - }, - // Magnitude bar - div( - list{Attrs.class_("mt-2 w-full bg-gray-800 rounded-full h-1.5")}, - list{ - div( - list{ - Attrs.class_( - `h-1.5 rounded-full ${if status.lastMagnitude >= 0.7 { - "bg-red-500" - } else if status.lastMagnitude >= 0.4 { - "bg-amber-500" - } else { - "bg-emerald-500" - }}`, - ), - Attrs.style("width", Float.toFixed(status.lastMagnitude *. 100.0, ~digits=0) ++ "%"), - }, - list{}, - ), - }, - ), - // Alert count - span( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text(`${Int.toString(status.alertCount)} alerts`)}, - ), - }, - ) -} - -// ============================================================================ -// Alert Row -// ============================================================================ - -/// A single alert row in the timeline. -let alertRow = (alert: driftAlert): Tea_Vdom.t => { - let urgencyColor = alertColor(alert.urgency) - div( - list{ - Attrs.class_( - "flex items-start gap-3 px-3 py-3 border-b border-gray-800 hover:bg-gray-800/30", - ), - }, - list{ - // Urgency badge - span( - list{Attrs.class_(`px-2 py-0.5 text-xs rounded font-mono shrink-0 ${urgencyColor}`)}, - list{text(urgencyLabel(alert.urgency))}, - ), - // Alert content - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - // Model name + drift kind - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-semibold text-gray-100")}, - list{text(alert.modelName)}, - ), - span( - list{Attrs.class_(`text-xs ${severityTextColor(alert.severity)}`)}, - list{text(driftKindLabel(alert.kind))}, - ), - }, - ), - // Description - p(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(alert.description)}), - // Action recommendation - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Recommended:")}), - span( - list{Attrs.class_("text-xs text-cyan-400 font-mono")}, - list{text(actionLabel(alert.recommendedAction))}, - ), - }, - ), - }, - ), - // Timestamp - span( - list{Attrs.class_("text-xs text-gray-500 font-mono shrink-0")}, - list{text(alert.timestamp)}, - ), - }, - ) -} - -// ============================================================================ -// Urgency Filter -// ============================================================================ - -/// Urgency filter buttons. -let urgencyFilter = (currentFilter: option): Tea_Vdom.t => { - let filterBtn = (label: string, filterValue: option) => { - let isActive = currentFilter == filterValue - let baseClass = "px-3 py-1 text-xs rounded cursor-pointer" - let activeClass = if isActive { - "bg-emerald-600 text-white" - } else { - "bg-gray-700 text-gray-300 hover:bg-gray-600" - } - button(list{Attrs.class_(`${baseClass} ${activeClass}`)}, list{text(label)}) - } - div( - list{Attrs.class_("flex gap-2 mb-3")}, - list{ - filterBtn("All", None), - filterBtn("Immediate", Some(Immediate)), - filterBtn("Soon", Some(Soon)), - filterBtn("Scheduled", Some(Scheduled)), - filterBtn("FYI", Some(FYI)), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Top-level view for the NeSy Drift Dashboard panel. -let view = (state: nesyDriftState): Tea_Vdom.t => { - let filteredAlerts = filterByUrgency(state.alerts, state.urgencyFilter) - div( - list{Attrs.class_("flex flex-col h-full p-3 bg-gray-950 text-gray-100")}, - list{ - // Panel header - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold")}, list{text("NeSy Drift Dashboard")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.modelStatuses))} models monitored`)}, - ), - }, - ), - // Model status cards (horizontal scroll) - div( - list{Attrs.class_("flex gap-3 overflow-x-auto pb-3 mb-3")}, - state.modelStatuses->Array.map(modelStatusCard)->List.fromArray, - ), - // Urgency filter - urgencyFilter(state.urgencyFilter), - // Alert timeline (scrollable) - div( - list{Attrs.class_("flex-1 overflow-y-auto border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("divide-y divide-gray-800")}, - filteredAlerts->Array.map(alertRow)->List.fromArray, - ), - }, - ), - }, - ) -} diff --git a/src/components/NesyHarmonize.affine b/src/components/NesyHarmonize.affine new file mode 100644 index 00000000..1412132e --- /dev/null +++ b/src/components/NesyHarmonize.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NesyHarmonize; + +// TODO: Complete semantic implementation diff --git a/src/components/NesyHarmonize.res b/src/components/NesyHarmonize.res deleted file mode 100644 index d029e05f..00000000 --- a/src/components/NesyHarmonize.res +++ /dev/null @@ -1,182 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL NeSy Harmonization Monitor Panel — live neural-symbolic verdict -/// fusion display. -/// -/// Layout: Stats bar at top (3 verdict counters with colours), scrollable -/// entry list below. Each entry shows neural -> symbolic -> verdict with -/// colour coding. Filter dropdown for verdict type selection. - -open Msg -open NesyHarmonizeModel -open NesyHarmonizeEngine -open Tea.Html - -// ============================================================================ -// Stats Bar -// ============================================================================ - -/// A single stat counter pill with a coloured background. -let statPill = (label: string, count: int, colorClass: string): Tea_Vdom.t => { - div( - list{Attrs.class_(`flex flex-col items-center px-4 py-2 rounded ${colorClass}`)}, - list{ - span(list{Attrs.class_("text-2xl font-bold font-mono")}, list{text(Int.toString(count))}), - span(list{Attrs.class_("text-xs uppercase tracking-wide opacity-80")}, list{text(label)}), - }, - ) -} - -/// Stats bar showing 3 verdict counters and the symbolic win rate. -let statsBar = (stats: harmonizeStats): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-3 p-3 bg-gray-900/50 rounded-lg mb-3")}, - list{ - statPill("Certified Safe", stats.certifiedSafe, "bg-emerald-600/80 text-white"), - statPill("Requires Review", stats.requiresReview, "bg-amber-500/80 text-white"), - statPill("Critical Unsafe", stats.criticalUnsafe, "bg-red-600/80 text-white"), - div( - list{ - Attrs.class_("flex flex-col items-center px-4 py-2 rounded bg-gray-700/50 text-gray-200"), - }, - list{ - span( - list{Attrs.class_("text-2xl font-bold font-mono")}, - list{text(Float.toFixed(stats.symbolicWinRate *. 100.0, ~digits=1) ++ "%")}, - ), - span( - list{Attrs.class_("text-xs uppercase tracking-wide opacity-80")}, - list{text("Symbolic Win Rate")}, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Verdict Arrow Display -// ============================================================================ - -/// Arrow separator between verdict stages. -let arrow: Tea_Vdom.t = { - span(list{Attrs.class_("text-gray-500 mx-1")}, list{text("->")}) -} - -/// Verdict badge with colour coding. -let verdictBadge = (label: string, colorClass: string): Tea_Vdom.t => { - span(list{Attrs.class_(`px-2 py-0.5 text-xs rounded font-mono ${colorClass}`)}, list{text(label)}) -} - -// ============================================================================ -// Entry Row -// ============================================================================ - -/// A single harmonization entry row showing the verdict pipeline. -let entryRow = (entry: harmonizationEntry): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-2 px-3 py-2 border-b border-gray-800 hover:bg-gray-800/30", - ), - }, - list{ - // Timestamp - span( - list{Attrs.class_("text-xs text-gray-500 font-mono w-40 shrink-0")}, - list{text(entry.timestamp)}, - ), - // Source - span( - list{Attrs.class_("text-xs text-gray-400 w-24 shrink-0 truncate")}, - list{text(entry.source)}, - ), - // Neural verdict - verdictBadge(neuralLabel(entry.neural), neuralVerdictColor(entry.neural)), - arrow, - // Symbolic verdict - verdictBadge(symbolicLabel(entry.symbolic), symbolicVerdictColor(entry.symbolic)), - arrow, - // Harmonized verdict (main result) - verdictBadge(verdictLabel(entry.verdict), verdictColor(entry.verdict)), - // Confidence indicator - span( - list{Attrs.class_(`text-xs ml-2 ${confidenceColor(entry.confidence)}`)}, - list{text(confidenceLabel(entry.confidence))}, - ), - // Symbolic wins indicator - if entry.symbolicWins { - span(list{Attrs.class_("text-xs text-blue-400 ml-auto")}, list{text("[S wins]")}) - } else { - noNode - }, - }, - ) -} - -// ============================================================================ -// Filter Controls -// ============================================================================ - -/// Filter dropdown for selecting verdict type. -let filterControls = (currentFilter: option): Tea_Vdom.t => { - let filterBtn = (label: string, filterValue: option) => { - let isActive = currentFilter == filterValue - let baseClass = "px-3 py-1 text-xs rounded cursor-pointer" - let activeClass = if isActive { - "bg-emerald-600 text-white" - } else { - "bg-gray-700 text-gray-300 hover:bg-gray-600" - } - button(list{Attrs.class_(`${baseClass} ${activeClass}`)}, list{text(label)}) - } - div( - list{Attrs.class_("flex gap-2 mb-3")}, - list{ - filterBtn("All", None), - filterBtn("Certified Safe", Some(CertifiedSafe)), - filterBtn("Requires Review", Some(RequiresReview)), - filterBtn("Critical Unsafe", Some(CriticalUnsafe)), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Top-level view for the NeSy Harmonization Monitor panel. -let view = (state: nesyHarmonizeState): Tea_Vdom.t => { - let filtered = filterEntries(state.entries, state.filter) - div( - list{Attrs.class_("flex flex-col h-full p-3 bg-gray-950 text-gray-100")}, - list{ - // Panel header - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold")}, list{text("NeSy Harmonization Monitor")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(state.stats.totalCount)} entries`)}, - ), - }, - ), - // Stats bar - statsBar(state.stats), - // Filter controls - filterControls(state.filter), - // Entry list (scrollable) - div( - list{Attrs.class_("flex-1 overflow-y-auto border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("divide-y divide-gray-800")}, - filtered->Array.map(entryRow)->List.fromArray, - ), - }, - ), - }, - ) -} diff --git a/src/components/NesyModes.affine b/src/components/NesyModes.affine new file mode 100644 index 00000000..77afdc15 --- /dev/null +++ b/src/components/NesyModes.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NesyModes; + +// TODO: Complete semantic implementation diff --git a/src/components/NesyModes.res b/src/components/NesyModes.res deleted file mode 100644 index fe09000c..00000000 --- a/src/components/NesyModes.res +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL NeSy Reasoning Mode Selector Panel — mode grid for choosing -/// neural-symbolic reasoning strategies. -/// -/// Layout: 6 mode cards in a 2x3 grid. Each card shows mode name, -/// symbolic/neural indicators, and description. Click to select. -/// Active mode is highlighted with an emerald border. - -open Msg -open NesyModesModel -open NesyModesEngine -open Tea.Html - -// ============================================================================ -// Subsystem Indicator -// ============================================================================ - -/// Small indicator badge showing whether a subsystem is active. -let subsystemIndicator = (label: string, isActive: bool): Tea_Vdom.t => { - let colorClass = if isActive { - "bg-emerald-600 text-white" - } else { - "bg-gray-700 text-gray-500" - } - span(list{Attrs.class_(`px-2 py-0.5 text-xs rounded font-mono ${colorClass}`)}, list{text(label)}) -} - -// ============================================================================ -// Mode Card -// ============================================================================ - -/// A single mode card in the selection grid. -let modeCard = (info: modeInfo, isActive: bool): Tea_Vdom.t => { - let borderClass = modeBorderColor(info.mode, isActive) - let bgClass = modeBgColor(info.mode) - div( - list{ - Attrs.class_( - `flex flex-col p-4 rounded-lg border-2 cursor-pointer transition-all ${borderClass} ${bgClass} hover:brightness-110`, - ), - }, - list{ - // Mode name - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("font-semibold text-sm text-gray-100")}, - list{text(info.displayName)}, - ), - if isActive { - span(list{Attrs.class_("text-xs text-emerald-400 font-mono")}, list{text("ACTIVE")}) - } else { - noNode - }, - }, - ), - // Description - p( - list{Attrs.class_("text-xs text-gray-400 mb-3 leading-relaxed")}, - list{text(info.description)}, - ), - // Subsystem indicators - div( - list{Attrs.class_("flex gap-2 mt-auto")}, - list{ - subsystemIndicator("Symbolic", info.usesSymbolic), - subsystemIndicator("Neural", info.usesNeural), - if info.isHybrid { - span( - list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono bg-purple-600 text-white")}, - list{text("Hybrid")}, - ) - } else { - noNode - }, - }, - ), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Top-level view for the NeSy Reasoning Mode Selector panel. -let view = (state: nesyModesState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full p-3 bg-gray-950 text-gray-100")}, - list{ - // Panel header - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold")}, - list{text("NeSy Reasoning Mode Selector")}, - ), - if state.switching { - span( - list{Attrs.class_("text-xs text-amber-400 animate-pulse")}, - list{text("Switching mode...")}, - ) - } else { - noNode - }, - }, - ), - // Error display - switch state.lastError { - | Some(err) => - div( - list{ - Attrs.class_( - "p-2 mb-3 rounded bg-red-900/30 border border-red-500/40 text-xs text-red-400", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Mode grid (2x3) - div( - list{Attrs.class_("grid grid-cols-2 gap-3 flex-1")}, - state.availableModes - ->Array.map(info => modeCard(info, info.mode == state.activeMode)) - ->List.fromArray, - ), - }, - ) -} diff --git a/src/components/NetworkTopology.affine b/src/components/NetworkTopology.affine new file mode 100644 index 00000000..d485e2e4 --- /dev/null +++ b/src/components/NetworkTopology.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NetworkTopology; + +// TODO: Complete semantic implementation diff --git a/src/components/NetworkTopology.res b/src/components/NetworkTopology.res deleted file mode 100644 index 098bb358..00000000 --- a/src/components/NetworkTopology.res +++ /dev/null @@ -1,419 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Network Topology Component — view for the IDApTIK in-game -/// network topology viewer. Force-directed graph of devices, zones, -/// security levels, packet flow, and DNS resolution. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: networkTopologyCategory, - active: networkTopologyCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(NetworkTopology(SetTopologyCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render a single device card in the graph view. -let renderDeviceCard = (device: networkDevice, isSelected: bool): Tea_Vdom.t => { - let borderCls = if device.compromised { - "border-red-500" - } else if isSelected { - "border-cyan-400" - } else { - "border-gray-600" - } - let zoneCls = NetworkTopologyEngine.zoneColour(device.zone) - div( - list{ - Attrs.class_( - `p-3 bg-gray-800 rounded border ${borderCls} cursor-pointer hover:border-gray-400`, - ), - Events.onClick(NetworkTopology(SelectDevice(device.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-100")}, list{text(device.name)}), - span( - list{Attrs.class_(`text-xs ${zoneCls}`)}, - list{text(NetworkTopologyEngine.zoneLabel(device.zone))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2 text-xs text-gray-400")}, - list{ - span(list{}, list{text(device.deviceType)}), - span(list{}, list{text(`Sec: ${Int.toString(device.securityLevel)}`)}), - if device.compromised { - span(list{Attrs.class_("text-red-400 font-bold")}, list{text("COMPROMISED")}) - } else if device.active { - span(list{Attrs.class_("text-emerald-400")}, list{text("Active")}) - } else { - span(list{Attrs.class_("text-gray-500")}, list{text("Inactive")}) - }, - }, - ), - if Array.length(device.defenceFlags) > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - device.defenceFlags - ->Array.map(flag => - span( - list{ - Attrs.class_("px-1.5 py-0.5 text-xs bg-emerald-900/50 text-emerald-300 rounded"), - }, - list{text(flag)}, - ) - ) - ->List.fromArray, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the zones summary view. -let renderZones = (state: networkTopologyState): Tea_Vdom.t => { - let zones: array = [ZonePublic, ZoneDmz, ZoneInternal, ZoneRestricted, ZoneAirGapped] - div( - list{Attrs.class_("space-y-3")}, - zones - ->Array.map(zone => { - let devices = NetworkTopologyEngine.devicesByZone(state.devices, zone) - let bgCls = NetworkTopologyEngine.zoneBgColour(zone) - let colourCls = NetworkTopologyEngine.zoneColour(zone) - div( - list{Attrs.class_(`p-3 rounded ${bgCls}`)}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_(`text-sm font-medium ${colourCls}`)}, - list{text(NetworkTopologyEngine.zoneLabel(zone))}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(Array.length(devices))} devices`)}, - ), - }, - ), - if Array.length(devices) === 0 { - div( - list{Attrs.class_("text-xs text-gray-500 italic")}, - list{text("No devices in this zone")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - devices - ->Array.map(d => - div( - list{ - Attrs.class_( - "flex items-center gap-2 text-xs text-gray-300 cursor-pointer hover:text-white", - ), - }, - list{ - span(list{}, list{text(d.name)}), - span(list{Attrs.class_("text-gray-500")}, list{text(d.deviceType)}), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) - }) - ->List.fromArray, - ) -} - -/// Render DNS entries table. -let renderDns = (state: networkTopologyState): Tea_Vdom.t => { - if Array.length(state.dnsEntries) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No DNS entries — connect to a running game to see DNS resolution")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.dnsEntries - ->Array.map(entry => - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span( - list{Attrs.class_("text-cyan-400 font-mono w-40 truncate")}, - list{text(entry.hostname)}, - ), - span(list{Attrs.class_("text-gray-500")}, list{text(entry.recordType)}), - span(list{Attrs.class_("text-gray-300 font-mono")}, list{text(entry.resolvedIp)}), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`TTL: ${Int.toString(entry.ttl)}`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render packet flow animation view. -let renderPacketFlow = (state: networkTopologyState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - // Controls - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - if state.animatePackets { - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - }, - ), - Events.onClick(NetworkTopology(TogglePacketAnimation)), - KeyboardNav.onActivate(NetworkTopology(TogglePacketAnimation)), - }, - list{ - text( - if state.animatePackets { - "Animating..." - } else { - "Start Animation" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(Array.length(state.packetFlow))} events`)}, - ), - }, - ), - // Recent packet events - if Array.length(state.packetFlow) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No packet flow events captured")}, - ) - } else { - div( - list{Attrs.class_("space-y-1 max-h-96 overflow-y-auto")}, - state.packetFlow - ->Array.map(evt => - div( - list{ - Attrs.class_( - `flex items-center gap-3 p-2 rounded text-xs ${if evt.blocked { - "bg-red-900/30" - } else { - "bg-gray-800" - }}`, - ), - }, - list{ - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(Float.toString(evt.timestamp))}, - ), - span(list{Attrs.class_("text-gray-300")}, list{text(evt.connectionId)}), - span(list{Attrs.class_("text-gray-500")}, list{text(`${Int.toString(evt.size)}B`)}), - if evt.blocked { - span(list{Attrs.class_("text-red-400 font-bold")}, list{text("BLOCKED")}) - } else { - span(list{Attrs.class_("text-emerald-400")}, list{text("OK")}) - }, - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Main view function. -let view = (state: networkTopologyState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Network Topology panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Network Topology")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(Array.length(state.devices))} devices`)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Toggle labels - button( - list{ - Attrs.class_( - if state.showLabels { - "px-2 py-1 text-xs bg-cyan-800 text-cyan-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(NetworkTopology(ToggleLabels)), - KeyboardNav.onActivate(NetworkTopology(ToggleLabels)), - }, - list{text("Labels")}, - ), - // Toggle security levels - button( - list{ - Attrs.class_( - if state.showSecurityLevels { - "px-2 py-1 text-xs bg-amber-800 text-amber-200 rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(NetworkTopology(ToggleSecurityLevels)), - KeyboardNav.onActivate(NetworkTopology(ToggleSecurityLevels)), - }, - list{text("Security")}, - ), - // Refresh - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(NetworkTopology(RefreshTopology)), - KeyboardNav.onActivate(NetworkTopology(RefreshTopology)), - }, - list{text("Refresh")}, - ), - }, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Graph", TopologyGraph, state.activeCategory), - renderTab("Zones", TopologyZones, state.activeCategory), - renderTab("DNS", TopologyDns, state.activeCategory), - renderTab("Packet Flow", TopologyPacketFlow, state.activeCategory), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(NetworkTopology(DismissTopoError)), - KeyboardNav.onActivate(NetworkTopology(DismissTopoError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Loading indicator - if state.loading { - div( - list{Attrs.class_("px-4 py-2 text-xs text-cyan-400 animate-pulse")}, - list{text("Loading topology...")}, - ) - } else { - noNode - }, - // Main content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | TopologyGraph => - if Array.length(state.devices) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-16")}, - list{ - div(list{Attrs.class_("text-4xl mb-4")}, list{text("~")}), - div(list{}, list{text("No network topology loaded")}), - div( - list{Attrs.class_("mt-2 text-xs")}, - list{text("Connect to a running IDApTIK game to see the network graph")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-3")}, - state.devices - ->Array.map(device => { - let isSelected = state.selectedDeviceId === Some(device.id) - renderDeviceCard(device, isSelected) - }) - ->List.fromArray, - ) - } - | TopologyZones => renderZones(state) - | TopologyDns => renderDns(state) - | TopologyPacketFlow => renderPacketFlow(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/NeurosymBridge.affine b/src/components/NeurosymBridge.affine new file mode 100644 index 00000000..aef7f141 --- /dev/null +++ b/src/components/NeurosymBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module NeurosymBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/NeurosymBridge.res b/src/components/NeurosymBridge.res deleted file mode 100644 index 7aac59cf..00000000 --- a/src/components/NeurosymBridge.res +++ /dev/null @@ -1,380 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Neurosymbolic Bridge Component — ECHIDNA guard AI behaviour reasoning. -/// Displays guard rule lists, behaviour tree viewers, simulation controls, and -/// analysis results for guard AI correctness. - -open Model -open Msg -open Tea.Html - -/// Render a rule status badge. -let ruleStatusBadge = (status: ruleStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | RuleVerified => ("bg-green-700 text-green-100", "Verified") - | RuleUnverified => ("bg-gray-700 text-gray-300", "Unverified") - | RuleViolated => ("bg-red-700 text-red-100", "Violated") - | RuleConflict => ("bg-yellow-700 text-yellow-100", "Conflict") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render a priority indicator. -let priorityIndicator = (prio: rulePriority): Tea_Vdom.t => { - let (color, label) = switch prio { - | PriorityCritical => ("text-red-400", "CRIT") - | PriorityHigh => ("text-orange-400", "HIGH") - | PriorityNormal => ("text-blue-400", "NORM") - | PriorityLow => ("text-gray-500", "LOW") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a behaviour tree node type badge. -let nodeTypeBadge = (nt: behaviourNodeType): Tea_Vdom.t => { - let (color, label) = switch nt { - | NodeSequence => ("text-blue-400", "SEQ") - | NodeSelector => ("text-purple-400", "SEL") - | NodeParallel => ("text-cyan-400", "PAR") - | NodeDecorator => ("text-yellow-400", "DEC") - | NodeAction => ("text-green-400", "ACT") - | NodeCondition => ("text-orange-400", "CND") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text("[" ++ label ++ "]")}) -} - -/// Main view function for the Neurosym Bridge panel. -let view = (state: neurosymBridgeState): Tea_Vdom.t => { - let verifiedCount = state.guardRules->Array.filter(r => r.status == RuleVerified)->Array.length - let totalRules = Array.length(state.guardRules) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Neurosymbolic Bridge — ECHIDNA Guard AI Reasoning"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-violet-300")}, - list{text("Neurosymbolic Bridge")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(verifiedCount) ++ - "/" ++ - Int.toString(totalRules) ++ " rules verified", - ), - }, - ), - if state.simulating { - span( - list{Attrs.class_("text-xs text-yellow-400 animate-pulse")}, - list{text("Simulating...")}, - ) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-violet-800 hover:bg-violet-700 text-white rounded", - ), - Events.onClick(NeurosymBridge(NbStarted)), - KeyboardNav.onActivate(NeurosymBridge(NbStarted)), - }, - list{text("Run Simulation")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Rules { - "bg-violet-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(NeurosymBridge(SetNbTab(Rules))), - }, - list{text("Rules")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == BehaviourTree { - "bg-violet-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(NeurosymBridge(SetNbTab(BehaviourTree))), - }, - list{text("Behaviour Tree")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Simulation { - "bg-violet-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(NeurosymBridge(SetNbTab(Simulation))), - }, - list{text("Simulation")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Analysis { - "bg-violet-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(NeurosymBridge(SetNbTab(Analysis))), - }, - list{text("Analysis")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(NeurosymBridge(DismissNbError)), - KeyboardNav.onActivate(NeurosymBridge(DismissNbError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Rules => - div( - list{}, - state.guardRules - ->Array.map(r => - div( - list{Attrs.class_("flex items-center gap-3 py-2 border-b border-gray-800/50")}, - list{ - priorityIndicator(r.priority), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm font-mono text-gray-200")}, - list{text(r.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(r.condition ++ " => " ++ r.expectedAction)}, - ), - }, - ), - ruleStatusBadge(r.status), - }, - ) - ) - ->List.fromArray, - ) - | BehaviourTree => - div( - list{Attrs.class_("font-mono text-sm")}, - state.behaviourNodes - ->Array.map(node => - div( - list{ - Attrs.class_( - "py-1 flex items-center gap-2 pl-" ++ - Int.toString(Array.length(node.children) > 0 ? 0 : 4), - ), - }, - list{ - nodeTypeBadge(node.nodeType), - span(list{Attrs.class_("text-gray-200")}, list{text(node.label)}), - switch node.condition { - | Some(cond) => - span( - list{Attrs.class_("text-xs text-yellow-400")}, - list{text("when: " ++ cond)}, - ) - | None => Tea_Html.noNode - }, - switch node.action { - | Some(act) => - span(list{Attrs.class_("text-xs text-green-400")}, list{text("do: " ++ act)}) - | None => Tea_Html.noNode - }, - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Int.toString(Array.length(node.children)) ++ " children")}, - ), - }, - ) - ) - ->List.fromArray, - ) - | Simulation => - switch state.simulationResults { - | Some(sim) => - div( - list{}, - list{ - div( - list{Attrs.class_("flex gap-4 mb-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text("Steps: " ++ Int.toString(sim.totalSteps))}), - span(list{}, list{text("Deviations: " ++ Int.toString(sim.deviations))}), - span( - list{ - Attrs.class_( - if sim.completed { - "text-green-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if sim.completed { - "Completed" - } else { - "Interrupted" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - sim.steps - ->Array.map(step => { - let outcomeColor = switch step.outcome { - | StepSuccess => "border-green-800" - | StepDeviation => "border-yellow-800" - | StepDeadlock => "border-red-800" - } - div( - list{ - Attrs.class_( - "flex items-center gap-3 py-1 border-l-2 pl-2 " ++ outcomeColor, - ), - }, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-8")}, - list{text("#" ++ Int.toString(step.stepNumber))}, - ), - span( - list{Attrs.class_("text-sm text-gray-300 flex-1")}, - list{text(step.actionTaken)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Float.toFixed(step.elapsedMs, ~digits=1) ++ "ms")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{ - text("No simulation results yet. Run a simulation to see guard AI behaviour."), - }, - ) - } - | Analysis => - switch state.simulationResults { - | Some(sim) => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("px-3 py-3 bg-gray-900 border border-gray-800 rounded")}, - list{ - h3( - list{Attrs.class_("text-sm text-violet-300 mb-2")}, - list{text("ECHIDNA Analysis Summary")}, - ), - p( - list{Attrs.class_("text-sm text-gray-300")}, - list{text(sim.analysisSummary)}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - "Guard rules: " ++ - Int.toString(totalRules) ++ - " total, " ++ - Int.toString(verifiedCount) ++ - " verified (" ++ - Float.toFixed( - if totalRules > 0 { - Int.toFloat(verifiedCount) /. Int.toFloat(totalRules) *. 100.0 - } else { - 0.0 - }, - ~digits=1, - ) ++ "%)", - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Run a simulation to generate analysis results.")}, - ) - } - }, - }, - ), - }, - ) -} diff --git a/src/components/Observatory.affine b/src/components/Observatory.affine new file mode 100644 index 00000000..223ff150 --- /dev/null +++ b/src/components/Observatory.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Observatory; + +// TODO: Complete semantic implementation diff --git a/src/components/Observatory.res b/src/components/Observatory.res deleted file mode 100644 index ad47269f..00000000 --- a/src/components/Observatory.res +++ /dev/null @@ -1,204 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Observatory Component — integrative dashboard. -/// -/// Single pane of glass for all panel health, service status, resource -/// usage, and ambient metrics. Aggregates data from PanelRegistry and -/// Gossamer system info queries. - -open Model -open Msg -open Tea.Html - -/// Render a health badge. -let healthBadge = (health: serviceHealth): Tea_Vdom.t => { - let (color, label) = switch health { - | Healthy => ("text-green-400", "Healthy") - | Degraded(reason) => ("text-yellow-400", "Degraded: " ++ reason) - | Unreachable => ("text-red-400", "Unreachable") - | Unknown => ("text-gray-500", "Unknown") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a resource snapshot row. -let snapshotRow = (snapshot: resourceSnapshot): Tea_Vdom.t => { - let memLabel = if snapshot.memoryBytes > 0 { - Int.toString(snapshot.memoryBytes / (1024 * 1024)) ++ " MiB" - } else { - "N/A" - } - div( - list{ - Attrs.class_("flex items-center justify-between py-1 px-2 border-b border-gray-800"), - Attrs.role("row"), - }, - list{ - span(list{Attrs.class_("text-sm text-gray-200 w-40")}, list{text(snapshot.name)}), - span(list{Attrs.class_("text-xs text-gray-400 w-20")}, list{text(memLabel)}), - span( - list{ - Attrs.class_( - "text-xs w-16 " ++ if snapshot.active { - "text-blue-400" - } else { - "text-gray-600" - }, - ), - }, - list{ - text( - if snapshot.active { - "Active" - } else { - "Idle" - }, - ), - }, - ), - healthBadge(snapshot.health), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: observatoryTab, target: observatoryTab, label: string): Tea_Vdom.t => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(Observatory(SetObsTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Observatory panel. -let view = (state: observatoryState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Observatory — Integrative Dashboard"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - h2(list{Attrs.class_("text-lg font-bold text-blue-300")}, list{text("Observatory")}), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600", - ), - Events.onClick(Observatory(RunHealthCheck)), - KeyboardNav.onActivate(Observatory(RunHealthCheck)), - }, - list{ - text( - if state.checking { - "Checking..." - } else { - "Health Check" - }, - ), - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - list{ - tabBtn(state.activeTab, TabOverview, "Overview"), - tabBtn(state.activeTab, TabServices, "Services"), - tabBtn(state.activeTab, TabResources, "Resources"), - tabBtn(state.activeTab, TabActivity, "Activity"), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - Events.onClick(Observatory(DismissObsError)), - KeyboardNav.onActivate(Observatory(DismissObsError)), - }, - list{text(err)}, - ) - | None => Tea_Html.noNode - }, - // System summary bar - div( - list{Attrs.class_("flex gap-6 px-4 py-2 text-xs text-gray-400 border-b border-gray-800")}, - list{ - span(list{}, list{text("CPU: " ++ Float.toFixed(state.systemCpu, ~digits=1) ++ "%")}), - span( - list{}, - list{ - text( - "Memory: " ++ - Int.toString(state.systemMemory / (1024 * 1024)) ++ - " / " ++ - Int.toString(state.systemMemoryTotal / (1024 * 1024)) ++ " MiB", - ), - }, - ), - span(list{}, list{text("Panels: " ++ Int.toString(Array.length(state.snapshots)))}), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.activeTab { - | TabOverview | TabServices | TabResources => - div( - list{Attrs.role("table"), Attrs.ariaLabel("Panel health")}, - state.snapshots->Array.map(snapshotRow)->List.fromArray, - ) - | TabActivity => - div( - list{}, - state.activity - ->Array.map(entry => - div( - list{Attrs.class_("flex gap-4 py-1 text-xs border-b border-gray-800")}, - list{ - span( - list{Attrs.class_("text-gray-500 w-40 shrink-0")}, - list{text(entry.timestamp)}, - ), - span( - list{Attrs.class_("text-blue-300 w-28 shrink-0")}, - list{text(entry.panelName)}, - ), - span(list{Attrs.class_("text-gray-300")}, list{text(entry.event)}), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Oo7Toolchain.affine b/src/components/Oo7Toolchain.affine new file mode 100644 index 00000000..4edd0199 --- /dev/null +++ b/src/components/Oo7Toolchain.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Oo7Toolchain; + +// TODO: Complete semantic implementation diff --git a/src/components/Oo7Toolchain.res b/src/components/Oo7Toolchain.res deleted file mode 100644 index 3c55a8e6..00000000 --- a/src/components/Oo7Toolchain.res +++ /dev/null @@ -1,488 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL 007 Toolchain Component — Agentic compiler and high-rigor execution. -/// -/// Monitor lexing, parsing, analysis, and execution in real-time. -/// Controls the Groove daemon lifecycle and permissions. - -open Model -open Msg -open Tea.Html - -let renderCategoryTab = (active: oo7Category, cat: oo7Category, label: string): Tea_Vdom.t => { - let isActive = active === cat - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-cyan-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(Oo7Toolchain(SetCategory(cat))), - }, - list{text(label)}, - ) -} - -let renderStageButton = (stage: oo7Stage, label: string): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-gray-800 text-gray-300 rounded border border-gray-700 hover:bg-gray-700", - ), - Events.onClick(Oo7Toolchain(RunStage(stage))), - }, - list{text(label)}, - ) -} - -let view = (state: oo7State): Tea_Vdom.t => { - div( - list{Attrs.class_("fixed inset-0 bg-slate-950/95 z-40 flex flex-col")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-slate-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-slate-200")}, - list{text("007 Toolchain")}, - ), - span( - list{Attrs.class_("text-xs text-slate-500")}, - list{text("agentic compiler & high-rigor execution")}, - ), - div( - list{ - Attrs.class_( - `px-2 py-0.5 rounded-full text-[10px] ${state.isConnected - ? "bg-green-900/40 text-green-400" - : "bg-red-900/40 text-red-400"}`, - ), - }, - list{text(state.isConnected ? "Groove Active" : "Groove Offline")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-slate-800 text-slate-300 rounded hover:bg-slate-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("px-6 pt-4 border-b border-slate-800 flex gap-1")}, - list{ - renderCategoryTab(state.activeCategory, Oo7Dashboard, "Dashboard"), - renderCategoryTab(state.activeCategory, Oo7ControlPlane, "Control Plane"), - renderCategoryTab(state.activeCategory, Oo7Permissions, "Permissions"), - renderCategoryTab(state.activeCategory, Oo7Monitoring, "Monitoring"), - }, - ), - // Main Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - switch state.activeCategory { - | Oo7Dashboard => - div( - list{Attrs.class_("grid grid-cols-2 gap-6 h-full")}, - list{ - // Left: Editor - div( - list{Attrs.class_("flex flex-col gap-3")}, - list{ - div( - list{ - Attrs.class_( - "text-sm font-semibold text-slate-400 uppercase tracking-wider", - ), - }, - list{text("Source (.007)")}, - ), - textarea( - list{ - Attrs.class_( - "flex-1 bg-slate-900 border border-slate-800 rounded p-4 font-mono text-sm text-slate-300 focus:outline-none focus:border-cyan-500/50", - ), - Attrs.value(state.sourceCode), - Events.onInput(code => Oo7Toolchain(UpdateSource(code))), - }, - list{}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - renderStageButton(Oo7Lexer, "Lex"), - renderStageButton(Oo7Parser, "Parse"), - renderStageButton(Oo7Analyser, "Analyse"), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-cyan-600 text-white rounded hover:bg-cyan-500", - ), - Events.onClick(Oo7Toolchain(RunStage(Oo7Evaluator))), - }, - list{text("Evaluate")}, - ), - renderStageButton(Oo7Linker, "Link"), - }, - ), - }, - ), - // Right: Output & Nesy - div( - list{Attrs.class_("flex flex-col gap-6")}, - list{ - div( - list{Attrs.class_("flex-1 flex flex-col gap-3")}, - list{ - div( - list{ - Attrs.class_( - "text-sm font-semibold text-slate-400 uppercase tracking-wider", - ), - }, - list{text("Toolchain Output")}, - ), - div( - list{ - Attrs.class_( - "flex-1 bg-black/50 border border-slate-800 rounded p-4 font-mono text-xs text-cyan-400 overflow-auto", - ), - }, - state.stageOutputs - ->Array.map(((stage, out)) => - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{ - Attrs.class_( - "text-slate-500 mb-1 border-b border-slate-900 pb-1", - ), - }, - list{ - text( - switch stage { - | Oo7Lexer => "LEXER" - | Oo7Parser => "PARSER" - | Oo7Analyser => "ANALYSER" - | Oo7Evaluator => "EVALUATOR" - | Oo7Linker => "LINKER" - }, - ), - }, - ), - text(out), - }, - ) - ) - ->List.fromArray, - ), - }, - ), - div( - list{Attrs.class_("bg-slate-900/50 border border-slate-800 rounded p-4")}, - list{ - div( - list{Attrs.class_("text-xs font-semibold text-slate-500 uppercase mb-2")}, - list{text("Neurosymbolic Status")}, - ), - div( - list{Attrs.class_("text-sm text-slate-300")}, - list{text(state.nesyStatus)}, - ), - div( - list{Attrs.class_("mt-3 flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "text-[10px] px-2 py-0.5 bg-slate-800 text-slate-400 rounded hover:text-slate-200", - ), - }, - list{text("Query TypeLL")}, - ), - button( - list{ - Attrs.class_( - "text-[10px] px-2 py-0.5 bg-slate-800 text-slate-400 rounded hover:text-slate-200", - ), - }, - list{text("Echidna Proof")}, - ), - }, - ), - }, - ), - }, - ), - }, - ) - | Oo7ControlPlane => - div( - list{Attrs.class_("max-w-2xl space-y-8")}, - list{ - div( - list{}, - list{ - h3( - list{Attrs.class_("text-slate-200 font-medium mb-2")}, - list{text("Groove Daemon Control")}, - ), - p( - list{Attrs.class_("text-sm text-slate-500 mb-4")}, - list{ - text( - "Manage the lifecycle of the 007 control plane daemon. The daemon must be active to perform toolchain operations.", - ), - }, - ), - div( - list{Attrs.class_("flex gap-4")}, - list{ - if !state.isConnected { - button( - list{ - Attrs.class_( - "px-4 py-2 bg-green-600 text-white rounded hover:bg-green-500", - ), - Events.onClick(Oo7Toolchain(ConnectDaemon)), - KeyboardNav.onActivate(Oo7Toolchain(ConnectDaemon)), - }, - list{text("Start Daemon")}, - ) - } else { - button( - list{ - Attrs.class_( - "px-4 py-2 bg-red-600 text-white rounded hover:bg-red-500", - ), - Events.onClick(Oo7Toolchain(DisconnectDaemon)), - KeyboardNav.onActivate(Oo7Toolchain(DisconnectDaemon)), - }, - list{text("Stop Daemon")}, - ) - }, - button( - list{ - Attrs.class_( - "px-4 py-2 bg-slate-800 text-slate-300 rounded hover:bg-slate-700", - ), - }, - list{text("Restart")}, - ), - }, - ), - }, - ), - div( - list{}, - list{ - h3( - list{Attrs.class_("text-slate-200 font-medium mb-2")}, - list{text("Port Registry")}, - ), - div( - list{ - Attrs.class_( - "bg-slate-900 border border-slate-800 rounded p-4 font-mono text-sm", - ), - }, - list{ - div( - list{Attrs.class_("flex justify-between py-1")}, - list{ - span(list{}, list{text("007 Control Plane")}), - span(list{Attrs.class_("text-cyan-400")}, list{text(":7007")}), - }, - ), - div( - list{Attrs.class_("flex justify-between py-1")}, - list{ - span(list{}, list{text("PanLL Groove")}), - span(list{Attrs.class_("text-cyan-400")}, list{text(":8000")}), - }, - ), - div( - list{Attrs.class_("flex justify-between py-1")}, - list{ - span(list{}, list{text("Burble Gateway")}), - span(list{Attrs.class_("text-cyan-400")}, list{text(":4020")}), - }, - ), - }, - ), - }, - ), - }, - ) - | Oo7Permissions => - div( - list{Attrs.class_("max-w-xl space-y-6")}, - list{ - h3( - list{Attrs.class_("text-slate-200 font-medium")}, - list{text("Daemon Permissions")}, - ), - div( - list{Attrs.class_("space-y-3")}, - [ - ( - PermissionReadOnly, - "Read Only", - "Can view toolchain status and results but cannot trigger stages.", - ), - ( - PermissionExecute, - "Execute", - "Can trigger lexer, parser, and analysis stages.", - ), - ( - PermissionAdministrative, - "Administrative", - "Full control over daemon lifecycle and memory allocators.", - ), - ] - ->Array.map(((p, label, desc)) => - div( - list{ - Attrs.class_( - `p-4 border rounded-lg cursor-pointer transition-colors ${state.permissions === - p - ? "bg-cyan-900/20 border-cyan-500/50" - : "bg-slate-900 border-slate-800 hover:border-slate-700"}`, - ), - Events.onClick(Oo7Toolchain(SetPermissions(p))), - }, - list{ - div(list{Attrs.class_("font-medium text-slate-200")}, list{text(label)}), - div(list{Attrs.class_("text-xs text-slate-500 mt-1")}, list{text(desc)}), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | Oo7Monitoring => - div( - list{Attrs.class_("space-y-6")}, - list{ - div( - list{Attrs.class_("grid grid-cols-3 gap-4")}, - list{ - div( - list{Attrs.class_("bg-slate-900 p-4 rounded-lg border border-slate-800")}, - list{ - div( - list{Attrs.class_("text-xs text-slate-500 uppercase mb-1")}, - list{text("Memory Usage")}, - ), - div( - list{Attrs.class_("text-xl text-cyan-400 font-mono")}, - list{text("42.5 MB")}, - ), - }, - ), - div( - list{Attrs.class_("bg-slate-900 p-4 rounded-lg border border-slate-800")}, - list{ - div( - list{Attrs.class_("text-xs text-slate-500 uppercase mb-1")}, - list{text("JIT Latency")}, - ), - div( - list{Attrs.class_("text-xl text-green-400 font-mono")}, - list{text("0.8 ms")}, - ), - }, - ), - div( - list{Attrs.class_("bg-slate-900 p-4 rounded-lg border border-slate-800")}, - list{ - div( - list{Attrs.class_("text-xs text-slate-500 uppercase mb-1")}, - list{text("Active Sessions")}, - ), - div( - list{Attrs.class_("text-xl text-slate-200 font-mono")}, - list{text("1")}, - ), - }, - ), - }, - ), - div( - list{ - Attrs.class_( - "bg-black/30 border border-slate-800 rounded p-4 font-mono text-[10px] text-slate-500 h-64 overflow-auto", - ), - }, - list{ - text("[groove] Initializing Level 2 handshake...\n"), - text("[oo7] Backend Zig evaluator loaded (High-Rigor mode)\n"), - text("[oo7] V-lang control plane listening on :7007\n"), - text("[nesy] TypeLL service attached to toolchain analyzer\n"), - text("[groove] Handshake complete. Session oo7-session-1 active.\n"), - }, - ), - }, - ) - }, - if state.loading { - div( - list{ - Attrs.class_( - "fixed bottom-10 right-10 bg-cyan-600 text-white px-4 py-2 rounded-full shadow-lg animate-pulse text-sm", - ), - }, - list{text("Communicating with Daemon...")}, - ) - } else { - noNode - }, - }, - ), - // Footer / Error - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "bg-red-900/50 border-t border-red-700 p-3 text-red-200 text-sm flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-200 hover:text-white"), - Events.onClick(Oo7Toolchain(ClearError)), - KeyboardNav.onActivate(Oo7Toolchain(ClearError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => noNode - }, - }, - ) -} diff --git a/src/components/PaneA.affine b/src/components/PaneA.affine new file mode 100644 index 00000000..17a1a8e7 --- /dev/null +++ b/src/components/PaneA.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PaneA; + +// TODO: Complete semantic implementation diff --git a/src/components/PaneA.res b/src/components/PaneA.res deleted file mode 100644 index fbe71b81..00000000 --- a/src/components/PaneA.res +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Pane-A: Ambient Substrate Component (Cognitive Ergonomics) -/// -/// This component implements the deep-reaching ambient interface layer -/// designed for cognitive relief. It monitors operator load (Vexometer) -/// and provides sensory feedback via Information Humidity and the Vexation Index. - -open Model -open Msg -open Tea.Html - -/// Render the Information Humidity indicator. -/// Higher humidity = more revealed detail, lower = simplified view. -let renderHumidityIndicator = (humidity: humidityLevel): Tea_Vdom.t => { - let (label, colour, description) = switch humidity { - | High => ("High Humidity", "text-blue-400", "Maximum detail revealed") - | Medium => ("Balanced", "text-sky-400", "Standard information density") - | Low => ("Low Humidity", "text-amber-400", "Environment simplified (Anti-Inflammatory)") - } - - div( - list{Attrs.class_("mb-4 p-3 rounded bg-gray-800/40 border border-gray-700/50")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 font-medium")}, list{text("INFORMATION HUMIDITY")}), - div(list{Attrs.class_(`text-xs font-bold ${colour}`)}, list{text(label)}), - }, - ), - div(list{Attrs.class_("text-[10px] text-gray-600 italic")}, list{text(description)}), - }, - ) -} - -/// Render ergonomic metrics (cancellations and corrections). -let renderErgonomicMetrics = (state: paneAState): Tea_Vdom.t => { - div( - list{Attrs.class_("grid grid-cols-2 gap-3 mb-4")}, - list{ - div( - list{Attrs.class_("p-2 bg-gray-800/30 rounded border border-gray-800")}, - list{ - div(list{Attrs.class_("text-[10px] text-gray-500 uppercase")}, list{text("Cancellations")}), - div( - list{Attrs.class_("text-lg font-mono text-amber-300")}, - list{text(Int.toString(state.recentCancellations))}, - ), - }, - ), - div( - list{Attrs.class_("p-2 bg-gray-800/30 rounded border border-gray-800")}, - list{ - div(list{Attrs.class_("text-[10px] text-gray-500 uppercase")}, list{text("Corrections")}), - div( - list{Attrs.class_("text-lg font-mono text-amber-300")}, - list{text(Int.toString(state.recentCorrections))}, - ), - }, - ), - }, - ) -} - -/// The main Pane-A view. -/// This sitting alongside L, N, and W to provide the ergonomic control layer. -let view = (state: paneAState): Tea_Vdom.t => { - let vexPercent = Int.toString(Int.fromFloat(state.vexationIndex *. 100.0)) - let vexColour = if state.vexationIndex > 0.7 { - "text-red-400" - } else if state.vexationIndex > 0.4 { - "text-amber-400" - } else { - "text-emerald-400" - } - - div( - list{ - Attrs.class_("h-full flex flex-col p-4 bg-gray-950 border-r border-gray-900"), - Attrs.role("region"), - Attrs.ariaLabel("Ambient Substrate Panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-6")}, - list{ - div(list{Attrs.class_("text-gray-400 font-semibold flex items-center gap-2")}, list{ - span(list{Attrs.class_("w-2 h-2 rounded-full bg-blue-500 animate-pulse")}, list{}), - text("Ambient Substrate") - }), - button( - list{ - Attrs.class_("text-[10px] text-gray-600 hover:text-gray-400 font-mono"), - Events.onClick(PaneA(ToggleExpansion)), - }, - list{text(state.expanded ? "COLLAPSE" : "EXPAND")} - ) - }, - ), - - // Vexation Summary - div( - list{Attrs.class_("mb-6")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("VEXATION INDEX")}), - div( - list{Attrs.class_(`text-4xl font-light tracking-tighter ${vexColour}`)}, - list{text(vexPercent ++ "%")}, - ), - if state.antiInflammatoryActive { - div( - list{Attrs.class_("mt-2 text-[10px] bg-indigo-900/40 text-indigo-300 px-2 py-1 rounded border border-indigo-800/50 inline-block")}, - list{text("ANTI-INFLAMMATORY ACTIVE")}, - ) - } else { - noNode - }, - }, - ), - - // Humidity Control - renderHumidityIndicator(state.humidity), - - // Detailed metrics (only shown when expanded) - if state.expanded { - div( - list{Attrs.class_("mt-2 animate-in fade-in slide-in-from-top-2 duration-300")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2 font-medium")}, list{text("FRICTION ANALYSIS")}), - renderErgonomicMetrics(state), - div( - list{Attrs.class_("text-[10px] text-gray-500 leading-relaxed")}, - list{text("Panel-A implements the Cognitive Relief Layer (CRL). It dynamically rescales the information density of Panels L, N, and W to prevent operator stasis.")}, - ) - } - ) - } else { - noNode - } - }, - ) -} diff --git a/src/components/PaneL.affine b/src/components/PaneL.affine new file mode 100644 index 00000000..9900ee38 --- /dev/null +++ b/src/components/PaneL.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PaneL; + +// TODO: Complete semantic implementation diff --git a/src/components/PaneL.res b/src/components/PaneL.res deleted file mode 100644 index 0aceed0b..00000000 --- a/src/components/PaneL.res +++ /dev/null @@ -1,366 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Pane-L: Symbolic Mass Component -/// -/// The constraint/logic editor - "The Law" that governs neural inference. -/// Implements the Tractatus view for symbolic constraints. - -open Model -open Msg -open Tea.Html - -/// Render a single constraint item -let renderConstraint = (c: symbolicConstraint): Tea_Vdom.t => { - let activeClass = c.active ? "border-indigo-500" : "border-gray-700" - let pinnedIcon = c.pinned ? " [pinned]" : "" - - div( - list{ - Attrs.class_(`p-2 mb-2 border ${activeClass} rounded bg-gray-800/50`), - Attrs.role("listitem"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("font-mono text-sm text-indigo-300")}, - list{text(c.expression ++ pinnedIcon)}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-indigo-400"), - Events.onClick(PaneL(ToggleConstraint(c.id))), - Attrs.ariaPressed(c.active), - Attrs.ariaLabel( - c.active ? "Disable constraint " ++ c.id : "Enable constraint " ++ c.id, - ), - }, - list{text(c.active ? "disable" : "enable")}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-amber-400"), - Events.onClick(PaneL(PinConstraint(c.id))), - Attrs.ariaPressed(c.pinned), - Attrs.ariaLabel( - c.pinned ? "Unpin constraint " ++ c.id : "Pin constraint " ++ c.id, - ), - }, - list{text(c.pinned ? "unpin" : "pin")}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-red-400"), - Events.onClick(PaneL(RemoveConstraint(c.id))), - Attrs.ariaLabel("Remove constraint " ++ c.id), - }, - list{text("x")}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render the constraint list with active count and add button -let renderConstraintList = (constraints: array): Tea_Vdom.t => { - let activeCount = constraints->Array.filter(c => c.active)->Array.length - let totalCount = Array.length(constraints) - - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("ACTIVE CONSTRAINTS")}), - if totalCount > 0 { - span( - list{ - Attrs.class_( - "text-xs px-1.5 py-0.5 rounded bg-indigo-900/40 text-indigo-400 font-mono", - ), - }, - list{text(Int.toString(activeCount) ++ "/" ++ Int.toString(totalCount))}, - ) - } else { - noNode - }, - }, - ), - button( - list{ - Attrs.class_( - "text-xs px-2 py-0.5 bg-indigo-900/30 hover:bg-indigo-800/40 text-indigo-400 rounded border border-indigo-800/50", - ), - Events.onClick( - PaneL( - AddConstraint({ - id: "user-" ++ Int.toString(totalCount + 1), - expression: "// New constraint", - active: true, - pinned: false, - }), - ), - ), - Attrs.ariaLabel("Add new constraint"), - }, - list{text("+ Add")}, - ), - }, - ), - if totalCount === 0 { - div( - list{Attrs.class_("text-gray-600 text-sm italic")}, - list{text("No constraints defined")}, - ) - } else { - div(list{Attrs.role("list")}, constraints->Array.map(renderConstraint)->List.fromArray) - }, - }, - ) -} - -// =========================================================================== -// TypeLL Inferred Type Display -// =========================================================================== - -/// Render the TypeLL-inferred type for the current editor expression. -/// Compact display showing type signature in monospace. -let viewInferredType = (lastInferredType: option): Tea_Vdom.t => { - switch lastInferredType { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - div( - list{Attrs.class_("mt-2 p-2 rounded border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL")}, - ), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono")}, - list{text(result.typeSignature)}, - ), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -/// Render the constraint editor -let renderEditor = (content: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("TRACTATUS EDITOR")}), - textarea( - list{ - Attrs.class_( - "w-full h-64 bg-gray-800 border border-gray-700 rounded p-3 font-mono text-sm text-indigo-200 resize-none focus:border-indigo-500 focus:outline-none", - ), - Attrs.placeholder( - "// Define symbolic constraints...\n// e.g., type User = { name: string, age: int }", - ), - Attrs.value(content), - Events.onInput(value => PaneL(UpdateEditorContent(value))), - Attrs.ariaLabel("Tractatus Editor"), - }, - list{}, - ), - }, - ) -} - -/// Render proof obligations from a VCL-total query result certificate. -/// Displays proof type, contract, verification status, and hash for each -/// obligation the type checker inferred during query execution. -let renderProofObligations = (proofs: array): Tea_Vdom.t => { - if Array.length(proofs) === 0 { - text("") - } else { - let rows = - proofs - ->Array.map(p => { - let statusColour = switch p.status { - | "verified" => "text-emerald-300" - | "failed" => "text-red-400" - | _ => "text-amber-300" - } - - let hashTruncated = - String.length(p.proofHash) > 16 - ? String.slice(p.proofHash, ~start=0, ~end=16) ++ "..." - : p.proofHash - - div( - list{ - Attrs.class_( - "flex items-center justify-between text-xs border-b border-gray-800/60 py-1", - ), - }, - list{ - div( - list{Attrs.class_("text-indigo-300 font-mono")}, - list{text(String.toUpperCase(p.proofType))}, - ), - div(list{Attrs.class_("text-gray-400")}, list{text(p.contractName)}), - div(list{Attrs.class_(statusColour ++ " font-semibold")}, list{text(p.status)}), - div( - list{Attrs.class_("text-gray-600 font-mono text-[10px]")}, - list{text(hashTruncated)}, - ), - }, - ) - }) - ->List.fromArray - - div( - list{Attrs.class_("mt-4 p-3 border border-indigo-900/30 rounded bg-indigo-900/20 space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-indigo-400 tracking-widest uppercase")}, - list{text("PROOF OBLIGATIONS (VCL-total)")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text(Int.toString(Array.length(proofs)) ++ " proof(s) from last VCL-total query")}, - ), - div(list{Attrs.class_("space-y-0.5")}, rows), - }, - ) - } -} - -/// Render the symbolic mass density bar — shows how much constraint -/// content is feeding into the barycentre calculation. -let renderMassDensityBar = ( - constraints: array, - editorContent: string, -): Tea_Vdom.t => { - let activeConstraints = constraints->Array.filter(c => c.active)->Array.length - let tokenCount = - String.split(editorContent, " ") - ->Array.filter(t => String.length(String.trim(t)) > 0) - ->Array.length - let mass = Math.min(1.0, Int.toFloat(tokenCount) /. 500.0) - let massPercent = Int.toString(Int.fromFloat(mass *. 100.0)) - let barWidth = massPercent ++ "%" - let barColour = if mass > 0.5 { - "bg-indigo-500" - } else if mass > 0.2 { - "bg-indigo-600" - } else { - "bg-indigo-800" - } - - div( - list{Attrs.class_("mb-4 p-2 rounded bg-gray-800/50 border border-gray-700/50")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("MASS DENSITY")}), - div( - list{Attrs.class_("text-xs font-mono text-indigo-400")}, - list{ - text( - massPercent ++ - "% (" ++ - Int.toString(tokenCount) ++ - " tokens, " ++ - Int.toString(activeConstraints) ++ " active)", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("w-full h-1.5 bg-gray-700 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_(`h-full ${barColour} rounded-full transition-all`), - Attrs.style("width", barWidth), - }, - list{}, - ), - }, - ), - }, - ) -} - -/// Main Pane-L view -let view = (state: paneLState, proofs: array): Tea_Vdom.t => { - div( - list{ - Attrs.class_("h-full flex flex-col p-4 bg-gray-900"), - Attrs.role("region"), - Attrs.ariaLabel("Symbolic Mass Panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - div(list{Attrs.class_("text-indigo-400 font-semibold")}, list{text("Symbolic Mass")}), - div(list{Attrs.class_("text-xs text-gray-600")}, list{text("Ctrl+Shift+L")}), - }, - ), - // Mass density indicator - renderMassDensityBar(state.constraints, state.editorContent), - // Constraint list - renderConstraintList(state.constraints), - // Proof obligations from VCL-total queries - renderProofObligations(proofs), - // Editor - renderEditor(state.editorContent), - // TypeLL inferred type for editor expression - viewInferredType(state.lastInferredType), - }, - ) -} diff --git a/src/components/PaneN.affine b/src/components/PaneN.affine new file mode 100644 index 00000000..b306e0c0 --- /dev/null +++ b/src/components/PaneN.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PaneN; + +// TODO: Complete semantic implementation diff --git a/src/components/PaneN.res b/src/components/PaneN.res deleted file mode 100644 index 2f841a1a..00000000 --- a/src/components/PaneN.res +++ /dev/null @@ -1,2024 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Pane-N: Neural Stream Component -/// -/// The inference manifold showing the Agent's internal monologue, -/// OODA loop visibility, and Thing-Agency monitor. - -open Model -open Msg -open Tea.Html - -/// Render the OODA phase indicator -let renderOodaPhase = (phase: oodaPhase): Tea_Vdom.t => { - let phases = [ - (Observe, "O", "Observe"), - (Orient, "O", "Orient"), - (Decide, "D", "Decide"), - (Act, "A", "Act"), - ] - - div( - list{Attrs.class_("flex gap-1 mb-4")}, - phases - ->Array.map(((p, letter, label)) => { - let isActive = p === phase - let bgClass = isActive ? "bg-emerald-600" : "bg-gray-700" - let textClass = isActive ? "text-white" : "text-gray-500" - - div( - list{ - Attrs.class_( - `${bgClass} ${textClass} w-8 h-8 rounded flex items-center justify-center text-xs font-bold`, - ), - Attrs.title(label), - Attrs.ariaCurrent(isActive ? "step" : "false"), - }, - list{text(letter)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the Thing-Agency monitor -let renderAgencyMonitor = (agency: agencyState): Tea_Vdom.t => { - let autonomyPercent = Int.toString(Int.fromFloat(agency.autonomyLevel *. 100.0)) - let barWidth = autonomyPercent ++ "%" - - div( - list{Attrs.class_("mb-4 p-3 bg-gray-800/50 rounded")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("THING-AGENCY MONITOR")}), - renderOodaPhase(agency.phase), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 w-20")}, list{text("Autonomy:")}), - div( - list{ - Attrs.class_("flex-1 h-2 bg-gray-700 rounded overflow-hidden"), - Attrs.role("progressbar"), - Attrs.ariaLabel("Autonomy Level"), - Attrs.ariaValueNow(agency.autonomyLevel *. 100.0), - Attrs.ariaValueMin(0.0), - Attrs.ariaValueMax(100.0), - }, - list{ - div( - list{ - Attrs.class_("h-full bg-emerald-500 transition-all duration-300"), - Attrs.style("width", barWidth), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-emerald-400 w-12 text-right")}, - list{text(autonomyPercent ++ "%")}, - ), - }, - ), - }, - ) -} - -/// Source badge colour and label for token provenance display. -let sourceLabel = (source: tokenSource): (string, string) => - switch source { - | NeuralInference => ("N", "bg-emerald-800 text-emerald-300") - | EchidnaProver => ("E", "bg-indigo-800 text-indigo-300") - | TypeLLKernel => ("T", "bg-violet-800 text-violet-300") - | VeriSimInference => ("V", "bg-cyan-800 text-cyan-300") - | AntiCrashGate => ("!", "bg-red-800 text-red-300") - | OperatorInput => ("H", "bg-amber-800 text-amber-300") - | OrbitalSync => ("S", "bg-blue-800 text-blue-300") - } - -/// Category icon for semantic reasoning step display. -let categoryIcon = (cat: tokenCategory): string => - switch cat { - | Observation => "?" - | Hypothesis => "~" - | Deduction => ">" - | Abduction => "<" - | ProofStep => "#" - | Violation => "X" - | Correction => "^" - | Synthesis => "*" - } - -/// OODA phase short label for inline display. -let phaseLabel = (phase: oodaPhase): string => - switch phase { - | Observe => "OBS" - | Orient => "ORI" - | Decide => "DEC" - | Act => "ACT" - } - -/// Render a neural token with full provenance, category, and causal metadata. -let renderToken = (token: neuralToken): Tea_Vdom.t => { - let validatedClass = token.validated ? "border-emerald-700" : "border-amber-700" - let confidencePercent = Int.toString(Int.fromFloat(token.confidence *. 100.0)) - let (srcLetter, srcColour) = sourceLabel(token.source) - let catIcon = categoryIcon(token.category) - let phase = phaseLabel(token.emittedDuring) - let hasCauses = Array.length(token.causedBy) > 0 - let hasProof = token.proofHash !== None - - div( - list{ - Attrs.class_(`p-2 mb-1 border-l-2 ${validatedClass} bg-gray-800/30`), - Attrs.ariaLabel(token.content), - }, - list{ - // Top row: source badge + content - div( - list{Attrs.class_("flex items-start gap-2")}, - list{ - // Source badge (single letter, colour-coded) - span( - list{ - Attrs.class_( - `w-5 h-5 rounded flex items-center justify-center text-[10px] font-bold shrink-0 ${srcColour}`, - ), - Attrs.title( - switch token.source { - | NeuralInference => "Neural Inference" - | EchidnaProver => "ECHIDNA Prover" - | TypeLLKernel => "TypeLL Kernel" - | VeriSimInference => "VeriSimDB Inference" - | AntiCrashGate => "Anti-Crash Gate" - | OperatorInput => "Operator Input" - | OrbitalSync => "OrbitalSync" - }, - ), - }, - list{text(srcLetter)}, - ), - // Token content - div(list{Attrs.class_("text-sm text-gray-300 flex-1")}, list{text(token.content)}), - }, - ), - // Bottom row: metadata chips - div( - list{Attrs.class_("flex items-center gap-2 mt-1 pl-7")}, - list{ - // Confidence - span( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text(`${confidencePercent}%`)}, - ), - // Category icon - span( - list{ - Attrs.class_("text-[10px] text-gray-600 font-mono"), - Attrs.title( - switch token.category { - | Observation => "Observation" - | Hypothesis => "Hypothesis" - | Deduction => "Deduction" - | Abduction => "Abduction" - | ProofStep => "Proof Step" - | Violation => "Violation" - | Correction => "Correction" - | Synthesis => "Synthesis" - }, - ), - }, - list{text(`[${catIcon}]`)}, - ), - // OODA phase - span(list{Attrs.class_("text-[10px] text-gray-600")}, list{text(phase)}), - // Causal chain indicator - if hasCauses { - span( - list{ - Attrs.class_("text-[10px] text-gray-600"), - Attrs.title("Caused by: " ++ Array.join(token.causedBy, ", ")), - }, - list{text(`<${Int.toString(Array.length(token.causedBy))}`)}, - ) - } else { - noNode - }, - // Proof hash indicator - if hasProof { - span( - list{ - Attrs.class_("text-[10px] text-emerald-600 font-mono"), - Attrs.title( - switch token.proofHash { - | Some(h) => h - | None => "" - }, - ), - }, - list{text("#")}, - ) - } else { - noNode - }, - }, - ), - }, - ) -} - -/// OODA phase colour for timeline segments. -let phaseColour = (phase: oodaPhase): string => - switch phase { - | Observe => "bg-cyan-600" - | Orient => "bg-amber-600" - | Decide => "bg-violet-600" - | Act => "bg-emerald-600" - } - -/// Render the OODA phase timeline — a horizontal bar showing the sequence of -/// phases across the token stream, with segment widths proportional to token counts. -let renderOodaTimeline = (tokens: array): Tea_Vdom.t => { - let total = Float.fromInt(Array.length(tokens)) - if total === 0.0 { - noNode - } else { - // Group consecutive tokens by phase into segments - let segments: array<(oodaPhase, int)> = [] - Array.forEach(tokens, token => { - let len = Array.length(segments) - if len > 0 { - let (lastPhase, lastCount) = segments->Array.getUnsafe(len - 1) - if lastPhase === token.emittedDuring { - ignore( - segments->Array.splice(~start=len - 1, ~remove=1, ~insert=[(lastPhase, lastCount + 1)]), - ) - } else { - ignore(Array.push(segments, (token.emittedDuring, 1))) - } - } else { - ignore(Array.push(segments, (token.emittedDuring, 1))) - } - }) - - div( - list{Attrs.class_("mb-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div( - list{Attrs.class_("text-[10px] text-gray-500 uppercase tracking-wider")}, - list{text("OODA Timeline")}, - ), - div( - list{Attrs.class_("flex gap-2 text-[9px] text-gray-600")}, - list{ - span( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("w-2 h-2 rounded-sm bg-cyan-600")}, list{}), - text("Observe"), - }, - ), - span( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("w-2 h-2 rounded-sm bg-amber-600")}, list{}), - text("Orient"), - }, - ), - span( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("w-2 h-2 rounded-sm bg-violet-600")}, list{}), - text("Decide"), - }, - ), - span( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("w-2 h-2 rounded-sm bg-emerald-600")}, list{}), - text("Act"), - }, - ), - }, - ), - }, - ), - // Timeline bar - div( - list{ - Attrs.class_("flex h-3 rounded overflow-hidden"), - Attrs.role("img"), - Attrs.ariaLabel("OODA phase distribution across inference tokens"), - }, - segments - ->Array.map(((phase, count)) => { - let widthPct = Float.toString(Float.fromInt(count) /. total *. 100.0) ++ "%" - div( - list{ - Attrs.class_(`${phaseColour(phase)} transition-all duration-300`), - Attrs.style("width", widthPct), - Attrs.title(phaseLabel(phase) ++ ": " ++ Int.toString(count) ++ " tokens"), - }, - list{}, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render source distribution — small horizontal bar showing which subsystems -/// contributed tokens, colour-coded by source. -let renderSourceDistribution = (tokens: array): Tea_Vdom.t => { - let total = Float.fromInt(Array.length(tokens)) - if total === 0.0 { - noNode - } else { - // Count by source - let counts: array<(tokenSource, string, string, int)> = [ - (NeuralInference, "N", "bg-emerald-700", 0), - (EchidnaProver, "E", "bg-indigo-700", 0), - (TypeLLKernel, "T", "bg-violet-700", 0), - (VeriSimInference, "V", "bg-cyan-700", 0), - (AntiCrashGate, "!", "bg-red-700", 0), - (OperatorInput, "H", "bg-amber-700", 0), - (OrbitalSync, "S", "bg-blue-700", 0), - ] - Array.forEach(tokens, token => { - let idx = counts->Array.findIndex(((src, _, _, _)) => src === token.source) - if idx >= 0 { - let (src, lbl, col, n) = counts->Array.getUnsafe(idx) - ignore(counts->Array.splice(~start=idx, ~remove=1, ~insert=[(src, lbl, col, n + 1)])) - } - }) - let active = counts->Array.filter(((_, _, _, n)) => n > 0) - - div( - list{Attrs.class_("mb-3")}, - list{ - div( - list{Attrs.class_("text-[10px] text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Source Distribution")}, - ), - div( - list{Attrs.class_("flex h-2 rounded overflow-hidden mb-1")}, - active - ->Array.map(((_, _, col, n)) => { - let widthPct = Float.toString(Float.fromInt(n) /. total *. 100.0) ++ "%" - div( - list{ - Attrs.class_(`${col} transition-all duration-300`), - Attrs.style("width", widthPct), - }, - list{}, - ) - }) - ->List.fromArray, - ), - // Legend with counts - div( - list{Attrs.class_("flex gap-2 flex-wrap")}, - active - ->Array.map(((_, lbl, col, n)) => { - span( - list{Attrs.class_("flex items-center gap-1 text-[9px] text-gray-500")}, - list{ - span(list{Attrs.class_(`w-2 h-2 rounded-sm ${col}`)}, list{}), - text(lbl ++ ":" ++ Int.toString(n)), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render the causal inference graph — a compact ASCII-style DAG showing how -/// tokens are causally linked. Each token shows its ID and arrows to parents. -let renderCausalGraph = (tokens: array): Tea_Vdom.t => { - // Only show tokens that have causal links (either cause or are caused by) - let linked = - tokens->Array.filter(t => - Array.length(t.causedBy) > 0 || - tokens->Array.some(other => other.causedBy->Array.some(id => id === t.id)) - ) - if Array.length(linked) === 0 { - noNode - } else { - div( - list{Attrs.class_("mb-3")}, - list{ - div( - list{Attrs.class_("text-[10px] text-gray-500 uppercase tracking-wider mb-1")}, - list{text("Inference Chain")}, - ), - div( - list{ - Attrs.class_("max-h-24 overflow-y-auto bg-gray-900/50 rounded p-2"), - Attrs.role("img"), - Attrs.ariaLabel("Causal inference graph"), - }, - linked - ->Array.map(token => { - let (_, srcColour) = sourceLabel(token.source) - let arrow = if Array.length(token.causedBy) > 0 { - Array.join(token.causedBy, ",") ++ " -> " - } else { - "" - } - let proofMark = switch token.proofHash { - | Some(_) => " #" - | None => "" - } - div( - list{Attrs.class_("flex items-center gap-1 py-0.5 font-mono text-[10px]")}, - list{ - // Causal arrow - if arrow !== "" { - span(list{Attrs.class_("text-gray-600")}, list{text(arrow)}) - } else { - span(list{Attrs.class_("text-gray-700")}, list{text(" root -> ")}) - }, - // Token ID badge - span( - list{Attrs.class_(`px-1 py-0.5 rounded text-[9px] font-bold ${srcColour}`)}, - list{text(token.id)}, - ), - // Truncated content - span( - list{Attrs.class_("text-gray-500 truncate flex-1")}, - list{text(String.slice(token.content, ~start=0, ~end=40))}, - ), - // Proof indicator - if proofMark !== "" { - span(list{Attrs.class_("text-emerald-500 font-bold")}, list{text("#")}) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Apply filter state to a token array — returns only tokens matching all active filters. -let applyFilters = (tokens: array, filters: tokenFilters): array => { - tokens->Array.filter(token => { - let passSource = - Array.length(filters.sources) === 0 || filters.sources->Array.some(s => s === token.source) - let passCategory = - Array.length(filters.categories) === 0 || - filters.categories->Array.some(c => c === token.category) - let passPhase = - Array.length(filters.phases) === 0 || - filters.phases->Array.some(p => p === token.emittedDuring) - let passConfidence = token.confidence >= filters.confidenceThreshold - let passValidated = !filters.validatedOnly || token.validated - let passProof = !filters.proofOnly || token.proofHash !== None - passSource && passCategory && passPhase && passConfidence && passValidated && passProof - }) -} - -/// Source filter chip — name and colour for each source type. -let sourceChipInfo = (source: tokenSource): (string, string, string) => - switch source { - | NeuralInference => ("Neural", "bg-emerald-800 text-emerald-300", "bg-emerald-600 text-white") - | EchidnaProver => ("ECHIDNA", "bg-indigo-800 text-indigo-300", "bg-indigo-600 text-white") - | TypeLLKernel => ("TypeLL", "bg-violet-800 text-violet-300", "bg-violet-600 text-white") - | VeriSimInference => ("VeriSim", "bg-cyan-800 text-cyan-300", "bg-cyan-600 text-white") - | AntiCrashGate => ("AntiCrash", "bg-red-800 text-red-300", "bg-red-600 text-white") - | OperatorInput => ("Operator", "bg-amber-800 text-amber-300", "bg-amber-600 text-white") - | OrbitalSync => ("Orbital", "bg-blue-800 text-blue-300", "bg-blue-600 text-white") - } - -/// Category filter chip label. -let categoryChipLabel = (cat: tokenCategory): string => - switch cat { - | Observation => "Obs" - | Hypothesis => "Hyp" - | Deduction => "Ded" - | Abduction => "Abd" - | ProofStep => "Proof" - | Violation => "Viol" - | Correction => "Corr" - | Synthesis => "Synth" - } - -/// Render the interactive filter bar — source chips, category chips, phase chips, -/// confidence slider, validated/proof toggles. -let renderFilterBar = (filters: tokenFilters, totalCount: int, filteredCount: int): Tea_Vdom.t< - msg, -> => { - let allSources: array = [ - NeuralInference, - EchidnaProver, - TypeLLKernel, - VeriSimInference, - AntiCrashGate, - OperatorInput, - OrbitalSync, - ] - let allCategories: array = [ - Observation, - Hypothesis, - Deduction, - Abduction, - ProofStep, - Violation, - Correction, - Synthesis, - ] - let allPhases: array = [Observe, Orient, Decide, Act] - let hasActiveFilters = - Array.length(filters.sources) > 0 || - Array.length(filters.categories) > 0 || - Array.length(filters.phases) > 0 || - filters.confidenceThreshold > 0.0 || - filters.validatedOnly || - filters.proofOnly - - div( - list{Attrs.class_("mb-3 p-2 bg-gray-900/50 rounded border border-gray-800")}, - list{ - // Header with count and clear button - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("text-[10px] text-gray-500 uppercase tracking-wider")}, - list{text("Filters")}, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - if hasActiveFilters { - span( - list{Attrs.class_("text-[10px] text-amber-500")}, - list{text(Int.toString(filteredCount) ++ "/" ++ Int.toString(totalCount))}, - ) - } else { - noNode - }, - if hasActiveFilters { - button( - list{ - Attrs.class_("text-[10px] text-gray-500 hover:text-gray-300 px-1"), - Events.onClick(PaneN(ClearFilters)), - KeyboardNav.onActivate(PaneN(ClearFilters)), - }, - list{text("Clear")}, - ) - } else { - noNode - }, - }, - ), - }, - ), - // Source filter chips - div( - list{Attrs.class_("flex flex-wrap gap-1 mb-1.5")}, - allSources - ->Array.map(source => { - let (label, inactiveClass, activeClass) = sourceChipInfo(source) - let isActive = filters.sources->Array.some(s => s === source) - let chipClass = if isActive { - activeClass - } else { - inactiveClass ++ " opacity-50" - } - button( - list{ - Attrs.class_( - `px-1.5 py-0.5 rounded text-[9px] font-medium cursor-pointer transition-opacity ${chipClass}`, - ), - Events.onClick(PaneN(ToggleSourceFilter(source))), - Attrs.ariaPressed(isActive), - Attrs.ariaLabel("Filter by " ++ label), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ), - // Category filter chips - div( - list{Attrs.class_("flex flex-wrap gap-1 mb-1.5")}, - allCategories - ->Array.map(cat => { - let label = categoryChipLabel(cat) - let isActive = filters.categories->Array.some(c => c === cat) - let chipClass = if isActive { - "bg-gray-600 text-white" - } else { - "bg-gray-800 text-gray-500 opacity-50" - } - button( - list{ - Attrs.class_( - `px-1.5 py-0.5 rounded text-[9px] font-medium cursor-pointer transition-opacity ${chipClass}`, - ), - Events.onClick(PaneN(ToggleCategoryFilter(cat))), - Attrs.ariaPressed(isActive), - Attrs.ariaLabel("Filter by " ++ label), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ), - // Phase filter chips + toggles row - div( - list{Attrs.class_("flex items-center gap-2 mb-1.5")}, - list{ - // OODA phase chips - div( - list{Attrs.class_("flex gap-1")}, - allPhases - ->Array.map(phase => { - let label = phaseLabel(phase) - let isActive = filters.phases->Array.some(p => p === phase) - let colour = switch phase { - | Observe => - if isActive { - "bg-cyan-600 text-white" - } else { - "bg-cyan-900 text-cyan-600 opacity-50" - } - | Orient => - if isActive { - "bg-amber-600 text-white" - } else { - "bg-amber-900 text-amber-600 opacity-50" - } - | Decide => - if isActive { - "bg-violet-600 text-white" - } else { - "bg-violet-900 text-violet-600 opacity-50" - } - | Act => - if isActive { - "bg-emerald-600 text-white" - } else { - "bg-emerald-900 text-emerald-600 opacity-50" - } - } - button( - list{ - Attrs.class_( - `px-1.5 py-0.5 rounded text-[9px] font-bold cursor-pointer transition-opacity ${colour}`, - ), - Events.onClick(PaneN(TogglePhaseFilter(phase))), - Attrs.ariaPressed(isActive), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ), - // Validated-only toggle - button( - list{ - Attrs.class_( - "px-1.5 py-0.5 rounded text-[9px] cursor-pointer " ++ if filters.validatedOnly { - "bg-emerald-700 text-emerald-200" - } else { - "bg-gray-800 text-gray-600 opacity-50" - }, - ), - Events.onClick(PaneN(ToggleValidatedOnly)), - KeyboardNav.onActivate(PaneN(ToggleValidatedOnly)), - Attrs.ariaPressed(filters.validatedOnly), - Attrs.ariaLabel("Show validated tokens only"), - }, - list{text("Validated")}, - ), - // Proof-only toggle - button( - list{ - Attrs.class_( - "px-1.5 py-0.5 rounded text-[9px] cursor-pointer " ++ if filters.proofOnly { - "bg-emerald-700 text-emerald-200" - } else { - "bg-gray-800 text-gray-600 opacity-50" - }, - ), - Events.onClick(PaneN(ToggleProofOnly)), - KeyboardNav.onActivate(PaneN(ToggleProofOnly)), - Attrs.ariaPressed(filters.proofOnly), - Attrs.ariaLabel("Show proof-bearing tokens only"), - }, - list{text("Proof #")}, - ), - }, - ), - // Confidence threshold slider - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-[9px] text-gray-600 w-16")}, - list{ - text( - "Conf >= " ++ - Int.toString(Int.fromFloat(filters.confidenceThreshold *. 100.0)) ++ "%", - ), - }, - ), - input( - list{ - Attrs.type_("range"), - Attrs.class_("flex-1 h-1 accent-emerald-500"), - Attrs.min("0"), - Attrs.max("100"), - Attrs.step("5"), - Attrs.value(Int.toString(Int.fromFloat(filters.confidenceThreshold *. 100.0))), - Events.onInput(value => { - let v = Int.fromString(value)->Option.getOr(0) - PaneN(SetConfidenceThreshold(Float.fromInt(v) /. 100.0)) - }), - Attrs.ariaLabel("Confidence threshold"), - }, - list{}, - ), - }, - ), - }, - ) -} - -/// Render the token stream with filters, OODA timeline, source distribution, -/// causal graph, and the full token log. -let renderTokenStream = (tokens: array, filters: tokenFilters): Tea_Vdom.t => { - let filtered = applyFilters(tokens, filters) - let totalCount = Array.length(tokens) - let filteredCount = Array.length(filtered) - - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2 flex items-center justify-between")}, - list{ - text("TOKEN STREAM"), - if totalCount > 0 { - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text(Int.toString(totalCount) ++ " tokens")}, - ) - } else { - noNode - }, - }, - ), - // Filter bar (always visible when there are tokens) - if totalCount > 0 { - renderFilterBar(filters, totalCount, filteredCount) - } else { - noNode - }, - if totalCount === 0 { - div(list{Attrs.class_("text-gray-600 text-sm italic")}, list{text("No tokens received")}) - } else if filteredCount === 0 { - div( - list{Attrs.class_("text-amber-600/60 text-sm italic")}, - list{text("No tokens match current filters")}, - ) - } else { - div( - list{}, - list{ - // OODA phase timeline (shows filtered tokens) - renderOodaTimeline(filtered), - // Source distribution bar (shows filtered tokens) - renderSourceDistribution(filtered), - // Causal inference graph (shows filtered tokens) - renderCausalGraph(filtered), - // Token log - div( - list{ - Attrs.class_("max-h-40 overflow-y-auto"), - Attrs.role("log"), - Attrs.ariaLabel("Token Stream"), - }, - filtered->Array.map(renderToken)->List.fromArray, - ), - }, - ) - }, - }, - ) -} - -/// Render the monologue/inference stream -let renderMonologue = (monologue: string, inferenceActive: bool): Tea_Vdom.t => { - let statusClass = inferenceActive ? "text-emerald-400" : "text-gray-500" - let statusText = inferenceActive ? "streaming..." : "idle" - - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("INFERENCE MANIFOLD")}), - div( - list{ - Attrs.class_(`text-xs ${statusClass}`), - Attrs.role("status"), - Attrs.ariaLive("polite"), - }, - list{text(statusText)}, - ), - }, - ), - div( - list{ - Attrs.class_( - "h-48 bg-gray-800/50 rounded p-3 overflow-y-auto text-sm text-emerald-200 whitespace-pre-wrap", - ), - }, - list{text(monologue === "" ? "Awaiting neural inference..." : monologue)}, - ), - }, - ) -} - -// =========================================================================== -// ECHIDNA Theorem Prover Panel -// =========================================================================== - -/// Render the ECHIDNA connection indicator — green dot when connected, -/// red dot when disconnected. Shows the version string and a "Ping" button -/// to manually trigger a health check. -let renderEchidnaConnectionIndicator = (echidna: echidnaState): Tea_Vdom.t => { - let dotClass = echidna.connected ? "bg-emerald-400" : "bg-red-500" - let statusText = switch (echidna.connected, echidna.version) { - | (true, Some(v)) => "ECHIDNA v" ++ v - | (true, None) => "ECHIDNA connected" - | (false, _) => "ECHIDNA disconnected" - } - - div( - list{Attrs.class_("flex items-center gap-2 mb-3")}, - list{ - div( - list{ - Attrs.class_(`w-2 h-2 rounded-full ${dotClass}`), - Attrs.role("status"), - Attrs.ariaLabel(statusText), - }, - list{}, - ), - div(list{Attrs.class_("text-xs text-gray-400 flex-1")}, list{text(statusText)}), - button( - list{ - Attrs.class_("text-xs px-2 py-0.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded"), - Attrs.ariaLabel("Ping ECHIDNA"), - Events.onClick(Echidna(CheckHealth)), - KeyboardNav.onActivate(Echidna(CheckHealth)), - }, - list{text("Ping")}, - ), - }, - ) -} - -/// Render a selectable list of provers from the ECHIDNA catalog. -/// Each prover shows its name, tier badge, and complexity class. -/// The selected prover is highlighted with an active background. -let renderProverCatalog = (echidna: echidnaState): Tea_Vdom.t => { - if Array.length(echidna.provers) === 0 { - div( - list{Attrs.class_("mb-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("PROVERS")}), - button( - list{ - Attrs.class_( - "text-xs px-2 py-0.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded", - ), - Attrs.ariaLabel("List Provers"), - Events.onClick(Echidna(ListProvers)), - KeyboardNav.onActivate(Echidna(ListProvers)), - }, - list{text("List Provers")}, - ), - }, - ), - div(list{Attrs.class_("text-gray-600 text-xs italic")}, list{text("No provers loaded")}), - }, - ) - } else { - div( - list{Attrs.class_("mb-3")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("PROVERS")}), - button( - list{ - Attrs.class_( - "text-xs px-2 py-0.5 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded", - ), - Events.onClick(Echidna(ListProvers)), - KeyboardNav.onActivate(Echidna(ListProvers)), - }, - list{text("Refresh")}, - ), - }, - ), - div( - list{ - Attrs.class_("max-h-24 overflow-y-auto"), - Attrs.role("list"), - Attrs.ariaLabel("Prover Catalog"), - }, - echidna.provers - ->Array.map(prover => { - let isSelected = echidna.selectedProver === Some(prover.name) - let bgClass = isSelected - ? "bg-indigo-900/50 border-indigo-600" - : "bg-gray-800/30 border-gray-700" - div( - list{ - Attrs.class_( - `flex items-center gap-2 p-1.5 border-l-2 ${bgClass} mb-0.5 cursor-pointer hover:bg-gray-700/50`, - ), - Attrs.role("listitem"), - Attrs.tabIndex(0), - Events.onClick(Echidna(SelectProver(isSelected ? None : Some(prover.name)))), - KeyboardUtil.onEnterOrSpace( - Echidna(SelectProver(isSelected ? None : Some(prover.name))), - ), - }, - list{ - div(list{Attrs.class_("text-xs text-gray-300 flex-1")}, list{text(prover.name)}), - div( - list{Attrs.class_("text-xs px-1 py-0.5 bg-gray-700 rounded text-gray-400")}, - list{text(prover.tier)}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(prover.complexity)}), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render the proof input area with a textarea, prover selector, and -/// action buttons (Prove / Verify). -let renderProofInput = (echidna: echidnaState): Tea_Vdom.t => { - div( - list{Attrs.class_("mb-3")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("PROOF INPUT")}), - textarea( - list{ - Attrs.class_( - "w-full h-20 bg-gray-800/50 rounded p-2 text-xs text-gray-200 border border-gray-700 focus:border-indigo-500 focus:outline-none resize-none font-mono", - ), - Attrs.placeholder("Enter proof content..."), - Attrs.value(echidna.proofInput), - Attrs.ariaLabel("Proof content input"), - Events.onInput(text => Echidna(UpdateProofInput(text))), - }, - list{}, - ), - div( - list{Attrs.class_("flex gap-2 mt-1")}, - list{ - button( - list{ - Attrs.class_( - "text-xs px-3 py-1 bg-indigo-700 hover:bg-indigo-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed", - ), - Attrs.disabled(echidna.proofLoading || echidna.proofInput === ""), - Attrs.ariaLabel("Submit Proof"), - Events.onClick(Echidna(SubmitProof)), - KeyboardNav.onActivate(Echidna(SubmitProof)), - }, - list{text(echidna.proofLoading ? "Proving..." : "Prove")}, - ), - button( - list{ - Attrs.class_( - "text-xs px-3 py-1 bg-emerald-700 hover:bg-emerald-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed", - ), - Attrs.disabled(echidna.proofLoading || echidna.proofInput === ""), - Attrs.ariaLabel("Verify Proof"), - Events.onClick(Echidna(SubmitVerify)), - KeyboardNav.onActivate(Echidna(SubmitVerify)), - }, - list{text(echidna.proofLoading ? "Verifying..." : "Verify")}, - ), - }, - ), - }, - ) -} - -/// Render the trust level badge — colour-coded from red (Level 1) through -/// green (Level 5). This is the primary signal for proof confidence. -let renderTrustBadge = (trustLevel: echidnaTrustLevel): Tea_Vdom.t => { - let (label, colour) = switch trustLevel { - | TrustLevel1 => ("Trust 1", "bg-red-700 text-red-200") - | TrustLevel2 => ("Trust 2", "bg-orange-700 text-orange-200") - | TrustLevel3 => ("Trust 3", "bg-yellow-700 text-yellow-200") - | TrustLevel4 => ("Trust 4", "bg-emerald-700 text-emerald-200") - | TrustLevel5 => ("Trust 5", "bg-green-700 text-green-200") - } - span( - list{Attrs.class_(`text-xs px-2 py-0.5 rounded font-bold ${colour}`), Attrs.ariaLabel(label)}, - list{text(label)}, - ) -} - -/// Render the axiom report — a list of axiom usage warnings colour-coded -/// by danger level. Reject-level axioms are shown in red; safe axioms -/// are dimmed. This alerts the operator to unsound assumptions. -let renderAxiomReport = (axioms: array): Tea_Vdom.t => { - if Array.length(axioms) === 0 { - noNode - } else { - div( - list{Attrs.class_("mt-2"), Attrs.role("region"), Attrs.ariaLabel("Axiom Report")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("AXIOM REPORT")}), - div( - list{Attrs.class_("max-h-16 overflow-y-auto")}, - axioms - ->Array.map(axiom => { - let (colour, icon) = switch axiom.dangerLevel { - | Safe => ("text-gray-500", "") - | Noted => ("text-blue-400", "i ") - | Warning => ("text-yellow-400", "! ") - | Reject => ("text-red-400", "X ") - } - div( - list{Attrs.class_(`text-xs ${colour} py-0.5`)}, - list{text(icon ++ axiom.axiomName ++ " - " ++ axiom.description)}, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render the full proof result panel — verified/failed status, trust badge, -/// provers used, proof time, remaining goals, certificate hash, and axiom report. -let renderProofResult = (result: echidnaDispatchResult): Tea_Vdom.t => { - let statusClass = result.verified ? "text-emerald-400" : "text-red-400" - let statusText = result.verified ? "VERIFIED" : "FAILED" - let proversText = Array.join(result.proversUsed, ", ") - let timeText = Float.toString(result.proofTimeMs) ++ "ms" - - div( - list{Attrs.class_("mt-2 p-2 bg-gray-800/50 rounded border border-gray-700")}, - list{ - // Status row with trust badge - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div(list{Attrs.class_(`text-sm font-bold ${statusClass}`)}, list{text(statusText)}), - renderTrustBadge(result.trustLevel), - }, - ), - // Details grid - div( - list{Attrs.class_("grid grid-cols-2 gap-1 text-xs")}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Provers:")}), - div( - list{Attrs.class_("text-gray-300")}, - list{text(proversText === "" ? "none" : proversText)}, - ), - div(list{Attrs.class_("text-gray-500")}, list{text("Time:")}), - div(list{Attrs.class_("text-gray-300")}, list{text(timeText)}), - div(list{Attrs.class_("text-gray-500")}, list{text("Goals left:")}), - div(list{Attrs.class_("text-gray-300")}, list{text(Int.toString(result.goalsRemaining))}), - }, - ), - // Certificate hash (if present) - switch result.certificateHash { - | Some(hash) => - div(list{Attrs.class_("text-xs text-gray-500 mt-1 truncate")}, list{text("cert: " ++ hash)}) - | None => noNode - }, - // Message - if result.message !== "" { - div(list{Attrs.class_("text-xs text-gray-400 mt-1 italic")}, list{text(result.message)}) - } else { - noNode - }, - // Axiom report - renderAxiomReport(result.axiomReport), - // Clear button - div( - list{Attrs.class_("mt-2 text-right")}, - list{ - button( - list{ - Attrs.class_( - "text-xs px-2 py-0.5 bg-gray-700 hover:bg-gray-600 text-gray-400 rounded", - ), - Attrs.ariaLabel("Clear proof result"), - Events.onClick(Echidna(ClearProofResult)), - KeyboardNav.onActivate(Echidna(ClearProofResult)), - }, - list{text("Clear")}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// ECHIDNA Interactive Session UI -// =========================================================================== - -/// Render session controls — "Start Session" button when no session is active, -/// "Cancel" button when a session is running. Disabled during loading. -let renderSessionControls = (echidna: echidnaState): Tea_Vdom.t => { - switch echidna.session { - | None => - div( - list{Attrs.class_("flex gap-2 mt-2")}, - list{ - button( - list{ - Attrs.class_( - "text-xs px-3 py-1 bg-indigo-700 hover:bg-indigo-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed", - ), - Attrs.disabled(echidna.sessionLoading || echidna.proofInput === ""), - Attrs.ariaLabel("Start Proof Session"), - Events.onClick(Echidna(CreateSession)), - KeyboardNav.onActivate(Echidna(CreateSession)), - }, - list{text(echidna.sessionLoading ? "Creating..." : "Start Session")}, - ), - }, - ) - | Some(_session) => - div( - list{Attrs.class_("flex gap-2 mt-2")}, - list{ - button( - list{ - Attrs.class_("text-xs px-3 py-1 bg-red-700 hover:bg-red-600 text-white rounded"), - Attrs.ariaLabel("Cancel Session"), - Events.onClick(Echidna(CancelSession)), - KeyboardNav.onActivate(Echidna(CancelSession)), - }, - list{text("Cancel Session")}, - ), - button( - list{ - Attrs.class_("text-xs px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-300 rounded"), - Attrs.ariaLabel("Refresh Session State"), - Events.onClick(Echidna(GetSessionState)), - KeyboardNav.onActivate(Echidna(GetSessionState)), - }, - list{text("Refresh")}, - ), - }, - ) - } -} - -/// Render session status header — session ID (truncated), prover, status badge, -/// and elapsed time. -let renderSessionStatus = (session: echidnaSessionState): Tea_Vdom.t => { - let (statusText, statusColour) = switch session.status { - | Pending => ("PENDING", "bg-gray-600 text-gray-200") - | InProgress => ("IN PROGRESS", "bg-blue-700 text-blue-200") - | ProofSuccess => ("SUCCESS", "bg-green-700 text-green-200") - | ProofFailed => ("FAILED", "bg-red-700 text-red-200") - | ProofTimeout => ("TIMEOUT", "bg-yellow-700 text-yellow-200") - | ProofError => ("ERROR", "bg-red-800 text-red-200") - } - - let truncatedId = if String.length(session.sessionId) > 12 { - String.slice(session.sessionId, ~start=0, ~end=12) ++ "..." - } else { - session.sessionId - } - - let timeText = switch session.timeElapsed { - | Some(t) => Float.toFixed(t, ~digits=1) ++ "s" - | None => "-" - } - - div( - list{Attrs.class_("p-2 bg-gray-800/50 rounded mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400")}, list{text("Session: " ++ truncatedId)}), - span( - list{ - Attrs.class_(`text-xs px-2 py-0.5 rounded font-bold ${statusColour}`), - Attrs.ariaLabel("Proof status: " ++ statusText), - }, - list{text(statusText)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3 text-xs text-gray-400")}, - list{ - div(list{}, list{text("Prover: " ++ session.prover)}), - div(list{}, list{text("Time: " ++ timeText)}), - }, - ), - switch session.errorMessage { - | Some(err) => div(list{Attrs.class_("text-xs text-red-400 mt-1")}, list{text(err)}) - | None => noNode - }, - }, - ) -} - -/// Render the current goal list — numbered, first goal highlighted as "active". -let renderGoalList = (goals: array): Tea_Vdom.t => { - if Array.length(goals) === 0 { - div( - list{Attrs.class_("text-xs text-emerald-400 italic mb-2")}, - list{text("All goals discharged")}, - ) - } else { - div( - list{Attrs.class_("mb-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text("GOALS (" ++ Int.toString(Array.length(goals)) ++ ")")}, - ), - div( - list{ - Attrs.class_("max-h-24 overflow-y-auto"), - Attrs.role("list"), - Attrs.ariaLabel("Proof Goals"), - }, - goals - ->Array.mapWithIndex((goal, idx) => { - let isActive = idx === 0 - let bgClass = isActive - ? "bg-indigo-900/30 border-indigo-500" - : "bg-gray-800/30 border-gray-700" - div( - list{ - Attrs.class_(`text-xs p-1.5 border-l-2 ${bgClass} mb-0.5 font-mono`), - Attrs.role("listitem"), - }, - list{text(Int.toString(idx + 1) ++ ". " ++ goal)}, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render a manual tactic input field with an "Apply" button. -/// Disabled when no session is active. -let renderTacticInput = (echidna: echidnaState): Tea_Vdom.t => { - let hasSession = switch echidna.session { - | Some(_) => true - | None => false - } - - div( - list{Attrs.class_("mb-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("TACTIC INPUT")}), - div( - list{Attrs.class_("flex gap-1")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-800/50 rounded px-2 py-1 text-xs text-gray-200 border border-gray-700 focus:border-indigo-500 focus:outline-none font-mono", - ), - Attrs.placeholder("e.g., intro x"), - Attrs.value(echidna.tacticInput), - Attrs.disabled(!hasSession), - Attrs.ariaLabel("Manual tactic input"), - Events.onInput(text => Echidna(UpdateTacticInput(text))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "text-xs px-3 py-1 bg-indigo-700 hover:bg-indigo-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed", - ), - Attrs.disabled(!hasSession || echidna.tacticInput === ""), - Attrs.ariaLabel("Apply Tactic"), - Events.onClick(Echidna(ApplyTactic(echidna.tacticInput, []))), - }, - list{text("Apply")}, - ), - }, - ), - }, - ) -} - -/// Render the tactic suggestion ribbon — horizontal scrollable row of clickable chips -/// sorted by confidence (descending). Each chip shows "tactic (confidence%)". -/// Clicking a chip dispatches ApplyTactic with the tactic name and args. -let renderTacticSuggestionRibbon = (suggestions: array): Tea_Vdom.t< - msg, -> => { - if Array.length(suggestions) === 0 { - noNode - } else { - // Sort by confidence descending - let sorted = Array.copy(suggestions) - sorted->Array.sort((a, b) => Float.compare(b.confidence, a.confidence)) - - div( - list{Attrs.class_("mb-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("SUGGESTED TACTICS")}), - div( - list{ - Attrs.class_("flex gap-1 overflow-x-auto pb-1"), - Attrs.role("list"), - Attrs.ariaLabel("Tactic Suggestions"), - }, - sorted - ->Array.map(suggestion => { - let pct = Int.toString(Int.fromFloat(suggestion.confidence *. 100.0)) - button( - list{ - Attrs.class_( - "text-xs px-2 py-1 bg-indigo-900/50 hover:bg-indigo-800/70 text-indigo-300 rounded whitespace-nowrap border border-indigo-700/50 cursor-pointer", - ), - Attrs.title(suggestion.description), - Attrs.role("listitem"), - Attrs.ariaLabel(suggestion.tactic ++ " (" ++ pct ++ "% confidence)"), - Events.onClick(Echidna(ApplyTactic(suggestion.tactic, suggestion.args))), - }, - list{text(suggestion.tactic ++ " (" ++ pct ++ "%)")}, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Render the proof script — a scrollable list of applied tactics (proof history). -/// Displayed in monospace font with sequential numbering. -let renderProofScript = (script: array): Tea_Vdom.t => { - if Array.length(script) === 0 { - noNode - } else { - div( - list{Attrs.class_("mb-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("PROOF SCRIPT")}), - div( - list{ - Attrs.class_("max-h-20 overflow-y-auto bg-gray-800/30 rounded p-1.5"), - Attrs.role("log"), - Attrs.ariaLabel("Proof Script"), - }, - script - ->Array.mapWithIndex((step, idx) => - div( - list{Attrs.class_("text-xs text-gray-300 font-mono py-0.5")}, - list{text(Int.toString(idx + 1) ++ ". " ++ step)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } -} - -// =========================================================================== -// TypeLL Proof Obligations Display -// =========================================================================== - -/// Render TypeLL proof obligations result (if available). -/// Parses the raw JSON via TypeLLEngine.parseCheckResult and displays -/// proof obligation details with linearity notes. -let viewProofObligations = (lastProofObligations: option): Tea_Vdom.t => { - switch lastProofObligations { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let narrative = TypeLLEngine.generateNarrative(result) - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - let statusText = if result.valid { - "Obligations generated" - } else { - "No obligations" - } - div( - list{Attrs.class_("mt-4 p-3 rounded-lg border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL Proof Obligations")}, - ), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono mb-1")}, - list{text(result.typeSignature)}, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(narrative.celebrate)}), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -// =========================================================================== -// Enterprise Model Checking Tab (MOF / OCL / ArchiMate) -// =========================================================================== - -/// Render metamodel standard label. -let metamodelLabel = (m: metamodelStandard): string => { - switch m { - | UML => "UML" - | SysML => "SysML" - | ArchiMate => "ArchiMate" - | BPMN => "BPMN" - | DMN => "DMN" - | CMMN => "CMMN" - | ODM => "ODM" - | CustomProfile => "Custom Profile" - } -} - -/// Render MOF layer label. -let mofLayerLabel = (l: mofLayer): string => { - switch l { - | M3_MetaMetaModel => "M3 (MOF)" - | M2_Metamodel => "M2 (Metamodel)" - | M2_Profile => "M2 (Profile)" - | M1_Model => "M1 (Model)" - | M0_Instance => "M0 (Instance)" - } -} - -/// Render OCL severity badge. -let oclSeverityBadge = (s: oclSeverity): Tea_Vdom.t => { - let (label, colour) = switch s { - | OclInvariant => ("inv", "bg-indigo-900/50 text-indigo-300 border-indigo-700/40") - | OclPrecondition => ("pre", "bg-amber-900/50 text-amber-300 border-amber-700/40") - | OclPostcondition => ("post", "bg-emerald-900/50 text-emerald-300 border-emerald-700/40") - | OclDerive => ("derive", "bg-cyan-900/50 text-cyan-300 border-cyan-700/40") - | OclInit => ("init", "bg-gray-800 text-gray-400 border-gray-700") - | OclBody => ("body", "bg-gray-800 text-gray-400 border-gray-700") - } - span( - list{Attrs.class_(`text-[9px] px-1.5 py-0.5 rounded border font-mono ${colour}`)}, - list{text(label)}, - ) -} - -/// Render a model element row. -let renderModelElement = (elem: modelElement): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2 py-1.5 px-2 bg-gray-900/50 rounded text-[10px]")}, - list{ - span(list{Attrs.class_("text-gray-500 font-mono")}, list{text(mofLayerLabel(elem.layer))}), - span( - list{Attrs.class_("text-indigo-300 font-medium truncate flex-1")}, - list{text(elem.qualifiedName)}, - ), - span(list{Attrs.class_("text-gray-600")}, list{text(elem.metaclass)}), - span( - list{Attrs.class_("text-[9px] px-1 py-0.5 bg-gray-800 rounded text-gray-500")}, - list{text(metamodelLabel(elem.metamodel))}, - ), - }, - ) -} - -/// Render an OCL constraint row with check result. -let renderOclConstraintRow = ( - c: oclConstraint, - index: int, - result: option, -): Tea_Vdom.t => { - let statusIndicator = switch result { - | Some(r) if r.satisfied => - span(list{Attrs.class_("text-emerald-400 font-mono text-[10px]")}, list{text("[OK]")}) - | Some(_) => span(list{Attrs.class_("text-red-400 font-mono text-[10px]")}, list{text("[!!]")}) - | None => span(list{Attrs.class_("text-gray-600 font-mono text-[10px]")}, list{text("[..]")}) - } - - div( - list{Attrs.class_("py-2 px-2 bg-gray-900/50 rounded space-y-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - statusIndicator, - oclSeverityBadge(c.severity), - span(list{Attrs.class_("text-xs text-gray-200 font-medium")}, list{text(c.name)}), - span(list{Attrs.class_("text-[10px] text-gray-600 ml-auto")}, list{text(c.context)}), - button( - list{ - Attrs.class_("text-gray-600 hover:text-red-400 text-[10px] px-1"), - Attrs.title("Remove constraint"), - Events.onClick(Echidna(RemoveOclConstraint(index))), - }, - list{text("x")}, - ), - }, - ), - div( - list{Attrs.class_("font-mono text-[10px] text-cyan-200/80 pl-6")}, - list{text(c.expression)}, - ), - switch result { - | Some(r) => - switch r.counterExample { - | Some(ce) => - div( - list{Attrs.class_("text-[10px] text-red-400/80 pl-6")}, - list{text("Counter-example: " ++ ce)}, - ) - | None => noNode - } - | None => noNode - }, - }, - ) -} - -/// Render the enterprise model checking tab content. -let renderEnterpriseModelTab = (echidna: echidnaState): Tea_Vdom.t => { - let em = echidna.enterpriseModel - let elementCount = Array.length(em.elements) - let constraintCount = Array.length(em.constraints) - let passedCount = em.checkResults->Array.filter(r => r.satisfied)->Array.length - let failedCount = Array.length(em.checkResults) - passedCount - - div( - list{Attrs.class_("space-y-3")}, - list{ - // ─── Overview strip ─── - div( - list{Attrs.class_("flex items-center gap-3 text-[10px]")}, - list{ - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(elementCount)} elements`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(constraintCount)} constraints`)}, - ), - if Array.length(em.checkResults) > 0 { - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`${Int.toString(passedCount)} passed`)}, - ) - } else { - noNode - }, - if failedCount > 0 { - span( - list{Attrs.class_("text-red-400")}, - list{text(`${Int.toString(failedCount)} failed`)}, - ) - } else { - noNode - }, - }, - ), - // ─── Import / Actions ─── - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-indigo-900/50 hover:bg-indigo-800/50 border border-indigo-700/40 rounded text-indigo-300", - ), - Events.onClick(Echidna(ImportXmiModel)), - KeyboardNav.onActivate(Echidna(ImportXmiModel)), - }, - list{text("Import XMI")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium ${em.checking - ? "bg-amber-900/50 text-amber-300 border border-amber-700/40" - : "bg-emerald-900/50 hover:bg-emerald-800/50 text-emerald-300 border border-emerald-700/40"}`, - ), - Events.onClick(Echidna(RunOclCheck)), - KeyboardNav.onActivate(Echidna(RunOclCheck)), - }, - list{text(em.checking ? "Checking..." : "Run OCL Check")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-400", - ), - Events.onClick(Echidna(ClearEnterpriseModel)), - KeyboardNav.onActivate(Echidna(ClearEnterpriseModel)), - }, - list{text("Clear")}, - ), - }, - ), - // ─── Metamodel / Layer Filters ─── - div( - list{Attrs.class_("flex items-center gap-2 flex-wrap")}, - list{ - span(list{Attrs.class_("text-[10px] text-gray-500")}, list{text("Filter:")}), - ...[UML, SysML, ArchiMate, BPMN, DMN] - ->Array.map(m => { - let isActive = em.activeMetamodel === Some(m) - button( - list{ - Attrs.class_( - `px-2 py-0.5 text-[10px] rounded ${isActive - ? "bg-indigo-700 text-white" - : "bg-gray-800 text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(Echidna(SetMetamodelFilter(isActive ? None : Some(m)))), - }, - list{text(metamodelLabel(m))}, - ) - }) - ->List.fromArray, - }, - ), - // ─── Model Elements ─── - if elementCount > 0 { - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_( - "text-[10px] text-gray-500 font-semibold uppercase tracking-wider mb-1", - ), - }, - list{text("Model Elements")}, - ), - ...em.elements - ->Array.filter(e => { - let metamodelMatch = switch em.activeMetamodel { - | Some(m) => e.metamodel === m - | None => true - } - let layerMatch = switch em.activeLayer { - | Some(l) => e.layer === l - | None => true - } - metamodelMatch && layerMatch - }) - ->Array.map(renderModelElement) - ->List.fromArray, - }, - ) - } else { - div( - list{Attrs.class_("text-xs text-gray-600 italic p-3 text-center")}, - list{ - text( - "No model loaded. Import XMI from Visual Paradigm, Sparx EA, Archi, or other MOF-compliant tools.", - ), - }, - ) - }, - // ─── OCL Constraints ─── - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{ - Attrs.class_("text-[10px] text-gray-500 font-semibold uppercase tracking-wider"), - }, - list{text("OCL Constraints")}, - ), - span( - list{Attrs.class_("text-[9px] text-gray-600")}, - list{text("(Object Constraint Language)")}, - ), - }, - ), - ...em.constraints - ->Array.mapWithIndex((c, i) => { - let result = em.checkResults->Array.find(r => r.oclRule.name === c.name) - renderOclConstraintRow(c, i, result) - }) - ->List.fromArray, - if constraintCount === 0 { - div( - list{Attrs.class_("text-[10px] text-gray-600 italic")}, - list{ - text( - "No constraints defined. Add OCL invariants, preconditions, or postconditions.", - ), - }, - ) - } else { - noNode - }, - }, - ), - // ─── Standards Reference ─── - div( - list{ - Attrs.class_( - "p-2 bg-gray-900/30 rounded border border-gray-800 text-[10px] text-gray-600 space-y-1", - ), - }, - list{ - div(list{Attrs.class_("font-semibold text-gray-500")}, list{text("Supported Standards")}), - div( - list{}, - list{text("OMG: MOF 2.5, UML 2.5, SysML 1.7, BPMN 2.0, OCL 2.4, XMI 2.5, QVT 1.3")}, - ), - div(list{}, list{text("The Open Group: ArchiMate 3.2, TOGAF 10 (via ArchiMate)")}), - div( - list{}, - list{text("Tools: Visual Paradigm, Sparx EA, Archi, Camunda, MagicDraw/Cameo")}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// ECHIDNA Panel (tabbed: Proof + Enterprise Model) -// =========================================================================== - -/// Render the complete ECHIDNA panel — a collapsible container with tab -/// switching between the theorem prover workbench and enterprise model -/// checking (MOF/OCL/ArchiMate). -let renderEchidnaPanel = (echidna: echidnaState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("mt-4 border-t border-gray-700 pt-3"), - Attrs.role("region"), - Attrs.ariaLabel("ECHIDNA Theorem Prover"), - }, - list{ - // Collapsible header - button( - list{ - Attrs.class_("flex items-center justify-between w-full mb-2 cursor-pointer"), - Attrs.ariaExpanded(echidna.menuExpanded), - Attrs.ariaLabel("Toggle ECHIDNA panel"), - Events.onClick(Echidna(ToggleMenu)), - KeyboardNav.onActivate(Echidna(ToggleMenu)), - }, - list{ - div( - list{Attrs.class_("text-indigo-400 font-semibold text-sm")}, - list{text("ECHIDNA Prover")}, - ), - div( - list{Attrs.class_("text-gray-500 text-xs")}, - list{text(echidna.menuExpanded ? "[-]" : "[+]")}, - ), - }, - ), - // Connection indicator (always visible) - renderEchidnaConnectionIndicator(echidna), - // Collapsible content - if echidna.menuExpanded { - div( - list{}, - list{ - // Tab bar - div( - list{Attrs.class_("flex gap-1 mb-3 border-b border-gray-800 pb-1")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded-t ${echidna.activeTab === EchidnaProofTab - ? "bg-gray-800 text-indigo-300 border-b-2 border-indigo-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(Echidna(SelectEchidnaTab(EchidnaProofTab))), - }, - list{text("Proof")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded-t ${echidna.activeTab === EchidnaEnterpriseTab - ? "bg-gray-800 text-indigo-300 border-b-2 border-indigo-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(Echidna(SelectEchidnaTab(EchidnaEnterpriseTab))), - }, - list{text("Enterprise (MOF/OCL)")}, - ), - }, - ), - // Tab content - switch echidna.activeTab { - | EchidnaProofTab => - div( - list{}, - list{ - renderProverCatalog(echidna), - renderProofInput(echidna), - renderSessionControls(echidna), - switch echidna.session { - | Some(session) => - div( - list{ - Attrs.class_("mt-2"), - Attrs.role("region"), - Attrs.ariaLabel("Interactive Proof Session"), - }, - list{ - renderSessionStatus(session), - renderGoalList(session.goals), - renderTacticSuggestionRibbon(echidna.tacticSuggestions), - renderTacticInput(echidna), - renderProofScript(session.proofScript), - ProofChain.view(session), - }, - ) - | None => noNode - }, - switch echidna.proofError { - | Some(err) => - div( - list{Attrs.class_("text-xs text-red-400 mt-1 p-1 bg-red-900/20 rounded")}, - list{text(err)}, - ) - | None => noNode - }, - switch echidna.lastProofResult { - | Some(result) => renderProofResult(result) - | None => noNode - }, - viewProofObligations(echidna.lastProofObligations), - }, - ) - | EchidnaEnterpriseTab => renderEnterpriseModelTab(echidna) - }, - }, - ) - } else { - noNode - }, - }, - ) -} - -/// Main Pane-N view — renders the neural stream panel and the ECHIDNA -/// theorem prover panel below it. -/// Render VCL inference stream suggestions from VeriSimDB. -let renderInferenceStream = (suggestions: array): Tea_Vdom.t => { - if Array.length(suggestions) == 0 { - noNode - } else { - div( - list{Attrs.class_("mt-3 p-3 bg-violet-900/20 border border-violet-500/20 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("text-xs text-violet-400 font-semibold tracking-wide uppercase")}, - list{text(`VCL Inference Stream (${Int.toString(Array.length(suggestions))})`)}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-[10px] bg-violet-800/40 hover:bg-violet-700/40 rounded text-violet-300", - ), - Events.onClick(VeriSimDB(ClearInferenceSuggestions)), - KeyboardNav.onActivate(VeriSimDB(ClearInferenceSuggestions)), - }, - list{text("Clear")}, - ), - }, - ), - div( - list{Attrs.class_("space-y-1 max-h-32 overflow-y-auto")}, - suggestions - ->Array.map(suggestion => - div( - list{ - Attrs.class_( - "text-xs text-violet-300/80 font-mono pl-2 border-l-2 border-violet-500/30", - ), - }, - list{text(suggestion)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } -} - -let view = ( - state: paneNState, - echidna: echidnaState, - ~inferenceStream: array=[], -): Tea_Vdom.t => { - div( - list{ - Attrs.class_("h-full flex flex-col p-4 bg-gray-900"), - Attrs.role("region"), - Attrs.ariaLabel("Neural Stream Panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - div(list{Attrs.class_("text-emerald-400 font-semibold")}, list{text("Neural Stream")}), - div(list{Attrs.class_("text-xs text-gray-600")}, list{text("Ctrl+Shift+N")}), - }, - ), - // Agency monitor - renderAgencyMonitor(state.agency), - // Token stream - renderTokenStream(state.tokens, state.filters), - // VCL Inference stream (from VeriSimDB) - renderInferenceStream(inferenceStream), - // Monologue - renderMonologue(state.monologue, state.inferenceActive), - // ECHIDNA Theorem Prover Panel - renderEchidnaPanel(echidna), - }, - ) -} diff --git a/src/components/PaneW.affine b/src/components/PaneW.affine new file mode 100644 index 00000000..62ac2b29 --- /dev/null +++ b/src/components/PaneW.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PaneW; + +// TODO: Complete semantic implementation diff --git a/src/components/PaneW.res b/src/components/PaneW.res deleted file mode 100644 index ca24177d..00000000 --- a/src/components/PaneW.res +++ /dev/null @@ -1,1968 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Pane-W: World/Task Barycentre Component -/// -/// The central shared canvas where results manifest. -/// Contains the Topology View (Binary Star diagram) and -/// the shared world state. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// TypeLL Cross-Panel Type Intelligence (shared helper) -// =========================================================================== - -/// Render TypeLL cross-panel type intelligence result (if available). -/// Parses the raw JSON via TypeLLEngine.parseCheckResult and displays an -/// evangeliser-style narrative with proof obligations and linearity notes. -let viewTypeCheckResult = (lastTypeCheck: option): Tea_Vdom.t => { - switch lastTypeCheck { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let narrative = TypeLLEngine.generateNarrative(result) - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - let statusText = if result.valid { - "Type-safe" - } else { - "Type issues detected" - } - div( - list{Attrs.class_("mt-4 p-3 rounded-lg border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL")}, - ), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono mb-1")}, - list{text(result.typeSignature)}, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(narrative.celebrate)}), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -// =========================================================================== -// VeriSimDB Database Panel -// =========================================================================== - -/// Connection indicator: green dot when connected, red when disconnected. -let renderDbConnectionIndicator = (db: verisimdbState): Tea_Vdom.t => { - let dotColour = db.connected ? "bg-emerald-400" : "bg-red-500" - let statusText = db.connected ? "Connected" : "Disconnected" - - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full " ++ dotColour)}, list{}), - div( - list{Attrs.class_("text-[10px] text-gray-400")}, - list{text(statusText ++ " · " ++ db.endpoint)}, - ), - button( - list{ - Attrs.class_( - "ml-auto px-2 py-0.5 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(VeriSimDB(CheckHealth)), - KeyboardNav.onActivate(VeriSimDB(CheckHealth)), - }, - list{text("Ping")}, - ), - }, - ) -} - -/// VCL query textarea and execute button. -let renderVclQueryArea = (db: verisimdbState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-2")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("VCL Query")}), - textarea( - list{ - Attrs.class_( - "w-full h-20 bg-gray-950 border border-gray-800 rounded p-2 font-mono text-[11px] text-cyan-200 resize-none focus:border-cyan-600 focus:outline-none", - ), - Attrs.placeholder("SELECT GRAPH.* FROM HEXAD 'entity-id'"), - Attrs.value(db.lastQuery), - Events.onInput(value => VeriSimDB(UpdateQueryInput(value))), - Attrs.ariaLabel("VCL query input"), - }, - list{}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-cyan-600 hover:bg-cyan-500 rounded text-gray-900 font-semibold", - ), - Events.onClick(VeriSimDB(SubmitQuery(db.lastQuery))), - }, - list{text("Execute")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300"), - Events.onClick(VeriSimDB(ListEntities)), - KeyboardNav.onActivate(VeriSimDB(ListEntities)), - }, - list{text("List Entities")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-900 hover:bg-gray-800 rounded text-gray-400"), - Events.onClick(VeriSimDB(ClearQueryResult)), - KeyboardNav.onActivate(VeriSimDB(ClearQueryResult)), - }, - list{text("Clear")}, - ), - }, - ), - }, - ) -} - -/// Query result display area — formatted JSON output or error message. -let renderQueryResult = (db: verisimdbState): Tea_Vdom.t => { - let resultContent = switch (db.queryResult, db.queryError) { - | (Some(json), _) => - div( - list{ - Attrs.class_("p-2 bg-gray-950 border border-cyan-900/40 rounded max-h-40 overflow-y-auto"), - }, - list{ - node( - "pre", - list{Attrs.class_("font-mono text-[10px] text-cyan-100 whitespace-pre-wrap")}, - list{text(json)}, - ), - }, - ) - | (None, Some(err)) => div(list{Attrs.class_("text-xs text-red-400")}, list{text(err)}) - | (None, None) => - div(list{Attrs.class_("text-[10px] text-gray-600 italic")}, list{text("No query results yet.")}) - } - - div( - list{Attrs.class_("space-y-1")}, - list{div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Result")}), resultContent}, - ) -} - -/// Entity list sidebar with clickable items that load drift status. -let renderEntityList = (db: verisimdbState): Tea_Vdom.t => { - if Array.length(db.entities) === 0 { - text("") - } else { - let entityItems = - db.entities - ->Array.map(entityId => { - let isSelected = db.selectedEntity == Some(entityId) - let itemClass = isSelected - ? "text-xs text-cyan-300 bg-gray-800 px-2 py-1 rounded cursor-pointer" - : "text-xs text-gray-400 hover:text-cyan-200 px-2 py-1 rounded cursor-pointer" - - div( - list{ - Attrs.class_(itemClass), - Events.onClick(VeriSimDB(SelectEntity(entityId))), - Attrs.role("option"), - }, - list{text(entityId)}, - ) - }) - ->List.fromArray - - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-[11px] text-gray-400")}, - list{text("Entities (" ++ Int.toString(Array.length(db.entities)) ++ ")")}, - ), - div( - list{Attrs.class_("max-h-24 overflow-y-auto space-y-0.5"), Attrs.role("listbox")}, - entityItems, - ), - }, - ) - } -} - -/// Render a single drift bar for a modality. -/// Shows modality name and a coloured bar proportional to the drift score. -/// Colour transitions: green (0.0) -> amber (0.3) -> red (0.7+). -let renderDriftBar = (modality: string, score: float): Tea_Vdom.t => { - let widthPercent = Int.toString(Int.fromFloat(score *. 100.0)) - let barColour = if score >= 0.7 { - "bg-red-500" - } else if score >= 0.3 { - "bg-amber-400" - } else { - "bg-emerald-400" - } - - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("w-16 text-[10px] text-gray-400 font-mono text-right")}, - list{text(modality)}, - ), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full rounded-full " ++ barColour), - Attrs.style("width", widthPercent ++ "%"), - Attrs.role("progressbar"), - Attrs.ariaValueNow(score), - Attrs.ariaValueMin(0.0), - Attrs.ariaValueMax(1.0), - Attrs.ariaLabel(modality ++ " drift score"), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-8 text-[10px] text-gray-500 font-mono")}, - list{text(Float.toFixed(score, ~digits=2))}, - ), - }, - ) -} - -/// Drift heatmap: visual representation of drift across all 8 octad modalities. -/// Renders as a vertical bar chart with colour-coded severity indicators. -let renderDriftHeatmap = (scores: driftScores): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-1")}, - list{ - renderDriftBar("GRAPH", scores.graph), - renderDriftBar("VECTOR", scores.vector), - renderDriftBar("TENSOR", scores.tensor), - renderDriftBar("SEMANTIC", scores.semantic), - renderDriftBar("DOCUMENT", scores.document), - renderDriftBar("TEMPORAL", scores.temporal), - renderDriftBar("PROV", scores.provenance), - renderDriftBar("SPATIAL", scores.spatial), - }, - ) -} - -/// Drift status display for the selected entity. -/// Shows a drift heatmap when structured scores are available, with a -/// normalise button for entities above the warning threshold. -let renderDriftStatus = (db: verisimdbState): Tea_Vdom.t => { - switch (db.selectedEntity, db.driftStatus) { - | (Some(entityId), Some(_json)) => - let heatmapView = switch db.driftScores { - | Some(scores) => renderDriftHeatmap(scores) - | None => - // Fallback to raw JSON if parsing failed - div( - list{ - Attrs.class_( - "p-2 bg-gray-950 border border-amber-900/40 rounded max-h-24 overflow-y-auto", - ), - }, - list{ - node( - "pre", - list{Attrs.class_("font-mono text-[10px] text-amber-200 whitespace-pre-wrap")}, - list{ - text( - switch db.driftStatus { - | Some(j) => j - | None => "" - }, - ), - }, - ), - }, - ) - } - - let isNormalising = db.normalisingEntity == Some(entityId) - let normaliseButton = button( - list{ - Attrs.class_( - if isNormalising { - "px-2 py-0.5 text-[10px] bg-gray-700 rounded text-gray-500 cursor-not-allowed" - } else { - "px-2 py-0.5 text-[10px] bg-amber-600 hover:bg-amber-500 rounded text-gray-900 font-semibold" - }, - ), - Events.onClick( - if isNormalising { - VeriSimDB(ClearQueryResult) - } else { - VeriSimDB(TriggerNormalise(entityId)) - }, - ), - }, - list{ - text( - if isNormalising { - "Normalising..." - } else { - "Normalise" - }, - ), - }, - ) - - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Drift: " ++ entityId)}), - normaliseButton, - }, - ), - heatmapView, - }, - ) - | (Some(_), None) => - div( - list{Attrs.class_("text-[10px] text-gray-600 italic")}, - list{text("Loading drift status...")}, - ) - | _ => text("") - } -} - -/// Telemetry dashboard panel — shows aggregate product development metrics. -/// Displays modality usage heatmap, query pattern distribution, performance, -/// drift frequency, and VCL-total proof adoption. All data is aggregate-only. -let renderTelemetryPanel = (db: verisimdbState): Tea_Vdom.t => { - if !db.telemetryVisible { - text("") - } else { - switch db.telemetry { - | None => - div( - list{ - Attrs.class_("mt-2 p-3 bg-gray-900/80 border border-emerald-900/30 rounded space-y-2"), - }, - list{ - div( - list{Attrs.class_("text-[10px] text-gray-500 italic")}, - list{text("No telemetry data. Click 'Fetch Telemetry' to load product insights.")}, - ), - div( - list{Attrs.class_("text-[9px] text-gray-600")}, - list{text("Aggregate metrics only. No query content or entity data is captured.")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-[10px] bg-emerald-700 hover:bg-emerald-600 rounded text-gray-100", - ), - Events.onClick(VeriSimDB(FetchTelemetry)), - KeyboardNav.onActivate(VeriSimDB(FetchTelemetry)), - }, - list{text("Fetch Telemetry")}, - ), - }, - ) - | Some(snapshot) => - let modalityBars = - snapshot.modalityHeatmap - ->Array.map(((name, pct)) => { - let widthPct = Int.toString(Int.fromFloat(pct)) - let barColour = if pct >= 30.0 { - "bg-emerald-400" - } else if pct >= 10.0 { - "bg-cyan-400" - } else { - "bg-gray-600" - } - - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("w-16 text-[10px] text-gray-400 font-mono text-right")}, - list{text(name)}, - ), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full rounded-full " ++ barColour), - Attrs.style("width", widthPct ++ "%"), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-10 text-[10px] text-gray-500 font-mono")}, - list{text(Float.toFixed(pct, ~digits=1) ++ "%")}, - ), - }, - ) - }) - ->List.fromArray - - let patternRows = - snapshot.queryPatterns - ->Array.map(((pattern, count)) => - div( - list{Attrs.class_("flex justify-between text-[10px]")}, - list{ - div(list{Attrs.class_("text-cyan-300 font-mono")}, list{text(pattern)}), - div(list{Attrs.class_("text-gray-500")}, list{text(Int.toString(count))}), - }, - ) - ) - ->List.fromArray - - div( - list{ - Attrs.class_("mt-2 p-3 bg-gray-900/80 border border-emerald-900/30 rounded space-y-3"), - }, - list{ - // Privacy notice - div( - list{Attrs.class_("text-[9px] text-gray-600 italic")}, - list{text(snapshot.privacyNotice)}, - ), - // Modality usage heatmap - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Modality Usage")}), - div(list{Attrs.class_("space-y-1")}, modalityBars), - }, - ), - // Query patterns - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Query Patterns")}), - div(list{Attrs.class_("space-y-0.5")}, patternRows), - }, - ), - // Performance + drift summary - div( - list{Attrs.class_("grid grid-cols-3 gap-2 text-center")}, - list{ - div( - list{}, - list{ - div( - list{Attrs.class_("text-lg font-light text-cyan-300")}, - list{text(Float.toFixed(snapshot.avgQueryDurationMs, ~digits=1) ++ "ms")}, - ), - div(list{Attrs.class_("text-[9px] text-gray-500")}, list{text("Avg Query")}), - }, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-lg font-light text-amber-300")}, - list{text(Int.toString(snapshot.driftDetectedCount))}, - ), - div(list{Attrs.class_("text-[9px] text-gray-500")}, list{text("Drift Events")}), - }, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-lg font-light text-emerald-300")}, - list{text(Float.toFixed(snapshot.normaliseSuccessRate, ~digits=0) ++ "%")}, - ), - div(list{Attrs.class_("text-[9px] text-gray-500")}, list{text("Normalise OK")}), - }, - ), - }, - ), - // Refresh button - div( - list{Attrs.class_("flex justify-between items-center")}, - list{ - div( - list{Attrs.class_("text-[9px] text-gray-600")}, - list{text("Generated: " ++ snapshot.generatedAt)}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(VeriSimDB(FetchTelemetry)), - KeyboardNav.onActivate(VeriSimDB(FetchTelemetry)), - }, - list{text("Refresh")}, - ), - }, - ), - }, - ) - } - } -} - -/// The complete VeriSimDB database tools panel, rendered in Pane-W. -let renderDatabaseTools = (db: verisimdbState): Tea_Vdom.t => { - let submenu = if !db.dbMenuExpanded { - text("") - } else { - div( - list{ - Attrs.class_("mt-2 p-3 bg-gray-900/80 border border-cyan-900/30 rounded space-y-3"), - Attrs.ariaExpanded(db.dbMenuExpanded), - }, - list{ - renderDbConnectionIndicator(db), - renderVclQueryArea(db), - renderQueryResult(db), - viewTypeCheckResult(db.lastTypeCheck), - renderEntityList(db), - renderDriftStatus(db), - // Telemetry section with toggle - div( - list{Attrs.class_("flex items-center gap-2 mt-2")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Product Telemetry")}), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(VeriSimDB(ToggleTelemetryPanel)), - KeyboardNav.onActivate(VeriSimDB(ToggleTelemetryPanel)), - }, - list{ - text( - if db.telemetryVisible { - "Hide" - } else { - "Show" - }, - ), - }, - ), - }, - ), - renderTelemetryPanel(db), - // Proof obligation display toggle - div( - list{Attrs.class_("flex items-center gap-2 mt-2")}, - list{ - div( - list{Attrs.class_("text-[11px] text-gray-400")}, - list{text("VCL-total Proof Obligations")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(VeriSimDB(ToggleProofDisplay)), - KeyboardNav.onActivate(VeriSimDB(ToggleProofDisplay)), - }, - list{ - text( - if db.proofDisplayActive { - "Hide in Panel-L" - } else { - "Show in Panel-L" - }, - ), - }, - ), - }, - ), - // Anti-Crash validation toggle - div( - list{Attrs.class_("flex items-center gap-2 mt-2")}, - list{ - div( - list{Attrs.class_("text-[11px] text-gray-400")}, - list{text("Anti-Crash VCL Validation")}, - ), - button( - list{ - Attrs.class_( - if db.antiCrashValidation { - "px-2 py-0.5 text-[10px] bg-green-900/40 hover:bg-green-800/40 rounded text-green-400 border border-green-500/30" - } else { - "px-2 py-0.5 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-400" - }, - ), - Events.onClick(VeriSimDB(ToggleAntiCrashValidation)), - KeyboardNav.onActivate(VeriSimDB(ToggleAntiCrashValidation)), - }, - list{ - text( - if db.antiCrashValidation { - "Active" - } else { - "Inactive" - }, - ), - }, - ), - }, - ), - // BoJ routing toggle for VeriSimDB operations - div( - list{Attrs.class_("flex items-center gap-2 mt-2")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("BoJ Routing")}), - button( - list{ - Attrs.class_( - if db.bojRouting { - "px-2 py-0.5 text-[10px] bg-blue-700 text-white rounded" - } else { - "px-2 py-0.5 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300" - }, - ), - Attrs.ariaLabel( - if db.bojRouting { - "Disable BoJ routing" - } else { - "Enable BoJ routing" - }, - ), - Events.onClick(VeriSimDB(ToggleVeriSimBojRouting)), - KeyboardNav.onActivate(VeriSimDB(ToggleVeriSimBojRouting)), - }, - list{ - text( - if db.bojRouting { - "BoJ On" - } else { - "BoJ" - }, - ), - }, - ), - }, - ), - // Query count and inference stream summary - div( - list{Attrs.class_("flex items-center gap-3 mt-2 text-[10px] text-gray-500")}, - list{ - span(list{}, list{text(`Queries: ${Int.toString(db.queryCount)}`)}), - if Array.length(db.inferenceStream) > 0 { - span( - list{Attrs.class_("text-violet-400")}, - list{ - text(`${Int.toString(Array.length(db.inferenceStream))} inference suggestions`), - }, - ) - } else { - noNode - }, - }, - ), - }, - ) - } - - div( - list{Attrs.class_("mt-4 space-y-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 tracking-widest uppercase")}, - list{text("Database Tools")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(VeriSimDB(ToggleDbMenu)), - KeyboardNav.onActivate(VeriSimDB(ToggleDbMenu)), - }, - list{ - text( - if db.dbMenuExpanded { - "Hide" - } else { - "Show" - }, - ), - }, - ), - }, - ), - submenu, - }, - ) -} - -// =========================================================================== -// Security Tools (existing) -// =========================================================================== - -/// Render the security tools menu with panic-attacker and trace-agent buttons. -let renderSecurityTools = (state: paneWState): Tea_Vdom.t => { - let tools = [("panic-attacker", "panic-attacker"), ("trace-agent", "trace-agent (future)")] - - let toolButtons = - tools - ->Array.map(((toolId, label)) => - button( - list{ - Attrs.class_( - "w-full text-left px-2 py-1 text-xs text-gray-300 hover:bg-gray-800 rounded", - ), - Events.onClick(PaneW(OpenSecurityDialog(toolId))), - }, - list{text(label)}, - ) - ) - ->List.fromArray - - let submenu = if !state.securityMenuExpanded { - text("") - } else { - div( - list{ - Attrs.class_("mt-2 bg-gray-900/80 border border-gray-800 rounded p-2 space-y-1"), - Attrs.ariaExpanded(state.securityMenuExpanded), - }, - toolButtons, - ) - } - - div( - list{Attrs.class_("mt-4 space-y-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 tracking-widest uppercase")}, - list{text("Security Tools")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-[10px] bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(PaneW(ToggleSecurityTools)), - KeyboardNav.onActivate(PaneW(ToggleSecurityTools)), - }, - list{ - text( - if state.securityMenuExpanded { - "Hide" - } else { - "Show" - }, - ), - }, - ), - }, - ), - submenu, - }, - ) -} - -/// Render the security dialog overlay for configuring and launching security scans. -let renderSecurityDialog = (state: paneWState): Tea_Vdom.t => { - if !state.securityDialogOpen { - text("") - } else { - let statusView = switch state.securityStatus { - | Some(msg) => div(list{Attrs.class_("text-xs text-emerald-300")}, list{text(msg)}) - | None => text("") - } - - let errorView = switch state.securityError { - | Some(err) => div(list{Attrs.class_("text-xs text-red-400")}, list{text(err)}) - | None => text("") - } - - let toolName = switch state.securityDialogTool { - | Some(tool) => tool - | None => "security tool" - } - - div( - list{Attrs.class_("fixed inset-0 bg-black/60 z-40 flex items-start justify-center p-6")}, - list{ - div( - list{ - Attrs.class_( - "relative w-full max-w-3xl bg-gray-950 border border-gray-800 rounded-lg shadow-2xl p-6 space-y-4", - ), - Attrs.role("dialog"), - Attrs.ariaLabel("Security Tool: " ++ toolName), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 uppercase tracking-widest")}, - list{text("Security Menu · " ++ toolName)}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-300 px-2 py-1 border border-gray-700 rounded"), - Events.onClick(PaneW(CloseSecurityDialog)), - KeyboardNav.onActivate(PaneW(CloseSecurityDialog)), - }, - list{text("Close")}, - ), - }, - ), - statusView, - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-[11px] text-gray-400")}, - list{text("Target program / binary")}, - ), - input( - list{ - Attrs.class_( - "w-full text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-300", - ), - Attrs.placeholder("/path/to/program"), - Attrs.value(state.securityTarget), - Events.onInput(value => PaneW(SetSecurityTarget(value))), - Attrs.ariaLabel("Target program or binary path"), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-[11px] text-gray-400")}, - list{text("Timeline file (JSON/YAML)")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-300", - ), - Attrs.placeholder("timeline.yaml"), - Attrs.value(state.securityTimeline), - Events.onInput(value => PaneW(SetSecurityTimeline(value))), - Attrs.ariaLabel("Timeline file path"), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(PaneW(LoadSecurityTimelineFile)), - KeyboardNav.onActivate(PaneW(LoadSecurityTimelineFile)), - }, - list{text("Browse")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Axes")}), - input( - list{ - Attrs.class_( - "w-full text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-300", - ), - Attrs.placeholder("cpu,memory,concurrency"), - Attrs.value(state.securityAxes), - Events.onInput(value => PaneW(SetSecurityAxes(value))), - Attrs.ariaLabel("Security test axes"), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - div( - list{Attrs.class_("flex-1 space-y-1")}, - list{ - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text("Intensity")}), - input( - list{ - Attrs.class_( - "w-full text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-300", - ), - Attrs.placeholder("medium"), - Attrs.value(state.securityIntensity), - Events.onInput(value => PaneW(SetSecurityIntensity(value))), - Attrs.ariaLabel("Test intensity"), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("flex-1 space-y-1")}, - list{ - div( - list{Attrs.class_("text-[11px] text-gray-400")}, - list{text("Duration (s)")}, - ), - input( - list{ - Attrs.class_( - "w-full text-xs bg-gray-950 border border-gray-800 rounded px-2 py-1 text-gray-300", - ), - Attrs.placeholder("30"), - Attrs.value(state.securityDuration), - Events.onInput(value => PaneW(SetSecurityDuration(value))), - Attrs.ariaLabel("Test duration in seconds"), - }, - list{}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-xs bg-emerald-500 hover:bg-emerald-400 rounded text-gray-900 font-semibold", - ), - Events.onClick(PaneW(LaunchSecurityAmbush)), - KeyboardNav.onActivate(PaneW(LaunchSecurityAmbush)), - }, - list{text("Launch Ambush")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(PaneW(ClearEventChain)), - KeyboardNav.onActivate(PaneW(ClearEventChain)), - }, - list{text("Reset")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-gray-700 hover:bg-gray-600 rounded text-gray-200", - ), - Events.onClick(PaneW(ToggleSecurityStudyView)), - KeyboardNav.onActivate(PaneW(ToggleSecurityStudyView)), - }, - list{ - text( - if state.securityViewActive { - "Hide Time/Space View" - } else { - "Show Time/Space View" - }, - ), - }, - ), - }, - ), - errorView, - }, - ), - }, - ) - } -} - -/// Render the security study view with event chain timeline and analysis results. -let renderSecurityStudyView = (state: paneWState): Tea_Vdom.t => { - if !state.securityViewActive { - text("") - } else { - let timelineInfo = switch state.eventChainTimeline { - | Some(timeline) => - "Timeline: " ++ - Int.toString(timeline.events) ++ - " events · " ++ - Float.toString(timeline.durationMs) ++ "ms" - | None => "Timeline metadata unavailable" - } - - let eventRows = - state.eventChain - ->Array.map(ev => { - div( - list{ - Attrs.class_( - "text-xs text-gray-300 flex justify-between border-b border-gray-800/60 py-1", - ), - }, - list{ - div(list{}, list{text(ev.id)}), - div(list{}, list{text(ev.axis ++ " · " ++ ev.status)}), - div(list{}, list{text(Float.toString(ev.durationMs) ++ "ms")}), - }, - ) - }) - ->List.fromArray - - div( - list{ - Attrs.class_("mt-4 p-3 border border-emerald-500/20 rounded bg-emerald-900/30 space-y-2"), - }, - list{ - div( - list{Attrs.class_("text-xs font-semibold text-emerald-300")}, - list{text("Time/Space Study")}, - ), - div(list{Attrs.class_("text-[11px] text-gray-400")}, list{text(timelineInfo)}), - div(list{Attrs.class_("max-h-32 overflow-y-auto")}, eventRows), - }, - ) - } -} - -/// Render the event chain panel showing security event timeline, summary, and controls. -let renderEventChainPanel = (state: paneWState): Tea_Vdom.t => { - let eventCount = Array.length(state.eventChain) - let summaryView = switch state.eventChainSummary { - | Some(summary) => - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{ - text( - "Program: " ++ - summary.program ++ - " · Weak points: " ++ - Int.toString(summary.weakPoints) ++ - " · Crashes: " ++ - Int.toString(summary.totalCrashes) ++ - " · Robustness: " ++ - Float.toString(summary.robustnessScore), - ), - }, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-600 mb-2")}, - list{text("No event-chain summary loaded.")}, - ) - } - - let timelineView = switch state.eventChainTimeline { - | Some(timeline) => - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{ - text( - "Timeline: " ++ - Int.toString(timeline.events) ++ - " events · Duration: " ++ - Float.toString(timeline.durationMs) ++ "ms", - ), - }, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-600 mb-2")}, - list{text("No timeline metadata loaded.")}, - ) - } - - let capabilityTone = PanicAttackerMode.toneClass(state.panicAttackerMode) - let capabilityLabel = PanicAttackerMode.label(state.panicAttackerMode) - - let capabilityBinaryView = switch state.panicAttackerBinary { - | Some(binary) => - div(list{Attrs.class_("text-xs text-gray-600")}, list{text("Binary: " ++ binary)}) - | None => text("") - } - - let capabilityDetailView = switch state.panicAttackerStatusDetail { - | Some(detail) => div(list{Attrs.class_("text-xs text-gray-500")}, list{text(detail)}) - | None => text("") - } - - let errorView = switch state.eventChainError { - | Some(err) => div(list{Attrs.class_("text-xs text-red-400 mb-2")}, list{text(err)}) - | None => text("") - } - - let previewCount = eventCount > 8 ? 8 : eventCount - let eventRows = - state.eventChain - ->Array.slice(~start=0, ~end=previewCount) - ->Array.map(ev => { - let startLabel = switch ev.startMs { - | Some(ms) => Float.toString(ms) ++ "ms" - | None => "n/a" - } - div( - list{Attrs.class_("text-xs text-gray-400 flex justify-between")}, - list{ - div(list{Attrs.class_("truncate")}, list{text(ev.id)}), - div(list{}, list{text(ev.axis)}), - div(list{}, list{text(startLabel)}), - div(list{}, list{text(Float.toString(ev.durationMs) ++ "ms")}), - div(list{}, list{text(ev.status)}), - }, - ) - }) - ->List.fromArray - - div( - list{Attrs.class_("mt-4 p-3 border border-gray-800 rounded bg-gray-900/60 space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 tracking-widest")}, - list{text("EVENT CHAIN (PANLL IMPORT)")}, - ), - summaryView, - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300"), - Events.onClick(PaneW(ImportEventChain)), - KeyboardNav.onActivate(PaneW(ImportEventChain)), - }, - list{text("Import JSON")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300"), - Events.onClick(PaneW(ImportEventChainFile)), - KeyboardNav.onActivate(PaneW(ImportEventChainFile)), - }, - list{text("Load File")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300"), - Events.onClick(PaneW(CheckPanicAttackerCapability)), - KeyboardNav.onActivate(PaneW(CheckPanicAttackerCapability)), - }, - list{text("Probe panic-attacker")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300"), - Events.onClick(PaneW(ImportPanicAttackerReportFile)), - KeyboardNav.onActivate(PaneW(ImportPanicAttackerReportFile)), - }, - list{text("Load panic-attacker Report")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300"), - Events.onClick(PaneW(ImportLatestPanicAttacker)), - KeyboardNav.onActivate(PaneW(ImportLatestPanicAttacker)), - }, - list{text("Latest panic-attacker")}, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-gray-900 hover:bg-gray-800 rounded text-gray-400"), - Events.onClick(PaneW(ClearEventChain)), - KeyboardNav.onActivate(PaneW(ClearEventChain)), - }, - list{text("Clear")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 ml-auto")}, - list{text("Events: " ++ Int.toString(eventCount))}, - ), - }, - ), - renderSecurityTools(state), - timelineView, - div( - list{Attrs.class_("space-y-1")}, - list{ - div(list{Attrs.class_("text-xs " ++ capabilityTone)}, list{text(capabilityLabel)}), - capabilityBinaryView, - capabilityDetailView, - }, - ), - errorView, - textarea( - list{ - Attrs.class_( - "w-full h-24 bg-gray-950 border border-gray-800 rounded p-2 font-mono text-[11px] text-gray-400 resize-none focus:border-gray-600 focus:outline-none", - ), - Attrs.placeholder( - "Paste event-chain JSON here then click \"Import JSON\".\nFormat: {\"events\":[{\"id\":\"e1\",\"axis\":\"cpu\",\"durationMs\":100,\"intensity\":\"high\",\"status\":\"pass\"}]}", - ), - Attrs.value(state.eventChainInput), - Events.onInput(value => PaneW(UpdateEventChainInput(value))), - Attrs.ariaLabel("Event chain JSON input"), - }, - list{}, - ), - div(list{Attrs.class_("space-y-1")}, eventRows), - renderSecurityDialog(state), - }, - ) -} - -// =========================================================================== -// Barycentre Tour — Guided Walkthrough -// =========================================================================== - -/// Tour step content: title, explanation, and what to look at. -let tourStepContent = (step: tourStep): (string, string) => { - switch step { - | TourIntro => ( - "Welcome to the Task Barycentre", - "The Barycentre is the gravitational centre of your work. Like a binary star system, your symbolic reasoning (Panel-L) and neural inference (Panel-N) co-orbit around a shared centre of mass. This view shows you where that centre is and how healthy the orbit is.", - ) - | TourBinaryStar => ( - "The Binary Star System", - "The indigo star represents your Symbolic Panel (formal logic, constraints, type-checked code). The emerald star represents your Neural Panel (AI inference, natural language, creative output). They orbit each other — when balanced, your work is strongest.", - ) - | TourBarycentrePosition => ( - "Barycentre Position", - "The golden diamond shows where the centre of gravity currently sits. If it drifts toward Symbolic, you may be over-constraining. If it drifts toward Neural, you may lack formal grounding. The position bar below the diagram shows this as a spectrum.", - ) - | TourOrbitalMetrics => ( - "Orbital Metrics", - "Four gauges measure orbit health: Stability (how steady the co-orbit is), Divergence (how far apart the stars have drifted), Symbolic Mass (density of your formal content), and Neural Stream (throughput of inference). Green is healthy, amber needs attention, red means intervention needed.", - ) - | TourContractiles => ( - "Contractile Boundaries", - "Contractiles are elastic adaptive constraints that keep the orbit safe. Each one has an enforcement level (Strict/Warn/Adaptive) and an elasticity score. When a contractile is violated, it appears red. Elastic contractiles can stretch — rigid ones halt immediately.", - ) - | TourSyncHealth => ( - "Synchronisation Health", - "Sync Health measures how well the three panels communicate. Low latency and fresh hash states mean healthy sync. When sync degrades, the drift aura shifts from indigo to amber, and the system sheds visual complexity (Information Humidity drops) to reduce cognitive load.", - ) - | TourComplete => ( - "Tour Complete", - "You now understand the Task Barycentre. Use it to monitor your work balance, catch drift early, and understand when the system adapts to protect you. Click the tour button anytime to revisit.", - ) - } -} - -/// Tour step number (1-indexed, for display). -let tourStepNumber = (step: tourStep): int => { - switch step { - | TourIntro => 1 - | TourBinaryStar => 2 - | TourBarycentrePosition => 3 - | TourOrbitalMetrics => 4 - | TourContractiles => 5 - | TourSyncHealth => 6 - | TourComplete => 7 - } -} - -/// Render the tour overlay card. -let renderTourOverlay = (tour: tourState): Tea_Vdom.t => { - if !tour.active { - noNode - } else { - let (title, body) = tourStepContent(tour.currentStep) - let stepNum = tourStepNumber(tour.currentStep) - let isFirst = tour.currentStep === TourIntro - let isLast = tour.currentStep === TourComplete - - div( - list{ - Attrs.class_( - "absolute inset-0 z-50 flex items-end justify-center pb-4 pointer-events-none", - ), - }, - list{ - div( - list{ - Attrs.class_( - "pointer-events-auto w-[90%] max-w-lg bg-gray-900/95 border border-indigo-700/60 rounded-xl shadow-2xl shadow-indigo-900/30 p-5 backdrop-blur-sm", - ), - Attrs.role("dialog"), - Attrs.ariaLabel("Barycentre tour"), - }, - list{ - // Step indicator - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - span( - list{Attrs.class_("text-xs text-indigo-400 font-mono")}, - list{text(`Step ${Int.toString(stepNum)} of 7`)}, - ), - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300 text-xs px-2 py-1"), - Events.onClick(PaneW(CloseTour)), - KeyboardNav.onActivate(PaneW(CloseTour)), - }, - list{text("Skip tour")}, - ), - }, - ), - // Progress dots - div( - list{Attrs.class_("flex gap-1.5 mb-4")}, - [1, 2, 3, 4, 5, 6, 7] - ->Array.map(n => { - let active = n <= stepNum - div( - list{ - Attrs.class_( - `h-1 flex-1 rounded-full transition-all ${active - ? "bg-indigo-500" - : "bg-gray-700"}`, - ), - }, - list{}, - ) - }) - ->List.fromArray, - ), - // Title - div(list{Attrs.class_("text-sm font-semibold text-gray-100 mb-2")}, list{text(title)}), - // Body - div(list{Attrs.class_("text-xs text-gray-400 leading-relaxed mb-4")}, list{text(body)}), - // Navigation - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - if isFirst { - div(list{}, list{}) - } else { - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-800 hover:bg-gray-700 rounded text-gray-300", - ), - Events.onClick(PaneW(PrevTourStep)), - KeyboardNav.onActivate(PaneW(PrevTourStep)), - }, - list{text("Back")}, - ) - }, - button( - list{ - Attrs.class_( - `px-4 py-1.5 text-xs rounded font-medium ${isLast - ? "bg-indigo-600 hover:bg-indigo-500 text-white" - : "bg-indigo-600 hover:bg-indigo-500 text-white"}`, - ), - Events.onClick( - if isLast { - PaneW(CloseTour) - } else { - PaneW(NextTourStep) - }, - ), - }, - list{ - text( - if isLast { - "Finish" - } else { - "Next" - }, - ), - }, - ), - }, - ), - }, - ), - }, - ) - } -} - -// =========================================================================== -// Metric Gauge Component -// =========================================================================== - -/// Render a circular-style metric gauge with label, value, and colour coding. -let renderMetricGauge = ( - label: string, - value: float, - unit: string, - ~lowColour: string="text-red-400", - ~midColour: string="text-amber-300", - ~highColour: string="text-emerald-400", - ~invert: bool=false, -): Tea_Vdom.t => { - let displayValue = Int.toString(Int.fromFloat(value *. 100.0)) - let effective = if invert { - 1.0 -. value - } else { - value - } - let colour = if effective >= 0.7 { - highColour - } else if effective >= 0.4 { - midColour - } else { - lowColour - } - let barWidth = Float.toFixed(value *. 100.0, ~digits=0) - - div( - list{Attrs.class_("flex flex-col items-center gap-1")}, - list{ - // Value - div(list{Attrs.class_(`text-xl font-light ${colour}`)}, list{text(`${displayValue}${unit}`)}), - // Bar - div( - list{Attrs.class_("w-full h-1.5 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full transition-all duration-700 ${if effective >= 0.7 { - "bg-emerald-500" - } else if effective >= 0.4 { - "bg-amber-500" - } else { - "bg-red-500" - }}`, - ), - Attrs.style("width", `${barWidth}%`), - }, - list{}, - ), - }, - ), - // Label - div(list{Attrs.class_("text-[10px] text-gray-500 text-center")}, list{text(label)}), - }, - ) -} - -// =========================================================================== -// Contractile Status Row -// =========================================================================== - -/// Render a compact contractile status indicator. -let renderContractileRow = (c: contractile): Tea_Vdom.t => { - let (statusColour, statusIcon) = switch c.status { - | Satisfied => ("text-emerald-400", "[OK]") - | Violated(_) => ("text-red-400", "[!!]") - | Pending => ("text-gray-500", "[..]") - | Suspended => ("text-gray-600", "[--]") - } - let enfLabel = switch c.enforcement { - | Strict => "Strict" - | Warn => "Warn" - | Adaptive => "Adapt" - } - let elasticityBar = Float.toFixed(c.elasticity *. 100.0, ~digits=0) - - div( - list{Attrs.class_("flex items-center gap-2 py-1 px-2 bg-gray-900/50 rounded text-[10px]")}, - list{ - span(list{Attrs.class_(`font-mono ${statusColour}`)}, list{text(statusIcon)}), - span(list{Attrs.class_("text-gray-300 flex-1 truncate")}, list{text(c.name)}), - span(list{Attrs.class_("text-gray-600")}, list{text(enfLabel)}), - // Elasticity mini-bar - div( - list{Attrs.class_("w-10 h-1 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-indigo-500/60 rounded-full"), - Attrs.style("width", `${elasticityBar}%`), - }, - list{}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Enhanced Binary Star Topology View -// =========================================================================== - -/// Render the Binary Star topology diagram with full orbital metrics, -/// barycentre position indicator, contractile status, sync health, and -/// guided tour. -let renderTopologyView = ( - orbital: orbitalState, - contractiles: array, - tour: tourState, -): Tea_Vdom.t => { - // Barycentre position as a CSS percentage offset from centre. - // -1.0 maps to 15%, 0.0 maps to 50%, +1.0 maps to 85%. - let baryPct = Float.toFixed(50.0 +. orbital.barycentrePosition *. 35.0, ~digits=1) - - div( - list{Attrs.class_("h-full flex flex-col items-center overflow-y-auto py-4 relative")}, - list{ - // Section title - div( - list{Attrs.class_("text-center mb-2")}, - list{ - div( - list{Attrs.class_("text-sm font-semibold text-gray-300 tracking-wide")}, - list{text("Task Barycentre")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("Binary Star Co-Orbit Monitor")}, - ), - }, - ), - // Tour start button (if not active) - if !tour.active { - div( - list{Attrs.class_("mb-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-[10px] bg-indigo-900/50 hover:bg-indigo-800/50 border border-indigo-700/40 rounded-full text-indigo-300 transition-colors", - ), - Attrs.title("Take a guided tour of the Task Barycentre"), - Events.onClick(PaneW(StartTour)), - KeyboardNav.onActivate(PaneW(StartTour)), - }, - list{ - text( - if tour.completed { - "Retake Tour" - } else { - "Take the Tour" - }, - ), - }, - ), - }, - ) - } else { - noNode - }, - // ─── Binary Star Diagram ─── - div( - list{Attrs.class_("relative w-full max-w-md mx-auto"), Attrs.style("height", "200px")}, - list{ - // Orbital path (ellipse) - div( - list{ - Attrs.class_("absolute border-2 border-dashed border-gray-700/50 rounded-full"), - Attrs.style("width", "320px"), - Attrs.style("height", "140px"), - Attrs.style("top", "50%"), - Attrs.style("left", "50%"), - Attrs.style("transform", "translate(-50%, -50%)"), - }, - list{}, - ), - // Symbolic star (left) — size scales with symbolicMass - div( - list{ - Attrs.class_( - "absolute rounded-full bg-indigo-600/60 border-2 border-indigo-400 flex items-center justify-center shadow-lg shadow-indigo-500/30 transition-all duration-500", - ), - Attrs.style( - "width", - `${Int.toString(60 + Int.fromFloat(orbital.symbolicMass *. 30.0))}px`, - ), - Attrs.style( - "height", - `${Int.toString(60 + Int.fromFloat(orbital.symbolicMass *. 30.0))}px`, - ), - Attrs.style("left", "10%"), - Attrs.style("top", "50%"), - Attrs.style("transform", "translateY(-50%)"), - }, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div(list{Attrs.class_("text-indigo-200 text-xs font-bold")}, list{text("L")}), - div(list{Attrs.class_("text-indigo-300 text-[9px]")}, list{text("Symbolic")}), - }, - ), - }, - ), - // Neural star (right) — size scales with neuralStream - div( - list{ - Attrs.class_( - "absolute rounded-full bg-emerald-600/60 border-2 border-emerald-400 flex items-center justify-center shadow-lg shadow-emerald-500/30 transition-all duration-500", - ), - Attrs.style( - "width", - `${Int.toString(60 + Int.fromFloat(orbital.neuralStream *. 30.0))}px`, - ), - Attrs.style( - "height", - `${Int.toString(60 + Int.fromFloat(orbital.neuralStream *. 30.0))}px`, - ), - Attrs.style("right", "10%"), - Attrs.style("top", "50%"), - Attrs.style("transform", "translateY(-50%)"), - }, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div(list{Attrs.class_("text-emerald-200 text-xs font-bold")}, list{text("N")}), - div(list{Attrs.class_("text-emerald-300 text-[9px]")}, list{text("Neural")}), - }, - ), - }, - ), - // Barycentre marker (diamond) — positioned dynamically on the axis - div( - list{ - Attrs.class_( - "absolute w-8 h-8 bg-amber-500/70 border-2 border-amber-300 shadow-lg shadow-amber-500/40 flex items-center justify-center transition-all duration-700", - ), - Attrs.style("top", "50%"), - Attrs.style("left", baryPct ++ "%"), - Attrs.style("transform", "translate(-50%, -50%) rotate(45deg)"), - }, - list{ - div( - list{ - Attrs.class_("text-amber-100 text-[9px] font-bold"), - Attrs.style("transform", "rotate(-45deg)"), - }, - list{text("W")}, - ), - }, - ), - // Drift aura glow (subtle ring around the diagram) - div( - list{ - Attrs.class_( - `absolute rounded-full pointer-events-none transition-all duration-1000 ${if ( - orbital.driftAuraColour === "indigo" - ) { - "shadow-[0_0_40px_rgba(99,102,241,0.15)]" - } else { - "shadow-[0_0_40px_rgba(245,158,11,0.2)]" - }}`, - ), - Attrs.style("width", "340px"), - Attrs.style("height", "160px"), - Attrs.style("top", "50%"), - Attrs.style("left", "50%"), - Attrs.style("transform", "translate(-50%, -50%)"), - }, - list{}, - ), - }, - ), - // ─── Barycentre Position Bar ─── - div( - list{Attrs.class_("w-full max-w-sm mx-auto mt-2 px-4")}, - list{ - div( - list{Attrs.class_("flex justify-between text-[9px] text-gray-600 mb-0.5")}, - list{ - span(list{}, list{text("Symbolic")}), - span(list{}, list{text("Balanced")}), - span(list{}, list{text("Neural")}), - }, - ), - div( - list{Attrs.class_("h-2 bg-gray-800 rounded-full relative overflow-hidden")}, - list{ - // Gradient background - div( - list{ - Attrs.class_("absolute inset-0 opacity-30"), - Attrs.style( - "background", - "linear-gradient(to right, rgb(99,102,241), rgb(107,114,128), rgb(16,185,129))", - ), - }, - list{}, - ), - // Position marker - div( - list{ - Attrs.class_( - "absolute top-0 bottom-0 w-1 bg-amber-400 rounded-full transition-all duration-700", - ), - Attrs.style("left", baryPct ++ "%"), - Attrs.style("transform", "translateX(-50%)"), - }, - list{}, - ), - }, - ), - }, - ), - // ─── Metric Gauges Grid ─── - div( - list{Attrs.class_("w-full max-w-md mx-auto mt-4 grid grid-cols-4 gap-3 px-4")}, - list{ - renderMetricGauge("Stability", orbital.stability, "%"), - renderMetricGauge("Divergence", orbital.divergenceLevel, "%", ~invert=true), - renderMetricGauge("Sym. Mass", orbital.symbolicMass, "%"), - renderMetricGauge("Neu. Stream", orbital.neuralStream, "%"), - }, - ), - // ─── Sync Health Bar ─── - div( - list{Attrs.class_("w-full max-w-md mx-auto mt-3 px-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-[10px] text-gray-500")}, list{text("Sync Health")}), - div( - list{Attrs.class_("flex-1 h-1.5 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full transition-all duration-500 ${if ( - orbital.syncHealth >= 0.8 - ) { - "bg-emerald-500" - } else if orbital.syncHealth >= 0.5 { - "bg-amber-500" - } else { - "bg-red-500" - }}`, - ), - Attrs.style( - "width", - `${Float.toFixed(orbital.syncHealth *. 100.0, ~digits=0)}%`, - ), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-[10px] text-gray-500 font-mono")}, - list{text(`${Int.toString(Int.fromFloat(orbital.syncHealth *. 100.0))}%`)}, - ), - }, - ), - }, - ), - // ─── Contractile Boundaries ─── - div( - list{Attrs.class_("w-full max-w-md mx-auto mt-3 px-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1.5")}, - list{ - span( - list{ - Attrs.class_("text-[10px] text-gray-500 font-semibold uppercase tracking-wider"), - }, - list{text("Contractiles")}, - ), - span( - list{Attrs.class_("text-[9px] text-gray-600")}, - list{text("(elastic adaptive boundaries)")}, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - contractiles - ->Array.map(c => renderContractileRow(c)) - ->List.fromArray, - ), - }, - ), - // ─── Action Bar ─── - div( - list{Attrs.class_("mt-4 flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded text-sm text-gray-400 transition-colors", - ), - Attrs.title("Toggle between topology graph and code output view"), - Attrs.ariaLabel("Switch to Code View"), - Events.onClick(PaneW(ToggleTopologyView)), - KeyboardNav.onActivate(PaneW(ToggleTopologyView)), - }, - list{text("Code View")}, - ), - }, - ), - // ─── Tour Overlay ─── - renderTourOverlay(tour), - }, - ) -} - -/// Render the code/content view -let renderContentView = (state: paneWState, db: verisimdbState): Tea_Vdom.t => { - div( - list{Attrs.class_("h-full flex flex-col")}, - list{ - // Last validated output - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text("LAST VALIDATED OUTPUT")}, - ), - div( - list{ - Attrs.class_( - "p-3 bg-gray-800/50 rounded border border-emerald-900/30 font-mono text-sm text-emerald-200 min-h-[60px]", - ), - }, - list{ - text( - state.lastValidatedOutput === "" - ? "No validated output yet" - : state.lastValidatedOutput, - ), - }, - ), - }, - ), - // Current content - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text("SHARED WORLD STATE")}), - textarea( - list{ - Attrs.class_( - "w-full h-full bg-gray-800 border border-gray-700 rounded p-3 font-mono text-sm text-gray-300 resize-none focus:border-gray-500 focus:outline-none", - ), - Attrs.placeholder("Task output manifests here..."), - Attrs.value(state.content), - Events.onInput(value => PaneW(UpdateContent(value))), - Attrs.ariaLabel("Shared World State"), - }, - list{}, - ), - }, - ), - renderDatabaseTools(db), - renderEventChainPanel(state), - renderSecurityStudyView(state), - renderSecurityDialog(state), - // Toggle button - div( - list{Attrs.class_("mt-4 text-right")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded text-sm text-gray-400 transition-colors", - ), - Events.onClick(PaneW(ToggleTopologyView)), - KeyboardNav.onActivate(PaneW(ToggleTopologyView)), - }, - list{text("Switch to Topology View")}, - ), - }, - ), - }, - ) -} - -/// Main Pane-W view -let view = ( - state: paneWState, - orbital: orbitalState, - db: verisimdbState, - ~contractiles: array=[], - ~tour: tourState={active: false, currentStep: TourIntro, completed: false}, -): Tea_Vdom.t => { - div( - list{ - Attrs.class_("h-full flex flex-col p-4 bg-gray-900"), - Attrs.role("region"), - Attrs.ariaLabel("Task Barycentre Panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - div(list{Attrs.class_("text-gray-400 font-semibold")}, list{text("Task Barycentre")}), - div(list{Attrs.class_("text-xs text-gray-600")}, list{text("Ctrl+Shift+B")}), - }, - ), - // Content - if state.topologyView { - renderTopologyView(orbital, contractiles, tour) - } else { - renderContentView(state, db) - }, - }, - ) -} diff --git a/src/components/PanelSwitcher.affine b/src/components/PanelSwitcher.affine new file mode 100644 index 00000000..31701e26 --- /dev/null +++ b/src/components/PanelSwitcher.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PanelSwitcher; + +// TODO: Complete semantic implementation diff --git a/src/components/PanelSwitcher.res b/src/components/PanelSwitcher.res deleted file mode 100644 index 49c6312c..00000000 --- a/src/components/PanelSwitcher.res +++ /dev/null @@ -1,283 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Panel Switcher Component — grouped slide-out panel navigator. -/// -/// Renders a vertical sidebar on the right edge with **group headers** -/// (one per clade kind: AI, Bridge, Builder, etc.). Clicking a group -/// slides out a panel list showing all panels in that kind with their -/// full names, descriptions, and connection status. Clicking a panel -/// opens it as a full-screen overlay. -/// -/// Design reference: Visual Paradigm's grouped selection panel — compact -/// category strip with contextual slide-out for discovery. - -open Model -open Msg -open Tea.Html - -/// Panel-to-kind mapping. Returns the kind string for a panel based on -/// its cladeId in the builtin clade data. Panels without a clade default -/// to "meta". -let panelKind = (panel: panelMeta): string => { - let clades = CladeBrowserEngine.builtinClades - switch panel.cladeId { - | None => "meta" - | Some(cid) => - switch clades->Array.find(c => c.id == cid) { - | Some(clade) => clade.kind - | None => "meta" - } - } -} - -/// The ordered list of groups to show in the sidebar. -/// Each group has a kind key, display label, and accent colour. -type groupDef = { - kind: string, - label: string, - colour: string, - icon: string, -} - -let groups: array = [ - {kind: "ai", label: "AI", colour: "#a78bfa", icon: "ai"}, - {kind: "bridge", label: "Bridge", colour: "#60a5fa", icon: "link"}, - {kind: "builder", label: "Build", colour: "#f59e0b", icon: "hammer"}, - {kind: "database", label: "Data", colour: "#34d399", icon: "db"}, - {kind: "directive", label: "Direct", colour: "#f87171", icon: "flag"}, - {kind: "loader", label: "Load", colour: "#818cf8", icon: "folder"}, - {kind: "meta", label: "Meta", colour: "#9ca3af", icon: "cog"}, - {kind: "network", label: "Net", colour: "#2dd4bf", icon: "wifi"}, - {kind: "scanner", label: "Scan", colour: "#fb923c", icon: "shield"}, - {kind: "terminal", label: "Term", colour: "#a3e635", icon: "term"}, - {kind: "viewer", label: "View", colour: "#c084fc", icon: "eye"}, -] - -/// Render the connection status indicator dot. -let renderStatusDot = (status: connectionStatus): Tea_Vdom.t => { - let colour = switch status { - | ServiceConnected => "bg-emerald-400" - | ServiceDisconnected => "bg-gray-600" - | ServiceChecking => "bg-amber-400 animate-pulse" - | ServiceError(_) => "bg-red-400" - } - div(list{Attrs.class_(`w-2 h-2 rounded-full ${colour} flex-shrink-0`)}, list{}) -} - -/// Render a single panel entry in the expanded group. -let renderPanelEntry = (panel: panelMeta, isActive: bool): Tea_Vdom.t => { - let activeBg = isActive ? "bg-gray-700/80" : "hover:bg-gray-800/60" - let activeText = isActive ? "text-white" : "text-gray-300" - - div( - list{ - Attrs.class_( - `flex items-center gap-1 w-full px-1.5 py-1 rounded-lg ${activeBg} transition-colors group`, - ), - }, - list{ - // Main panel button (opens/closes panel) - button( - list{ - Attrs.class_( - `flex items-center gap-2 flex-1 px-1.5 py-1 rounded text-left ${activeText} transition-colors`, - ), - Attrs.title(panel.description), - Attrs.ariaLabel(`Open ${panel.name} panel`), - Events.onClick(PanelSwitcher(TogglePanel(panel.id))), - }, - list{ - // Connection dot - if panel.hasBackend { - renderStatusDot(panel.connectionStatus) - } else { - noNode - }, - // Panel name - span(list{Attrs.class_("text-sm truncate flex-1")}, list{text(panel.name)}), - // Short name badge - span( - list{Attrs.class_("text-xs text-gray-500 font-mono opacity-60")}, - list{text(panel.shortName)}, - ), - }, - ), - // Detach button (pop-out into separate window) - button( - list{ - Attrs.class_( - "px-1 py-1 text-gray-600 hover:text-gray-300 opacity-0 group-hover:opacity-100 transition-opacity rounded hover:bg-gray-600/50", - ), - Attrs.title(`Detach ${panel.name} into separate window`), - Attrs.ariaLabel(`Detach ${panel.name}`), - Events.onClick(Tiling(DetachPanel(panel.id))), - }, - list{ - // Pop-out icon (Unicode box with arrow) - span(list{Attrs.class_("text-xs")}, list{text("\xe2\x86\x97")}), - }, - ), - }, - ) -} - -/// Render the slide-out panel list for an expanded group. -let renderGroupPanels = ( - group: groupDef, - panels: array, - activePanel: option, -): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "absolute right-full top-0 mr-1 w-56 bg-gray-900/95 border border-gray-700 rounded-lg shadow-xl py-1.5 px-1.5 z-50 backdrop-blur-sm", - ), - Attrs.style("border-left-color", group.colour), - Attrs.style("border-left-width", "2px"), - }, - list{ - // Group header inside the flyout - div( - list{Attrs.class_("px-2 py-1 text-xs font-semibold tracking-wider uppercase mb-1")}, - list{ - span(list{Attrs.style("color", group.colour)}, list{text(group.label)}), - span( - list{Attrs.class_("text-gray-600 ml-1")}, - list{text(`(${Int.toString(Array.length(panels))})`)}, - ), - }, - ), - // Panel entries - div( - list{Attrs.class_("flex flex-col gap-0.5 max-h-80 overflow-y-auto")}, - panels - ->Array.map(panel => { - let isActive = activePanel === Some(panel.id) - renderPanelEntry(panel, isActive) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Render a single group header button in the sidebar strip. -let renderGroupButton = ( - group: groupDef, - isExpanded: bool, - panelCount: int, - hasActivePanel: bool, -): Tea_Vdom.t => { - let bgClass = if isExpanded { - "bg-gray-700" - } else if hasActivePanel { - "bg-gray-800" - } else { - "bg-transparent hover:bg-gray-800" - } - - button( - list{ - Attrs.class_( - `relative flex flex-col items-center justify-center w-11 py-1.5 rounded-lg ${bgClass} transition-colors cursor-pointer group`, - ), - Attrs.title(`${group.label} — ${Int.toString(panelCount)} panels`), - Attrs.ariaLabel(`${group.label} panel group — ${Int.toString(panelCount)} panels`), - Attrs.ariaExpanded(isExpanded), - Events.onClick(PanelSwitcher(ExpandGroup(group.kind))), - }, - list{ - // Colour accent bar at top - div( - list{ - Attrs.class_("w-5 h-0.5 rounded-full mb-0.5"), - Attrs.style("background-color", group.colour), - }, - list{}, - ), - // Label - span( - list{ - Attrs.class_("text-[10px] font-medium leading-tight select-none"), - Attrs.style( - "color", - if isExpanded { - group.colour - } else { - "#9ca3af" - }, - ), - }, - list{text(group.label)}, - ), - // Panel count badge - span( - list{Attrs.class_("text-[8px] text-gray-600 leading-none")}, - list{text(Int.toString(panelCount))}, - ), - // Active indicator - if hasActivePanel { - div( - list{ - Attrs.class_("absolute left-0.5 top-1/2 -translate-y-1/2 w-1 h-3 rounded-full"), - Attrs.style("background-color", group.colour), - }, - list{}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the full panel bar — vertical strip on the right edge with -/// grouped categories and slide-out panel lists. -let view = (switcher: panelSwitcherState): Tea_Vdom.t => { - // Build a map of kind → panels - let panelsByKind = groups->Array.map(group => { - let panels = switcher.panels->Array.filter(p => panelKind(p) == group.kind) - (group, panels) - }) - - div( - list{ - Attrs.class_( - "fixed right-0 top-0 bottom-0 w-12 bg-gray-900/90 border-l border-gray-800 flex flex-col items-center py-2 gap-0.5 z-50", - ), - Attrs.ariaLabel("Panel switcher — grouped by category"), - Attrs.role("navigation"), - }, - list{ - // Group buttons with optional slide-out - div( - list{Attrs.class_("flex flex-col gap-0.5 w-full px-0.5")}, - panelsByKind - ->Array.map(((group, panels)) => { - let isExpanded = switcher.expandedGroup === Some(group.kind) - let hasActivePanel = panels->Array.some(p => switcher.activePanel === Some(p.id)) - let panelCount = Array.length(panels) - - // Skip groups with no panels - if panelCount === 0 { - noNode - } else { - div( - list{Attrs.class_("relative")}, - list{ - renderGroupButton(group, isExpanded, panelCount, hasActivePanel), - // Slide-out panel list - if isExpanded { - renderGroupPanels(group, panels, switcher.activePanel) - } else { - noNode - }, - }, - ) - } - }) - ->List.fromArray, - ), - }, - ) -} diff --git a/src/components/PanicAttack.affine b/src/components/PanicAttack.affine new file mode 100644 index 00000000..76b6c736 --- /dev/null +++ b/src/components/PanicAttack.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PanicAttack; + +// TODO: Complete semantic implementation diff --git a/src/components/PanicAttack.res b/src/components/PanicAttack.res deleted file mode 100644 index 795bcef9..00000000 --- a/src/components/PanicAttack.res +++ /dev/null @@ -1,363 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Panic-Attack Panel — stress testing and weak point analysis. -/// -/// Displays scan results from panic-attack across 20 weak point categories, -/// with severity-based filtering, scan controls, report history, and -/// report comparison. Connects to the panic-attack binary via Gossamer. - -open Msg -open PanicAttackModel -open Tea.Html - -/// Severity badge colour class. -let severityClass = (sev: weakPointSeverity): string => - switch sev { - | Critical => "bg-red-600 text-white" - | High => "bg-orange-500 text-white" - | Medium => "bg-amber-400 text-gray-900" - | Low => "bg-blue-400 text-white" - | Info => "bg-gray-400 text-white" - } - -/// Severity display label. -let severityLabel = (sev: weakPointSeverity): string => - switch sev { - | Critical => "CRITICAL" - | High => "HIGH" - | Medium => "MEDIUM" - | Low => "LOW" - | Info => "INFO" - } - -/// Category display label. -let categoryLabel = (cat: weakPointCategory): string => - switch cat { - | UnsafeCode => "Unsafe Code" - | PanicPath => "Panic Path" - | CommandInjection => "Command Injection" - | UnsafeDeserialization => "Unsafe Deserialization" - | DOMInjection => "DOM Injection" - | HardcodedSecret => "Hardcoded Secret" - | PathTraversal => "Path Traversal" - | InsecureProtocol => "Insecure Protocol" - | AtomExhaustion => "Atom Exhaustion" - | UnsafeFFI => "Unsafe FFI" - | ResourceLeak => "Resource Leak" - | DeadlockPotential => "Deadlock Potential" - | RaceCondition => "Race Condition" - | ErrorHandling => "Error Handling" - | MemoryManagement => "Memory Management" - | TypeUnsafety => "Type Unsafety" - | ExceptionHandling => "Exception Handling" - | ConcurrencyIssues => "Concurrency Issues" - | DeprecatedAPIs => "Deprecated APIs" - | MissingValidation => "Missing Validation" - | DynamicCodeExecution => "Dynamic Code Execution" - | ExcessivePermissions => "Excessive Permissions" - | UncheckedError => "Unchecked Error" - | OtherCategory(name) => name - } - -/// Mode indicator badge. -let modeView = (mode: string): Tea_Vdom.t => { - let (colour, lbl) = switch mode { - | "full" => ("text-emerald-400", "FULL") - | "fallback" => ("text-amber-400", "FALLBACK") - | "unavailable" => ("text-red-400", "UNAVAILABLE") - | _ => ("text-gray-500", "PROBING...") - } - span( - list{Attrs.class_(`text-xs font-mono px-2 py-0.5 rounded ${colour} bg-gray-800`)}, - list{text(lbl)}, - ) -} - -/// Summary bar showing severity counts. -let summaryBar = (summary: option): Tea_Vdom.t => { - switch summary { - | None => - div( - list{Attrs.class_("text-gray-500 text-sm italic py-2")}, - list{text("No scan results. Select a target and run assail.")}, - ) - | Some(s) => - div( - list{Attrs.class_("flex gap-3 items-center py-2")}, - list{ - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{ - text( - `${Int.toString(s.totalFindings)} findings in ${Int.toString( - s.filesScanned, - )} files (${s.language})`, - ), - }, - ), - div( - list{Attrs.class_("flex gap-1")}, - list{ - if s.critical > 0 { - span( - list{Attrs.class_("px-2 py-0.5 text-xs rounded bg-red-600 text-white font-mono")}, - list{text(`${Int.toString(s.critical)} critical`)}, - ) - } else { - noNode - }, - if s.high > 0 { - span( - list{ - Attrs.class_("px-2 py-0.5 text-xs rounded bg-orange-500 text-white font-mono"), - }, - list{text(`${Int.toString(s.high)} high`)}, - ) - } else { - noNode - }, - if s.medium > 0 { - span( - list{ - Attrs.class_("px-2 py-0.5 text-xs rounded bg-amber-400 text-gray-900 font-mono"), - }, - list{text(`${Int.toString(s.medium)} medium`)}, - ) - } else { - noNode - }, - if s.low > 0 { - span( - list{Attrs.class_("px-2 py-0.5 text-xs rounded bg-blue-400 text-white font-mono")}, - list{text(`${Int.toString(s.low)} low`)}, - ) - } else { - noNode - }, - }, - ), - }, - ) - } -} - -/// Render a single finding row. -let findingRow = (wp: weakPoint): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-start gap-3 py-2 px-3 border-b border-gray-700 hover:bg-gray-800/50", - ), - }, - list{ - span( - list{ - Attrs.class_( - `px-1.5 py-0.5 text-xs rounded font-mono whitespace-nowrap ${severityClass( - wp.severity, - )}`, - ), - }, - list{text(severityLabel(wp.severity))}, - ), - span( - list{Attrs.class_("text-xs text-cyan-400 font-mono whitespace-nowrap min-w-[140px]")}, - list{text(categoryLabel(wp.category))}, - ), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(wp.description)}), - switch wp.file { - | "" => noNode - | file => - div( - list{Attrs.class_("text-xs text-gray-500 font-mono truncate")}, - list{ - text( - switch wp.line { - | Some(line) => `${file}:${Int.toString(line)}` - | None => file - }, - ), - }, - ) - }, - }, - ), - }, - ) -} - -/// Filter findings by the active category. -let filterFindings = ( - findings: array, - category: panicCategory, - filterText: string, -): array => { - findings->Array.filter(wp => { - let catMatch = switch category { - | AllFindings => true - | BySeverity(sev) => wp.severity == sev - | ByCategory(cat) => wp.category == cat - } - let textMatch = - filterText == "" || - String.includes(String.toLowerCase(wp.description), String.toLowerCase(filterText)) || - String.includes(String.toLowerCase(wp.file), String.toLowerCase(filterText)) - catMatch && textMatch - }) -} - -/// Main panel view. -let view = (state: panicAttackState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100 overflow-hidden")}, - list{ - // Header bar - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-3 bg-gray-800 border-b border-gray-700", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-bold text-amber-400")}, - list{text("panic-attack")}, - ), - modeView(state.mode), - switch state.version { - | Some(v) => - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(`v${v}`)}) - | None => noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - if state.scanning { - span( - list{Attrs.class_("text-xs text-amber-400 animate-pulse")}, - list{text("Scanning...")}, - ) - } else { - noNode - }, - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-amber-600 hover:bg-amber-500 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled(state.scanning || state.mode == "unavailable"), - Events.onClick(PanicAttack(RunAssail)), - KeyboardNav.onActivate(PanicAttack(RunAssail)), - }, - list{text("assail")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-red-700 hover:bg-red-600 text-white font-mono disabled:opacity-50", - ), - Attrs.disabled(state.scanning || state.mode == "unavailable"), - Events.onClick(PanicAttack(RunAssault)), - KeyboardNav.onActivate(PanicAttack(RunAssault)), - }, - list{text("assault")}, - ), - }, - ), - }, - ), - // Target path and filter bar - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-850 border-b border-gray-700"), - }, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Target:")}), - input( - list{ - Attrs.type_("text"), - Attrs.class_( - "flex-1 bg-gray-800 text-sm text-gray-200 px-2 py-1 rounded border border-gray-600 font-mono", - ), - Attrs.placeholder("/path/to/project"), - Attrs.value(state.targetPath), - Events.onInput(v => PanicAttack(SetTargetPath(v))), - }, - list{}, - ), - input( - list{ - Attrs.type_("text"), - Attrs.class_( - "w-48 bg-gray-800 text-sm text-gray-200 px-2 py-1 rounded border border-gray-600", - ), - Attrs.placeholder("Filter findings..."), - Attrs.value(state.filterText), - Events.onInput(v => PanicAttack(SetPanicFilter(v))), - }, - list{}, - ), - }, - ), - // Summary bar - div(list{Attrs.class_("px-4 border-b border-gray-700")}, list{summaryBar(state.summary)}), - // Error display - switch state.lastError { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 border-b border-red-700 text-red-300 text-sm"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Findings list - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - { - let filtered = filterFindings(state.findings, state.activeCategory, state.filterText) - if Array.length(filtered) == 0 && !state.scanning { - list{ - div( - list{Attrs.class_("flex items-center justify-center h-32 text-gray-500 text-sm")}, - list{ - text( - if Array.length(state.findings) == 0 { - "No findings yet. Run a scan to analyse your code." - } else { - "No findings match the current filter." - }, - ), - }, - ), - } - } else { - filtered->Array.map(findingRow)->List.fromArray - } - }, - ), - // Footer with report count - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-2 bg-gray-800 border-t border-gray-700 text-xs text-gray-500", - ), - }, - list{ - span(list{}, list{text(`${Int.toString(Array.length(state.reports))} saved reports`)}), - span(list{}, list{text("panic-attack 2.0.0 — 47 languages, 20 categories")}), - }, - ), - }, - ) -} diff --git a/src/components/PerformanceProfiler.affine b/src/components/PerformanceProfiler.affine new file mode 100644 index 00000000..5ca039f8 --- /dev/null +++ b/src/components/PerformanceProfiler.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PerformanceProfiler; + +// TODO: Complete semantic implementation diff --git a/src/components/PerformanceProfiler.res b/src/components/PerformanceProfiler.res deleted file mode 100644 index 5752aee2..00000000 --- a/src/components/PerformanceProfiler.res +++ /dev/null @@ -1,453 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL PerformanceProfiler — frame budget monitoring, GC pressure tracking, -/// memory snapshots, and performance alert display for IDApTIK game profiling. -/// -/// Five tabs: Frame Budget (FPS counter + frame time chart placeholder), -/// Memory (heap usage bars), GC Pressure (event log), Alerts (severity-coloured -/// list), and Flamegraph (placeholder for future integration). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for performanceTab variants. -let tabLabel = (tab: performanceTab): string => - switch tab { - | TabFrameBudget => "Frame Budget" - | TabMemory => "Memory" - | TabGcPressure => "GC Pressure" - | TabAlerts => "Alerts" - | TabFlamegraph => "Flamegraph" - } - -/// Render the tab bar. -let renderTabs = (active: performanceTab): Tea_Vdom.t => { - let tabs: array = [ - TabFrameBudget, - TabMemory, - TabGcPressure, - TabAlerts, - TabFlamegraph, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(PerformanceProfiler(SetPpTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Alert severity colour and label. -let severityStyle = (sev: perfAlertSeverity): (string, string) => - switch sev { - | PerfInfo => ("bg-blue-600 text-white", "INFO") - | PerfWarning => ("bg-amber-500 text-white", "WARN") - | PerfCritical => ("bg-red-600 text-white", "CRIT") - } - -/// Format bytes into a human-readable string (KB/MB/GB). -let formatBytes = (bytes: int): string => { - let b = Int.toFloat(bytes) - if b >= 1073741824.0 { - `${Float.toFixed(b /. 1073741824.0, ~digits=2)} GB` - } else if b >= 1048576.0 { - `${Float.toFixed(b /. 1048576.0, ~digits=1)} MB` - } else if b >= 1024.0 { - `${Float.toFixed(b /. 1024.0, ~digits=0)} KB` - } else { - `${Int.toString(bytes)} B` - } -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Frame Budget tab: FPS counter, budget display, and recent frame samples. -let renderFrameBudgetTab = (state: performanceProfilerState): Tea_Vdom.t => { - let sampleCount = Array.length(state.frameSamples) - let avgFps = if sampleCount > 0 { - let totalMs = state.frameSamples->Array.reduce(0.0, (acc, s) => acc +. s.totalMs) - let avg = totalMs /. Int.toFloat(sampleCount) - if avg > 0.0 { - 1000.0 /. avg - } else { - 0.0 - } - } else { - 0.0 - } - let fpsColour = if avgFps >= state.targetFps { - "text-emerald-400" - } else if avgFps >= state.targetFps *. 0.75 { - "text-amber-400" - } else { - "text-red-400" - } - - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // FPS + budget row - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("p-4 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_(`text-3xl font-light ${fpsColour}`)}, - list{text(Float.toFixed(avgFps, ~digits=1))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("AVG FPS")}), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-3xl font-light text-cyan-400")}, - list{text(Float.toFixed(state.targetFps, ~digits=0))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("TARGET FPS")}), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-3xl font-light text-gray-300")}, - list{text(`${Float.toFixed(state.frameBudgetMs, ~digits=1)}ms`)}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("BUDGET")}), - }, - ), - }, - ), - // Frame time chart placeholder - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-32 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text(`Frame time chart (${Int.toString(sampleCount)} samples)`)}, - ), - }, - ), - // Recent samples list - div( - list{Attrs.class_("flex flex-col gap-1 max-h-40 overflow-y-auto")}, - state.frameSamples - ->Array.sliceToEnd(~start=max(0, sampleCount - 10)) - ->Array.map(s => { - let overBudget = s.totalMs > state.frameBudgetMs - let cls = overBudget ? "text-red-400" : "text-gray-400" - div( - list{Attrs.class_("flex justify-between text-xs font-mono px-2 py-1")}, - list{ - span( - list{Attrs.class_("text-gray-500")}, - list{text(`#${Int.toString(s.frameNumber)}`)}, - ), - span(list{Attrs.class_(cls)}, list{text(`${Float.toFixed(s.totalMs, ~digits=2)}ms`)}), - span( - list{Attrs.class_("text-gray-600")}, - list{ - text( - `R:${Float.toFixed(s.renderMs, ~digits=1)} U:${Float.toFixed( - s.updateMs, - ~digits=1, - )} GC:${Float.toFixed(s.gcMs, ~digits=1)}`, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Memory tab: heap usage display with usage bars. -let renderMemoryTab = (state: performanceProfilerState): Tea_Vdom.t => { - let latest = state.memorySnapshots->Array.get(Array.length(state.memorySnapshots) - 1) - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - switch latest { - | Some(snap) => { - let usedPct = - Int.toFloat(snap.heapUsedBytes) /. Int.toFloat(max(1, snap.heapTotalBytes)) *. 100.0 - let usedWidth = Int.toString(Int.fromFloat(usedPct)) - let barColour = if usedPct > 90.0 { - "bg-red-500" - } else if usedPct > 70.0 { - "bg-amber-500" - } else { - "bg-emerald-500" - } - div( - list{Attrs.class_("bg-gray-800 rounded p-4")}, - list{ - div( - list{Attrs.class_("flex justify-between text-sm mb-2")}, - list{ - span(list{Attrs.class_("text-gray-300")}, list{text("Heap Usage")}), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{ - text( - `${formatBytes(snap.heapUsedBytes)} / ${formatBytes(snap.heapTotalBytes)}`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("w-full h-3 bg-gray-700 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full ${barColour} transition-all duration-300 w-[${usedWidth}%]`, - ), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3 mt-3")}, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-300")}, - list{text(formatBytes(snap.externalBytes))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("External")}), - }, - ), - div( - list{Attrs.class_("text-center")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-300")}, - list{text(formatBytes(snap.arrayBufferBytes))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("ArrayBuffers")}), - }, - ), - }, - ), - }, - ) - } - | None => - div( - list{Attrs.class_("text-gray-500 text-sm italic p-4")}, - list{text("No memory snapshots yet. Start profiling to collect data.")}, - ) - }, - }, - ) -} - -/// GC Pressure tab: event log of garbage collection pauses. -let renderGcPressureTab = (state: performanceProfilerState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.gcEvents))} GC event(s) recorded`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - state.gcEvents - ->Array.map(evt => { - let pauseColour = if evt.pauseMs > 16.0 { - "text-red-400" - } else if evt.pauseMs > 5.0 { - "text-amber-400" - } else { - "text-gray-400" - } - div( - list{ - Attrs.class_( - "flex items-center justify-between px-3 py-2 bg-gray-800 rounded text-xs font-mono", - ), - }, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text(evt.kind)}), - span( - list{Attrs.class_(pauseColour)}, - list{text(`${Float.toFixed(evt.pauseMs, ~digits=2)}ms`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`-${formatBytes(evt.reclaimedBytes)}`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Alerts tab: severity-coloured list of performance alerts. -let renderAlertsTab = (state: performanceProfilerState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.alerts))} alert(s)`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - state.alerts - ->Array.map(alert => { - let (sevCls, sevLbl) = severityStyle(alert.severity) - div( - list{Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-800 rounded text-sm")}, - list{ - span( - list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded font-mono ${sevCls}`)}, - list{text(sevLbl)}, - ), - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(alert.message)}), - span( - list{Attrs.class_("text-gray-500 text-xs font-mono")}, - list{text(`${alert.metric}: ${Float.toFixed(alert.value, ~digits=1)}`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Flamegraph tab: placeholder for future integration. -let renderFlamegraphTab = (_state: performanceProfilerState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4 flex items-center justify-center h-64")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Flamegraph integration pending (Phase 2)")}, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: performanceProfilerState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabFrameBudget => renderFrameBudgetTab(state) - | TabMemory => renderMemoryTab(state) - | TabGcPressure => renderGcPressureTab(state) - | TabAlerts => renderAlertsTab(state) - | TabFlamegraph => renderFlamegraphTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with Start/Stop Profiling - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Performance Profiler")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(PerformanceProfiler(StartProfiling)), - KeyboardNav.onActivate(PerformanceProfiler(StartProfiling)), - }, - list{text("Start Profiling")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-red-700 text-white rounded hover:bg-red-600 cursor-pointer font-medium", - ), - Events.onClick(PerformanceProfiler(StopProfiling)), - KeyboardNav.onActivate(PerformanceProfiler(StopProfiling)), - }, - list{text("Stop Profiling")}, - ), - }, - ), - }, - ), - // Profiling indicator - if state.profiling { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-red-400 rounded-full animate-pulse")}, list{}), - span(list{Attrs.class_("text-sm text-red-300")}, list{text("Profiling active...")}), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/Playgrounds.affine b/src/components/Playgrounds.affine new file mode 100644 index 00000000..7dc8aef5 --- /dev/null +++ b/src/components/Playgrounds.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Playgrounds; + +// TODO: Complete semantic implementation diff --git a/src/components/Playgrounds.res b/src/components/Playgrounds.res deleted file mode 100644 index 820cf481..00000000 --- a/src/components/Playgrounds.res +++ /dev/null @@ -1,585 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Playgrounds Component — Code sandbox and NQC console. -/// -/// Multi-language editor (VCL, KQL, GQL, ReScript, Gleam, Idris2, Nickel), -/// NQC database console connecting to proxy at :4000, snippet library, -/// and tutorial mode. - -open Model -open Msg -open Tea.Html - -/// Render the language selector radio group (VCL, KQL, GQL, ReScript, Gleam, Idris2, Nickel). -let renderLanguageSelector = (active: playgroundLanguage): Tea_Vdom.t => { - let langs: array = [ - LangVcl, - LangKql, - LangGql, - LangRescript, - LangGleam, - LangIdris2, - LangNickel, - ] - div( - list{ - Attrs.class_("flex gap-1 flex-wrap"), - Attrs.role("radiogroup"), - Attrs.ariaLabel("Select language"), - }, - langs - ->Array.map(lang => { - let isActive = lang === active - let isDb = PlaygroundsEngine.isDbLanguage(lang) - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded transition-colors ${isActive - ? "bg-indigo-600 text-white" - : isDb - ? "bg-gray-800 text-teal-400 hover:bg-gray-700" - : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`, - ), - Attrs.role("radio"), - Attrs.ariaSelected(isActive), - Events.onClick(Playgrounds(SetLanguage(lang))), - }, - list{text(PlaygroundsEngine.languageLabel(lang))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the category tab bar (Editor, NQC Console, Snippets, Tutorials). -let renderTabs = (active: playgroundsCategory): Tea_Vdom.t => { - let tabs: array = [PlayEditor, PlayNqc, PlaySnippets, PlayTutorials] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-teal-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Playgrounds(SetPlayCategory(tab))), - }, - list{text(PlaygroundsEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Main Playgrounds panel view — full-screen overlay with code editor, NQC console, and snippets. -let view = (pg: playgroundsState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Playgrounds code sandbox"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Playgrounds")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("code sandbox + NQC console")}, - ), - if pg.nqcConnected { - span(list{Attrs.class_("text-xs text-green-400 ml-2")}, list{text("NQC connected")}) - } else { - span( - list{Attrs.class_("text-xs text-gray-600 ml-2")}, - list{text("NQC disconnected")}, - ) - }, - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700"), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(pg.activeCategory), - switch pg.activeCategory { - | PlayEditor => - div( - list{Attrs.class_("space-y-4")}, - list{ - renderLanguageSelector(pg.activeLanguage), - // Editor area - div( - list{Attrs.class_("flex gap-4 h-80")}, - list{ - // Code input - div( - list{Attrs.class_("flex-1 flex flex-col")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{ - text( - `${PlaygroundsEngine.languageLabel( - pg.activeLanguage, - )} ${PlaygroundsEngine.languageExt(pg.activeLanguage)}`, - ), - }, - ), - textarea( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded p-3 font-mono text-sm text-gray-200 resize-none", - ), - Attrs.placeholder("Write code here..."), - Attrs.ariaLabel("Code editor"), - Attrs.value(pg.editorContent), - Events.onInput(v => Playgrounds(UpdateCode(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - `mt-2 px-4 py-2 text-sm rounded ${pg.executing - ? "bg-gray-700 text-gray-400" - : "bg-teal-600 text-white hover:bg-teal-500"}`, - ), - Events.onClick(Playgrounds(Execute)), - KeyboardNav.onActivate(Playgrounds(Execute)), - }, - list{text(pg.executing ? "Running..." : "Execute")}, - ), - }, - ), - // Output pane - div( - list{Attrs.class_("flex-1 flex flex-col")}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Output")}), - div( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded p-3 font-mono text-xs text-gray-300 overflow-auto", - ), - Attrs.role("log"), - }, - list{ - switch pg.lastResult { - | Some(result) => - if result.success { - div( - list{}, - list{ - div( - list{Attrs.class_("text-green-400 mb-1")}, - list{ - text( - `OK (${Float.toFixed( - result.durationMs, - ~digits=1, - )}ms, ${Int.toString(result.rowCount)} rows)`, - ), - }, - ), - switch result.data { - | Some(data) => - pre( - list{Attrs.class_("text-gray-300 whitespace-pre-wrap")}, - list{text(data)}, - ) - | None => noNode - }, - }, - ) - } else { - div( - list{Attrs.class_("text-red-400")}, - list{ - text( - switch result.error { - | Some(e) => e - | None => "Unknown error" - }, - ), - }, - ) - } - | None => - div( - list{Attrs.class_("text-gray-600")}, - list{text("No output yet. Write code and hit Execute.")}, - ) - }, - }, - ), - }, - ), - }, - ), - }, - ) - | PlayNqc => - div( - list{Attrs.class_("space-y-4")}, - list{ - // NQC language selector (VCL/KQL/GQL only) - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_("flex gap-1"), - Attrs.role("radiogroup"), - Attrs.ariaLabel("NQC query language"), - }, - [LangVcl, LangKql, LangGql] - ->Array.map(lang => { - let isActive = lang === pg.nqcLanguage - let accentColor = switch lang { - | LangVcl => "bg-teal-600 text-white" - | LangKql => "bg-purple-600 text-white" - | LangGql => "bg-amber-600 text-white" - | _ => "bg-indigo-600 text-white" - } - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded transition-colors ${isActive - ? accentColor - : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`, - ), - Attrs.role("radio"), - Attrs.ariaSelected(isActive), - Events.onClick(Playgrounds(SetNqcLanguage(lang))), - }, - list{text(PlaygroundsEngine.languageLabel(lang))}, - ) - }) - ->List.fromArray, - ), - // Connection indicator - div( - list{ - Attrs.class_( - `flex items-center gap-1 text-xs ${pg.nqcConnected - ? "text-green-400" - : "text-gray-600"}`, - ), - }, - list{ - div( - list{ - Attrs.class_( - `w-1.5 h-1.5 rounded-full ${pg.nqcConnected - ? "bg-green-400" - : "bg-gray-600"}`, - ), - }, - list{}, - ), - text(pg.nqcConnected ? "Connected to :4000" : "Disconnected"), - }, - ), - // Clear history - if Array.length(pg.nqcHistory) > 0 { - button( - list{ - Attrs.class_("ml-auto text-xs text-gray-600 hover:text-gray-400"), - Events.onClick(Playgrounds(ClearNqcHistory)), - KeyboardNav.onActivate(Playgrounds(ClearNqcHistory)), - }, - list{text("Clear History")}, - ) - } else { - noNode - }, - }, - ), - // Query input + execute - div( - list{Attrs.class_("flex gap-2")}, - list{ - textarea( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded p-3 font-mono text-sm text-gray-200 resize-none h-20", - ), - Attrs.placeholder( - switch pg.nqcLanguage { - | LangVcl => "SELECT * FROM entities WHERE confidence > 0.9 LIMIT 10" - | LangKql => "MATCH (n:Concept)-[r:RELATES_TO]->(m) RETURN n, r, m" - | LangGql => "{ entities(filter: {type: \"document\"}) { id name confidence } }" - | _ => "Enter query..." - }, - ), - Attrs.ariaLabel("NQC query input"), - Attrs.value(pg.nqcInput), - Events.onInput(v => Playgrounds(SetNqcInput(v))), - }, - list{}, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - list{ - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded flex-1 ${pg.executing - ? "bg-gray-700 text-gray-400" - : "bg-teal-600 text-white hover:bg-teal-500"}`, - ), - Events.onClick(Playgrounds(ExecuteNqc)), - KeyboardNav.onActivate(Playgrounds(ExecuteNqc)), - Attrs.disabled(pg.executing || pg.nqcInput === ""), - }, - list{text(pg.executing ? "Running..." : "Execute")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-600 text-center")}, - list{text(PlaygroundsEngine.languageLabel(pg.nqcLanguage))}, - ), - }, - ), - }, - ), - // Last result - switch pg.lastResult { - | Some(result) => - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3")}, - list{ - if result.success { - div( - list{}, - list{ - div( - list{Attrs.class_("flex items-center gap-3 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs text-green-400 font-medium")}, - list{text("OK")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Float.toFixed(result.durationMs, ~digits=1)}ms`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(result.rowCount)} rows`)}, - ), - }, - ), - switch result.data { - | Some(data) => - pre( - list{ - Attrs.class_( - "text-xs text-gray-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto", - ), - }, - list{text(data)}, - ) - | None => noNode - }, - }, - ) - } else { - div( - list{Attrs.class_("text-xs text-red-400")}, - list{ - text( - switch result.error { - | Some(e) => e - | None => "Unknown error" - }, - ), - }, - ) - }, - }, - ) - | None => noNode - }, - // Query history - if Array.length(pg.nqcHistory) > 0 { - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider")}, - list{text("Query History")}, - ), - div( - list{Attrs.class_("space-y-1 max-h-48 overflow-y-auto")}, - pg.nqcHistory - ->Array.map(((query, lang, result)) => { - let statusColor = switch result { - | Some(r) => r.success ? "border-l-green-600" : "border-l-red-600" - | None => "border-l-gray-600" - } - div( - list{ - Attrs.class_( - `flex items-center gap-2 p-2 bg-gray-900/50 rounded border-l-2 ${statusColor} cursor-pointer hover:bg-gray-800/50`, - ), - Events.onClick(Playgrounds(SetNqcInput(query))), - }, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-8")}, - list{text(PlaygroundsEngine.languageLabel(lang))}, - ), - span( - list{ - Attrs.class_("flex-1 text-xs text-gray-400 font-mono truncate"), - }, - list{text(query)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ) - | PlaySnippets => - div( - list{Attrs.class_("space-y-3")}, - pg.snippets - ->Array.map(s => - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-3 hover:border-gray-500 cursor-pointer", - ), - Events.onClick(Playgrounds(LoadSnippet(s.id))), - }, - list{ - div( - list{Attrs.class_("flex justify-between items-center mb-1")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(s.title)}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(PlaygroundsEngine.languageLabel(s.language))}, - ), - }, - ), - pre(list{Attrs.class_("text-xs text-gray-400 truncate")}, list{text(s.code)}), - }, - ) - ) - ->List.fromArray, - ) - | PlayTutorials => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{ - Attrs.class_("text-sm font-medium text-gray-400 border-b border-gray-800 pb-2"), - }, - list{text("Interactive Tutorials")}, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium mb-2")}, - list{text("Getting Started")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Tutorials run in the sandbox environment. Select a language, follow the guided steps, and experiment with code in a safe, isolated context.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium mb-2")}, - list{text("NQC Playground")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Explore NQC queries against QuandleDB. Write and test quantum-safe database operations in a sandboxed environment.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium mb-2")}, - list{text("Proof Sketching")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Draft proof outlines using the ECHIDNA multi-solver dispatch. Connect Panel-L constraints to see how formal verification works.", - ), - }, - ), - }, - ), - }, - ) - }, - switch pg.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/PlaytestRecorder.affine b/src/components/PlaytestRecorder.affine new file mode 100644 index 00000000..2bb60864 --- /dev/null +++ b/src/components/PlaytestRecorder.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module PlaytestRecorder; + +// TODO: Complete semantic implementation diff --git a/src/components/PlaytestRecorder.res b/src/components/PlaytestRecorder.res deleted file mode 100644 index 480c0a87..00000000 --- a/src/components/PlaytestRecorder.res +++ /dev/null @@ -1,396 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Playtest Recorder Component — record, replay, and annotate gameplay -/// sessions. Displays Record/Stop/Play buttons, session timeline, annotation -/// list with timestamps, and session library. - -open Model -open Msg -open Tea.Html - -/// Render a playback state indicator. -let playbackIndicator = (pb: playbackState): Tea_Vdom.t => { - let (color, label) = switch pb { - | Stopped => ("text-gray-500", "Stopped") - | Playing(t) => ("text-green-400 animate-pulse", "Playing " ++ Float.toFixed(t, ~digits=1) ++ "s") - | Paused(t) => ("text-yellow-400", "Paused " ++ Float.toFixed(t, ~digits=1) ++ "s") - | Recording => ("text-red-400 animate-pulse", "Recording") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Format duration from milliseconds to human-readable form. -let formatDuration = (ms: float): string => { - let seconds = ms /. 1000.0 - if seconds >= 60.0 { - let mins = Math.floor(seconds /. 60.0) - let secs = seconds -. mins *. 60.0 - Float.toFixed(mins, ~digits=0) ++ "m " ++ Float.toFixed(secs, ~digits=0) ++ "s" - } else { - Float.toFixed(seconds, ~digits=1) ++ "s" - } -} - -/// Render an annotation category badge. -let categoryBadge = (cat: string): Tea_Vdom.t => { - let color = switch cat { - | "bug" => "bg-red-700 text-red-100" - | "balance" => "bg-yellow-700 text-yellow-100" - | "design" => "bg-blue-700 text-blue-100" - | "ux" => "bg-purple-700 text-purple-100" - | _ => "bg-gray-700 text-gray-300" - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(cat)}) -} - -/// Main view function for the Playtest Recorder panel. -let view = (state: playtestRecorderState): Tea_Vdom.t => { - let sessionCount = Array.length(state.sessions) - let annotationCount = Array.length(state.annotations) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Playtest Recorder — Session Recording and Replay"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-red-300")}, - list{text("Playtest Recorder")}, - ), - playbackIndicator(state.playback), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - // Record button - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ - switch state.playback { - | Recording => "bg-red-600 text-white" - | _ => "bg-red-800 hover:bg-red-700 text-white" - }, - ), - Events.onClick(PlaytestRecorder(PrStarted)), - KeyboardNav.onActivate(PlaytestRecorder(PrStarted)), - }, - list{ - text( - switch state.playback { - | Recording => "Stop" - | _ => "Record" - }, - ), - }, - ), - // Play button - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ - switch state.playback { - | Playing(_) => "bg-green-600 text-white" - | _ => "bg-green-800 hover:bg-green-700 text-white" - }, - ), - }, - list{text("Play")}, - ), - }, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Record { - "bg-red-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(PlaytestRecorder(SetPrCategory(Record))), - }, - list{text("Record")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Replay { - "bg-red-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(PlaytestRecorder(SetPrCategory(Replay))), - }, - list{text("Replay")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Annotations { - "bg-red-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(PlaytestRecorder(SetPrCategory(Annotations))), - }, - list{text("Annotations (" ++ Int.toString(annotationCount) ++ ")")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Sessions { - "bg-red-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(PlaytestRecorder(SetPrCategory(Sessions))), - }, - list{text("Sessions (" ++ Int.toString(sessionCount) ++ ")")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(PlaytestRecorder(DismissPrError)), - KeyboardNav.onActivate(PlaytestRecorder(DismissPrError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Record => - switch state.currentSession { - | Some(session) => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text("Session: " ++ session.name)}), - span(list{}, list{text("Duration: " ++ formatDuration(session.durationMs))}), - span(list{}, list{text("Actions: " ++ Int.toString(session.actionCount))}), - }, - ), - // Timeline placeholder - div( - list{ - Attrs.class_( - "w-full h-8 bg-gray-900 border border-gray-800 rounded relative overflow-hidden", - ), - }, - list{ - div(list{Attrs.class_("h-full bg-red-800/30")}, list{}), - // Annotation markers - div( - list{Attrs.class_("absolute inset-0 flex items-center")}, - session.annotations - ->Array.map(ann => { - let pos = if session.durationMs > 0.0 { - ann.timestamp *. 1000.0 /. session.durationMs *. 100.0 - } else { - 0.0 - } - div( - list{ - Attrs.class_("absolute w-1 h-full bg-yellow-500 opacity-70"), - Attrs.style("left", Float.toFixed(pos, ~digits=1) ++ "%"), - }, - list{}, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Press Record to begin capturing a playtest session.")}, - ) - } - | Replay => - switch state.currentSession { - | Some(session) => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("text-sm text-red-300 font-bold")}, - list{text(session.name)}, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text("Started: " ++ session.startedAt)}), - span(list{}, list{text("Duration: " ++ formatDuration(session.durationMs))}), - span(list{}, list{text("Actions: " ++ Int.toString(session.actionCount))}), - }, - ), - // Playback timeline - div( - list{Attrs.class_("w-full h-3 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-green-500 transition-all"), - Attrs.style( - "width", - switch state.playback { - | Playing(t) | Paused(t) => - Float.toFixed( - if session.durationMs > 0.0 { - t *. 1000.0 /. session.durationMs *. 100.0 - } else { - 0.0 - }, - ~digits=1, - ) ++ "%" - | _ => "0%" - }, - ), - }, - list{}, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-500 py-8")}, - list{text("Select a session to replay.")}, - ) - } - | Annotations => - div( - list{Attrs.class_("space-y-2")}, - state.annotations - ->Array.map(ann => { - let isSelected = state.selectedAnnotation == Some(ann.id) - div( - list{ - Attrs.class_( - "px-3 py-2 border rounded " ++ if isSelected { - "bg-red-900/20 border-red-700" - } else { - "bg-gray-900 border-gray-800" - }, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 font-mono w-16")}, - list{text(Float.toFixed(ann.timestamp, ~digits=1) ++ "s")}, - ), - categoryBadge(ann.category), - span( - list{Attrs.class_("text-sm text-gray-200 flex-1")}, - list{text(ann.text)}, - ), - }, - ), - switch ann.screenshotPath { - | Some(path) => - div( - list{Attrs.class_("text-xs text-gray-500 mt-1 font-mono")}, - list{text("Screenshot: " ++ path)}, - ) - | None => Tea_Html.noNode - }, - }, - ) - }) - ->List.fromArray, - ) - | Sessions => - div( - list{Attrs.class_("space-y-2")}, - state.sessions - ->Array.map(s => - div( - list{ - Attrs.class_( - "px-3 py-2 bg-gray-900 border border-gray-800 rounded cursor-pointer hover:border-gray-700", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-gray-200")}, - list{text(s.name)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(formatDuration(s.durationMs))}, - ), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500 mt-1")}, - list{ - span(list{}, list{text("Started: " ++ s.startedAt)}), - span(list{}, list{text(Int.toString(s.actionCount) ++ " actions")}), - span( - list{}, - list{text(Int.toString(Array.length(s.annotations)) ++ " annotations")}, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Plaza.affine b/src/components/Plaza.affine new file mode 100644 index 00000000..fb6d449b --- /dev/null +++ b/src/components/Plaza.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Plaza; + +// TODO: Complete semantic implementation diff --git a/src/components/Plaza.res b/src/components/Plaza.res deleted file mode 100644 index 5ac9db3c..00000000 --- a/src/components/Plaza.res +++ /dev/null @@ -1,828 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Palimpsest Plaza Component — view layer for the PMPL licensing panel. -/// -/// The Plaza is designed to be the first thing a new FOSS developer encounters -/// when they open PanLL. It should feel welcoming, helpful, and demonstrate -/// that PMPL licensing isn't bureaucracy — it's protection. -/// -/// Layout: -/// - Header with PMPL branding, adoption stats, and close button -/// - Category tabs (Dashboard | Compliance | Provenance | Compatibility | Ethical Use | Governance | Adopt) -/// - Content area varies by tab - -open Model -open Msg -open Tea.Html - -/// Render a single category tab. -let renderCategoryTab = (cat: plazaCategory, isActive: bool): Tea_Vdom.t => { - let activeClass = isActive - ? "border-indigo-500 text-indigo-300 bg-gray-800/50" - : "border-transparent text-gray-500 hover:text-gray-300 hover:border-gray-600" - - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium border-b-2 cursor-pointer transition-colors ${activeClass}`, - ), - Attrs.role("tab"), - Events.onClick(Plaza(SetPlazaCategory(cat))), - }, - list{text(PlazaEngine.categoryLabel(cat))}, - ) -} - -/// Render the category tab bar. -let renderCategoryTabBar = (activeCategory: plazaCategory): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex border-b border-gray-800 overflow-x-auto"), - Attrs.role("tablist"), - Attrs.ariaLabel("Plaza categories"), - }, - PlazaEngine.allCategories - ->Array.map(cat => renderCategoryTab(cat, cat === activeCategory)) - ->List.fromArray, - ) -} - -/// Render a stat card for the dashboard. -let renderStatCard = (label: string, value: string, colour: string, subtitle: string): Tea_Vdom.t< - msg, -> => { - div( - list{Attrs.class_("bg-gray-800/50 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text(label)}, - ), - div(list{Attrs.class_(`text-2xl font-bold ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-xs text-gray-600 mt-1")}, list{text(subtitle)}), - }, - ) -} - -/// Render a progress bar with label. -let renderProgressBar = (label: string, count: int, total: int, colour: string): Tea_Vdom.t< - msg, -> => { - let pct = if total > 0 { - Float.toFixed(Int.toFloat(count) /. Int.toFloat(total) *. 100.0, ~digits=0) - } else { - "0" - } - div( - list{Attrs.class_("flex items-center gap-3 py-1")}, - list{ - div(list{Attrs.class_("w-32 text-sm text-gray-400")}, list{text(label)}), - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_(`h-full ${colour} rounded-full transition-all`), - Attrs.style("width", `${pct}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-20 text-xs text-gray-500 text-right")}, - list{text(`${Int.toString(count)} (${pct}%)`)}, - ), - }, - ) -} - -/// Render the Dashboard tab — adoption statistics overview. -let renderDashboard = (plaza: plazaState): Tea_Vdom.t => { - switch plaza.stats { - | None => - div( - list{Attrs.class_("flex-1 flex flex-col items-center justify-center gap-4")}, - list{ - div( - list{Attrs.class_("text-gray-400 text-lg")}, - list{text("Palimpsest License Ecosystem")}, - ), - div( - list{Attrs.class_("text-gray-600 text-sm max-w-md text-center")}, - list{ - text( - "Scan your ecosystem to see PMPL adoption, compliance, and provenance statistics across all repositories.", - ), - }, - ), - button( - list{ - Attrs.class_( - "px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 transition-colors font-medium", - ), - Events.onClick(Plaza(LoadAdoptionStats)), - KeyboardNav.onActivate(Plaza(LoadAdoptionStats)), - }, - list{text("Scan Ecosystem")}, - ), - }, - ) - | Some(stats) => - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6 space-y-6")}, - list{ - // Stat cards row - div( - list{Attrs.class_("grid grid-cols-5 gap-4")}, - list{ - renderStatCard( - "Total Repos", - Int.toString(stats.totalRepos), - "text-gray-200", - "in ecosystem", - ), - renderStatCard( - "PMPL Licensed", - Int.toString(stats.pmplRepos), - "text-indigo-400", - `${Float.toFixed(PlazaEngine.adoptionPercentage(stats), ~digits=1)}% adoption`, - ), - renderStatCard( - "MPL-2.0 Fallback", - Int.toString(stats.mplFallbackRepos), - "text-amber-400", - "platform requirement", - ), - renderStatCard( - "Unlicensed", - Int.toString(stats.unlicensedRepos), - stats.unlicensedRepos > 0 ? "text-red-400" : "text-emerald-400", - stats.unlicensedRepos > 0 ? "need attention" : "all licensed", - ), - renderStatCard( - "Quantum-Signed", - Int.toString(stats.quantumSignedRepos), - "text-purple-400", - "post-quantum provenance", - ), - }, - ), - // License breakdown - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("License Distribution")}, - ), - div( - list{Attrs.class_("space-y-1")}, - stats.byLicense - ->Array.map(((license, count)) => { - let colour = switch license { - | "PMPL-1.0-or-later" => "bg-indigo-500" - | "MPL-2.0" => "bg-amber-500" - | "MIT" => "bg-emerald-500" - | "Apache-2.0" => "bg-blue-500" - | "unlicensed" => "bg-red-500" - | _ => "bg-gray-500" - } - renderProgressBar(license, count, stats.totalRepos, colour) - }) - ->List.fromArray, - ), - }, - ), - // Quick actions - div( - list{Attrs.class_("flex gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors text-sm", - ), - Events.onClick(Plaza(LoadAdoptionStats)), - KeyboardNav.onActivate(Plaza(LoadAdoptionStats)), - }, - list{text("Refresh Stats")}, - ), - }, - ), - }, - ) - } -} - -/// Render the Compatibility tab — license compatibility checker. -let renderCompatibility = (_plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6")}, - list{ - div( - list{Attrs.class_("max-w-2xl")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-300 mb-4")}, - list{text("PMPL Compatibility Matrix")}, - ), - div( - list{Attrs.class_("text-sm text-gray-500 mb-6")}, - list{ - text( - "PMPL-1.0-or-later uses file-level copyleft (inherited from MPL-2.0), making it compatible with most permissive and weak-copyleft licenses. PMPL files and MIT files can coexist in the same project.", - ), - }, - ), - // Static compatibility table - div( - list{Attrs.class_("space-y-2")}, - [ - ("MIT", true, "Fully compatible"), - ("Apache-2.0", true, "Compatible with patent grant"), - ("BSD-2/3-Clause", true, "Fully compatible"), - ("MPL-2.0", true, "Base layer — fully compatible"), - ("GPL-2.0+", true, "Compatible via MPL-2.0 Section 3.3"), - ("GPL-3.0+", true, "Compatible via MPL-2.0 Section 3.3"), - ("LGPL-2.1+", true, "Compatible for library use"), - ("AGPL-3.0", false, "Network copyleft conflicts with file-level scope"), - ("ISC", true, "Fully compatible"), - ("CC0/Unlicense", true, "Public domain — compatible with anything"), - ] - ->Array.map(((license, compat, notes)) => { - let statusClass = compat - ? "text-emerald-400 bg-emerald-900/30 border-emerald-800" - : "text-red-400 bg-red-900/30 border-red-800" - let statusLabel = compat ? "Compatible" : "Incompatible" - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 rounded border border-gray-800 bg-gray-800/20", - ), - }, - list{ - div( - list{Attrs.class_("w-32 text-sm font-medium text-gray-300")}, - list{text(license)}, - ), - span( - list{Attrs.class_(`text-xs px-2 py-0.5 rounded border ${statusClass}`)}, - list{text(statusLabel)}, - ), - div(list{Attrs.class_("flex-1 text-xs text-gray-500")}, list{text(notes)}), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ) -} - -/// Render the Adopt tab — quick-start guide for new projects. -let renderAdoptionWizard = (_plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6")}, - list{ - div( - list{Attrs.class_("max-w-2xl space-y-6")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-300 mb-2")}, - list{text("Adopt PMPL for Your Project")}, - ), - div( - list{Attrs.class_("text-sm text-gray-500 mb-6")}, - list{ - text( - "Three steps to protect your work with the Palimpsest License. File-level copyleft means you can mix PMPL with MIT, Apache, BSD — no project-wide infection.", - ), - }, - ), - // Step 1 - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span(list{Attrs.class_("text-indigo-400 font-bold")}, list{text("1")}), - span( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Add LICENSE file")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono bg-gray-900 rounded p-2")}, - list{ - text( - "Copy LICENSE.txt from palimpsest-license/v1.0/LICENSE.txt to your project root", - ), - }, - ), - }, - ), - // Step 2 - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span(list{Attrs.class_("text-indigo-400 font-bold")}, list{text("2")}), - span( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Add SPDX headers to source files")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono bg-gray-900 rounded p-2")}, - list{text("// SPDX-License-Identifier: MPL-2.0")}, - ), - }, - ), - // Step 3 - div( - list{Attrs.class_("bg-gray-800/30 border border-gray-800 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span(list{Attrs.class_("text-indigo-400 font-bold")}, list{text("3")}), - span( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Optional: Add exhibits and provenance")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Exhibit A (Ethical Use) and Exhibit B (Quantum-Safe Provenance) are optional but recommended for projects with AI training or long-term archival needs.", - ), - }, - ), - }, - ), - // Why PMPL - div( - list{Attrs.class_("border-t border-gray-800 pt-4 mt-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-400 mb-2")}, - list{text("Why PMPL over MPL-2.0?")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 space-y-1")}, - list{ - div( - list{}, - list{text("Ethical Use Guidelines — community norms for responsible use")}, - ), - div( - list{}, - list{ - text( - "Quantum-Safe Provenance — attribution that survives decades, not just years", - ), - }, - ), - div( - list{}, - list{ - text("Emotional Lineage — recognition that code carries cultural meaning"), - }, - ), - div( - list{}, - list{ - text( - "Same file-level copyleft as MPL-2.0 — no GPL-style project infection", - ), - }, - ), - }, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render a compliance level badge. -let renderComplianceBadge = (level: complianceLevel): Tea_Vdom.t => { - let (label, colour) = switch level { - | FullCompliance => ("Full", "bg-green-700") - | PartialCompliance => ("Partial", "bg-yellow-700") - | NonCompliant => ("Non-Compliant", "bg-red-700") - | Unknown => ("Unknown", "bg-gray-700") - } - span(list{Attrs.class_(`px-2 py-0.5 rounded text-xs ${colour} text-white`)}, list{text(label)}) -} - -/// Render the Compliance Audit tab — SPDX headers, LICENSE files, exhibit completeness. -let renderCompliance = (plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("mb-4 flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`${Int.toString(Array.length(plaza.audits))} repos audited`)}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-800 text-indigo-200 rounded hover:bg-indigo-700", - ), - Attrs.ariaLabel("Run compliance scan on all repos"), - Events.onClick(Plaza(LoadAdoptionStats)), - KeyboardNav.onActivate(Plaza(LoadAdoptionStats)), - }, - list{text("Scan All")}, - ), - }, - ), - if Array.length(plaza.audits) == 0 { - div( - list{Attrs.class_("text-center py-12")}, - list{ - div(list{Attrs.class_("text-gray-500 mb-2")}, list{text("No audits yet")}), - div( - list{Attrs.class_("text-xs text-gray-600 max-w-md mx-auto")}, - list{ - text( - "Scan repos for SPDX headers, LICENSE files, and exhibit completeness. Connect to pmpl-audit for deep scanning.", - ), - }, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - plaza.audits - ->Array.map(audit => - div( - list{ - Attrs.class_( - "p-3 bg-gray-900/50 rounded border border-gray-800 flex items-center justify-between", - ), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300 flex items-center gap-2")}, - list{text(audit.repoName), renderComplianceBadge(audit.level)}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{ - text( - `${Int.toString(audit.filesWithHeaders)}/${Int.toString( - audit.filesScanned, - )} files with SPDX headers`, - ), - }, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-600")}, list{text(audit.lastAudit)}), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the Provenance Verification tab — quantum-safe signatures. -let renderProvenance = (plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("mb-4 flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`${Int.toString(Array.length(plaza.signatures))} signatures`)}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-800 text-indigo-200 rounded hover:bg-indigo-700", - ), - Attrs.ariaLabel("Verify all provenance signatures"), - Events.onClick(Plaza(LoadAdoptionStats)), - KeyboardNav.onActivate(Plaza(LoadAdoptionStats)), - }, - list{text("Verify All")}, - ), - }, - ), - if Array.length(plaza.signatures) == 0 { - div( - list{Attrs.class_("text-center py-12")}, - list{ - div(list{Attrs.class_("text-gray-500 mb-2")}, list{text("No signatures found")}), - div( - list{Attrs.class_("text-xs text-gray-600 max-w-md mx-auto")}, - list{ - text( - "Verify quantum-safe signatures (ML-DSA, SLH-DSA) on files and commits. Connect to pmpl-verify for signature chain validation.", - ), - }, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - plaza.signatures - ->Array.map(sig => { - let (statusText, statusColour) = switch sig.status { - | SignatureValid => ("Valid", "text-green-400") - | SignatureInvalid(reason) => (`Invalid: ${reason}`, "text-red-400") - | NoSignature => ("Missing", "text-gray-500") - | ClassicalOnly => ("Classical", "text-yellow-400") - } - div( - list{Attrs.class_("p-3 bg-gray-900/50 rounded border border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div(list{Attrs.class_("text-sm text-gray-300")}, list{text(sig.target)}), - span(list{Attrs.class_(`text-xs ${statusColour}`)}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-1 flex items-center gap-3")}, - list{ - text(`Algorithm: ${sig.algorithm}`), - text(`Signer: ${sig.signer}`), - text(sig.timestamp), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the Ethical Use Guide tab — AI training disclosure and responsible use. -let renderEthicalUse = (_plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("max-w-2xl mx-auto space-y-6")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 border-b border-gray-800 pb-2")}, - list{text("Ethical Use Guide (Exhibit A)")}, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800 space-y-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("AI Training Disclosure")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "If you use PMPL-licensed code in AI training datasets, Exhibit A requires disclosure. Provide a clear statement in your model card or data sheet identifying the PMPL-licensed sources used.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800 space-y-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("Cultural Sensitivity")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Respect the cultural context of code contributions. Attribution should preserve original authorship information and acknowledge cultural origins where relevant.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800 space-y-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("Responsible Use")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "PMPL code should not be used in systems designed to cause harm, violate human rights, or circumvent legal protections. The stewardship council reviews edge cases.", - ), - }, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render the Governance tab — stewardship decisions and amendments. -let renderGovernance = (_plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("max-w-2xl mx-auto space-y-6")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 border-b border-gray-800 pb-2")}, - list{text("Stewardship Council")}, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800 space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("License Governance")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "The Palimpsest License is governed by the Stewardship Council. Proposed amendments require council review and community feedback. Governance decisions are recorded in the transparency log.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800 space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 font-medium")}, - list{text("Amendment Process")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "1. Proposal submission with rationale. 2. Community comment period (30 days). 3. Council deliberation. 4. Decision recorded in transparency log. 5. Implementation in next license version.", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("p-4 bg-gray-900/50 rounded border border-gray-800 space-y-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400 font-medium")}, list{text("Contact")}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text("Governance proposals and ethical use questions: j.d.a.jewell@open.ac.uk"), - }, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render content based on active category. -let renderContent = (plaza: plazaState): Tea_Vdom.t => { - switch plaza.activeCategory { - | Dashboard => renderDashboard(plaza) - | Compatibility => renderCompatibility(plaza) - | Adopt => renderAdoptionWizard(plaza) - | Compliance => renderCompliance(plaza) - | Provenance => renderProvenance(plaza) - | EthicalUse => renderEthicalUse(plaza) - | Governance => renderGovernance(plaza) - } -} - -/// Render the header bar. -let renderHeader = (plaza: plazaState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center justify-between px-6 py-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Palimpsest Plaza")}, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("PMPL-1.0-or-later adoption and governance hub")}, - ), - switch plaza.stats { - | Some(stats) => - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{Attrs.class_("text-indigo-400")}, - list{text(`${Int.toString(stats.pmplRepos)} PMPL`)}, - ), - span(list{Attrs.class_("text-gray-700")}, list{text("/")}), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(stats.totalRepos)} repos`)}, - ), - }, - ) - | None => noNode - }, - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ) -} - -/// Main Plaza panel view — full-screen overlay. -let view = (plaza: plazaState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Palimpsest Plaza panel"), - }, - list{ - renderHeader(plaza), - renderCategoryTabBar(plaza.activeCategory), - if plaza.loading { - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-gray-500 animate-pulse")}, - list{text("Scanning ecosystem...")}, - ), - }, - ) - } else { - switch plaza.error { - | Some(e) => - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-center")}, - list{ - div(list{Attrs.class_("text-red-400 mb-2")}, list{text("Error")}), - div(list{Attrs.class_("text-sm text-gray-500")}, list{text(e)}), - }, - ), - }, - ) - | None => renderContent(plaza) - } - }, - }, - ) -} diff --git a/src/components/ProofChain.affine b/src/components/ProofChain.affine new file mode 100644 index 00000000..de9ed607 --- /dev/null +++ b/src/components/ProofChain.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProofChain; + +// TODO: Complete semantic implementation diff --git a/src/components/ProofChain.res b/src/components/ProofChain.res deleted file mode 100644 index 433a3f8d..00000000 --- a/src/components/ProofChain.res +++ /dev/null @@ -1,551 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// ProofChain — CI/CD-style visual proof pipeline. -/// -/// Transforms an ECHIDNA interactive proof session into a GitHub Actions–like -/// pipeline view. Each node is a proof step (goal or tactic application) with -/// status indicators (green/amber/red), connectors showing dependencies, and -/// dashed outlines for detected gaps. -/// -/// Visual language: -/// ● Green (discharged) — goal solved, tactic succeeded -/// ◐ Amber (in progress) — goal exists, work underway -/// ○ Red (failed/stuck) — tactic didn't close, error -/// ╌ Dashed (gap) — missing step detected by constraint propagation -/// -/// The pipeline reads top-to-bottom: root goal → tactic applications → subgoals. -/// Branching occurs when a tactic (e.g., induction) produces multiple subgoals. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// Proof Chain Node Types -// =========================================================================== - -/// The status of a node in the proof pipeline. -type nodeStatus = - | Discharged // Goal solved — all green - | Active // Currently being worked on - | Pending // Exists but no tactic applied yet - | Failed // Tactic failed or goal stuck - | Gap // Missing step detected - -/// The type of a node in the proof pipeline. -type nodeType = - | GoalNode // A proof obligation / goal - | TacticNode // A tactic application - | QedNode // Final QED / proof complete marker - -/// A single node in the proof pipeline graph. -type pipelineNode = { - id: string, - label: string, - detail: string, - status: nodeStatus, - nodeType: nodeType, - children: array, // IDs of child nodes -} - -/// The full pipeline graph. -type pipelineGraph = { - nodes: array, - rootId: string, -} - -// =========================================================================== -// Session → Pipeline Graph Conversion -// =========================================================================== - -/// Build a pipeline graph from an ECHIDNA session state. -/// The graph structure: -/// Root Goal → [Tactic 1] → [Subgoal A, Subgoal B, ...] -/// → [Tactic 2] → [Subgoal C, ...] -/// → QED (if complete) -let buildGraph = (session: echidnaSessionState): pipelineGraph => { - let nodes: array = [] - - // Root goal node - let rootId = "goal-root" - let rootStatus = if session.complete { - Discharged - } else if Array.length(session.goals) === 0 { - Discharged - } else { - Active - } - - let rootNode = { - id: rootId, - label: "Goal", - detail: session.goal, - status: rootStatus, - nodeType: GoalNode, - children: [], - } - let _ = Array.push(nodes, rootNode) - - // Build tactic chain - let prevNodeId = ref(rootId) - - session.proofScript->Array.forEachWithIndex((tactic, idx) => { - let tacticId = "tactic-" ++ Int.toString(idx) - - // Determine tactic status - let tacticStatus = if session.complete || idx < Array.length(session.proofScript) - 1 { - Discharged // Past tactics are resolved - } else { - // Last tactic — check session status - switch session.status { - | ProofSuccess => Discharged - | InProgress => Active - | ProofFailed => Failed - | ProofError => Failed - | ProofTimeout => Failed - | Pending => Pending - } - } - - let tacticNode = { - id: tacticId, - label: tactic, - detail: "Step " ++ Int.toString(idx + 1), - status: tacticStatus, - nodeType: TacticNode, - children: [], - } - let _ = Array.push(nodes, tacticNode) - - // Link previous node to this tactic - nodes->Array.forEach(n => { - if n.id === prevNodeId.contents { - let _ = Array.push(n.children, tacticId) - } - }) - - prevNodeId := tacticId - }) - - // Add remaining goals as pending subgoals off the last tactic - session.goals->Array.forEachWithIndex((goal, idx) => { - let goalId = "subgoal-" ++ Int.toString(idx) - let goalStatus = if idx === 0 { - Active - } else { - Pending - } - - let goalNode = { - id: goalId, - label: "Subgoal " ++ Int.toString(idx + 1), - detail: goal, - status: goalStatus, - nodeType: GoalNode, - children: [], - } - let _ = Array.push(nodes, goalNode) - - // Link from last tactic (or root if no tactics applied) - nodes->Array.forEach(n => { - if n.id === prevNodeId.contents { - let _ = Array.push(n.children, goalId) - } - }) - }) - - // If proof is complete, add QED node - if session.complete { - let qedId = "qed" - let qedNode = { - id: qedId, - label: "QED", - detail: "Proof complete", - status: Discharged, - nodeType: QedNode, - children: [], - } - let _ = Array.push(nodes, qedNode) - - // Link from last node - nodes->Array.forEach(n => { - if n.id === prevNodeId.contents { - let _ = Array.push(n.children, qedId) - } - }) - } - - // Detect gaps: if there are remaining goals but no tactic suggestions, - // add gap nodes to signal missing steps - if !session.complete && Array.length(session.goals) > 0 && Array.length(session.proofScript) > 0 { - session.goals->Array.forEachWithIndex((_, idx) => { - let subgoalId = "subgoal-" ++ Int.toString(idx) - let gapId = "gap-" ++ Int.toString(idx) - let gapNode = { - id: gapId, - label: "?", - detail: "Missing tactic — apply a step to close this goal", - status: Gap, - nodeType: TacticNode, - children: [], - } - let _ = Array.push(nodes, gapNode) - - // Link from the subgoal to the gap - nodes->Array.forEach(n => { - if n.id === subgoalId { - let _ = Array.push(n.children, gapId) - } - }) - }) - } - - {nodes, rootId} -} - -// =========================================================================== -// Pipeline Rendering -// =========================================================================== - -/// Status colour classes for node badges. -let statusClasses = (status: nodeStatus): (string, string, string) => { - // (border, bg, text) - switch status { - | Discharged => ("border-green-500", "bg-green-900/40", "text-green-400") - | Active => ("border-blue-500", "bg-blue-900/40", "text-blue-400") - | Pending => ("border-amber-500", "bg-amber-900/30", "text-amber-400") - | Failed => ("border-red-500", "bg-red-900/40", "text-red-400") - | Gap => ("border-gray-500 border-dashed", "bg-gray-800/30", "text-gray-500") - } -} - -/// Status icon character. -let statusIcon = (status: nodeStatus): string => { - switch status { - | Discharged => `\u2713` // ✓ - | Active => `\u25D0` // ◐ - | Pending => `\u25CB` // ○ - | Failed => `\u2717` // ✗ - | Gap => "?" - } -} - -/// Status label text. -let statusLabel = (status: nodeStatus): string => { - switch status { - | Discharged => "Discharged" - | Active => "In progress" - | Pending => "Pending" - | Failed => "Failed" - | Gap => "Gap detected" - } -} - -/// Node type icon. -let nodeTypeIcon = (nt: nodeType): string => { - switch nt { - | GoalNode => `\u25A0` // ■ - | TacticNode => `\u25B6` // ▶ - | QedNode => `\u2605` // ★ - } -} - -/// Render a vertical connector line between pipeline stages. -let renderConnector = (status: nodeStatus): Tea_Vdom.t => { - let (_, _, textColour) = statusClasses(status) - let lineStyle = switch status { - | Gap => "border-l border-dashed border-gray-600" - | _ => "border-l border-gray-600" - } - div( - list{Attrs.class_("flex justify-center py-0")}, - list{ - div(list{Attrs.class_(`h-4 w-0 ml-4 ${lineStyle}`), Attrs.ariaHidden(true)}, list{}), - div(list{Attrs.class_(`text-[8px] ${textColour} ml-1 self-center`)}, list{text({`\u25BC`})}), // ▼ - }, - ) -} - -/// Render a single pipeline node. -let renderNode = (node: pipelineNode): Tea_Vdom.t => { - let (borderClass, bgClass, textClass) = statusClasses(node.status) - let icon = statusIcon(node.status) - let typeIcon = nodeTypeIcon(node.nodeType) - - div( - list{ - Attrs.class_(`flex items-start gap-2 p-2 rounded-lg border ${borderClass} ${bgClass} mx-1`), - Attrs.title(node.detail), - Attrs.ariaLabel(node.label ++ " — " ++ statusLabel(node.status)), - }, - list{ - // Status icon - div( - list{Attrs.class_(`text-sm ${textClass} w-5 text-center flex-shrink-0 mt-0.5`)}, - list{text(icon)}, - ), - // Content - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - // Header row: type icon + label + status badge - div( - list{Attrs.class_("flex items-center gap-1.5")}, - list{ - span(list{Attrs.class_(`text-[10px] ${textClass}`)}, list{text(typeIcon)}), - span(list{Attrs.class_(`text-xs font-bold ${textClass}`)}, list{text(node.label)}), - span( - list{ - Attrs.class_( - `text-[10px] px-1.5 py-0 rounded ${bgClass} ${textClass} border ${borderClass} ml-auto`, - ), - }, - list{text(statusLabel(node.status))}, - ), - }, - ), - // Detail (truncated) - if node.detail !== "" && node.detail !== node.label { - div( - list{Attrs.class_("text-[11px] text-gray-400 font-mono truncate mt-0.5")}, - list{text(node.detail)}, - ) - } else { - noNode - }, - }, - ), - }, - ) -} - -/// Render a branch — a node and all its children recursively. -let rec renderBranch = (graph: pipelineGraph, nodeId: string, depth: int): Tea_Vdom.t => { - let maybeNode = graph.nodes->Array.find(n => n.id === nodeId) - switch maybeNode { - | None => noNode - | Some(node) => - div( - list{Attrs.class_("flex flex-col")}, - list{ - renderNode(node), - // Children - if Array.length(node.children) === 0 { - noNode - } else if Array.length(node.children) === 1 { - // Single child — straight connector - let childId = node.children[0]->Option.getOr("") - let childNode = graph.nodes->Array.find(n => n.id === childId) - let childStatus = switch childNode { - | Some(cn) => cn.status - | None => Pending - } - div(list{}, list{renderConnector(childStatus), renderBranch(graph, childId, depth + 1)}) - } else { - // Multiple children — branching pipeline - div( - list{Attrs.class_("mt-1")}, - list{ - // Branch indicator - div( - list{Attrs.class_("flex items-center gap-1 ml-4 mb-1")}, - list{ - div(list{Attrs.class_("h-px flex-1 bg-gray-600")}, list{}), - span( - list{Attrs.class_("text-[9px] text-gray-500 px-1")}, - list{text(Int.toString(Array.length(node.children)) ++ " branches")}, - ), - div(list{Attrs.class_("h-px flex-1 bg-gray-600")}, list{}), - }, - ), - // Render each branch - div( - list{Attrs.class_("grid gap-1 pl-3 border-l border-gray-700")}, - node.children - ->Array.map(childId => renderBranch(graph, childId, depth + 1)) - ->List.fromArray, - ), - }, - ) - }, - }, - ) - } -} - -// =========================================================================== -// Summary Stats -// =========================================================================== - -/// Count nodes by status in the pipeline. -let countByStatus = (graph: pipelineGraph, status: nodeStatus): int => { - graph.nodes->Array.filter(n => n.status === status)->Array.length -} - -/// Render pipeline summary stats — a compact status bar. -let renderSummaryStats = (graph: pipelineGraph): Tea_Vdom.t => { - let discharged = countByStatus(graph, Discharged) - let active = countByStatus(graph, Active) - let pending = countByStatus(graph, Pending) - let failed = countByStatus(graph, Failed) - let gaps = countByStatus(graph, Gap) - let total = Array.length(graph.nodes) - - // Progress percentage (discharged / total non-gap nodes) - let nonGapTotal = total - gaps - let progressPct = if nonGapTotal > 0 { - Int.toString(Int.fromFloat(Int.toFloat(discharged) /. Int.toFloat(nonGapTotal) *. 100.0)) - } else { - "0" - } - - div( - list{Attrs.class_("flex items-center gap-3 text-[10px] mb-2 px-1")}, - list{ - // Progress bar - div( - list{ - Attrs.class_("flex-1 h-1.5 bg-gray-800 rounded-full overflow-hidden"), - Attrs.role("progressbar"), - Attrs.ariaLabel("Proof progress"), - Attrs.ariaValueNow(Int.toFloat(discharged)), - Attrs.ariaValueMax(Int.toFloat(nonGapTotal)), - }, - list{ - div( - list{ - Attrs.class_("h-full bg-green-500 transition-all duration-300"), - Attrs.style("width", progressPct ++ "%"), - }, - list{}, - ), - }, - ), - span(list{Attrs.class_("text-gray-400")}, list{text(progressPct ++ "%")}), - // Status counts - if discharged > 0 { - span( - list{Attrs.class_("text-green-400")}, - list{text({`\u2713`} ++ Int.toString(discharged))}, - ) - } else { - noNode - }, - if active > 0 { - span(list{Attrs.class_("text-blue-400")}, list{text({`\u25D0`} ++ Int.toString(active))}) - } else { - noNode - }, - if pending > 0 { - span(list{Attrs.class_("text-amber-400")}, list{text({`\u25CB`} ++ Int.toString(pending))}) - } else { - noNode - }, - if failed > 0 { - span(list{Attrs.class_("text-red-400")}, list{text({`\u2717`} ++ Int.toString(failed))}) - } else { - noNode - }, - if gaps > 0 { - span(list{Attrs.class_("text-gray-500")}, list{text("?" ++ Int.toString(gaps))}) - } else { - noNode - }, - }, - ) -} - -// =========================================================================== -// Public API -// =========================================================================== - -/// Render the full proof chain pipeline for an ECHIDNA session. -/// This is the CI/CD-style visual proof view. -let view = (session: echidnaSessionState): Tea_Vdom.t => { - let graph = buildGraph(session) - - div( - list{ - Attrs.class_("mt-3 p-3 bg-gray-850/50 rounded-lg border border-indigo-700/30"), - Attrs.role("region"), - Attrs.ariaLabel("Proof Pipeline"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold text-indigo-400 uppercase tracking-wider")}, - list{text("Proof Pipeline")}, - ), - span( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text(Int.toString(Array.length(graph.nodes)) ++ " nodes")}, - ), - }, - ), - // Legend - div( - list{Attrs.class_("flex items-center gap-2 text-[9px]")}, - list{ - span(list{Attrs.class_("text-green-400")}, list{text({`\u2713`} ++ " done")}), - span(list{Attrs.class_("text-blue-400")}, list{text({`\u25D0`} ++ " active")}), - span(list{Attrs.class_("text-amber-400")}, list{text({`\u25CB`} ++ " pending")}), - span(list{Attrs.class_("text-red-400")}, list{text({`\u2717`} ++ " failed")}), - span(list{Attrs.class_("text-gray-500")}, list{text("? gap")}), - }, - ), - }, - ), - // Summary stats bar - renderSummaryStats(graph), - // Pipeline graph - div( - list{ - Attrs.class_("max-h-64 overflow-y-auto pr-1"), - Attrs.role("tree"), - Attrs.ariaLabel("Proof pipeline tree"), - }, - list{renderBranch(graph, graph.rootId, 0)}, - ), - }, - ) -} - -/// Render a compact inline proof status for use outside the ECHIDNA panel. -/// Shows a single-line summary: "3/5 goals ✓ 60%" with a tiny progress bar. -let viewCompact = (session: echidnaSessionState): Tea_Vdom.t => { - let graph = buildGraph(session) - let discharged = countByStatus(graph, Discharged) - let total = Array.length(graph.nodes) - countByStatus(graph, Gap) - let pct = if total > 0 { - Int.toString(Int.fromFloat(Int.toFloat(discharged) /. Int.toFloat(total) *. 100.0)) - } else { - "0" - } - - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span(list{Attrs.class_("text-indigo-400 font-bold")}, list{text("Proof")}), - div( - list{ - Attrs.class_("w-16 h-1 bg-gray-800 rounded-full overflow-hidden"), - Attrs.role("progressbar"), - Attrs.ariaLabel("Proof progress"), - }, - list{ - div(list{Attrs.class_("h-full bg-green-500"), Attrs.style("width", pct ++ "%")}, list{}), - }, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text(Int.toString(discharged) ++ "/" ++ Int.toString(total) ++ " " ++ pct ++ "%")}, - ), - }, - ) -} diff --git a/src/components/ProofsBridge.affine b/src/components/ProofsBridge.affine new file mode 100644 index 00000000..bbc08872 --- /dev/null +++ b/src/components/ProofsBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProofsBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/ProofsBridge.res b/src/components/ProofsBridge.res deleted file mode 100644 index acfac8bc..00000000 --- a/src/components/ProofsBridge.res +++ /dev/null @@ -1,412 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Proofs Bridge Component — proven repo formal verification integration. -/// Displays proven module list with proof coverage bars, verification result -/// badges, and overall coverage percentage. - -open Model -open Msg -open Tea.Html - -/// Render a module verification status badge. -let moduleStatusBadge = (status: moduleVerificationStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | FullyProven => ("bg-green-700 text-green-100", "Fully Proven") - | PartiallyProven => ("bg-yellow-700 text-yellow-100", "Partial") - | Unverified => ("bg-gray-700 text-gray-300", "Unverified") - | Stale => ("bg-orange-700 text-orange-100", "Stale") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render a verification result kind badge. -let verificationKindBadge = (kind: verificationResultKind): Tea_Vdom.t => { - let (color, label) = switch kind { - | VerificationProved => ("text-green-400", "Proved") - | VerificationCounterexample => ("text-red-400", "Counterexample") - | VerificationTimeout => ("text-yellow-400", "Timeout") - | VerificationError => ("text-red-500", "Error") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Main view function for the Proofs Bridge panel. -let view = (state: proofsBridgeState): Tea_Vdom.t => { - let totalModules = Array.length(state.provenModules) - let fullyProvenCount = - state.provenModules->Array.filter(m => m.status == FullyProven)->Array.length - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Proofs Bridge — Proven Repo Formal Verification"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-lime-300")}, - list{text("Proofs Bridge")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(fullyProvenCount) ++ - "/" ++ - Int.toString(totalModules) ++ " fully proven", - ), - }, - ), - // Overall coverage percentage - span( - list{ - Attrs.class_( - "text-xs font-bold " ++ if state.coveragePercent >= 80.0 { - "text-green-400" - } else if state.coveragePercent >= 50.0 { - "text-yellow-400" - } else { - "text-red-400" - }, - ), - }, - list{text(Float.toFixed(state.coveragePercent, ~digits=1) ++ "% coverage")}, - ), - if state.verifying { - span( - list{Attrs.class_("text-xs text-yellow-400 animate-pulse")}, - list{text("Verifying...")}, - ) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-lime-800 hover:bg-lime-700 text-white rounded"), - Events.onClick(ProofsBridge(PrBStarted)), - KeyboardNav.onActivate(ProofsBridge(PrBStarted)), - }, - list{text("Verify All")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Modules { - "bg-lime-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProofsBridge(SetPrBTab(Modules))), - }, - list{text("Modules")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Proofs { - "bg-lime-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProofsBridge(SetPrBTab(Proofs))), - }, - list{text("Proofs")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Coverage { - "bg-lime-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProofsBridge(SetPrBTab(Coverage))), - }, - list{text("Coverage")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Verification { - "bg-lime-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProofsBridge(SetPrBTab(Verification))), - }, - list{text("Verification")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(ProofsBridge(DismissPrBError)), - KeyboardNav.onActivate(ProofsBridge(DismissPrBError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Modules => - div( - list{Attrs.class_("space-y-2")}, - state.provenModules - ->Array.map(m => { - let coveragePct = if m.functionCount > 0 { - Int.toFloat(m.provedCount) /. Int.toFloat(m.functionCount) *. 100.0 - } else { - 0.0 - } - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-lime-300")}, - list{text(m.name)}, - ), - moduleStatusBadge(m.status), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(m.provedCount) ++ - "/" ++ - Int.toString(m.functionCount) ++ " proved", - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(m.description)}, - ), - // Proof coverage bar (green fill) - div( - list{Attrs.class_("w-full h-2 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-green-500 transition-all"), - Attrs.style("width", Float.toFixed(coveragePct, ~digits=1) ++ "%"), - }, - list{}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - | Proofs => - div( - list{Attrs.class_("space-y-1")}, - state.verificationResults - ->Array.map(r => - div( - list{Attrs.class_("py-2 border-b border-gray-800/50")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - verificationKindBadge(r.kind), - span( - list{Attrs.class_("text-sm text-gray-200 font-mono")}, - list{text(r.moduleName ++ "." ++ r.functionName)}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(r.proverUsed)}), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(Float.toFixed(r.durationMs, ~digits=0) ++ "ms")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400 font-mono mt-1")}, - list{text(r.specification)}, - ), - switch r.counterexample { - | Some(ce) => - div( - list{Attrs.class_("text-xs text-red-400 mt-1")}, - list{text("Counterexample: " ++ ce)}, - ) - | None => Tea_Html.noNode - }, - switch r.errorMessage { - | Some(e) => - div( - list{Attrs.class_("text-xs text-red-400 mt-1")}, - list{text("Error: " ++ e)}, - ) - | None => Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ) - | Coverage => - div( - list{Attrs.class_("space-y-4")}, - list{ - // Overall coverage display - div( - list{Attrs.class_("text-center py-4")}, - list{ - div( - list{ - Attrs.class_( - "text-4xl font-bold " ++ if state.coveragePercent >= 80.0 { - "text-green-400" - } else if state.coveragePercent >= 50.0 { - "text-yellow-400" - } else { - "text-red-400" - }, - ), - }, - list{text(Float.toFixed(state.coveragePercent, ~digits=1) ++ "%")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text("Overall proof coverage")}, - ), - }, - ), - // Per-module coverage bars - div( - list{Attrs.class_("space-y-2")}, - state.provenModules - ->Array.map(m => { - let pct = if m.functionCount > 0 { - Int.toFloat(m.provedCount) /. Int.toFloat(m.functionCount) *. 100.0 - } else { - 0.0 - } - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-300 w-24 truncate")}, - list{text(m.name)}, - ), - div( - list{Attrs.class_("flex-1 h-3 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_("h-full bg-green-500"), - Attrs.style("width", Float.toFixed(pct, ~digits=1) ++ "%"), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 w-12 text-right")}, - list{text(Float.toFixed(pct, ~digits=0) ++ "%")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - | Verification => - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{ - text( - Int.toString( - Array.length(state.verificationResults), - ) ++ " verification results", - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - state.verificationResults - ->Array.filter(r => r.kind != VerificationProved) - ->Array.map(r => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - verificationKindBadge(r.kind), - span( - list{Attrs.class_("text-sm text-gray-200 font-mono")}, - list{text(r.moduleName ++ "." ++ r.functionName)}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text(r.specification)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/ProtocolBridge.affine b/src/components/ProtocolBridge.affine new file mode 100644 index 00000000..3dafe8b1 --- /dev/null +++ b/src/components/ProtocolBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProtocolBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/ProtocolBridge.res b/src/components/ProtocolBridge.res deleted file mode 100644 index deb50c4a..00000000 --- a/src/components/ProtocolBridge.res +++ /dev/null @@ -1,384 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Protocol Bridge Component — multiplayer sync protocol analysis. -/// Displays channel list with status dots, message log, latency display, -/// and protocol rules. - -open Model -open Msg -open Tea.Html - -/// Render a channel status dot (green for active, red for error/disconnected). -let channelStatusDot = (status: channelStatus): Tea_Vdom.t => { - let color = switch status { - | ChannelActive => "bg-green-500" - | ChannelIdle => "bg-green-700" - | ChannelDegraded => "bg-yellow-500" - | ChannelDisconnected => "bg-red-500" - | ChannelError => "bg-red-600 animate-pulse" - } - span(list{Attrs.class_("w-2.5 h-2.5 rounded-full inline-block " ++ color)}, list{}) -} - -/// Render a channel status label. -let channelStatusLabel = (status: channelStatus): string => { - switch status { - | ChannelActive => "Active" - | ChannelIdle => "Idle" - | ChannelDegraded => "Degraded" - | ChannelDisconnected => "Disconnected" - | ChannelError => "Error" - } -} - -/// Render a protocol rule status badge. -let ruleStatusBadge = (status: protocolRuleStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | RuleVerified => ("bg-green-700 text-green-100", "Verified") - | RuleUnverified => ("bg-gray-700 text-gray-300", "Unverified") - | RuleViolated => ("bg-red-700 text-red-100", "Violated") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Main view function for the Protocol Bridge panel. -let view = (state: protocolBridgeState): Tea_Vdom.t => { - let activeChannels = state.channels->Array.filter(c => c.status == ChannelActive)->Array.length - let totalChannels = Array.length(state.channels) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Protocol Bridge — Multiplayer Sync Protocol Analysis"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-sky-300")}, - list{text("Protocol Bridge")}, - ), - span( - list{ - Attrs.class_( - "text-xs " ++ if state.connected { - "text-green-400" - } else { - "text-red-400" - }, - ), - }, - list{ - text( - if state.connected { - "Monitor active" - } else { - "Disconnected" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(activeChannels) ++ - "/" ++ - Int.toString(totalChannels) ++ " channels active", - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-sky-800 hover:bg-sky-700 text-white rounded"), - Events.onClick(ProtocolBridge(PbStarted)), - KeyboardNav.onActivate(ProtocolBridge(PbStarted)), - }, - list{text("Refresh")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Channels { - "bg-sky-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProtocolBridge(SetPbTab(Channels))), - }, - list{text("Channels")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Messages { - "bg-sky-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProtocolBridge(SetPbTab(Messages))), - }, - list{text("Messages")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Latency { - "bg-sky-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProtocolBridge(SetPbTab(Latency))), - }, - list{text("Latency")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Rules { - "bg-sky-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ProtocolBridge(SetPbTab(Rules))), - }, - list{text("Rules")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(ProtocolBridge(DismissPbError)), - KeyboardNav.onActivate(ProtocolBridge(DismissPbError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Channels => - div( - list{Attrs.class_("space-y-2")}, - state.channels - ->Array.map(ch => - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-3 py-2 bg-gray-900 border border-gray-800 rounded", - ), - }, - list{ - channelStatusDot(ch.status), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text(ch.name)}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - ch.protocol ++ - " | " ++ - Int.toString(ch.subscribers) ++ " subscribers", - ), - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(channelStatusLabel(ch.status))}, - ), - span( - list{ - Attrs.class_( - "text-xs font-mono " ++ if ch.latencyMs > 100.0 { - "text-red-400" - } else if ch.latencyMs > 50.0 { - "text-yellow-400" - } else { - "text-green-400" - }, - ), - }, - list{text(Float.toFixed(ch.latencyMs, ~digits=1) ++ "ms")}, - ), - }, - ) - ) - ->List.fromArray, - ) - | Messages => - div( - list{Attrs.class_("space-y-1")}, - state.messageLog - ->Array.map(m => { - let dirLabel = switch m.direction { - | MessageInbound => "IN" - | MessageOutbound => "OUT" - } - let dirColor = switch m.direction { - | MessageInbound => "text-blue-400" - | MessageOutbound => "text-green-400" - } - div( - list{ - Attrs.class_( - "flex items-center gap-2 py-1 border-b border-gray-800/30 text-xs", - ), - }, - list{ - span(list{Attrs.class_("font-mono w-8 " ++ dirColor)}, list{text(dirLabel)}), - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(m.messageType)}), - span( - list{Attrs.class_("text-gray-500 font-mono")}, - list{text(Int.toString(m.payloadBytes) ++ "B")}, - ), - if !m.valid { - span(list{Attrs.class_("text-red-400")}, list{text("INVALID")}) - } else { - span(list{Attrs.class_("text-green-600")}, list{text("OK")}) - }, - }, - ) - }) - ->List.fromArray, - ) - | Latency => - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-3")}, - list{ - text( - Int.toString( - Array.length(state.latencySamples), - ) ++ " latency samples recorded", - ), - }, - ), - div( - list{}, - state.latencySamples - ->Array.map(sample => - div( - list{ - Attrs.class_("flex items-center gap-3 py-1 border-b border-gray-800/30"), - }, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 font-mono w-24")}, - list{text(sample.channelId)}, - ), - // Latency bar - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - "h-full " ++ if sample.exceededThreshold { - "bg-red-500" - } else { - "bg-sky-500" - }, - ), - Attrs.style( - "width", - Float.toFixed( - Math.min(sample.latencyMs /. 2.0, 100.0), - ~digits=1, - ) ++ "%", - ), - }, - list{}, - ), - }, - ), - span( - list{ - Attrs.class_( - "text-xs font-mono w-16 text-right " ++ if sample.exceededThreshold { - "text-red-400" - } else { - "text-gray-400" - }, - ), - }, - list{text(Float.toFixed(sample.latencyMs, ~digits=1) ++ "ms")}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | Rules => - div( - list{Attrs.class_("space-y-2")}, - state.protocolRules - ->Array.map(r => - div( - list{Attrs.class_("flex items-center gap-3 py-2 border-b border-gray-800/50")}, - list{ - ruleStatusBadge(r.status), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text(r.name)}), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(r.description)}), - }, - ), - span( - list{Attrs.class_("text-xs text-sky-400 font-mono")}, - list{text(r.expression)}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/ProtocolSquisher.affine b/src/components/ProtocolSquisher.affine new file mode 100644 index 00000000..224455f1 --- /dev/null +++ b/src/components/ProtocolSquisher.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProtocolSquisher; + +// TODO: Complete semantic implementation diff --git a/src/components/ProtocolSquisher.res b/src/components/ProtocolSquisher.res deleted file mode 100644 index 8747ad55..00000000 --- a/src/components/ProtocolSquisher.res +++ /dev/null @@ -1,407 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Protocol-Squisher Component — format analysis and compatibility panel. -/// -/// Analyse serialisation schemas across 13 formats, compare compatibility, -/// and view transport class classifications (Concorde/Business/Economy/Wheelbarrow). - -open Model -open Msg -open Tea.Html - -/// Render a transport class badge. -let renderTransportBadge = (tc: transportClass): Tea_Vdom.t => { - let label = ProtocolSquisherEngine.transportClassLabel(tc) - let colour = ProtocolSquisherEngine.transportClassColour(tc) - span(list{Attrs.class_(`px-2 py-0.5 text-xs font-medium rounded ${colour}`)}, list{text(label)}) -} - -/// Render an analysis result card. -let renderAnalysisCard = (result: analysisResult): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4 space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-300 truncate")}, - list{text(result.filePath)}, - ), - renderTransportBadge(result.transportClass), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500")}, - list{ - span(list{}, list{text(ProtocolSquisherEngine.formatLabel(result.format))}), - span(list{}, list{text(`${Int.toString(result.fieldCount)} fields`)}), - span(list{}, list{text(`${Float.toFixed(result.overheadRatio, ~digits=2)}x overhead`)}), - if result.hasRecursion { - span(list{Attrs.class_("text-amber-500")}, list{text("recursive")}) - } else { - noNode - }, - }, - ), - div(list{Attrs.class_("text-sm text-gray-400")}, list{text(result.summary)}), - }, - ) -} - -/// Render category tabs. -let renderTabs = (active: protocolSquisherCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - ProtocolSquisherEngine.allCategories - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-cyan-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(ProtocolSquisher(SetPsCategory(tab))), - }, - list{text(ProtocolSquisherEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render TypeLL cross-panel type intelligence result (if available). -/// Parses the raw JSON via TypeLLEngine.parseCheckResult and displays an -/// evangeliser-style narrative with proof obligations and linearity notes. -let viewTypeCheckResult = (lastTypeCheck: option): Tea_Vdom.t => { - switch lastTypeCheck { - | None => noNode - | Some(json) => - switch TypeLLEngine.parseCheckResult(json) { - | Error(_) => noNode - | Ok(result) => - let narrative = TypeLLEngine.generateNarrative(result) - let borderColour = if result.valid { - "border-green-700 bg-green-900/20" - } else { - "border-red-700 bg-red-900/20" - } - let labelColour = if result.valid { - "text-green-400" - } else { - "text-red-400" - } - let statusText = if result.valid { - "Type-safe" - } else { - "Type issues detected" - } - div( - list{Attrs.class_("mt-4 p-3 rounded-lg border " ++ borderColour)}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-bold uppercase tracking-wider " ++ labelColour)}, - list{text("TypeLL")}, - ), - span(list{Attrs.class_("text-xs text-gray-400")}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-300 font-mono mb-1")}, - list{text(result.typeSignature)}, - ), - div(list{Attrs.class_("text-xs text-gray-400 mb-1")}, list{text(narrative.celebrate)}), - if Array.length(result.proofObligations) > 0 { - div( - list{Attrs.class_("text-xs text-yellow-400 mt-1")}, - list{text("Proof obligations: " ++ Array.join(result.proofObligations, ", "))}, - ) - } else { - noNode - }, - if Array.length(result.linearityIssues) > 0 { - div( - list{Attrs.class_("text-xs text-orange-400 mt-1")}, - list{text("Linearity: " ++ Array.join(result.linearityIssues, ", "))}, - ) - } else { - noNode - }, - }, - ) - } - } -} - -/// Main view for the Protocol-Squisher panel. -let view = (ps: protocolSquisherState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Protocol-Squisher format analysis panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Protocol-Squisher")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("13-format schema analysis")}, - ), - if ps.cliAvailable { - span(list{Attrs.class_("text-xs text-emerald-500")}, list{text("CLI ready")}) - } else { - span(list{Attrs.class_("text-xs text-amber-500")}, list{text("CLI not found")}) - }, - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700"), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(ps.activeCategory), - switch ps.activeCategory { - | PsAnalyse => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Enter a schema file path to analyse:")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 font-mono", - ), - Attrs.placeholder("/path/to/schema.proto"), - Attrs.value(ps.analyseInput), - Events.onInput(v => ProtocolSquisher(SetAnalyseInput(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-cyan-600 text-white rounded hover:bg-cyan-500 disabled:opacity-50", - ), - Events.onClick(ProtocolSquisher(RunAnalysis)), - KeyboardNav.onActivate(ProtocolSquisher(RunAnalysis)), - }, - list{text("Analyse")}, - ), - }, - ), - switch ps.lastAnalysis { - | Some(result) => renderAnalysisCard(result) - | None => noNode - }, - viewTypeCheckResult(ps.lastTypeCheck), - }, - ) - | PsCompare => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Compare two schema files for compatibility:")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 font-mono", - ), - Attrs.placeholder("Left schema path"), - Attrs.value(ps.compareLeftInput), - Events.onInput(v => ProtocolSquisher(SetCompareLeft(v))), - }, - list{}, - ), - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 font-mono", - ), - Attrs.placeholder("Right schema path"), - Attrs.value(ps.compareRightInput), - Events.onInput(v => ProtocolSquisher(SetCompareRight(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-cyan-600 text-white rounded hover:bg-cyan-500", - ), - Events.onClick(ProtocolSquisher(RunComparison)), - KeyboardNav.onActivate(ProtocolSquisher(RunComparison)), - }, - list{text("Compare")}, - ), - }, - ), - switch ps.lastComparison { - | Some(cmp) => - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4 space-y-2"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - if cmp.compatible { - "text-emerald-400 text-sm" - } else { - "text-red-400 text-sm" - }, - ), - }, - list{ - text( - if cmp.compatible { - "Compatible" - } else { - "Incompatible" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Adapter cost: ${Int.toString(cmp.adapterCost)}/10`)}, - ), - }, - ), - div(list{Attrs.class_("text-sm text-gray-400")}, list{text(cmp.notes)}), - }, - ) - | None => noNode - }, - }, - ) - | PsResults => - if ps.analysisHistory->Array.length === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{text("No analysis results yet. Run an analysis first.")}, - ) - } else { - div( - list{Attrs.class_("space-y-3")}, - ps.analysisHistory->Array.map(r => renderAnalysisCard(r))->List.fromArray, - ) - } - | PsGuide => - div( - list{Attrs.class_("space-y-4 text-sm text-gray-400 max-w-2xl")}, - list{ - h3( - list{Attrs.class_("text-base font-medium text-gray-200")}, - list{text("Transport Class Guide")}, - ), - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - renderTransportBadge(Concorde), - text("Zero-copy, fixed-size fields, no allocation. Best wire efficiency."), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - renderTransportBadge(Business), - text("Binary format, schema-driven, minimal overhead."), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - renderTransportBadge(Economy), - text("Text-based or self-describing. Readable but slower."), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - renderTransportBadge(Wheelbarrow), - text("Format requires full runtime, significant overhead."), - }, - ), - }, - ), - h3( - list{Attrs.class_("text-base font-medium text-gray-200 mt-6")}, - list{text("Supported Formats")}, - ), - div( - list{Attrs.class_("grid grid-cols-3 gap-2")}, - ProtocolSquisherEngine.allFormats - ->Array.map(fmt => - span( - list{Attrs.class_("text-gray-300")}, - list{text(ProtocolSquisherEngine.formatLabel(fmt))}, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - switch ps.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/ProvenAdoption.affine b/src/components/ProvenAdoption.affine new file mode 100644 index 00000000..ec12b88c --- /dev/null +++ b/src/components/ProvenAdoption.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ProvenAdoption; + +// TODO: Complete semantic implementation diff --git a/src/components/ProvenAdoption.res b/src/components/ProvenAdoption.res deleted file mode 100644 index 01a71df3..00000000 --- a/src/components/ProvenAdoption.res +++ /dev/null @@ -1,261 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Proven Adoption Component — proven library adoption scanner. -/// -/// Two-column layout: left sidebar with repo list, right content with -/// detail view showing which SafeX modules each repo uses. - -open Model -open Msg -open Tea.Html - -/// Render a binding status badge. -let statusBadge = (status: provenModuleStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | Adopted => ("text-green-400", "Adopted") - | PartiallyAdopted => ("text-amber-400", "Partial") - | NotAdopted => ("text-red-400", "Not Adopted") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a repo row in the sidebar. -let repoRow = (repo: repoProvenSummary, selected: bool): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-2 border-b border-gray-800 hover:bg-gray-800/60 transition-colors " ++ if ( - selected - ) { - "bg-gray-800/80 border-l-2 border-l-blue-500" - } else { - "" - }, - ), - Events.onClick(ProvenAdoption(SelectRepo(repo.repoName))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(repo.repoName)}), - statusBadge(repo.bindingStatus), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text(`${Int.toString(Array.length(repo.modules))} modules`)}, - ), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: provenAdoptionTab, target: provenAdoptionTab, label: string): Tea_Vdom.t< - msg, -> => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(ProvenAdoption(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Proven Adoption panel. -let view = (state: provenAdoptionState): Tea_Vdom.t => { - let adopted = ProvenAdoptionEngine.adoptedCount(state.repos) - let total = Array.length(state.repos) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Proven Adoption — Formally Verified Safety Primitives"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-green-300")}, - list{text("Proven Adoption")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(adopted)}/${Int.toString(total)} repos`)}, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600"), - Events.onClick(ProvenAdoption(ScanRepos)), - KeyboardNav.onActivate(ProvenAdoption(ScanRepos)), - }, - list{ - text( - if state.scanning { - "Scanning..." - } else { - "Scan" - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - ProvenAdoptionEngine.allTabs - ->Array.map(t => tabBtn(state.activeTab, t, ProvenAdoptionEngine.tabLabel(t))) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar — repo list - div( - list{Attrs.class_("w-64 border-r border-gray-800 overflow-y-auto")}, - state.repos - ->Array.map(r => repoRow(r, state.selectedRepo == Some(r.repoName))) - ->List.fromArray, - ), - // Right content — detail view - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.selectedRepo { - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a repo to view proven module details")}, - ) - | Some(name) => - switch state.repos->Array.find(r => r.repoName == name) { - | None => div(list{}, list{text("Repo not found")}) - | Some(repo) => - div( - list{}, - list{ - h3( - list{Attrs.class_("text-md font-semibold text-gray-200 mb-3")}, - list{text(repo.repoName)}, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-32")}, - list{text("Dependency:")}, - ), - span( - list{ - Attrs.class_( - if repo.dependencyDeclared { - "text-xs text-green-400" - } else { - "text-xs text-red-400" - }, - ), - }, - list{ - text( - if repo.dependencyDeclared { - "Declared" - } else { - "Missing" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-32")}, - list{text("Binding Status:")}, - ), - statusBadge(repo.bindingStatus), - }, - ), - div( - list{Attrs.class_("mt-3")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 mb-1 block")}, - list{text("Modules:")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - repo.modules - ->Array.map(m => - span( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 rounded text-gray-300", - ), - }, - list{text(m)}, - ) - ) - ->List.fromArray, - ), - }, - ), - }, - ), - }, - ) - } - }, - }, - ), - }, - ), - // Footer - div( - list{Attrs.class_("px-4 py-2 border-t border-gray-800 text-xs text-gray-500")}, - list{ - text( - `${Int.toString(ProvenAdoptionEngine.fullyBoundCount(state.repos))} repos fully bound`, - ), - }, - ), - }, - ) -} diff --git a/src/components/Provenance.affine b/src/components/Provenance.affine new file mode 100644 index 00000000..697c2868 --- /dev/null +++ b/src/components/Provenance.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Provenance; + +// TODO: Complete semantic implementation diff --git a/src/components/Provenance.res b/src/components/Provenance.res deleted file mode 100644 index 016e194d..00000000 --- a/src/components/Provenance.res +++ /dev/null @@ -1,279 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Provenance Component — Code trust surface visualization. -/// -/// Renders the Qubes-style ambient provenance overlay that shows who wrote -/// each piece of code and how trustworthy it is. This is CORE infrastructure, -/// always visible, not a panel overlay. -/// -/// Visual design: -/// - Trust summary bar (compact, always visible in header area) -/// - Per-region colour coding with shape indicators -/// - Hostile UX: pulsing red borders on unreviewed AI regions -/// - Accessibility: palettes swap hues, shapes + labels provide redundancy -/// - Screen reader: ARIA labels announce trust level, not colour - -open Model -open Msg -open Tea.Html - -/// Render a single trust level indicator with its count and percentage. -/// Uses shape + colour + label for triple redundancy (accessibility). -let renderTrustIndicator = ( - level: trustLevel, - lineCount: int, - totalLines: int, - palette: accessibilityPalette, -): Tea_Vdom.t => { - let (_bgClass, textClass, borderClass) = ProvenanceEngine.trustColours(level, palette) - let label = ProvenanceEngine.trustShortLabel(level) - let ariaLabel = ProvenanceEngine.trustAriaLabel(level) - let shape = ProvenanceEngine.trustShape(level) - let pct = if totalLines > 0 { - Float.toFixed(Float.fromInt(lineCount) /. Float.fromInt(totalLines) *. 100.0, ~digits=0) - } else { - "0" - } - - div( - list{ - Attrs.class_( - `flex items-center gap-1 px-2 py-1 border rounded text-xs ${textClass} ${borderClass}`, - ), - Attrs.ariaLabel(ariaLabel), - Attrs.role("status"), - }, - list{ - // Shape indicator (redundant channel beyond colour) - // Using text labels instead of emoji for ReScript compatibility. - span( - list{Attrs.class_("text-xs font-mono"), Attrs.ariaHidden(true)}, - list{ - text( - switch shape { - | "shield-check" => "[V]" - | "user-check" => "[H]" - | "cpu" => "[A]" - | "alert-triangle" => "[!]" - | _ => "[?]" - }, - ), - }, - ), - span(list{}, list{text(`${label}: ${pct}%`)}), - span(list{Attrs.class_("text-gray-600")}, list{text(`(${Int.toString(lineCount)})`)}), - }, - ) -} - -/// Render the compact trust summary bar. -/// Shows the trust distribution as a horizontal bar with colour segments -/// and individual indicators for each trust level. -let renderSummaryBar = (summary: provenanceSummary, palette: accessibilityPalette): Tea_Vdom.t< - msg, -> => { - let total = summary.totalLines - let trustPct = ProvenanceEngine.trustPercentage(summary) - - div( - list{ - Attrs.class_("flex items-center gap-3 px-3 py-1.5 bg-gray-900/80 border-b border-gray-800"), - Attrs.role("region"), - Attrs.ariaLabel(`Code provenance: ${Float.toFixed(trustPct, ~digits=0)}% trusted`), - }, - list{ - // Title - span(list{Attrs.class_("text-xs text-gray-500 font-medium mr-2")}, list{text("Provenance")}), - // Segmented progress bar showing trust distribution - div( - list{Attrs.class_("flex-1 h-2 bg-gray-800 rounded-full overflow-hidden flex")}, - list{ - if total > 0 { - let segment = (lines, colour) => { - let width = Float.toFixed( - Float.fromInt(lines) /. Float.fromInt(total) *. 100.0, - ~digits=1, - ) - div(list{Attrs.class_(`h-full ${colour}`), Attrs.style("width", `${width}%`)}, list{}) - } - let (vBg, _, _) = ProvenanceEngine.trustColours(Verified, palette) - let (hBg, _, _) = ProvenanceEngine.trustColours(HumanReviewed, palette) - let (aBg, _, _) = ProvenanceEngine.trustColours(AiAssisted, palette) - let (rBg, _, _) = ProvenanceEngine.trustColours(UnreviewedAi, palette) - let (gBg, _, _) = ProvenanceEngine.trustColours(Unknown, palette) - div( - list{Attrs.class_("flex w-full h-full")}, - list{ - segment(summary.verifiedLines, vBg), - segment(summary.humanReviewedLines, hBg), - segment(summary.aiAssistedLines, aBg), - segment(summary.unreviewedAiLines, rBg), - segment(summary.unknownLines, gBg), - }, - ) - } else { - div(list{Attrs.class_("h-full w-full bg-gray-700")}, list{}) - }, - }, - ), - // Trust score - span( - list{ - Attrs.class_( - `text-xs font-mono ml-2 ${trustPct > 80.0 - ? "text-green-400" - : trustPct > 50.0 - ? "text-amber-400" - : "text-red-400"}`, - ), - }, - list{text(`${Float.toFixed(trustPct, ~digits=0)}%`)}, - ), - // Violation warning - if summary.hasViolations { - span( - list{ - Attrs.class_("text-xs text-red-400 animate-pulse ml-2"), - Attrs.role("alert"), - Attrs.ariaLabel("Unreviewed AI code detected"), - }, - list{text("[!] UNREVIEWED AI")}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the detailed trust breakdown (expanded view). -/// Shows per-level indicators with line counts and percentages. -let renderDetailedBreakdown = ( - summary: provenanceSummary, - palette: accessibilityPalette, -): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-wrap gap-2 px-3 py-2 bg-gray-900/60 border-b border-gray-800")}, - list{ - renderTrustIndicator(Verified, summary.verifiedLines, summary.totalLines, palette), - renderTrustIndicator(HumanReviewed, summary.humanReviewedLines, summary.totalLines, palette), - renderTrustIndicator(AiAssisted, summary.aiAssistedLines, summary.totalLines, palette), - renderTrustIndicator(UnreviewedAi, summary.unreviewedAiLines, summary.totalLines, palette), - renderTrustIndicator(Unknown, summary.unknownLines, summary.totalLines, palette), - // Author and co-author counts - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto")}, - list{ - text( - `${Int.toString(summary.authorCount)} authors, ${Int.toString( - summary.coAuthorCount, - )} AI co-authors`, - ), - }, - ), - }, - ) -} - -/// Render the palette selector for accessibility options. -let renderPaletteSelector = (active: accessibilityPalette): Tea_Vdom.t => { - let palettes: array<(accessibilityPalette, string)> = [ - (StandardPalette, "Standard"), - (DeuteranopiaPalette, "Deuteranopia"), - (ProtanopiaPalette, "Protanopia"), - (HighContrastPalette, "High Contrast"), - ] - div( - list{ - Attrs.class_("flex gap-1 px-3 py-1"), - Attrs.role("radiogroup"), - Attrs.ariaLabel("Select accessibility palette"), - }, - palettes - ->Array.map(((palette, label)) => { - let isActive = palette === active - button( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded ${isActive - ? "bg-gray-700 text-gray-200" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("radio"), - Attrs.ariaSelected(isActive), - Attrs.title(`Switch to ${label} colour palette`), - Events.onClick(Provenance(SetPalette(palette))), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the hostile UX suppression toggle. -/// This is the "pull the smoke alarm battery" button — visible, deliberate, -/// and everyone knows you did it. -let renderHostileUxToggle = (suppressed: bool): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded ${suppressed - ? "bg-red-900/50 text-red-300 border border-red-700" - : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`, - ), - Events.onClick(Provenance(ToggleHostileUx)), - KeyboardNav.onActivate(Provenance(ToggleHostileUx)), - Attrs.ariaLabel( - suppressed - ? "Hostile UX suppressed — click to re-enable violation warnings" - : "Click to suppress hostile UX warnings", - ), - }, - list{text(suppressed ? "[!] Warnings Suppressed" : "Suppress Warnings")}, - ) -} - -/// The main provenance view — renders as an ambient bar, not a panel overlay. -/// This is called from View.res and sits above the three-panel layout. -let view = (prov: provenanceState): Tea_Vdom.t => { - if !prov.enabled { - noNode - } else { - div( - list{ - Attrs.class_("relative z-20"), - Attrs.role("complementary"), - Attrs.ariaLabel("Code provenance trust surface"), - }, - list{ - switch prov.activeFile { - | Some(file) => - div( - list{}, - list{ - renderSummaryBar(file.summary, prov.palette), - renderDetailedBreakdown(file.summary, prov.palette), - }, - ) - | None => - // No file active — show minimal indicator - div( - list{ - Attrs.class_( - "px-3 py-1 bg-gray-900/60 border-b border-gray-800 flex items-center gap-2", - ), - }, - list{ - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("Provenance: no file selected")}, - ), - renderHostileUxToggle(prov.hostileUxSuppressed), - }, - ) - }, - }, - ) - } -} diff --git a/src/components/Provisioner.affine b/src/components/Provisioner.affine new file mode 100644 index 00000000..8ae2c48e --- /dev/null +++ b/src/components/Provisioner.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Provisioner; + +// TODO: Complete semantic implementation diff --git a/src/components/Provisioner.res b/src/components/Provisioner.res deleted file mode 100644 index 3832133d..00000000 --- a/src/components/Provisioner.res +++ /dev/null @@ -1,493 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Provisioner Component — Portfolio provisioning, panel configuration, -/// and installation management. -/// -/// Three modes in one panel: -/// Portfolios — browse curated bundles, one-click install -/// Configurator — per-panel settings (endpoints, isolation tier, env vars) -/// Installed — view what's running and at what isolation level -/// Custom — build your own portfolio from available panels - -open Model -open Msg -open Tea.Html - -/// Render the category tabs. -let renderTabs = (active: provisionerCategory): Tea_Vdom.t => { - let tabs: array = [Portfolios, Configurator, Installed, CustomPortfolio] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-indigo-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Provisioner(SetProvCategory(tab))), - }, - list{text(ProvisionerEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a portfolio card — shows name, description, panel list, and install button. -let renderPortfolioCard = ( - portfolio: portfolio, - installStatuses: array<(string, panelInstallStatus)>, -): Tea_Vdom.t => { - let allInstalled = - portfolio.panels->Array.every(p => - ProvisionerEngine.getInstallStatus(installStatuses, p) === Installed - ) - let installedCount = - portfolio.panels - ->Array.filter(p => ProvisionerEngine.getInstallStatus(installStatuses, p) === Installed) - ->Array.length - - div( - list{ - Attrs.class_( - `bg-gray-900 border rounded-lg p-4 ${allInstalled - ? "border-green-800" - : "border-gray-700 hover:border-gray-500"}`, - ), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-lg text-gray-200 font-medium")}, - list{text(portfolio.name)}, - ), - if portfolio.builtIn { - span( - list{ - Attrs.class_("text-xs bg-indigo-900/50 text-indigo-400 px-1.5 py-0.5 rounded"), - }, - list{text("Built-in")}, - ) - } else { - noNode - }, - }, - ), - if allInstalled { - span(list{Attrs.class_("text-xs text-green-400")}, list{text("All installed")}) - } else { - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-500 transition-colors", - ), - Events.onClick(Provisioner(InstallPortfolio(portfolio.id))), - }, - list{text("Install")}, - ) - }, - }, - ), - // Description - div(list{Attrs.class_("text-sm text-gray-400 mb-3")}, list{text(portfolio.description)}), - // Panel chips - div( - list{Attrs.class_("flex flex-wrap gap-1.5 mb-2")}, - portfolio.panels - ->Array.map(panelName => { - let status = ProvisionerEngine.getInstallStatus(installStatuses, panelName) - let colour = ProvisionerEngine.installStatusColour(status) - span( - list{Attrs.class_(`text-xs px-2 py-0.5 rounded bg-gray-800 ${colour}`)}, - list{text(panelName)}, - ) - }) - ->List.fromArray, - ), - // Footer - div( - list{Attrs.class_("flex items-center justify-between text-xs text-gray-600")}, - list{ - span( - list{}, - list{ - text( - `${Int.toString(installedCount)}/${Int.toString( - Array.length(portfolio.panels), - )} panels`, - ), - }, - ), - span(list{}, list{text(portfolio.audience)}), - }, - ), - }, - ) -} - -/// Render a panel config row in the Configurator tab. -let renderConfigRow = (config: panelConfig): Tea_Vdom.t => { - let tierColour = ProvisionerEngine.isolationColour(config.isolation) - - div( - list{ - Attrs.class_("flex items-center gap-3 px-3 py-2 bg-gray-900 border border-gray-800 rounded"), - }, - list{ - // Panel name - span( - list{Attrs.class_("text-sm text-gray-200 w-28 font-medium")}, - list{text(config.panelName)}, - ), - // Isolation tier - span( - list{Attrs.class_(`text-xs ${tierColour} w-20`)}, - list{text(ProvisionerEngine.isolationShortLabel(config.isolation))}, - ), - // Endpoint - span( - list{Attrs.class_("text-xs text-gray-500 flex-1 truncate")}, - list{text(config.endpoint === "" ? "No endpoint" : config.endpoint)}, - ), - // Auto-connect indicator - span( - list{Attrs.class_(`text-xs ${config.autoConnect ? "text-green-400" : "text-gray-600"}`)}, - list{text(config.autoConnect ? "Auto" : "Manual")}, - ), - // Enabled toggle - button( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded ${config.enabled - ? "bg-green-900/50 text-green-400" - : "bg-gray-800 text-gray-600"}`, - ), - Events.onClick(Provisioner(TogglePanelEnabled(config.panelName))), - }, - list{text(config.enabled ? "On" : "Off")}, - ), - // Isolation tier selector buttons - div( - list{Attrs.class_("flex gap-1")}, - list{ - button( - list{ - Attrs.class_( - `px-1.5 py-0.5 text-xs rounded ${config.isolation === Native - ? "bg-green-900 text-green-400" - : "bg-gray-800 text-gray-500"}`, - ), - Events.onClick(Provisioner(SetPanelIsolation(config.panelName, Native))), - }, - list{text("N")}, - ), - button( - list{ - Attrs.class_( - `px-1.5 py-0.5 text-xs rounded ${config.isolation === StandardPod - ? "bg-blue-900 text-blue-400" - : "bg-gray-800 text-gray-500"}`, - ), - Events.onClick(Provisioner(SetPanelIsolation(config.panelName, StandardPod))), - }, - list{text("S")}, - ), - button( - list{ - Attrs.class_( - `px-1.5 py-0.5 text-xs rounded ${config.isolation === HardenedPod - ? "bg-purple-900 text-purple-400" - : "bg-gray-800 text-gray-500"}`, - ), - Events.onClick(Provisioner(SetPanelIsolation(config.panelName, HardenedPod))), - }, - list{text("H")}, - ), - }, - ), - }, - ) -} - -/// Render the isolation tier summary bar. -let renderIsolationSummary = (configs: array): Tea_Vdom.t => { - let native = ProvisionerEngine.countByIsolation(configs, Native) - let standard = ProvisionerEngine.countByIsolation(configs, StandardPod) - let hardened = ProvisionerEngine.countByIsolation(configs, HardenedPod) - div( - list{Attrs.class_("flex gap-4 px-3 py-2 bg-gray-900/60 border border-gray-800 rounded mb-3")}, - list{ - span( - list{Attrs.class_("text-xs text-green-400")}, - list{text(`Native: ${Int.toString(native)}`)}, - ), - span( - list{Attrs.class_("text-xs text-blue-400")}, - list{text(`Standard Pod: ${Int.toString(standard)}`)}, - ), - span( - list{Attrs.class_("text-xs text-purple-400")}, - list{text(`Hardened Pod: ${Int.toString(hardened)}`)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto")}, - list{text(`${Int.toString(native + standard + hardened)} total`)}, - ), - }, - ) -} - -/// Main provisioner view. -let view = (prov: provisionerState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Panel Provisioner — portfolios, configuration, and installation"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Provisioner")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - ProvisionerEngine.countInstalled(prov.panelInstallStatus), - )} panels installed`, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700"), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(prov.activeCategory), - switch prov.activeCategory { - | Portfolios => { - let filtered = ProvisionerEngine.filterPortfolios(prov.portfolios, prov.filterText) - div( - list{Attrs.class_("space-y-4")}, - list{ - // Search - input( - list{ - Attrs.class_( - "w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 mb-4", - ), - Attrs.placeholder("Search portfolios..."), - Attrs.ariaLabel("Search portfolios"), - Attrs.value(prov.filterText), - Events.onInput(v => Provisioner(SetProvFilter(v))), - }, - list{}, - ), - div( - list{Attrs.class_("grid gap-4 grid-cols-1 lg:grid-cols-2")}, - filtered - ->Array.map(p => renderPortfolioCard(p, prov.panelInstallStatus)) - ->List.fromArray, - ), - }, - ) - } - | Configurator => - div( - list{Attrs.class_("space-y-2")}, - list{ - renderIsolationSummary(prov.panelConfigs), - // Column headers - div( - list{Attrs.class_("flex items-center gap-3 px-3 py-1 text-xs text-gray-600")}, - list{ - span(list{Attrs.class_("w-28")}, list{text("Panel")}), - span(list{Attrs.class_("w-20")}, list{text("Tier")}), - span(list{Attrs.class_("flex-1")}, list{text("Endpoint")}), - span(list{}, list{text("Connect")}), - span(list{}, list{text("Status")}), - span(list{}, list{text("Isolation")}), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - prov.panelConfigs->Array.map(c => renderConfigRow(c))->List.fromArray, - ), - }, - ) - | Installed => { - let installed = prov.panelInstallStatus->Array.filter(((_, s)) => s === Installed) - div( - list{Attrs.class_("space-y-2")}, - list{ - renderIsolationSummary(prov.panelConfigs), - div( - list{Attrs.class_("space-y-1")}, - installed - ->Array.map(((name, _status)) => { - let config = prov.panelConfigs->Array.find(c => c.panelName === name) - let tierColour = switch config { - | Some(c) => ProvisionerEngine.isolationColour(c.isolation) - | None => "text-gray-500" - } - let tierLabel = switch config { - | Some(c) => ProvisionerEngine.isolationLabel(c.isolation) - | None => "Unknown" - } - div( - list{ - Attrs.class_( - "flex items-center gap-3 px-3 py-2 bg-gray-900 border border-gray-800 rounded", - ), - }, - list{ - span( - list{Attrs.class_("text-sm text-gray-200 w-28 font-medium")}, - list{text(name)}, - ), - span(list{Attrs.class_(`text-xs ${tierColour}`)}, list{text(tierLabel)}), - span( - list{Attrs.class_("text-xs text-green-400 ml-auto")}, - list{text("Running")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-red-900/50 text-red-400 rounded hover:bg-red-900", - ), - Events.onClick(Provisioner(RemovePanel(name))), - }, - list{text("Remove")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } - | CustomPortfolio => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Build a custom portfolio by selecting panels:")}, - ), - input( - list{ - Attrs.class_( - "w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200", - ), - Attrs.placeholder("Portfolio name..."), - Attrs.ariaLabel("Custom portfolio name"), - Attrs.value(prov.customName), - Events.onInput(v => Provisioner(SetCustomName(v))), - }, - list{}, - ), - // Available panels as toggleable chips - div( - list{Attrs.class_("flex flex-wrap gap-2 mt-3")}, - prov.panelConfigs - ->Array.map(c => { - let selected = prov.customPanels->Array.some(p => p === c.panelName) - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-sm rounded transition-colors ${selected - ? "bg-indigo-600 text-white" - : "bg-gray-800 text-gray-400 hover:bg-gray-700"}`, - ), - Events.onClick(Provisioner(ToggleCustomPanel(c.panelName))), - }, - list{text(c.panelName)}, - ) - }) - ->List.fromArray, - ), - if Array.length(prov.customPanels) > 0 { - div( - list{Attrs.class_("mt-4")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-indigo-600 text-white rounded hover:bg-indigo-500", - ), - Events.onClick(Provisioner(SaveCustomPortfolio)), - KeyboardNav.onActivate(Provisioner(SaveCustomPortfolio)), - }, - list{ - text( - `Save Portfolio (${Int.toString( - Array.length(prov.customPanels), - )} panels)`, - ), - }, - ), - }, - ) - } else { - noNode - }, - }, - ) - | PluginBundles | CustomPluginBundle | DeploymentBundles => - div( - list{Attrs.class_("p-6 text-center text-gray-500")}, - list{text("Coming soon")}, - ) - }, - // Error display - switch prov.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/RainbowAgents.affine b/src/components/RainbowAgents.affine new file mode 100644 index 00000000..d6e070ff --- /dev/null +++ b/src/components/RainbowAgents.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RainbowAgents; + +// TODO: Complete semantic implementation diff --git a/src/components/RainbowAgents.res b/src/components/RainbowAgents.res deleted file mode 100644 index 3772b41d..00000000 --- a/src/components/RainbowAgents.res +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -// -// RainbowAgents — Colour-coded agent family for PanLL panels. -// -// Each agent colour represents a specialisation mapped to PanLL's -// three-panel model (L/N/W). Agents can be dispatched to panels, -// observed through Panel-N, and their results appear in Panel-W. -// -// The rainbow family provides a visual language for agent activity: -// which agent is doing what, where, and in what state — all visible -// as coloured indicators across the panel system. - -/// Agent colour — each maps to a domain and a visual indicator. -type agentColour = - | /// Red — Security & threat analysis. Maps to Panel-L constraints. - Red - | /// Orange — Infrastructure & deployment. Maps to Panel-W world state. - Orange - | /// Yellow — Quality & testing. Maps to Panel-N reasoning. - Yellow - | /// Green — Compliance & standards (RSR, SPDX, REUSE). Maps to Panel-L. - Green - | /// Blue — Documentation & communication. Maps to Panel-W output. - Blue - | /// Indigo — Formal verification & proofs (echidna). Maps to Panel-L + Panel-N. - Indigo - | /// Violet — Creative & design (UI, UX, aesthetics). Maps to Panel-W. - Violet - -/// Map a colour to its hex value for PixiJS rendering. -let colourHex = (colour: agentColour): int => - switch colour { - | Red => 0xff4444 - | Orange => 0xff8844 - | Yellow => 0xffcc44 - | Green => 0x44ff88 - | Blue => 0x4488ff - | Indigo => 0x6644ff - | Violet => 0xaa44ff - } - -/// Display name for the agent colour. -let colourName = (colour: agentColour): string => - switch colour { - | Red => "RED" - | Orange => "ORANGE" - | Yellow => "YELLOW" - | Green => "GREEN" - | Blue => "BLUE" - | Indigo => "INDIGO" - | Violet => "VIOLET" - } - -/// Domain description for the agent colour. -let colourDomain = (colour: agentColour): string => - switch colour { - | Red => "Security & Threat Analysis" - | Orange => "Infrastructure & Deployment" - | Yellow => "Quality & Testing" - | Green => "Compliance & Standards" - | Blue => "Documentation & Communication" - | Indigo => "Formal Verification & Proofs" - | Violet => "Creative & Design" - } - -/// Which panel type this agent colour primarily reports to. -type panelAffinity = - | /// Constraint-focused (Panel-L) - SymbolicPanel - | /// Reasoning-focused (Panel-N) - NeuralPanel - | /// Result-focused (Panel-W) - WorldPanel - -let colourPanelAffinity = (colour: agentColour): panelAffinity => - switch colour { - | Red => SymbolicPanel - | Orange => WorldPanel - | Yellow => NeuralPanel - | Green => SymbolicPanel - | Blue => WorldPanel - | Indigo => SymbolicPanel - | Violet => WorldPanel - } - -/// Map gitbot-fleet bots to rainbow colours. -let botToColour = (botId: FleetModel.botId): agentColour => - switch botId { - | Rhodibot => Green // RSR compliance - | Echidnabot => Indigo // Formal verification - | Sustainabot => Orange // Dependency management - | Glambot => Violet // UI/aesthetics - | Seambot => Yellow // Integration testing - | Finishbot => Blue // Documentation/completion - } - -/// All colours in rainbow order. -let allColours: array = [Red, Orange, Yellow, Green, Blue, Indigo, Violet] - -// --------------------------------------------------------------------------- -// Rainbow Agent State -// --------------------------------------------------------------------------- - -/// A rainbow agent instance — one per colour, tracks activity. -type rainbowAgent = { - colour: agentColour, - mutable active: bool, - mutable currentTask: option, - mutable completedTasks: int, - mutable failedTasks: int, - /// Which panel is currently displaying this agent's output. - mutable displayPanel: option, -} - -/// Create a dormant agent. -let makeAgent = (colour: agentColour): rainbowAgent => { - colour, - active: false, - currentTask: None, - completedTasks: 0, - failedTasks: 0, - displayPanel: None, -} - -/// The full rainbow — all 7 agents. -type rainbow = { - red: rainbowAgent, - orange: rainbowAgent, - yellow: rainbowAgent, - green: rainbowAgent, - blue: rainbowAgent, - indigo: rainbowAgent, - violet: rainbowAgent, -} - -/// Create a fresh rainbow with all agents dormant. -let makeRainbow = (): rainbow => { - red: makeAgent(Red), - orange: makeAgent(Orange), - yellow: makeAgent(Yellow), - green: makeAgent(Green), - blue: makeAgent(Blue), - indigo: makeAgent(Indigo), - violet: makeAgent(Violet), -} - -/// Get an agent by colour. -let getAgent = (rainbow: rainbow, colour: agentColour): rainbowAgent => - switch colour { - | Red => rainbow.red - | Orange => rainbow.orange - | Yellow => rainbow.yellow - | Green => rainbow.green - | Blue => rainbow.blue - | Indigo => rainbow.indigo - | Violet => rainbow.violet - } - -/// Activate an agent with a task description. -let activate = (agent: rainbowAgent, task: string): unit => { - agent.active = true - agent.currentTask = Some(task) - agent.displayPanel = Some(colourPanelAffinity(agent.colour)) -} - -/// Mark an agent's current task as completed. -let complete = (agent: rainbowAgent): unit => { - agent.active = false - agent.completedTasks = agent.completedTasks + 1 - agent.currentTask = None -} - -/// Mark an agent's current task as failed. -let fail = (agent: rainbowAgent): unit => { - agent.active = false - agent.failedTasks = agent.failedTasks + 1 - agent.currentTask = None -} - -/// Count active agents in the rainbow. -let activeCount = (rainbow: rainbow): int => - allColours->Array.filter(c => getAgent(rainbow, c).active)->Array.length - -/// Get all active agents with their colours. -let activeAgents = (rainbow: rainbow): array<(agentColour, string)> => - allColours->Array.filterMap(c => { - let agent = getAgent(rainbow, c) - if agent.active { - switch agent.currentTask { - | Some(task) => Some((c, task)) - | None => Some((c, "active")) - } - } else { - None - } - }) diff --git a/src/components/RegressionGuard.affine b/src/components/RegressionGuard.affine new file mode 100644 index 00000000..a15eddab --- /dev/null +++ b/src/components/RegressionGuard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RegressionGuard; + +// TODO: Complete semantic implementation diff --git a/src/components/RegressionGuard.res b/src/components/RegressionGuard.res deleted file mode 100644 index 73eccd50..00000000 --- a/src/components/RegressionGuard.res +++ /dev/null @@ -1,392 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL RegressionGuard — snapshot comparison and golden-file testing panel. -/// -/// Displays a snapshot inventory with matched/mismatched badges, an inline diff -/// viewer for old-vs-new comparison, history of regression check runs, and -/// settings for auto-update and diff thresholds. - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for regressionTab variants. -let tabLabel = (tab: regressionTab): string => - switch tab { - | TabSnapshots => "Snapshots" - | TabDiffs => "Diffs" - | TabHistory => "History" - | TabSettings => "Settings" - } - -/// Render the tab bar. -let renderTabs = (active: regressionTab): Tea_Vdom.t => { - let tabs: array = [TabSnapshots, TabDiffs, TabHistory, TabSettings] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(RegressionGuard(SetRgTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Badge for matched/mismatched snapshot state. -let matchBadge = (matched: option): Tea_Vdom.t => - switch matched { - | Some(true) => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-emerald-600 text-white font-mono")}, - list{text("MATCH")}, - ) - | Some(false) => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-red-600 text-white font-mono")}, - list{text("MISMATCH")}, - ) - | None => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-gray-600 text-gray-300 font-mono")}, - list{text("PENDING")}, - ) - } - -/// Snapshot kind label. -let kindLabel = (kind: snapshotKind): string => - switch kind { - | SnapshotGameState => "Game State" - | SnapshotRenderOutput => "Render" - | SnapshotApiResponse => "API" - | SnapshotTestOutput => "Test" - | SnapshotCustom(name) => name - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Snapshots tab: inventory list with match badges and snapshot kind labels. -let renderSnapshotsTab = (state: regressionGuardState): Tea_Vdom.t => { - let total = Array.length(state.snapshots) - let matched = state.snapshots->Array.filter(s => s.matched === Some(true))->Array.length - let mismatched = state.snapshots->Array.filter(s => s.matched === Some(false))->Array.length - - div( - list{Attrs.class_("flex flex-col gap-3 p-4")}, - list{ - // Summary counts - div( - list{Attrs.class_("flex gap-4 text-sm")}, - list{ - span(list{Attrs.class_("text-gray-400")}, list{text(`Total: ${Int.toString(total)}`)}), - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`Matched: ${Int.toString(matched)}`)}, - ), - span( - list{Attrs.class_("text-red-400")}, - list{text(`Mismatched: ${Int.toString(mismatched)}`)}, - ), - }, - ), - // Snapshot rows - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - state.snapshots - ->Array.map(snap => { - div( - list{ - Attrs.class_( - "flex items-center justify-between gap-3 px-3 py-2 bg-gray-800 rounded text-sm", - ), - }, - list{ - matchBadge(snap.matched), - span(list{Attrs.class_("text-gray-300 flex-1 font-medium")}, list{text(snap.name)}), - span( - list{Attrs.class_("text-gray-500 text-xs font-mono")}, - list{text(kindLabel(snap.kind))}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer", - ), - Events.onClick(RegressionGuard(ViewDiff(snap.id))), - }, - list{text("Diff")}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Diffs tab: side-by-side old vs new comparison for mismatched snapshots. -let renderDiffsTab = (state: regressionGuardState): Tea_Vdom.t => { - let mismatched = state.snapshots->Array.filter(s => s.matched === Some(false)) - if Array.length(mismatched) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No mismatches found. All snapshots match their golden files.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-3 p-4 max-h-96 overflow-y-auto")}, - mismatched - ->Array.map(snap => { - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-red-800")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(snap.name)}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-amber-700 text-white rounded hover:bg-amber-600 cursor-pointer", - ), - Events.onClick(RegressionGuard(UpdateSnapshot(snap.id))), - }, - list{text("Accept New")}, - ), - }, - ), - // Diff summary - switch snap.diffSummary { - | Some(diff) => - div( - list{Attrs.class_("grid grid-cols-2 gap-2")}, - list{ - div( - list{Attrs.class_("bg-gray-900 rounded p-2 border border-gray-700")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text("Golden (old)")}, - ), - div( - list{Attrs.class_("text-xs font-mono text-gray-400 whitespace-pre-wrap")}, - list{text(snap.goldenPath)}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 rounded p-2 border border-red-900")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text("Current (new)")}, - ), - div( - list{Attrs.class_("text-xs font-mono text-red-300 whitespace-pre-wrap")}, - list{text(diff)}, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-500 italic")}, - list{text("Diff not yet computed")}, - ) - }, - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// History tab: previous regression check results. -let renderHistoryTab = (state: regressionGuardState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - state.results - ->Array.map(result => { - let allMatched = result.mismatched === 0 && result.missing === 0 - let borderCls = allMatched ? "border-emerald-700" : "border-red-700" - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${borderCls}`)}, - list{ - div( - list{Attrs.class_("flex justify-between text-xs text-gray-400 mb-1")}, - list{ - span(list{}, list{text(result.timestamp)}), - span(list{}, list{text(`${Float.toFixed(result.durationMs, ~digits=0)}ms`)}), - }, - ), - div( - list{Attrs.class_("flex gap-3 text-sm")}, - list{ - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`${Int.toString(result.matched)} matched`)}, - ), - span( - list{Attrs.class_("text-red-400")}, - list{text(`${Int.toString(result.mismatched)} mismatched`)}, - ), - span( - list{Attrs.class_("text-amber-400")}, - list{text(`${Int.toString(result.missing)} missing`)}, - ), - span( - list{Attrs.class_("text-blue-400")}, - list{text(`${Int.toString(result.newSnapshots)} new`)}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) -} - -/// Settings tab: auto-update toggle and configuration. -let renderSettingsTab = (state: regressionGuardState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium cursor-pointer ${state.autoUpdate - ? "bg-cyan-700 text-white" - : "bg-gray-700 text-gray-400"}`, - ), - Events.onClick(RegressionGuard(ToggleAutoUpdate)), - KeyboardNav.onActivate(RegressionGuard(ToggleAutoUpdate)), - }, - list{text(state.autoUpdate ? "Auto-Update: ON" : "Auto-Update: OFF")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Automatically accept new snapshots when golden files are missing")}, - ), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{ - text( - `Filter: "${state.filter}" (${Int.toString(Array.length(state.snapshots))} visible)`, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: regressionGuardState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabSnapshots => renderSnapshotsTab(state) - | TabDiffs => renderDiffsTab(state) - | TabHistory => renderHistoryTab(state) - | TabSettings => renderSettingsTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with Check All / Update All - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Regression Guard")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(RegressionGuard(CheckAll)), - KeyboardNav.onActivate(RegressionGuard(CheckAll)), - }, - list{text("Check All")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-amber-700 text-white rounded hover:bg-amber-600 cursor-pointer font-medium", - ), - Events.onClick(RegressionGuard(UpdateAll)), - KeyboardNav.onActivate(RegressionGuard(UpdateAll)), - }, - list{text("Update All")}, - ), - }, - ), - }, - ), - // Running indicator - if state.running { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-amber-400 rounded-full animate-pulse")}, list{}), - span(list{Attrs.class_("text-sm text-amber-300")}, list{text("Checking snapshots...")}), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/ReleaseManager.affine b/src/components/ReleaseManager.affine new file mode 100644 index 00000000..709e8071 --- /dev/null +++ b/src/components/ReleaseManager.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ReleaseManager; + +// TODO: Complete semantic implementation diff --git a/src/components/ReleaseManager.res b/src/components/ReleaseManager.res deleted file mode 100644 index 46b1481f..00000000 --- a/src/components/ReleaseManager.res +++ /dev/null @@ -1,474 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Release Manager Component — view for versioning, changelog, -/// packaging, and distribution of IDApTIK builds. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = ( - label: string, - cat: releaseManagerCategory, - active: releaseManagerCategory, -): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(ReleaseManager(SetReleaseCategory(cat)))}, - list{text(label)}, - ) -} - -/// Render overview — version, channel, recent releases. -let renderOverview = (state: releaseManagerState): Tea_Vdom.t => { - let channelCls = ReleaseManagerEngine.channelColour(state.channel) - div( - list{Attrs.class_("space-y-4")}, - list{ - // Version card - div( - list{Attrs.class_("p-4 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("grid grid-cols-3 gap-4")}, - list{ - div( - list{}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text("Current Version")}, - ), - div( - list{Attrs.class_("text-2xl font-light text-gray-100 font-mono")}, - list{text(state.currentVersion)}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Next Version")}), - div( - list{Attrs.class_("text-2xl font-light text-cyan-400 font-mono")}, - list{text(state.nextVersion)}, - ), - }, - ), - div( - list{}, - list{ - div(list{Attrs.class_("text-xs text-gray-500 mb-1")}, list{text("Channel")}), - div( - list{Attrs.class_(`text-2xl font-light ${channelCls}`)}, - list{text(ReleaseManagerEngine.channelLabel(state.channel))}, - ), - }, - ), - }, - ), - // Version bump buttons - div( - list{Attrs.class_("flex items-center gap-2 mt-4")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(BumpVersion("patch"))), - }, - list{text("Patch")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(BumpVersion("minor"))), - }, - list{text("Minor")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(BumpVersion("major"))), - }, - list{text("Major")}, - ), - }, - ), - }, - ), - // Recent releases - if Array.length(state.releases) > 0 { - div( - list{Attrs.class_("space-y-2")}, - list{ - div(list{Attrs.class_("text-xs text-gray-400")}, list{text("Recent Releases")}), - ...state.releases - ->Array.map(rel => { - let statusCls = ReleaseManagerEngine.statusColour(rel.status) - let chCls = ReleaseManagerEngine.channelColour(rel.channel) - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 bg-gray-800 rounded border border-gray-700 cursor-pointer hover:border-gray-500", - ), - Events.onClick(ReleaseManager(SelectRelease(rel.version))), - }, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-100")}, - list{text(rel.version)}, - ), - span( - list{Attrs.class_(`text-xs ${chCls}`)}, - list{text(ReleaseManagerEngine.channelLabel(rel.channel))}, - ), - span( - list{Attrs.class_(`text-xs ${statusCls}`)}, - list{text(ReleaseManagerEngine.statusLabel(rel.status))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto")}, - list{text(`${Int.toString(Array.length(rel.artifacts))} artifacts`)}, - ), - }, - ) - }) - ->List.fromArray, - }, - ) - } else { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No releases yet")}, - ) - }, - }, - ) -} - -/// Render changelog view. -let renderChangelog = (state: releaseManagerState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(GenerateChangelog)), - KeyboardNav.onActivate(ReleaseManager(GenerateChangelog)), - }, - list{text("Generate from Git")}, - ), - button( - list{ - Attrs.class_( - if state.autoChangelog { - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded cursor-pointer" - }, - ), - Events.onClick(ReleaseManager(ToggleAutoChangelog)), - KeyboardNav.onActivate(ReleaseManager(ToggleAutoChangelog)), - }, - list{text("Auto-Generate")}, - ), - }, - ), - if Array.length(state.pendingChangelog) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text( - "No pending changelog entries — click 'Generate from Git' to create from commit history", - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.pendingChangelog - ->Array.map(entry => - div( - list{Attrs.class_("p-2 bg-gray-800 rounded text-xs")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("text-cyan-400 font-mono")}, - list{text(entry.commitHash)}, - ), - span(list{Attrs.class_("text-gray-500")}, list{text(entry.category)}), - span(list{Attrs.class_("text-gray-600")}, list{text(entry.date)}), - }, - ), - div(list{Attrs.class_("text-gray-300")}, list{text(entry.description)}), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render artifacts view. -let renderArtifacts = (state: releaseManagerState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - // Platform toggles - div( - list{Attrs.class_("flex items-center gap-1 flex-wrap")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400 mr-2")}, list{text("Platforms:")}), - ...ReleaseManagerEngine.allPlatforms - ->Array.map(platform => { - let isEnabled = state.enabledPlatforms->Array.includes(platform) - button( - list{ - Attrs.class_( - if isEnabled { - "px-2 py-1 text-xs bg-cyan-700 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(ReleaseManager(TogglePlatform(platform))), - }, - list{text(ReleaseManagerEngine.platformLabel(platform))}, - ) - }) - ->List.fromArray, - }, - ), - // Build button - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-purple-700 text-white rounded hover:bg-purple-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(BuildArtifacts)), - KeyboardNav.onActivate(ReleaseManager(BuildArtifacts)), - }, - list{text("Build Artifacts")}, - ), - // Existing artifacts - if Array.length(state.artifacts) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{text("No artifacts built yet")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.artifacts - ->Array.map(art => - div( - list{Attrs.class_("flex items-center gap-3 p-2 bg-gray-800 rounded text-xs")}, - list{ - span(list{Attrs.class_("text-gray-200 flex-1")}, list{text(art.name)}), - span( - list{Attrs.class_("text-gray-500")}, - list{text(ReleaseManagerEngine.platformLabel(art.platform))}, - ), - span( - list{Attrs.class_("text-gray-400 font-mono")}, - list{text(ReleaseManagerEngine.formatSize(art.sizeBytes))}, - ), - span( - list{Attrs.class_("text-gray-600 font-mono truncate w-24")}, - list{text(art.checksum)}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render distribution view. -let renderDistribution = (state: releaseManagerState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Publish controls - div( - list{Attrs.class_("p-4 bg-gray-800 rounded border border-gray-700")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 mb-3")}, list{text("Publish Release")}), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-mono text-gray-100")}, - list{text(state.nextVersion)}, - ), - span( - list{Attrs.class_(`text-sm ${ReleaseManagerEngine.channelColour(state.channel)}`)}, - list{text(ReleaseManagerEngine.channelLabel(state.channel))}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(PublishRelease)), - KeyboardNav.onActivate(ReleaseManager(PublishRelease)), - }, - list{text("Publish")}, - ), - }, - ), - // Sign toggle - div( - list{Attrs.class_("flex items-center gap-2 mt-3")}, - list{ - button( - list{ - Attrs.class_( - if state.signArtifacts { - "px-2 py-1 text-xs bg-emerald-700 text-white rounded" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-400 rounded cursor-pointer" - }, - ), - Events.onClick(ReleaseManager(ToggleSignArtifacts)), - KeyboardNav.onActivate(ReleaseManager(ToggleSignArtifacts)), - }, - list{ - text( - if state.signArtifacts { - "Signing: On" - } else { - "Signing: Off" - }, - ), - }, - ), - }, - ), - }, - ), - // Channel selector - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400 mr-2")}, list{text("Channel:")}), - ...[ChannelDev, ChannelAlpha, ChannelBeta, ChannelRC, ChannelStable] - ->Array.map(ch => { - let isActive = state.channel === ch - let chCls = ReleaseManagerEngine.channelColour(ch) - button( - list{ - Attrs.class_( - if isActive { - `px-2 py-1 text-xs bg-gray-600 ${chCls} rounded` - } else { - "px-2 py-1 text-xs bg-gray-800 text-gray-500 rounded cursor-pointer hover:text-gray-300" - }, - ), - Events.onClick(ReleaseManager(SetChannel(ch))), - }, - list{text(ReleaseManagerEngine.channelLabel(ch))}, - ) - }) - ->List.fromArray, - }, - ), - }, - ) -} - -/// Main view function. -let view = (state: releaseManagerState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Release Manager panel"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Release Manager")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono")}, - list{text(state.currentVersion)}, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(ReleaseManager(LoadReleases)), - KeyboardNav.onActivate(ReleaseManager(LoadReleases)), - }, - list{text("Refresh")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Overview", ReleaseOverview, state.activeCategory), - renderTab("Changelog", ReleaseChangelog, state.activeCategory), - renderTab("Artifacts", ReleaseArtifacts, state.activeCategory), - renderTab("Distribution", ReleaseDistribution, state.activeCategory), - }, - ), - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | ReleaseOverview => renderOverview(state) - | ReleaseChangelog => renderChangelog(state) - | ReleaseArtifacts => renderArtifacts(state) - | ReleaseDistribution => renderDistribution(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/RepoLoader.affine b/src/components/RepoLoader.affine new file mode 100644 index 00000000..adbc770a --- /dev/null +++ b/src/components/RepoLoader.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module RepoLoader; + +// TODO: Complete semantic implementation diff --git a/src/components/RepoLoader.res b/src/components/RepoLoader.res deleted file mode 100644 index 11eb7ad3..00000000 --- a/src/components/RepoLoader.res +++ /dev/null @@ -1,520 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Repo Loader Component — Repository scanner and panel configuration wizard. -/// -/// Three views: -/// 1. Browse: Directory picker + farm search to select a repo -/// 2. Configure: Panel suggestion cards with enable/disable toggles -/// 3. Recent: Quick-switch list of recently loaded repos -/// -/// When a repo is loaded, its manifests are scanned and the AI panel is given -/// full context about the project. Panel configs save to PANELS.a2ml. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// Category tab bar -// =========================================================================== - -/// Render a single category tab. -let renderCategoryTab = (cat: repoLoaderCategory, isActive: bool): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm transition-colors ${isActive - ? "text-gray-100 border-b-2 border-blue-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(RepoLoader(SetRepoCategory(cat))), - }, - list{text(RepoLoaderEngine.categoryLabel(cat))}, - ) -} - -/// Render the category tab bar. -let renderCategoryTabBar = (activeCategory: repoLoaderCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex border-b border-gray-800")}, - RepoLoaderEngine.allCategories - ->Array.map(cat => renderCategoryTab(cat, cat === activeCategory)) - ->List.fromArray, - ) -} - -// =========================================================================== -// Browse view -// =========================================================================== - -/// Render the repo browse/picker view. -let renderBrowse = (rl: repoLoaderState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6")}, - list{ - // Directory picker - div( - list{Attrs.class_("mb-8")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-300 mb-4")}, - list{text("Open Repository")}, - ), - div( - list{Attrs.class_("flex gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors", - ), - Events.onClick(RepoLoader(PickRepoDirectory)), - KeyboardNav.onActivate(RepoLoader(PickRepoDirectory)), - }, - list{text("Pick Directory")}, - ), - { - if rl.scanning { - div( - list{Attrs.class_("flex items-center text-sm text-gray-500 animate-pulse")}, - list{text("Scanning...")}, - ) - } else { - noNode - } - }, - }, - ), - }, - ), - // Quick path input - div( - list{Attrs.class_("mb-8")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Or enter a path directly:")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500", - ), - Attrs.placeholder("/path/to/repos/..."), - Attrs.value(rl.searchText), - Events.onInput(text => RepoLoader(SetRepoSearchText(text))), - KeyboardUtil.onEnterOrSpace(RepoLoader(ScanRepo(rl.searchText))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors text-sm", - ), - Attrs.disabled(rl.searchText === ""), - Events.onClick(RepoLoader(ScanRepo(rl.searchText))), - }, - list{text("Scan")}, - ), - }, - ), - }, - ), - { - switch rl.currentRepo { - | Some(repo) => - div( - list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-lg font-medium text-gray-200 mb-2")}, - list{text(repo.name)}, - ), - { - if repo.description !== "" { - div( - list{Attrs.class_("text-sm text-gray-400 mb-3")}, - list{text(repo.description)}, - ) - } else { - noNode - } - }, - div(list{Attrs.class_("text-xs text-gray-500 mb-2")}, list{text(repo.path)}), - { - if Array.length(repo.languages) > 0 { - div( - list{Attrs.class_("flex gap-1 flex-wrap mb-2")}, - repo.languages - ->Array.map(lang => - span( - list{ - Attrs.class_("text-xs px-2 py-0.5 rounded bg-blue-500/20 text-blue-300"), - }, - list{text(lang)}, - ) - ) - ->List.fromArray, - ) - } else { - noNode - } - }, - // Status badges - div( - list{Attrs.class_("flex gap-2 text-xs")}, - list{ - span( - list{ - Attrs.class_( - `px-2 py-0.5 rounded ${repo.hasAiManifest - ? "bg-green-500/20 text-green-300" - : "bg-gray-700 text-gray-500"}`, - ), - }, - list{text("AI Manifest")}, - ), - span( - list{ - Attrs.class_( - `px-2 py-0.5 rounded ${repo.hasMachineReadable - ? "bg-green-500/20 text-green-300" - : "bg-gray-700 text-gray-500"}`, - ), - }, - list{text(".machine_readable/")}, - ), - span( - list{ - Attrs.class_( - `px-2 py-0.5 rounded ${repo.hasPanelsManifest - ? "bg-green-500/20 text-green-300" - : "bg-gray-700 text-gray-500"}`, - ), - }, - list{text("PANELS.a2ml")}, - ), - span( - list{ - Attrs.class_( - `px-2 py-0.5 rounded ${repo.hasState - ? "bg-green-500/20 text-green-300" - : "bg-gray-700 text-gray-500"}`, - ), - }, - list{text("STATE.scm")}, - ), - }, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-center text-gray-600 mt-12")}, - list{ - div(list{Attrs.class_("text-2xl mb-4")}, list{text("No Repository Loaded")}), - div( - list{Attrs.class_("text-sm")}, - list{text("Pick a directory or enter a path to scan a repository.")}, - ), - }, - ) - } - }, - { - switch rl.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 px-3 py-2 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - }, - list{text(e)}, - ) - | None => noNode - } - }, - }, - ) -} - -// =========================================================================== -// Configure view -// =========================================================================== - -/// Render a single panel suggestion card with toggle. -let renderSuggestionCard = (suggestion: panelSuggestion): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - `border ${suggestion.enabled - ? "border-gray-700" - : "border-gray-800"} rounded-lg p-4 transition-colors`, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(suggestion.panelName)}, - ), - span( - list{ - Attrs.class_( - `text-xs px-2 py-0.5 rounded ${RepoLoaderEngine.priorityColour( - suggestion.priority, - )}`, - ), - }, - list{text(suggestion.priority)}, - ), - }, - ), - button( - list{ - Attrs.class_( - `px-3 py-1 rounded text-sm transition-colors ${suggestion.enabled - ? "bg-green-500/20 text-green-300 hover:bg-green-500/30" - : "bg-gray-700 text-gray-400 hover:bg-gray-600"}`, - ), - Events.onClick(RepoLoader(ToggleSuggestion(suggestion.panelName))), - }, - list{text(suggestion.enabled ? "Enabled" : "Disabled")}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(suggestion.reason)}), - }, - ) -} - -/// Render the panel configuration wizard. -let renderConfigure = (rl: repoLoaderState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6")}, - list{ - { - if Array.length(rl.suggestions) > 0 { - div( - list{}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-300")}, - list{ - text( - `Panel Configuration (${Int.toString( - RepoLoaderEngine.enabledCount(rl.suggestions), - )} enabled)`, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50", - ), - Attrs.disabled(rl.saved), - Events.onClick(RepoLoader(SavePanels)), - KeyboardNav.onActivate(RepoLoader(SavePanels)), - }, - list{text(rl.saved ? "Saved" : "Save to PANELS.a2ml")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("space-y-3")}, - rl.suggestions - ->Array.map(renderSuggestionCard) - ->List.fromArray, - ), - }, - ) - } else { - div( - list{Attrs.class_("text-center text-gray-600 mt-12")}, - list{ - div(list{Attrs.class_("text-lg mb-2")}, list{text("No panel suggestions")}), - div( - list{Attrs.class_("text-sm")}, - list{text("Scan a repository first to get panel recommendations.")}, - ), - }, - ) - } - }, - }, - ) -} - -// =========================================================================== -// Recent view -// =========================================================================== - -/// Render the recent repos list. -let renderRecent = (rl: repoLoaderState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-300 mb-4")}, - list{text("Recent Repositories")}, - ), - { - if Array.length(rl.recentPaths) > 0 { - div( - list{Attrs.class_("space-y-2")}, - rl.recentPaths - ->Array.map(path => { - let name = switch String.split(path, "/")->Array.at(-1) { - | Some(n) => n - | None => path - } - button( - list{ - Attrs.class_( - "w-full text-left px-4 py-3 border border-gray-800 rounded-lg hover:border-gray-600 transition-colors", - ), - Events.onClick(RepoLoader(ScanRepo(path))), - }, - list{ - div(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(name)}), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text(path)}), - }, - ) - }) - ->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-center text-gray-600 mt-12")}, - list{div(list{Attrs.class_("text-sm")}, list{text("No recently loaded repos.")})}, - ) - } - }, - }, - ) -} - -// =========================================================================== -// Farm search view -// =========================================================================== - -/// Render the farm search interface. -let renderFarmSearch = (rl: repoLoaderState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-y-auto p-6")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-300 mb-4")}, - list{text("Search Git-Private-Farm")}, - ), - div( - list{Attrs.class_("flex gap-2 mb-4")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-blue-500", - ), - Attrs.placeholder("Search by name or description..."), - Attrs.value(rl.searchText), - Events.onInput(text => RepoLoader(SetRepoSearchText(text))), - KeyboardUtil.onEnterOrSpace(RepoLoader(SearchFarm(rl.searchText))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded-lg hover:bg-gray-700 transition-colors text-sm", - ), - Attrs.disabled(rl.searchText === ""), - Events.onClick(RepoLoader(SearchFarm(rl.searchText))), - }, - list{text("Search")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text("Searches the farm-manifest.json for matching repos.")}, - ), - }, - ) -} - -// =========================================================================== -// Main view (full panel overlay) -// =========================================================================== - -/// Render the full Repo Loader panel overlay. -let view = (rl: repoLoaderState): Tea_Vdom.t => { - div( - list{Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col")}, - list{ - // Header bar - div( - list{Attrs.class_("flex items-center justify-between px-6 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-lg font-light text-gray-200")}, - list{text("Repo Loader")}, - ), - { - switch rl.currentRepo { - | Some(repo) => - span( - list{Attrs.class_("text-xs px-2 py-0.5 rounded bg-blue-500/20 text-blue-300")}, - list{text(repo.name)}, - ) - | None => noNode - } - }, - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Category tabs - renderCategoryTabBar(rl.activeCategory), - { - switch rl.activeCategory { - | Browse => renderBrowse(rl) - | Configure => renderConfigure(rl) - | Recent => renderRecent(rl) - | FarmSearch => renderFarmSearch(rl) - } - }, - }, - ) -} diff --git a/src/components/Reposystem.affine b/src/components/Reposystem.affine new file mode 100644 index 00000000..b775d0b9 --- /dev/null +++ b/src/components/Reposystem.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Reposystem; + +// TODO: Complete semantic implementation diff --git a/src/components/Reposystem.res b/src/components/Reposystem.res deleted file mode 100644 index 91ecdf3d..00000000 --- a/src/components/Reposystem.res +++ /dev/null @@ -1,616 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Reposystem Component — RSR compliance dashboard. -/// -/// Renders compliance rates per requirement (the "known audit" data: -/// .editorconfig 96.9%, STATE.scm 94.3%, AI manifest 34.7%, Justfile 28.3%, -/// TOPOLOGY.md 1.5%), per-repo audit tables, and language policy status. - -open Model -open Msg -open Tea.Html - -/// Render a compliance rate bar for a single RSR requirement. -let renderRequirementBar = (req: rsrRequirement, rate: float, count: int, total: int): Tea_Vdom.t< - msg, -> => { - let label = ReposystemEngine.requirementLabel(req) - let pct = Float.toFixed(rate *. 100.0, ~digits=1) - let barColor = if rate > 0.9 { - "bg-green-500" - } else if rate > 0.5 { - "bg-amber-500" - } else { - "bg-red-500" - } - - div( - list{ - Attrs.class_("flex items-center gap-3 mb-2"), - Attrs.role("meter"), - Attrs.ariaLabel(`${label}: ${pct}% compliance`), - }, - list{ - div(list{Attrs.class_("w-36 text-sm text-gray-300 text-right")}, list{text(label)}), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-3")}, - list{ - div( - list{ - Attrs.class_(`${barColor} h-full rounded-full transition-all`), - Attrs.prop("style", `width: ${pct}%`), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("w-20 text-xs text-gray-400 text-right")}, - list{text(`${Int.toString(count)}/${Int.toString(total)}`)}, - ), - div(list{Attrs.class_("w-14 text-xs text-gray-500 text-right")}, list{text(`${pct}%`)}), - }, - ) -} - -/// Render a repo compliance row. -let renderRepoRow = (audit: repoCompliance): Tea_Vdom.t => { - let scorePct = Float.toFixed(audit.score *. 100.0, ~digits=0) - let scoreColor = if audit.score >= 1.0 { - "text-green-400" - } else if audit.score > 0.6 { - "text-amber-400" - } else { - "text-red-400" - } - - div( - list{ - Attrs.class_("flex items-center gap-4 p-2 border-b border-gray-800 hover:bg-gray-900/50"), - Attrs.role("row"), - }, - list{ - span( - list{Attrs.class_(`text-sm font-mono ${scoreColor} w-12 text-right`)}, - list{text(`${scorePct}%`)}, - ), - span(list{Attrs.class_("text-sm text-gray-300 flex-1 truncate")}, list{text(audit.repoName)}), - span( - list{Attrs.class_("text-xs text-gray-500 w-24 text-right")}, - list{text(`${Int.toString(audit.metCount)}/${Int.toString(audit.totalCount)}`)}, - ), - }, - ) -} - -/// Render category tabs. -let renderTabs = (active: reposystemCategory): Tea_Vdom.t => { - let tabs: array = [ - RsrDashboard, - RsrRepoList, - RsrRequirements, - RsrLanguagePolicy, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-cyan-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(Reposystem(SetRsrCategory(tab))), - }, - list{text(ReposystemEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Main view for the Reposystem panel. -let view = (rsr: reposystemState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Reposystem RSR compliance panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("Reposystem")}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("RSR compliance across 265+ repos")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs bg-cyan-600 text-white rounded hover:bg-cyan-500", - ), - Events.onClick(Reposystem(ScanAll)), - KeyboardNav.onActivate(Reposystem(ScanAll)), - }, - list{text("Scan All")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - if rsr.loading { - div( - list{Attrs.class_("text-gray-400"), Attrs.role("status")}, - list{text("Scanning repositories...")}, - ) - } else if !rsr.loaded { - div( - list{Attrs.class_("text-center text-gray-500 mt-12")}, - list{ - div(list{Attrs.class_("text-4xl mb-2")}, list{text("Reposystem")}), - div( - list{Attrs.class_("text-sm mb-6")}, - list{text("Rhodium Standard Repository compliance auditing")}, - ), - button( - list{ - Attrs.class_("px-4 py-2 bg-cyan-600 text-white rounded hover:bg-cyan-500"), - Events.onClick(Reposystem(ScanAll)), - KeyboardNav.onActivate(Reposystem(ScanAll)), - }, - list{text("Run Compliance Scan")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-4")}, - list{ - renderTabs(rsr.activeCategory), - switch rsr.activeCategory { - | RsrDashboard => - switch rsr.stats { - | Some(stats) => - div( - list{Attrs.class_("space-y-6")}, - list{ - // Summary - div( - list{Attrs.class_("flex gap-6 text-sm")}, - list{ - div( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(stats.totalRepos)} repos audited`)}, - ), - div( - list{Attrs.class_("text-gray-400")}, - list{ - text( - `${Float.toFixed( - stats.avgScore *. 100.0, - ~digits=1, - )}% avg compliance`, - ), - }, - ), - div( - list{Attrs.class_("text-green-400")}, - list{text(`${Int.toString(stats.fullyCompliant)} fully compliant`)}, - ), - }, - ), - // Requirement bars - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-4")}, - list{text("Per-Requirement Compliance")}, - ), - div( - list{}, - stats.requirementRates - ->Array.map(((req, rate, count)) => - renderRequirementBar(req, rate, count, stats.totalRepos) - ) - ->List.fromArray, - ), - }, - ), - }, - ) - | None => - div(list{Attrs.class_("text-gray-500")}, list{text("No stats available")}) - } - | RsrRepoList => { - let filtered = ReposystemEngine.filterAudits(rsr.audits, rsr.filterText) - div( - list{Attrs.class_("space-y-4")}, - list{ - input( - list{ - Attrs.class_( - "w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600", - ), - Attrs.placeholder("Filter repos..."), - Attrs.value(rsr.filterText), - Events.onInput(v => Reposystem(SetRsrFilter(v))), - }, - list{}, - ), - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered->Array.map(a => renderRepoRow(a))->List.fromArray, - ), - }, - ), - }, - ) - } - | RsrRequirements => - div( - list{Attrs.class_("flex gap-4")}, - list{ - // Requirement list (left) - div( - list{Attrs.class_("w-64 space-y-1"), Attrs.role("list")}, - ReposystemEngine.allRequirements - ->Array.map(req => { - let label = ReposystemEngine.requirementLabel(req) - let rate = ReposystemEngine.requirementRate(rsr.audits, req) - let pct = Float.toFixed(rate *. 100.0, ~digits=1) - let isSelected = rsr.selectedRequirement === Some(req) - let rateColor = if rate > 0.9 { - "text-green-400" - } else if rate > 0.5 { - "text-amber-400" - } else { - "text-red-400" - } - button( - list{ - Attrs.class_( - `w-full text-left p-3 rounded transition-colors flex items-center justify-between ${isSelected - ? "bg-cyan-900/40 border border-cyan-700" - : "bg-gray-900 border border-gray-800 hover:bg-gray-800"}`, - ), - Events.onClick(Reposystem(SelectRequirement(Some(req)))), - Attrs.role("listitem"), - }, - list{ - span(list{Attrs.class_("text-sm text-gray-300")}, list{text(label)}), - span( - list{Attrs.class_(`text-xs font-mono ${rateColor}`)}, - list{text(`${pct}%`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - // Drill-down (right) - div( - list{Attrs.class_("flex-1")}, - list{ - switch rsr.selectedRequirement { - | Some(req) => { - let label = ReposystemEngine.requirementLabel(req) - let failing = ReposystemEngine.reposFailingRequirement( - rsr.audits, - req, - ) - let passing = ReposystemEngine.reposPassingRequirement( - rsr.audits, - req, - ) - let total = Array.length(failing) + Array.length(passing) - let rate = if total > 0 { - Int.toFloat(Array.length(passing)) /. Int.toFloat(total) *. 100.0 - } else { - 0.0 - } - div( - list{Attrs.class_("space-y-4")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(label)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - Array.length(passing), - )} passing, ${Int.toString( - Array.length(failing), - )} failing out of ${Int.toString(total)}`, - ), - }, - ), - }, - ), - div( - list{ - Attrs.class_( - `text-lg font-mono ${rate > 90.0 - ? "text-green-400" - : rate > 50.0 - ? "text-amber-400" - : "text-red-400"}`, - ), - }, - list{text(`${Float.toFixed(rate, ~digits=1)}%`)}, - ), - }, - ), - // Compliance bar - div( - list{Attrs.class_("w-full bg-gray-800 rounded-full h-3")}, - list{ - div( - list{ - Attrs.class_( - `h-full rounded-full transition-all ${rate > 90.0 - ? "bg-green-500" - : rate > 50.0 - ? "bg-amber-500" - : "bg-red-500"}`, - ), - Attrs.prop( - "style", - `width: ${Float.toFixed(rate, ~digits=0)}%`, - ), - }, - list{}, - ), - }, - ), - // Failing repos list - if Array.length(failing) > 0 { - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_( - "text-xs text-red-400 font-medium uppercase tracking-wider mb-2", - ), - }, - list{text("Failing Repos")}, - ), - div( - list{Attrs.class_("max-h-64 overflow-y-auto space-y-1")}, - failing - ->Array.map(a => - div( - list{ - Attrs.class_( - "flex items-center justify-between p-2 bg-red-900/20 border border-red-900/40 rounded text-sm", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-300 truncate")}, - list{text(a.repoName)}, - ), - span( - list{Attrs.class_("text-xs text-red-400")}, - list{text("FAIL")}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - div( - list{ - Attrs.class_( - "p-4 bg-green-900/20 border border-green-800 rounded text-center", - ), - }, - list{ - div( - list{Attrs.class_("text-green-400 text-sm")}, - list{text("All repos pass this requirement")}, - ), - }, - ) - }, - // Passing repos (collapsed count) - if Array.length(passing) > 0 { - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text( - `${Int.toString( - Array.length(passing), - )} repos passing (not shown)`, - ), - }, - ) - } else { - noNode - }, - }, - ) - } - | None => - div( - list{ - Attrs.class_( - "flex items-center justify-center h-48 text-gray-600 text-sm", - ), - }, - list{text("Select a requirement to see which repos pass or fail")}, - ) - }, - }, - ), - }, - ) - | RsrLanguagePolicy => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Language Policy Enforcement")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{ - text( - "Banned languages: TypeScript, Node/npm/bun, Go, Python, Java/Kotlin", - ), - }, - ), - // Policy violation categories - div( - list{Attrs.class_("grid grid-cols-2 gap-3")}, - list{ - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-red-400 font-medium mb-2")}, - list{text("TypeScript (.ts/.tsx)")}, - ), - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{text("Replacement: ReScript")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text("CI: ts-blocker.yml active")}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-red-400 font-medium mb-2")}, - list{text("npm / Bun")}, - ), - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{text("Replacement: Deno (first), Bun (fallback)")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text("CI: npm-bun-blocker.yml active")}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-amber-400 font-medium mb-2")}, - list{text("Go (.go)")}, - ), - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{text("Replacement: Rust")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text("Migration: long-term")}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-amber-400 font-medium mb-2")}, - list{text("Python (.py)")}, - ), - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{text("Replacement: Julia / Rust / ReScript")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text("Migration: medium-term")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-2")}, - list{ - text( - "Allowed: ReScript, Deno, Rust, Gleam, Elixir, Bash, Julia, OCaml, Haskell, Ada, Nickel, Guile Scheme", - ), - }, - ), - }, - ) - }, - }, - ) - }, - switch rsr.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/ScriptGist.affine b/src/components/ScriptGist.affine new file mode 100644 index 00000000..217f3e3f --- /dev/null +++ b/src/components/ScriptGist.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ScriptGist; + +// TODO: Complete semantic implementation diff --git a/src/components/ScriptGist.res b/src/components/ScriptGist.res deleted file mode 100644 index b8b90d74..00000000 --- a/src/components/ScriptGist.res +++ /dev/null @@ -1,460 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Script Gist Component — portable computation gist browser and editor. -/// -/// Minskian dual-axis design: -/// - Diachronic (time): Scripts as temporal sequences with rollback checkpoints. -/// - Synchronic (space): Schemata as spatial cardfiles — composable boards of gists. -/// -/// Gists are saveable, shareable, LLM-callable (MCP tool schema), and user-runnable -/// standalone. The editor supports code, schema definition, and template expansion. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button. -let renderTab = (label: string, cat: gistCategory, active: gistCategory, count: int): Tea_Vdom.t< - msg, -> => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{Attrs.class_(cls), Events.onClick(ScriptGist(SetGistCategory(cat)))}, - list{text(label ++ " (" ++ Int.toString(count) ++ ")")}, - ) -} - -/// Render a single gist row in the list. -let renderGistRow = (gist: scriptGist, isSelected: bool): Tea_Vdom.t => { - let bgCls = isSelected - ? "bg-gray-700 border-cyan-500" - : "bg-gray-800/60 border-transparent hover:bg-gray-800" - div( - list{ - Attrs.class_("p-3 rounded border " ++ bgCls ++ " cursor-pointer transition-colors"), - Events.onClick(ScriptGist(SelectGist(Some(gist.id)))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-xs font-medium text-gray-200")}, list{text(gist.title)}), - span( - list{ - Attrs.class_( - "text-[10px] px-1.5 py-0.5 rounded " ++ - ScriptGistEngine.languageColour(gist.language) ++ " bg-gray-900/60", - ), - }, - list{text(ScriptGistEngine.languageLabel(gist.language))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - if gist.pinned { - span(list{Attrs.class_("text-amber-400 text-[10px]")}, list{text("pinned")}) - } else { - noNode - }, - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("v" ++ Int.toString(gist.version))}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3 text-[10px] text-gray-500")}, - list{ - span(list{}, list{text(ScriptGistEngine.targetLabel(gist.target))}), - span(list{}, list{text(ScriptGistEngine.visibilityLabel(gist.visibility))}), - span(list{}, list{text(gist.schema.toolName)}), - span(list{}, list{text(Int.toString(Array.length(gist.history)) ++ " runs")}), - }, - ), - }, - ) -} - -/// Render the gist editor panel (right side). -let renderEditor = (state: scriptGistState): Tea_Vdom.t => { - switch state.selectedGistId { - | None => - div( - list{Attrs.class_("flex-1 flex items-center justify-center text-gray-600 text-sm")}, - list{text("Select a gist or create a new one")}, - ) - | Some(id) => - switch ScriptGistEngine.findGist(state.gists, id) { - | None => - div( - list{Attrs.class_("flex-1 flex items-center justify-center text-gray-600 text-sm")}, - list{text("Gist not found")}, - ) - | Some(gist) => - div( - list{Attrs.class_("flex-1 flex flex-col gap-3 overflow-y-auto")}, - list{ - // Title input - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-1.5 text-sm text-gray-200 focus:border-cyan-500 outline-none", - ), - Attrs.value(gist.title), - Events.onInput(v => ScriptGist(UpdateGistTitle(v))), - Attrs.placeholder("Gist title"), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1.5 text-xs bg-cyan-700 hover:bg-cyan-600 text-white rounded", - ), - Events.onClick(ScriptGist(ExecuteGist)), - KeyboardNav.onActivate(ScriptGist(ExecuteGist)), - }, - list{text("Run")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded", - ), - Events.onClick(ScriptGist(ToggleGistPin(gist.id))), - }, - list{text(gist.pinned ? "Unpin" : "Pin")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1.5 text-xs bg-gray-700 hover:bg-gray-600 text-gray-300 rounded", - ), - Events.onClick(ScriptGist(SnapshotDiachronic)), - KeyboardNav.onActivate(ScriptGist(SnapshotDiachronic)), - }, - list{text("Checkpoint")}, - ), - }, - ), - // Schema info bar - div( - list{Attrs.class_("flex items-center gap-3 text-[10px] text-gray-500 px-1")}, - list{ - span(list{}, list{text("MCP: " ++ gist.schema.toolName)}), - span( - list{}, - list{ - text( - "~" ++ Int.toString(ScriptGistEngine.schemaTokenCost(gist.schema)) ++ " tokens", - ), - }, - ), - span(list{}, list{text(ScriptGistEngine.languageLabel(gist.language))}), - span(list{}, list{text(ScriptGistEngine.targetLabel(gist.target))}), - }, - ), - // Code editor - textarea( - list{ - Attrs.class_( - "w-full flex-1 min-h-[300px] bg-gray-900 border border-gray-700 rounded p-3 text-xs text-gray-200 font-mono resize-none focus:border-cyan-500 outline-none", - ), - Attrs.value(gist.code), - Events.onInput(v => ScriptGist(UpdateGistCode(v))), - Attrs.placeholder("// Write your gist code here..."), - }, - list{}, - ), - // Last result - switch state.lastResult { - | Some(result) => - div( - list{ - Attrs.class_( - "p-2 rounded text-xs font-mono " ++ ( - result.success ? "bg-green-900/30 text-green-400" : "bg-red-900/30 text-red-400" - ), - ), - }, - list{ - div(list{}, list{text(result.success ? "Success" : "Error")}), - div(list{Attrs.class_("mt-1 text-gray-400")}, list{text(result.output)}), - div( - list{Attrs.class_("mt-1 text-gray-600")}, - list{text(Float.toString(result.durationMs) ++ "ms | " ++ result.invoker)}, - ), - }, - ) - | None => noNode - }, - }, - ) - } - } -} - -/// Render the diachronic timeline sidebar. -let renderDiachronicTimeline = (state: scriptGistState): Tea_Vdom.t => { - if Array.length(state.diachronicHistory) === 0 { - div( - list{Attrs.class_("text-[10px] text-gray-600 p-2")}, - list{text("No diachronic checkpoints yet. Click 'Checkpoint' to snapshot.")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.diachronicHistory - ->Array.map(cp => - button( - list{ - Attrs.class_( - "w-full text-left px-2 py-1 text-[10px] text-gray-400 hover:bg-gray-800 rounded", - ), - Events.onClick(ScriptGist(RestoreDiachronic(cp.index))), - }, - list{text(cp.label)}, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render the synchronic cardfiles sidebar. -let renderCardfiles = (state: scriptGistState): Tea_Vdom.t => { - if Array.length(state.cardfiles) === 0 { - div( - list{Attrs.class_("text-[10px] text-gray-600 p-2")}, - list{text("No cardfiles yet. Cardfiles compose gists into spatial arrangements.")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.cardfiles - ->Array.map(cf => - div( - list{Attrs.class_("px-2 py-1.5 bg-gray-800/40 rounded text-xs text-gray-400")}, - list{text(ScriptGistEngine.cardfileLabel(cf))}, - ) - ) - ->List.fromArray, - ) - } -} - -/// Render template list. -let renderTemplates = (state: scriptGistState): Tea_Vdom.t => { - div( - list{Attrs.class_("grid grid-cols-2 gap-2")}, - state.templates - ->Array.map(tpl => - button( - list{ - Attrs.class_( - "p-3 bg-gray-800/60 border border-gray-700 rounded hover:bg-gray-800 text-left transition-colors", - ), - Events.onClick(ScriptGist(CreateFromTemplate(tpl.id))), - }, - list{ - div(list{Attrs.class_("text-xs font-medium text-gray-200 mb-1")}, list{text(tpl.name)}), - div(list{Attrs.class_("text-[10px] text-gray-500")}, list{text(tpl.description)}), - div( - list{ - Attrs.class_("text-[10px] mt-1 " ++ ScriptGistEngine.languageColour(tpl.language)), - }, - list{text(ScriptGistEngine.languageLabel(tpl.language))}, - ), - }, - ) - ) - ->List.fromArray, - ) -} - -/// Main panel view. -let view = (state: scriptGistState): Tea_Vdom.t => { - let filtered = - state.gists - ->ScriptGistEngine.filterByCategory(state.activeCategory) - ->ScriptGistEngine.filterBySearch(state.filterText) - ->ScriptGistEngine.sortGists(state.sortBy) - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text("Script Gist")}, - ), - span( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("Minskian Drafting Board")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-cyan-700 hover:bg-cyan-600 text-white rounded", - ), - Events.onClick(ScriptGist(CreateGist)), - KeyboardNav.onActivate(ScriptGist(CreateGist)), - }, - list{text("+ New Gist")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs rounded " ++ ( - state.mcpToolsActive - ? "bg-green-700 text-white" - : "bg-gray-700 text-gray-400 hover:bg-gray-600" - ), - ), - Events.onClick(ScriptGist(ToggleMcpTools)), - KeyboardNav.onActivate(ScriptGist(ToggleMcpTools)), - }, - list{text("MCP " ++ (state.mcpToolsActive ? "ON" : "OFF"))}, - ), - }, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800/50")}, - ScriptGistEngine.allCategories - ->Array.map(cat => - renderTab( - ScriptGistEngine.categoryLabel(cat), - cat, - state.activeCategory, - ScriptGistEngine.countByCategory(state.gists, cat), - ) - ) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/30 border border-red-800 rounded text-xs text-red-400 flex justify-between", - ), - }, - list{ - span(list{}, list{text(err)}), - button( - list{ - Attrs.class_("text-red-500 hover:text-red-300"), - Events.onClick(ScriptGist(DismissGistError)), - KeyboardNav.onActivate(ScriptGist(DismissGistError)), - }, - list{text("dismiss")}, - ), - }, - ) - | None => noNode - }, - // Main content area - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar: gist list + filter - div( - list{Attrs.class_("w-64 border-r border-gray-800 flex flex-col overflow-hidden")}, - list{ - // Search - div( - list{Attrs.class_("p-2")}, - list{ - input( - list{ - Attrs.class_( - "w-full bg-gray-800 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 placeholder-gray-600 outline-none focus:border-cyan-600", - ), - Attrs.value(state.filterText), - Events.onInput(v => ScriptGist(SetGistFilter(v))), - Attrs.placeholder("Search gists..."), - }, - list{}, - ), - }, - ), - // Gist list or templates - div( - list{Attrs.class_("flex-1 overflow-y-auto p-2 space-y-1")}, - if state.activeCategory === GistTemplates { - list{renderTemplates(state)} - } else { - filtered - ->Array.map(g => renderGistRow(g, Some(g.id) === state.selectedGistId)) - ->List.fromArray - }, - ), - }, - ), - // Centre: editor - div( - list{Attrs.class_("flex-1 flex flex-col p-3 overflow-hidden")}, - list{renderEditor(state)}, - ), - // Right sidebar: diachronic timeline + synchronic cardfiles - div( - list{Attrs.class_("w-48 border-l border-gray-800 flex flex-col overflow-hidden")}, - list{ - div( - list{Attrs.class_("p-2 border-b border-gray-800/50")}, - list{ - span( - list{Attrs.class_("text-[10px] text-gray-500 uppercase tracking-wide")}, - list{text("Diachronic (Time)")}, - ), - }, - ), - div( - list{Attrs.class_("flex-1 overflow-y-auto p-1")}, - list{renderDiachronicTimeline(state)}, - ), - div( - list{Attrs.class_("p-2 border-t border-gray-800/50 border-b border-gray-800/50")}, - list{ - span( - list{Attrs.class_("text-[10px] text-gray-500 uppercase tracking-wide")}, - list{text("Synchronic (Space)")}, - ), - }, - ), - div(list{Attrs.class_("flex-1 overflow-y-auto p-1")}, list{renderCardfiles(state)}), - }, - ), - }, - ), - }, - ) -} diff --git a/src/components/ScriptingBridge.affine b/src/components/ScriptingBridge.affine new file mode 100644 index 00000000..e474ee4d --- /dev/null +++ b/src/components/ScriptingBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ScriptingBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/ScriptingBridge.res b/src/components/ScriptingBridge.res deleted file mode 100644 index 657fe29b..00000000 --- a/src/components/ScriptingBridge.res +++ /dev/null @@ -1,370 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Scripting Bridge Component — VM instruction scripting REPL. -/// Displays REPL with input/output history, instruction reference table, -/// saved scripts list, and analysis view. - -open Model -open Msg -open Tea.Html - -/// Render an instruction tier badge. -let tierBadge = (tier: instructionTier): Tea_Vdom.t => { - let (color, label) = switch tier { - | TierSafe => ("bg-green-700 text-green-100", "T0 Safe") - | TierControlled => ("bg-blue-700 text-blue-100", "T1 Ctrl") - | TierPrivileged => ("bg-yellow-700 text-yellow-100", "T2 Priv") - | TierSystem => ("bg-red-700 text-red-100", "T3 Sys") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render an analysis severity badge. -let analysisSevBadge = (sev: analysisSeverity): Tea_Vdom.t => { - let (color, label) = switch sev { - | AnalysisError => ("text-red-400", "ERR") - | AnalysisWarning => ("text-yellow-400", "WARN") - | AnalysisInfo => ("text-blue-400", "INFO") - | AnalysisOptimisation => ("text-green-400", "OPT") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Main view function for the Scripting Bridge panel. -let view = (state: scriptingBridgeState): Tea_Vdom.t => { - let scriptCount = Array.length(state.savedScripts) - let historyCount = Array.length(state.replHistory) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Scripting Bridge — VM Instruction Scripting REPL"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-rose-300")}, - list{text("Scripting Bridge")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(scriptCount) ++ - " scripts, " ++ - Int.toString(historyCount) ++ " entries", - ), - }, - ), - if state.executing { - span( - list{Attrs.class_("text-xs text-yellow-400 animate-pulse")}, - list{text("Executing...")}, - ) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-rose-800 hover:bg-rose-700 text-white rounded"), - Events.onClick(ScriptingBridge(ScBStarted)), - KeyboardNav.onActivate(ScriptingBridge(ScBStarted)), - }, - list{text("Execute")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Repl { - "bg-rose-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ScriptingBridge(SetScBTab(Repl))), - }, - list{text("REPL")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Instructions { - "bg-rose-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ScriptingBridge(SetScBTab(Instructions))), - }, - list{text("Instructions")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Scripts { - "bg-rose-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ScriptingBridge(SetScBTab(Scripts))), - }, - list{text("Scripts")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Analysis { - "bg-rose-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(ScriptingBridge(SetScBTab(Analysis))), - }, - list{text("Analysis")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(ScriptingBridge(DismissScBError)), - KeyboardNav.onActivate(ScriptingBridge(DismissScBError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Repl => - div( - list{Attrs.class_("flex flex-col h-full")}, - list{ - // REPL history - div( - list{Attrs.class_("flex-1 overflow-y-auto mb-3 space-y-2")}, - state.replHistory - ->Array.map(entry => - div( - list{Attrs.class_("font-mono text-sm")}, - list{ - div( - list{Attrs.class_("flex items-start gap-2")}, - list{ - span(list{Attrs.class_("text-rose-400")}, list{text("> ")}), - span(list{Attrs.class_("text-gray-200")}, list{text(entry.input)}), - }, - ), - div( - list{ - Attrs.class_( - "pl-4 " ++ if entry.success { - "text-gray-400" - } else { - "text-red-400" - }, - ), - }, - list{text(entry.output)}, - ), - }, - ) - ) - ->List.fromArray, - ), - // Input area - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-rose-400 font-mono")}, list{text("> ")}), - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-2 py-1 text-sm font-mono text-gray-200", - ), - Attrs.value(state.replInput), - Attrs.placeholder("Enter VM script..."), - }, - list{}, - ), - }, - ), - }, - ) - | Instructions => - div( - list{}, - list{ - // Table header - div( - list{ - Attrs.class_( - "flex gap-2 text-xs text-gray-500 font-mono border-b border-gray-800 pb-1 mb-2", - ), - }, - list{ - span(list{Attrs.class_("w-12")}, list{text("Op")}), - span(list{Attrs.class_("w-28")}, list{text("Mnemonic")}), - span(list{Attrs.class_("w-20")}, list{text("Tier")}), - span(list{Attrs.class_("w-24")}, list{text("Stack")}), - span(list{Attrs.class_("flex-1")}, list{text("Description")}), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - state.instructions - ->Array.map(instr => - div( - list{ - Attrs.class_( - "flex gap-2 text-xs py-1 border-b border-gray-800/30 items-center", - ), - }, - list{ - span( - list{Attrs.class_("w-12 font-mono text-gray-500")}, - list{text(Int.toString(instr.opcode))}, - ), - span( - list{ - Attrs.class_( - "w-28 font-mono " ++ if instr.allowed { - "text-gray-200" - } else { - "text-gray-600 line-through" - }, - ), - }, - list{text(instr.name)}, - ), - span(list{Attrs.class_("w-20")}, list{tierBadge(instr.tier)}), - span( - list{Attrs.class_("w-24 font-mono text-gray-500")}, - list{text(instr.stackEffect)}, - ), - span( - list{Attrs.class_("flex-1 text-gray-400")}, - list{text(instr.description)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | Scripts => - div( - list{Attrs.class_("space-y-2")}, - state.savedScripts - ->Array.map(s => { - let isSelected = state.selectedScript == Some(s.id) - div( - list{ - Attrs.class_( - "px-3 py-2 border rounded cursor-pointer " ++ if isSelected { - "bg-rose-900/30 border-rose-700" - } else { - "bg-gray-900 border-gray-800 hover:border-gray-700" - }, - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-sm font-bold text-gray-200")}, - list{text(s.name)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(s.description)}, - ), - }, - ), - div( - list{Attrs.class_("mt-1 text-xs text-gray-600 font-mono truncate")}, - list{text(s.code)}, - ), - }, - ) - }) - ->List.fromArray, - ) - | Analysis => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{text(Int.toString(Array.length(state.analysisFindings)) ++ " findings")}, - ), - div( - list{}, - state.analysisFindings - ->Array.map(f => - div( - list{Attrs.class_("px-3 py-2 bg-gray-900 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - analysisSevBadge(f.severity), - span( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(f.summary)}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("L" ++ Int.toString(f.line))}, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{text(f.detail)}), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Security.affine b/src/components/Security.affine new file mode 100644 index 00000000..76458e28 --- /dev/null +++ b/src/components/Security.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Security; + +// TODO: Complete semantic implementation diff --git a/src/components/Security.res b/src/components/Security.res deleted file mode 100644 index 83c1b457..00000000 --- a/src/components/Security.res +++ /dev/null @@ -1,376 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Security Panel — redaction, vault, 2FA, Trustfile, shoulder-safe (DD-026/027). -/// -/// This is the security command centre. It shows detected secrets, vault status, -/// 2FA state, and Trustfile compliance. The shoulder-safe mode blurs secrets -/// in real-time for when someone's looking over your shoulder. - -open Model -open Msg -open Tea.Html - -/// Render a status indicator (coloured dot + label). -let renderStatus = (label: string, ok: bool, tooltip: string): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2"), Attrs.title(tooltip)}, - list{ - div(list{Attrs.class_(`w-2 h-2 rounded-full ${ok ? "bg-green-500" : "bg-red-500"}`)}, list{}), - div(list{Attrs.class_("text-xs text-gray-400")}, list{text(label)}), - }, - ) -} - -/// Render the redaction patterns section with CRUD. -let renderPatterns = (security: securityState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-2")}, - list{ - // Pattern list - div( - list{Attrs.class_("space-y-1")}, - Array.map(security.patterns, pattern => - div( - list{ - Attrs.class_( - `flex items-center justify-between p-2 rounded ${pattern.enabled - ? "bg-gray-800" - : "bg-gray-900"} hover:bg-gray-700/50 transition-colors`, - ), - Attrs.title(`Regex: ${pattern.pattern}`), - }, - list{ - div( - list{ - Attrs.class_("flex items-center gap-2 flex-1 cursor-pointer"), - Events.onClick(Security(TogglePattern(pattern.id))), - }, - list{ - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${pattern.enabled ? "bg-green-500" : "bg-gray-600"}`, - ), - }, - list{}, - ), - div(list{Attrs.class_("text-xs text-gray-300")}, list{text(pattern.label)}), - if pattern.builtIn { - div(list{Attrs.class_("text-xs text-gray-600")}, list{text("built-in")}) - } else { - div(list{Attrs.class_("text-xs text-blue-600")}, list{text("custom")}) - }, - }, - ), - // Regex preview - span( - list{Attrs.class_("text-[10px] text-gray-700 font-mono truncate max-w-32")}, - list{text(pattern.pattern)}, - ), - // Delete button (custom only) - if !pattern.builtIn { - button( - list{ - Attrs.class_("ml-2 text-xs text-red-700 hover:text-red-400 transition-colors"), - Events.onClick(Security(RemovePattern(pattern.id))), - Attrs.ariaLabel(`Remove pattern ${pattern.label}`), - Attrs.title("Remove custom pattern"), - }, - list{text("x")}, - ) - } else { - noNode - }, - }, - ) - )->List.fromArray, - ), - // Add custom pattern form - div( - list{Attrs.class_("mt-3 p-3 bg-gray-900/50 border border-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Add Custom Pattern")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700 focus:border-red-600 focus:outline-none", - ), - Attrs.placeholder("Label (e.g. Stripe Key)"), - Attrs.value(security.newPatternLabel), - Events.onInput(v => Security(SetNewPatternLabel(v))), - Attrs.ariaLabel("New pattern label"), - }, - list{}, - ), - input( - list{ - Attrs.class_( - "flex-1 px-2 py-1 text-xs bg-gray-800 text-gray-200 rounded border border-gray-700 font-mono focus:border-red-600 focus:outline-none", - ), - Attrs.placeholder("Regex (e.g. sk_live_[a-zA-Z0-9]+)"), - Attrs.value(security.newPatternRegex), - Events.onInput(v => Security(SetNewPatternRegex(v))), - Attrs.ariaLabel("New pattern regex"), - }, - list{}, - ), - button( - list{ - Attrs.class_( - if security.newPatternLabel != "" && security.newPatternRegex != "" { - "px-3 py-1 text-xs bg-red-900/50 text-red-300 rounded border border-red-700 hover:bg-red-800/50 transition-colors" - } else { - "px-3 py-1 text-xs bg-gray-800 text-gray-600 rounded border border-gray-700 cursor-not-allowed" - }, - ), - Events.onClick(Security(SubmitNewPattern)), - KeyboardNav.onActivate(Security(SubmitNewPattern)), - Attrs.disabled(security.newPatternLabel == "" || security.newPatternRegex == ""), - Attrs.ariaLabel("Add pattern"), - }, - list{text("Add")}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Render the vault section. -let renderVault = (security: securityState): Tea_Vdom.t => { - let statusLabel = switch security.vaultStatus { - | VaultLocked => "Locked" - | VaultUnlocked => "Unlocked" - | VaultUnavailable => "Unavailable" - | VaultError(e) => "Error: " ++ e - } - let isOk = switch security.vaultStatus { - | VaultUnlocked => true - | _ => false - } - div( - list{Attrs.class_("space-y-3")}, - list{ - renderStatus("Vault: " ++ statusLabel, isOk, "reasonably-good-tool vault integration"), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Int.toString(Array.length(security.vaultKeys)) ++ " keys stored")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 bg-gray-800 text-gray-400 rounded text-xs hover:bg-gray-700 transition-colors", - ), - Events.onClick(Security(VaultList)), - KeyboardNav.onActivate(Security(VaultList)), - Attrs.title("List all keys in the vault (names only, never values)"), - }, - list{text("List Keys")}, - ), - }, - ) -} - -/// Render the 2FA section. -let render2FA = (security: securityState): Tea_Vdom.t => { - let statusLabel = switch security.twoFactorStatus { - | TwoFactorNotConfigured => "Not configured" - | TwoFactorConfigured => "Configured (not authenticated)" - | TwoFactorAuthenticated(_) => "Authenticated" - | TwoFactorExpired => "Session expired" - } - let isOk = switch security.twoFactorStatus { - | TwoFactorAuthenticated(_) => true - | _ => false - } - div( - list{Attrs.class_("space-y-3")}, - list{ - renderStatus("2FA: " ++ statusLabel, isOk, "TOTP-based two-factor authentication (RFC 6238)"), - div( - list{Attrs.class_("flex gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-800 text-gray-200 text-sm p-2 rounded border border-gray-700 focus:border-blue-600 focus:outline-none", - ), - Attrs.placeholder("Enter 6-digit TOTP code"), - Attrs.value(security.totpInput), - Events.onInput(v => Security(SetTotpInput(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 bg-blue-700 text-white rounded text-sm hover:bg-blue-600 transition-colors", - ), - Events.onClick(Security(SubmitTotp(security.totpInput))), - }, - list{text("Verify")}, - ), - }, - ), - }, - ) -} - -/// Full Security panel view. -let view = (security: securityState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 overflow-auto"), - Attrs.role("dialog"), - Attrs.ariaLabel("Security panel"), - }, - list{ - // Header - div( - list{ - Attrs.class_( - "sticky top-0 bg-gray-950 border-b border-gray-800 p-4 flex items-center justify-between z-10", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div(list{Attrs.class_("text-lg font-light text-gray-300")}, list{text("Security")}), - // Shoulder-safe toggle - button( - list{ - Attrs.class_( - `px-3 py-1 rounded text-xs font-medium ${security.shoulderSafe - ? "bg-red-700 text-white" - : "bg-gray-800 text-gray-400"} hover:opacity-80 transition-opacity`, - ), - Events.onClick(Security(ToggleShoulderSafe)), - KeyboardNav.onActivate(Security(ToggleShoulderSafe)), - Attrs.title( - "Toggle shoulder-surfing safe mode — blurs detected secrets in real-time", - ), - }, - list{text(security.shoulderSafe ? "Shoulder-Safe ON" : "Shoulder-Safe OFF")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text(Int.toString(Array.length(security.detectedSecrets)) ++ " secrets detected"), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 bg-gray-800 text-gray-400 rounded hover:bg-gray-700 transition-colors text-sm", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Body: three-column layout - div( - list{Attrs.class_("p-6 grid grid-cols-3 gap-6 max-w-6xl mx-auto")}, - list{ - // Column 1: Redaction patterns - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{ - Attrs.class_("text-sm font-medium text-gray-400 border-b border-gray-800 pb-1"), - }, - list{text("Redaction Patterns")}, - ), - renderPatterns(security), - }, - ), - // Column 2: Vault - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{ - Attrs.class_("text-sm font-medium text-gray-400 border-b border-gray-800 pb-1"), - }, - list{text("Vault (reasonably-good-tool)")}, - ), - renderVault(security), - }, - ), - // Column 3: 2FA + Trustfile - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{ - Attrs.class_("text-sm font-medium text-gray-400 border-b border-gray-800 pb-1"), - }, - list{text("Two-Factor Authentication")}, - ), - render2FA(security), - div( - list{ - Attrs.class_( - "mt-6 text-sm font-medium text-gray-400 border-b border-gray-800 pb-1", - ), - }, - list{text("Trustfile Policy")}, - ), - switch security.trustfile { - | Some(policy) => - div( - list{Attrs.class_("space-y-1")}, - list{ - renderStatus( - "Trustfile loaded", - policy.loaded, - "Loaded from " ++ Option.getOr(policy.filePath, "unknown"), - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Security level: " ++ - switch policy.securityLevel { - | SecurityLow => "Low" - | SecurityMedium => "Medium" - | SecurityHigh => "High" - | SecurityMaximum => "Maximum" - }, - ), - }, - ), - }, - ) - | None => - div( - list{ - Attrs.class_( - "p-3 bg-gray-900/50 rounded border border-gray-800 text-xs text-gray-600", - ), - Attrs.title("Load a repo with a Trustfile.a2ml to enable policy enforcement"), - }, - list{text("No Trustfile loaded — load a repo to enable policy enforcement")}, - ) - }, - }, - ), - }, - ), - }, - ) -} diff --git a/src/components/SoakMonitor.affine b/src/components/SoakMonitor.affine new file mode 100644 index 00000000..f6a49048 --- /dev/null +++ b/src/components/SoakMonitor.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SoakMonitor; + +// TODO: Complete semantic implementation diff --git a/src/components/SoakMonitor.res b/src/components/SoakMonitor.res deleted file mode 100644 index e74acae2..00000000 --- a/src/components/SoakMonitor.res +++ /dev/null @@ -1,440 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL SoakMonitor — long-running session tracking, memory leak detection, -/// and trend analysis for IDApTIK extended play sessions. -/// -/// Four tabs: Live Monitor (current session vitals), Trends (memory trend -/// display), Leak Detection (suspects table with growth rate and confidence), -/// and History (previous soak session summaries). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for soakTab variants. -let tabLabel = (tab: soakTab): string => - switch tab { - | TabLiveMonitor => "Live Monitor" - | TabTrends => "Trends" - | TabLeakDetection => "Leak Detection" - | TabHistory => "History" - } - -/// Render the tab bar. -let renderTabs = (active: soakTab): Tea_Vdom.t => { - let tabs: array = [TabLiveMonitor, TabTrends, TabLeakDetection, TabHistory] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(SoakMonitor(SetSmTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Format bytes into human-readable string. -let formatBytes = (bytes: int): string => { - let b = Int.toFloat(bytes) - if b >= 1073741824.0 { - `${Float.toFixed(b /. 1073741824.0, ~digits=2)} GB` - } else if b >= 1048576.0 { - `${Float.toFixed(b /. 1048576.0, ~digits=1)} MB` - } else if b >= 1024.0 { - `${Float.toFixed(b /. 1024.0, ~digits=0)} KB` - } else { - `${Int.toString(bytes)} B` - } -} - -/// Session status badge. -let sessionStatusBadge = (status: soakSessionStatus): Tea_Vdom.t => - switch status { - | SoakRunning => - span( - list{ - Attrs.class_( - "px-1.5 py-0.5 text-xs rounded bg-emerald-600 text-white font-mono animate-pulse", - ), - }, - list{text("RUNNING")}, - ) - | SoakCompleted => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-blue-600 text-white font-mono")}, - list{text("COMPLETED")}, - ) - | SoakAborted(_) => - span( - list{Attrs.class_("px-1.5 py-0.5 text-xs rounded bg-red-600 text-white font-mono")}, - list{text("ABORTED")}, - ) - } - -/// Confidence level colour for leak suspects. -let confidenceColour = (confidence: float): string => - if confidence >= 0.8 { - "text-red-400" - } else if confidence >= 0.5 { - "text-amber-400" - } else { - "text-gray-400" - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Live Monitor tab: current session vitals and real-time memory display. -let renderLiveMonitorTab = (state: soakMonitorState): Tea_Vdom.t => { - switch state.currentSession { - | None => - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No active soak session. Click Start Monitor to begin tracking.")}, - ) - | Some(session) => { - let latestMem = state.trendData->Array.get(Array.length(state.trendData) - 1) - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - // Session info - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - sessionStatusBadge(session.status), - span( - list{Attrs.class_("text-sm text-gray-300")}, - list{text(`Session: ${session.id}`)}, - ), - }, - ), - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`${Float.toFixed(session.durationMinutes, ~digits=1)} min`)}, - ), - }, - ), - // Key metrics - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-cyan-400")}, - list{text(formatBytes(session.peakMemoryBytes))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Peak Memory")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(`${Float.toFixed(session.gcFrequencyPerMinute, ~digits=1)}/min`)}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("GC Frequency")}), - }, - ), - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-amber-400")}, - list{text(Int.toString(Array.length(session.leakSuspects)))}, - ), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text("Leak Suspects")}), - }, - ), - }, - ), - // Current heap usage from latest trend point - switch latestMem { - | Some(point) => { - let usedPct = - Int.toFloat(point.heapUsedBytes) /. - Int.toFloat(max(1, point.heapTotalBytes)) *. 100.0 - let widthPct = Int.toString(Int.fromFloat(usedPct)) - let barColour = if usedPct > 85.0 { - "bg-red-500" - } else if usedPct > 60.0 { - "bg-amber-500" - } else { - "bg-emerald-500" - } - div( - list{Attrs.class_("bg-gray-800 rounded p-3")}, - list{ - div( - list{Attrs.class_("flex justify-between text-xs text-gray-400 mb-1")}, - list{ - text("Heap"), - text( - `${formatBytes(point.heapUsedBytes)} / ${formatBytes( - point.heapTotalBytes, - )}`, - ), - }, - ), - div( - list{Attrs.class_("w-full h-2 bg-gray-700 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full ${barColour} transition-all duration-300 w-[${widthPct}%]`, - ), - }, - list{}, - ), - }, - ), - }, - ) - } - | None => noNode - }, - }, - ) - } - } -} - -/// Trends tab: memory trend data points display. -let renderTrendsTab = (state: soakMonitorState): Tea_Vdom.t => { - let pointCount = Array.length(state.trendData) - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-32 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text(`Memory trend chart (${Int.toString(pointCount)} data points)`)}, - ), - }, - ), - // Recent trend data rows - div( - list{Attrs.class_("flex flex-col gap-1 max-h-64 overflow-y-auto")}, - state.trendData - ->Array.sliceToEnd(~start=max(0, pointCount - 15)) - ->Array.map(point => { - div( - list{ - Attrs.class_("flex justify-between text-xs font-mono px-2 py-1 bg-gray-800 rounded"), - }, - list{ - span( - list{Attrs.class_("text-gray-500")}, - list{text(formatBytes(point.heapUsedBytes))}, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text(`GC: ${Int.toString(point.gcCount)}`)}, - ), - span( - list{Attrs.class_("text-gray-600")}, - list{text(`Pause: ${Float.toFixed(point.gcPauseMs, ~digits=1)}ms`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Leak Detection tab: suspects table with growth rate and confidence. -let renderLeakDetectionTab = (state: soakMonitorState): Tea_Vdom.t => { - if Array.length(state.leakSuspects) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No leak suspects detected. Memory allocation patterns appear healthy.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{text(`${Int.toString(Array.length(state.leakSuspects))} suspect(s)`)}, - ), - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - state.leakSuspects - ->Array.map(suspect => { - let confColour = confidenceColour(suspect.confidence) - div( - list{ - Attrs.class_( - "flex items-center justify-between px-3 py-2 bg-gray-800 rounded text-sm", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-300 flex-1 font-mono text-xs")}, - list{text(suspect.source)}, - ), - span( - list{Attrs.class_("text-gray-400 text-xs")}, - list{text(`+${formatBytes(suspect.growthRatePerHour)}/hr`)}, - ), - span( - list{Attrs.class_(`text-xs font-mono ${confColour}`)}, - list{text(`${Float.toFixed(suspect.confidence *. 100.0, ~digits=0)}%`)}, - ), - span( - list{Attrs.class_("text-gray-600 text-xs")}, - list{text(`${Int.toString(suspect.samples)} samples`)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// History tab: previous soak session summaries. -let renderHistoryTab = (state: soakMonitorState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4 max-h-96 overflow-y-auto")}, - state.sessions - ->Array.map(session => { - let leakCount = Array.length(session.leakSuspects) - let borderCls = leakCount > 0 ? "border-amber-700" : "border-gray-700" - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${borderCls}`)}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm text-gray-300 font-mono")}, list{text(session.id)}), - sessionStatusBadge(session.status), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-400")}, - list{ - text(`${Float.toFixed(session.durationMinutes, ~digits=1)} min`), - text(`Peak: ${formatBytes(session.peakMemoryBytes)}`), - text(`GC: ${Float.toFixed(session.gcFrequencyPerMinute, ~digits=1)}/min`), - text(`${Int.toString(leakCount)} leak suspect(s)`), - }, - ), - }, - ) - }) - ->List.fromArray, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: soakMonitorState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabLiveMonitor => renderLiveMonitorTab(state) - | TabTrends => renderTrendsTab(state) - | TabLeakDetection => renderLeakDetectionTab(state) - | TabHistory => renderHistoryTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with Start/Stop - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2(list{Attrs.class_("text-lg font-semibold text-cyan-300")}, list{text("Soak Monitor")}), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(SoakMonitor(StartMonitor)), - KeyboardNav.onActivate(SoakMonitor(StartMonitor)), - }, - list{text("Start Monitor")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-red-700 text-white rounded hover:bg-red-600 cursor-pointer font-medium", - ), - Events.onClick(SoakMonitor(StopMonitor)), - KeyboardNav.onActivate(SoakMonitor(StopMonitor)), - }, - list{text("Stop Monitor")}, - ), - }, - ), - }, - ), - // Monitoring indicator - if state.monitoring { - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700"), - }, - list{ - div(list{Attrs.class_("w-3 h-3 bg-emerald-400 rounded-full animate-pulse")}, list{}), - span( - list{Attrs.class_("text-sm text-emerald-300")}, - list{text("Soak monitoring active...")}, - ), - }, - ) - } else { - noNode - }, - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/SpecBrowser.affine b/src/components/SpecBrowser.affine new file mode 100644 index 00000000..3271f3f9 --- /dev/null +++ b/src/components/SpecBrowser.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SpecBrowser; + +// TODO: Complete semantic implementation diff --git a/src/components/SpecBrowser.res b/src/components/SpecBrowser.res deleted file mode 100644 index 93079dfa..00000000 --- a/src/components/SpecBrowser.res +++ /dev/null @@ -1,843 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL SpecBrowser Component — browse language specifications side-by-side. -/// -/// Five tabs: Overview, Compare, Grammar, Typing Rules, Verification. -/// Shows all 16 nextgen-languages with their grammar, spec, typing rules, -/// taxonomy completeness, and verification status. - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Shared sub-views -// ============================================================================ - -/// Render category tabs. -let renderTabs = (active: specBrowserCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - SpecBrowserEngine.allCategories - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-teal-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(SpecBrowser(SetSpecCategory(tab))), - }, - list{text(SpecBrowserEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a taxonomy completeness badge. -let renderCompletenessBadge = (pct: int): Tea_Vdom.t => { - span( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded border ${SpecBrowserEngine.completenessBadge(pct)}`, - ), - }, - list{text(Int.toString(pct) ++ "%")}, - ) -} - -/// Render file presence indicators for a language. -let renderFilePresence = (files: array): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1")}, - files - ->Array.map(f => { - let colour = SpecBrowserEngine.presenceColour(f.exists) - let code = SpecBrowserEngine.fileKindCode(f.kind) - span( - list{ - Attrs.class_( - `px-1 py-0.5 text-[10px] rounded ${colour} ${if f.exists { - "bg-emerald-900/20" - } else { - "bg-red-900/20" - }}`, - ), - Attrs.ariaLabel( - SpecBrowserEngine.fileKindLabel(f.kind) ++ if f.exists { - " present" - } else { - " missing" - }, - ), - }, - list{text(code)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a language row in the overview grid. -let renderLanguageRow = (lang: SpecBrowserModel.specLanguageEntry): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 border-b border-gray-800 hover:bg-gray-900/50 cursor-pointer", - ), - Events.onClick(SpecBrowser(SelectSpecLanguage(Some(lang.name)))), - }, - list{ - div( - list{Attrs.class_("w-32")}, - list{ - div(list{Attrs.class_("text-sm font-medium text-gray-200")}, list{text(lang.name)}), - div(list{Attrs.class_("text-[10px] text-gray-500")}, list{text(lang.implLang)}), - }, - ), - div( - list{Attrs.class_("flex-1 text-xs text-gray-400 truncate")}, - list{text(lang.description)}, - ), - renderFilePresence(lang.files), - renderCompletenessBadge(lang.taxonomyCompleteness), - div( - list{Attrs.class_("w-20 text-right text-xs text-gray-500")}, - list{text(Int.toString(lang.verification.totalTests) ++ " tests")}, - ), - }, - ) -} - -/// Render a spec content pane (for grammar/typing rules/comparison). -let renderSpecContent = (title: string, content: option): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4 flex-1 min-h-64")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wide mb-2")}, - list{text(title)}, - ), - switch content { - | Some(c) => - pre( - list{ - Attrs.class_( - "font-mono text-xs text-gray-300 whitespace-pre-wrap overflow-auto max-h-96", - ), - }, - list{text(c)}, - ) - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic")}, - list{ - text("Content not loaded. Select a language and the spec will be loaded from disk."), - }, - ) - }, - }, - ) -} - -/// Render a comparison side selector. -let renderSideSelector = ( - side: SpecBrowserModel.comparisonSide, - selected: option, - langs: array, -): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-2")}, - list{ - label( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - switch side { - | LeftSide => "Left" - | RightSide => "Right" - }, - ), - }, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1")}, - langs - ->Array.map(l => { - let isSelected = selected === Some(l.name) - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded transition-colors ${isSelected - ? "bg-teal-600 text-white" - : "bg-gray-800 text-gray-400 hover:text-gray-200"}`, - ), - Events.onClick(SpecBrowser(SetComparisonSide(side, l.name))), - }, - list{text(l.name)}, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main view for the SpecBrowser panel. -let view = (sb: specBrowserState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("SpecBrowser language specification panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Spec Browser")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Language Specification Explorer")}, - ), - { - let summary = SpecBrowserEngine.portfolioSummary() - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text( - `${Int.toString(summary.totalLanguages)} languages, avg ${Int.toString( - summary.avgCompleteness, - )}% complete`, - ), - }, - ) - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - input( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded px-3 py-1 text-sm text-gray-200 placeholder-gray-600 w-48", - ), - Attrs.placeholder("Filter languages..."), - Attrs.value(sb.filterText), - Events.onInput(v => SpecBrowser(SetSpecFilter(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(sb.activeCategory), - { - let filtered = { - let base = SpecBrowserEngine.allLanguageSpecs - let searched = SpecBrowserEngine.filterBySearch(base, sb.filterText) - if sb.showIncompleteOnly { - SpecBrowserEngine.filterIncomplete(searched) - } else { - searched - } - } - switch sb.activeCategory { - // ── Overview Tab ── - | SpecOverview => - div( - list{Attrs.class_("space-y-4")}, - list{ - { - let summary = SpecBrowserEngine.portfolioSummary() - div( - list{Attrs.class_("grid grid-cols-5 gap-3 mb-4")}, - list{ - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-3 text-center", - ), - }, - list{ - div( - list{Attrs.class_("text-2xl font-mono text-teal-400")}, - list{text(Int.toString(summary.totalLanguages))}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Languages")}, - ), - }, - ), - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-3 text-center", - ), - }, - list{ - div( - list{ - Attrs.class_( - `text-2xl font-mono ${SpecBrowserEngine.completenessColour( - summary.avgCompleteness, - )}`, - ), - }, - list{text(Int.toString(summary.avgCompleteness) ++ "%")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Avg Completeness")}, - ), - }, - ), - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-3 text-center", - ), - }, - list{ - div( - list{Attrs.class_("text-2xl font-mono text-emerald-400")}, - list{text(Int.toString(summary.fullySpecified))}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Fully Specified")}, - ), - }, - ), - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-3 text-center", - ), - }, - list{ - div( - list{Attrs.class_("text-2xl font-mono text-cyan-400")}, - list{text(Int.toString(summary.totalTests))}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Total Tests")}, - ), - }, - ), - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg p-3 text-center", - ), - }, - list{ - div( - list{ - Attrs.class_( - `text-2xl font-mono ${if summary.totalAdmitted > 0 { - "text-amber-400" - } else { - "text-emerald-400" - }}`, - ), - }, - list{text(Int.toString(summary.totalAdmitted))}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Admitted/Sorry")}, - ), - }, - ), - }, - ) - }, - // Toggle incomplete filter - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded ${sb.showIncompleteOnly - ? "bg-amber-600 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(SpecBrowser(ToggleIncompleteOnly)), - KeyboardNav.onActivate(SpecBrowser(ToggleIncompleteOnly)), - }, - list{text("Show Incomplete Only")}, - ), - }, - ), - // Language grid - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 border-b border-gray-700 text-xs text-gray-500", - ), - }, - list{ - div(list{Attrs.class_("w-32")}, list{text("Language")}), - div(list{Attrs.class_("flex-1")}, list{text("Description")}), - div(list{Attrs.class_("w-56")}, list{text("Files")}), - div(list{Attrs.class_("w-12")}, list{text("Tax%")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Tests")}), - }, - ), - // Rows - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered->Array.map(renderLanguageRow)->List.fromArray, - ), - }, - ), - }, - ) - // ── Comparison Tab ── - | SpecComparison => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text("Select two languages to compare their specifications side-by-side."), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - renderSideSelector(LeftSide, sb.comparisonLeft, filtered), - renderSideSelector(RightSide, sb.comparisonRight, filtered), - }, - ), - // Side-by-side content - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - { - let left = sb.comparisonLeft->Option.flatMap(SpecBrowserEngine.findLanguage) - switch left { - | Some(l) => renderSpecContent(l.name ++ " — Grammar", l.grammarContent) - | None => renderSpecContent("Left — Grammar", None) - } - }, - { - let right = - sb.comparisonRight->Option.flatMap(SpecBrowserEngine.findLanguage) - switch right { - | Some(l) => renderSpecContent(l.name ++ " — Grammar", l.grammarContent) - | None => renderSpecContent("Right — Grammar", None) - } - }, - }, - ), - // File presence comparison - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - { - let left = sb.comparisonLeft->Option.flatMap(SpecBrowserEngine.findLanguage) - switch left { - | Some(l) => - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(l.name ++ " Files")}, - ), - div( - list{Attrs.class_("space-y-1")}, - l.files - ->Array.map(f => - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{ - Attrs.class_(SpecBrowserEngine.presenceColour(f.exists)), - }, - list{ - text( - if f.exists { - "Yes" - } else { - "No" - }, - ), - }, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text(SpecBrowserEngine.fileKindLabel(f.kind))}, - ), - if f.lineCount > 0 { - span( - list{Attrs.class_("text-gray-600")}, - list{text(Int.toString(f.lineCount) ++ " lines")}, - ) - } else { - noNode - }, - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic")}, - list{text("Select a language")}, - ) - } - }, - { - let right = - sb.comparisonRight->Option.flatMap(SpecBrowserEngine.findLanguage) - switch right { - | Some(l) => - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{text(l.name ++ " Files")}, - ), - div( - list{Attrs.class_("space-y-1")}, - l.files - ->Array.map(f => - div( - list{Attrs.class_("flex items-center gap-2 text-xs")}, - list{ - span( - list{ - Attrs.class_(SpecBrowserEngine.presenceColour(f.exists)), - }, - list{ - text( - if f.exists { - "Yes" - } else { - "No" - }, - ), - }, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text(SpecBrowserEngine.fileKindLabel(f.kind))}, - ), - if f.lineCount > 0 { - span( - list{Attrs.class_("text-gray-600")}, - list{text(Int.toString(f.lineCount) ++ " lines")}, - ) - } else { - noNode - }, - }, - ) - ) - ->List.fromArray, - ), - }, - ) - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic")}, - list{text("Select a language")}, - ) - } - }, - }, - ), - }, - ) - // ── Grammar Tab ── - | SpecGrammar => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Select a language to view its grammar definition (EBNF).")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1 mb-4")}, - filtered - ->Array.map(l => { - let isSelected = sb.selectedLanguage === Some(l.name) - let hasGrammar = l.files->Array.some(f => f.kind === GrammarEbnf && f.exists) - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded transition-colors ${isSelected - ? "bg-teal-600 text-white" - : hasGrammar - ? "bg-gray-800 text-gray-300 hover:text-gray-100" - : "bg-gray-800 text-gray-600"}`, - ), - Events.onClick(SpecBrowser(SelectSpecLanguage(Some(l.name)))), - }, - list{text(l.name)}, - ) - }) - ->List.fromArray, - ), - { - let selected = - sb.selectedLanguage->Option.flatMap(SpecBrowserEngine.findLanguage) - switch selected { - | Some(l) => renderSpecContent(l.name ++ " — grammar.ebnf", l.grammarContent) - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic mt-8 text-center")}, - list{text("Select a language above to view its grammar.")}, - ) - } - }, - }, - ) - // ── Typing Rules Tab ── - | SpecTypingRules => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Select a language to view its typing rules.")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-1 mb-4")}, - filtered - ->Array.map(l => { - let isSelected = sb.selectedLanguage === Some(l.name) - let hasRules = l.files->Array.some(f => f.kind === TypingRules && f.exists) - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded transition-colors ${isSelected - ? "bg-teal-600 text-white" - : hasRules - ? "bg-gray-800 text-gray-300 hover:text-gray-100" - : "bg-gray-800 text-gray-600"}`, - ), - Events.onClick(SpecBrowser(SelectSpecLanguage(Some(l.name)))), - }, - list{text(l.name)}, - ) - }) - ->List.fromArray, - ), - { - let selected = - sb.selectedLanguage->Option.flatMap(SpecBrowserEngine.findLanguage) - switch selected { - | Some(l) => - renderSpecContent(l.name ++ " — typing-rules.md", l.typingRulesContent) - | None => - div( - list{Attrs.class_("text-sm text-gray-600 italic mt-8 text-center")}, - list{text("Select a language above to view its typing rules.")}, - ) - } - }, - }, - ) - // ── Verification Tab ── - | SpecVerification => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{text("Test, proof, and conformance status across all language projects.")}, - ), - // Verification table - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 border-b border-gray-700 text-xs text-gray-500", - ), - }, - list{ - div(list{Attrs.class_("w-28")}, list{text("Language")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Tests")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Passing")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Pass%")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Proved")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Admitted")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Fuzz")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Conf")}), - }, - ), - // Rows - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered - ->Array.map(l => { - let v = l.verification - let passPct = if v.totalTests > 0 { - v.passingTests * 100 / v.totalTests - } else { - 0 - } - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 border-b border-gray-800 hover:bg-gray-900/50 text-xs", - ), - }, - list{ - div( - list{Attrs.class_("w-28 text-gray-200 font-medium")}, - list{text(l.name)}, - ), - div( - list{Attrs.class_("w-20 text-right text-gray-300 font-mono")}, - list{text(Int.toString(v.totalTests))}, - ), - div( - list{Attrs.class_("w-20 text-right text-emerald-400 font-mono")}, - list{text(Int.toString(v.passingTests))}, - ), - div( - list{ - Attrs.class_( - `w-16 text-right font-mono ${if passPct >= 90 { - "text-emerald-400" - } else if passPct >= 70 { - "text-amber-400" - } else { - "text-red-400" - }}`, - ), - }, - list{text(Int.toString(passPct) ++ "%")}, - ), - div( - list{Attrs.class_("w-20 text-right text-violet-400 font-mono")}, - list{text(Int.toString(v.provedCount))}, - ), - div( - list{ - Attrs.class_( - `w-20 text-right font-mono ${if v.admittedCount > 0 { - "text-amber-400" - } else { - "text-gray-600" - }}`, - ), - }, - list{text(Int.toString(v.admittedCount))}, - ), - div( - list{Attrs.class_("w-16 text-center")}, - list{ - text( - if v.hasFuzzing { - "Yes" - } else { - "-" - }, - ), - }, - ), - div( - list{ - Attrs.class_( - `w-16 text-center ${if v.conformancePassing { - "text-emerald-400" - } else { - "text-gray-600" - }}`, - ), - }, - list{ - text( - if v.conformancePassing { - "Pass" - } else { - "-" - }, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ) - } - }, - // Error display - switch sb.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/Stapeln.affine b/src/components/Stapeln.affine new file mode 100644 index 00000000..5595c82f --- /dev/null +++ b/src/components/Stapeln.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Stapeln; + +// TODO: Complete semantic implementation diff --git a/src/components/Stapeln.res b/src/components/Stapeln.res deleted file mode 100644 index 6e6bd5fb..00000000 --- a/src/components/Stapeln.res +++ /dev/null @@ -1,816 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Stapeln Assembly Pipeline — container stack assembly view. -/// -/// Renders within PanLL's three-panel framework to provide an overview -/// of container assembly constraints, reasoning, and generated artifacts. -/// The detailed node editor lives in stapeln's own frontend; this panel -/// shows the mission-control view: constraints, validation, and outputs. -/// -/// Three-panel layout: -/// Panel-L (Constraints & Discovery) — security, resource, network policy -/// Panel-N (Assembly Reasoning) — pipeline state, optimisation, posture -/// Panel-W (Results & Artifacts) — Containerfile, compose, scan results - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Panel-L: Constraints & Discovery -// ============================================================================ - -/// Render a single constraint row with label and value. -let constraintRow = (label: string, value: string, ~highlight: bool=false): Tea_Vdom.t => { - let valueCls = highlight ? "text-sm font-medium text-emerald-400" : "text-sm text-gray-300" - div( - list{Attrs.class_("flex items-center justify-between py-1.5 border-b border-gray-800")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(label)}), - span(list{Attrs.class_(valueCls)}, list{text(value)}), - }, - ) -} - -/// Render a boolean constraint as a coloured badge. -let boolBadge = (label: string, enabled: bool): Tea_Vdom.t => { - let (badgeCls, badgeText) = enabled - ? ("px-2 py-0.5 text-xs bg-emerald-900 text-emerald-300 rounded", "Required") - : ("px-2 py-0.5 text-xs bg-gray-800 text-gray-500 rounded", "Off") - div( - list{Attrs.class_("flex items-center justify-between py-1.5 border-b border-gray-800")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(label)}), - span(list{Attrs.class_(badgeCls)}, list{text(badgeText)}), - }, - ) -} - -/// Render a registry constraint row. -let registryRow = (rc: registryConstraint): Tea_Vdom.t => { - let (cls, icon) = rc.allowed ? ("text-emerald-400", "ALLOW") : ("text-red-400", "DENY") - div( - list{Attrs.class_("flex items-center justify-between py-1 px-2 bg-gray-900 rounded mb-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400 font-mono")}, list{text(rc.registry)}), - span(list{Attrs.class_(`text-xs font-medium ${cls}`)}, list{text(icon)}), - }, - ) -} - -/// Panel-L: Security and resource constraints editor. -let viewConstraints = (model: model): Tea_Vdom.t => { - let st = model.stapeln - let c = st.constraints - div( - list{Attrs.class_("p-4 space-y-4")}, - list{ - // Header - h3( - list{Attrs.class_("text-lg font-semibold text-white mb-2")}, - list{text("Assembly Constraints")}, - ), - // Security constraints section - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider mb-2")}, - list{text("Supply Chain Security")}, - ), - constraintRow("SLSA Level", StapelnEngine.slsaLabel(c.minSlsaLevel), ~highlight=true), - constraintRow("Signature Policy", StapelnEngine.signaturePolicyLabel(c.signaturePolicy)), - constraintRow("SBOM Format", StapelnEngine.sbomFormatLabel(c.sbomFormat)), - boolBadge("Healthcheck", c.requireHealthcheck), - boolBadge("Non-Root", c.requireNonRoot), - }, - ), - // Resource constraints section - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 mt-3"), - }, - list{text("Resource Limits")}, - ), - constraintRow("Max Image Size", Int.toString(c.maxImageSizeMb) ++ " MB"), - constraintRow("Memory Limit", Int.toString(c.memoryLimitMb) ++ " MB"), - constraintRow("CPU Limit", Float.toString(c.cpuLimit) ++ " cores"), - }, - ), - // Registry constraints - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 mt-3"), - }, - list{text("Registry Policy")}, - ), - div( - list{Attrs.class_("space-y-1")}, - c.registryConstraints->Array.map(registryRow)->List.fromArray, - ), - }, - ), - // Denied images - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 mt-3"), - }, - list{text("Denied Images")}, - ), - div( - list{Attrs.class_("space-y-1")}, - c.deniedImages - ->Array.map(img => - div( - list{Attrs.class_("px-2 py-1 bg-red-950 text-red-400 text-xs font-mono rounded")}, - list{text(img)}, - ) - ) - ->List.fromArray, - ), - }, - ), - // Network policy - div( - list{Attrs.class_("space-y-1")}, - list{ - div( - list{ - Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider mb-2 mt-3"), - }, - list{text("Network Policy")}, - ), - div( - list{Attrs.class_("space-y-1")}, - c.networkRules - ->Array.map(rule => { - let protocolStr = switch rule.protocol { - | Tcp => "TCP" - | Udp => "UDP" - | Sctp => "SCTP" - } - let dirCls = rule.direction === "ingress" ? "text-cyan-400" : "text-amber-400" - div( - list{Attrs.class_("flex items-center gap-2 px-2 py-1 bg-gray-900 rounded")}, - list{ - span( - list{Attrs.class_(`text-xs font-medium ${dirCls}`)}, - list{text(rule.direction)}, - ), - span( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(protocolStr ++ ":" ++ Int.toString(rule.port))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto")}, - list{text(rule.description)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Connect / Disconnect button - div( - list{Attrs.class_("pt-3")}, - list{ - button( - list{ - Attrs.class_( - if st.connected { - "w-full px-3 py-2 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer" - } else { - "w-full px-3 py-2 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - }, - ), - Events.onClick(Stapeln(Connect)), - KeyboardNav.onActivate(Stapeln(Connect)), - }, - list{ - text( - if st.connected { - "Connected" - } else { - "Connect to Pipeline" - }, - ), - }, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Panel-N: Assembly Reasoning -// ============================================================================ - -/// Render a stat card for the pipeline overview. -let statCard = (label: string, value: string, colour: string): Tea_Vdom.t => - div( - list{Attrs.class_("p-3 bg-gray-800 rounded text-center")}, - list{ - div(list{Attrs.class_(`text-2xl font-light ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-xs text-gray-500")}, list{text(label)}), - }, - ) - -/// Render a single optimisation suggestion row. -let suggestionRow = (s: optimisationSuggestion): Tea_Vdom.t => { - let severityCls = switch s.severity { - | "critical" => "text-red-400" - | "warning" => "text-amber-400" - | _ => "text-blue-400" - } - let categoryCls = switch s.category { - | "security" => "bg-red-900 text-red-300" - | "size" => "bg-cyan-900 text-cyan-300" - | "performance" => "bg-purple-900 text-purple-300" - | _ => "bg-gray-700 text-gray-300" - } - div( - list{Attrs.class_("p-2 bg-gray-900 rounded border border-gray-800 space-y-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_(`text-xs font-medium ${severityCls}`)}, list{text(s.severity)}), - span( - list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded ${categoryCls}`)}, - list{text(s.category)}, - ), - if s.autoFixAvailable { - span(list{Attrs.class_("text-xs text-emerald-500 ml-auto")}, list{text("auto-fix")}) - } else { - text("") - }, - }, - ), - div(list{Attrs.class_("text-xs text-gray-300")}, list{text(s.message)}), - }, - ) -} - -/// Panel-N: Pipeline state overview and reasoning engine output. -let viewReasoning = (model: model): Tea_Vdom.t => { - let st = model.stapeln - div( - list{Attrs.class_("p-4 space-y-4")}, - list{ - // Header - h3( - list{Attrs.class_("text-lg font-semibold text-white mb-2")}, - list{text("Assembly Reasoning")}, - ), - // Connection status - div( - list{Attrs.class_("flex items-center gap-2 mb-3")}, - list{ - div( - list{ - Attrs.class_( - if st.connected { - "w-2 h-2 rounded-full bg-emerald-400" - } else { - "w-2 h-2 rounded-full bg-gray-600" - }, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - if st.connected { - "Pipeline connected" - } else { - "Disconnected" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-600 ml-auto font-mono")}, - list{text(st.pipelineUrl)}, - ), - }, - ), - // Pipeline stats (if available) - switch st.pipelineStatus { - | Some(status) => - div( - list{Attrs.class_("space-y-4")}, - list{ - // Stats row - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - statCard("Nodes", Int.toString(status.nodeCount), "text-cyan-400"), - statCard("Connections", Int.toString(status.connectionCount), "text-purple-400"), - statCard( - "Health", - StapelnEngine.healthLabel(status.health), - StapelnEngine.healthColour(status.health), - ), - }, - ), - // Security posture - div( - list{Attrs.class_("p-3 bg-gray-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span( - list{Attrs.class_("text-xs font-medium text-gray-400 uppercase")}, - list{text("Security Posture")}, - ), - span( - list{ - Attrs.class_( - if status.securityPosture.score >= 0.8 { - "text-sm font-medium text-emerald-400" - } else if status.securityPosture.score >= 0.5 { - "text-sm font-medium text-amber-400" - } else { - "text-sm font-medium text-red-400" - }, - ), - }, - list{text(StapelnEngine.posturePercentage(status.securityPosture))}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-2")}, - list{ - boolBadge("SLSA Compliant", status.securityPosture.slsaCompliant), - boolBadge("SBOM Present", status.securityPosture.sbomPresent), - boolBadge("Signature Valid", status.securityPosture.signatureValid), - constraintRow( - "Vulnerabilities", - Int.toString(status.securityPosture.vulnerabilities) ++ - " (" ++ - Int.toString(status.securityPosture.criticalVulns) ++ " critical)", - ), - }, - ), - }, - ), - // Validation status - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - if status.validationPassing { - "text-xs font-medium text-emerald-400" - } else { - "text-xs font-medium text-red-400" - }, - ), - }, - list{ - text( - if status.validationPassing { - "Validation Passing" - } else { - "Validation Failing" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer ml-auto", - ), - Events.onClick(Stapeln(RefreshStatus)), - KeyboardNav.onActivate(Stapeln(RefreshStatus)), - }, - list{text("Refresh")}, - ), - }, - ), - // Optimisation suggestions - if Array.length(status.suggestions) > 0 { - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{ - Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider"), - }, - list{text(`Suggestions (${Int.toString(Array.length(status.suggestions))})`)}, - ), - div( - list{Attrs.class_("space-y-2")}, - status.suggestions->Array.map(suggestionRow)->List.fromArray, - ), - }, - ) - } else { - div( - list{Attrs.class_("text-xs text-gray-500 italic")}, - list{text("No optimisation suggestions")}, - ) - }, - }, - ) - | None => - div( - list{Attrs.class_("p-6 text-center")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-sm mb-3")}, - list{text("No pipeline data available")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Stapeln(RefreshStatus)), - KeyboardNav.onActivate(Stapeln(RefreshStatus)), - }, - list{text("Refresh Status")}, - ), - }, - ) - }, - }, - ) -} - -// ============================================================================ -// Panel-W: Results & Artifacts -// ============================================================================ - -/// Render an artifact format tab button. -let formatTab = (fmt: artifactFormat, active: artifactFormat): Tea_Vdom.t => { - let isActive = fmt === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button( - list{ - Attrs.class_(cls), - Events.onClick(Stapeln(RequestGenerate(StapelnEngine.artifactFormatLabel(fmt)))), - }, - list{text(StapelnEngine.artifactFormatLabel(fmt))}, - ) -} - -/// Render a single validation finding row. -let findingRow = (f: validationFinding): Tea_Vdom.t => { - let levelCls = StapelnEngine.findingLevelColour(f.level) - div( - list{Attrs.class_("flex items-start gap-2 py-1.5 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_(`text-xs font-medium ${levelCls} uppercase w-14 shrink-0`)}, - list{text(f.level)}, - ), - span(list{Attrs.class_("text-xs text-gray-500 font-mono w-16 shrink-0")}, list{text(f.rule)}), - div( - list{Attrs.class_("flex-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-300")}, list{text(f.message)}), - switch f.line { - | Some(ln) => - span( - list{Attrs.class_("text-xs text-gray-600 ml-2")}, - list{text("L" ++ Int.toString(ln))}, - ) - | None => text("") - }, - }, - ), - if f.autoFixAvailable { - span(list{Attrs.class_("text-xs text-emerald-500 shrink-0")}, list{text("fix")}) - } else { - text("") - }, - }, - ) -} - -/// Panel-W: Generated artifacts, security scan results, and deploy readiness. -let viewResults = (model: model): Tea_Vdom.t => { - let st = model.stapeln - div( - list{Attrs.class_("p-4 space-y-4")}, - list{ - // Header - h3( - list{Attrs.class_("text-lg font-semibold text-white mb-2")}, - list{text("Results & Artifacts")}, - ), - // Format tabs - div( - list{Attrs.class_("flex items-center gap-2 mb-3")}, - StapelnEngine.allFormats - ->Array.map(fmt => formatTab(fmt, st.selectedFormat)) - ->List.fromArray, - ), - // Generated artifact preview - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider")}, - list{text("Generated " ++ StapelnEngine.artifactFormatLabel(st.selectedFormat))}, - ), - switch st.generatedArtifact { - | Some(content) => - pre( - list{ - Attrs.class_( - "p-3 bg-gray-900 rounded text-xs text-gray-300 font-mono overflow-auto max-h-64 border border-gray-800", - ), - }, - list{text(content)}, - ) - | None => - div( - list{Attrs.class_("p-4 bg-gray-900 rounded text-center")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-sm mb-2")}, - list{text("No artifact generated yet")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer", - ), - Events.onClick( - Stapeln( - RequestGenerate(StapelnEngine.artifactFormatLabel(st.selectedFormat)), - ), - ), - }, - list{text("Generate " ++ StapelnEngine.artifactFormatLabel(st.selectedFormat))}, - ), - }, - ) - }, - }, - ), - // Validation results - switch st.lastValidation { - | Some(validation) => - div( - list{Attrs.class_("space-y-2")}, - list{ - // Summary bar - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider")}, - list{text("Security Scan")}, - ), - span( - list{ - Attrs.class_( - if validation.passed { - "px-2 py-0.5 text-xs rounded bg-emerald-900 text-emerald-300" - } else { - "px-2 py-0.5 text-xs rounded bg-red-900 text-red-300" - }, - ), - }, - list{ - text( - if validation.passed { - "PASSED" - } else { - "FAILED" - }, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto")}, - list{ - text( - Int.toString(validation.errorCount) ++ - " errors, " ++ - Int.toString(validation.warningCount) ++ - " warnings, " ++ - Int.toString(validation.infoCount) ++ " info", - ), - }, - ), - }, - ), - // Finding rows - if Array.length(validation.findings) > 0 { - div( - list{Attrs.class_("space-y-0")}, - validation.findings->Array.map(findingRow)->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-xs text-emerald-500 italic")}, - list{text("No findings — clean scan")}, - ) - }, - }, - ) - | None => - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-400 uppercase tracking-wider")}, - list{text("Security Scan")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 italic")}, - list{text("Not yet scanned")}, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Stapeln(RequestValidation)), - KeyboardNav.onActivate(Stapeln(RequestValidation)), - }, - list{text("Run Validation")}, - ), - }, - ) - }, - // Deploy readiness indicator - div( - list{Attrs.class_("pt-3 border-t border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span( - list{Attrs.class_("text-xs font-medium text-gray-400 uppercase")}, - list{text("Deploy Readiness")}, - ), - { - let ready = - st.connected && - st.generatedArtifact->Option.isSome && - st.lastValidation->Option.mapOr(false, v => v.passed) - span( - list{ - Attrs.class_( - if ready { - "px-3 py-1 text-xs font-medium bg-emerald-700 text-white rounded" - } else { - "px-3 py-1 text-xs font-medium bg-gray-800 text-gray-500 rounded" - }, - ), - }, - list{ - text( - if ready { - "READY" - } else { - "NOT READY" - }, - ), - }, - ) - }, - }, - ), - }, - ), - // Error display - switch st.error { - | Some(err) => - div( - list{Attrs.class_("p-3 bg-red-950 border border-red-800 rounded")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-xs text-red-400")}, list{text(err)}), - button( - list{ - Attrs.class_("text-xs text-red-500 hover:text-red-400 cursor-pointer"), - Events.onClick(Stapeln(DismissError)), - KeyboardNav.onActivate(Stapeln(DismissError)), - }, - list{text("dismiss")}, - ), - }, - ), - }, - ) - | None => text("") - }, - }, - ) -} - -// ============================================================================ -// Unified Panel View -// ============================================================================ - -/// Main entry point — renders the Stapeln panel content based on the active -/// tab. Called from the panel switcher or layout engine. -let view = (model: model): Tea_Vdom.t => { - let st = model.stapeln - div( - list{Attrs.class_("h-full flex flex-col bg-gray-950")}, - list{ - // Tab bar - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 border-b border-gray-800 bg-gray-900"), - }, - list{ - button( - list{ - Attrs.class_( - if st.activeTab === "constraints" { - "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - }, - ), - Events.onClick(Stapeln(SetActiveTab("constraints"))), - }, - list{text("Constraints")}, - ), - button( - list{ - Attrs.class_( - if st.activeTab === "reasoning" { - "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - }, - ), - Events.onClick(Stapeln(SetActiveTab("reasoning"))), - }, - list{text("Reasoning")}, - ), - button( - list{ - Attrs.class_( - if st.activeTab === "results" { - "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - }, - ), - Events.onClick(Stapeln(SetActiveTab("results"))), - }, - list{text("Results")}, - ), - // Loading indicator - if st.loading { - span( - list{Attrs.class_("text-xs text-gray-500 ml-auto animate-pulse")}, - list{text("Loading...")}, - ) - } else { - text("") - }, - }, - ), - // Panel content - div( - list{Attrs.class_("flex-1 overflow-auto")}, - list{ - switch st.activeTab { - | "constraints" => viewConstraints(model) - | "reasoning" => viewReasoning(model) - | "results" => viewResults(model) - | _ => viewConstraints(model) - }, - }, - ), - }, - ) -} diff --git a/src/components/StatusBar.affine b/src/components/StatusBar.affine new file mode 100644 index 00000000..bacb5304 --- /dev/null +++ b/src/components/StatusBar.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module StatusBar; + +// TODO: Complete semantic implementation diff --git a/src/components/StatusBar.res b/src/components/StatusBar.res deleted file mode 100644 index 2db79c26..00000000 --- a/src/components/StatusBar.res +++ /dev/null @@ -1,137 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Status Bar — configurable bottom bar with system info widgets (DD-025). -/// -/// Renders a VS Code-style status bar at the bottom of the window. Widgets -/// are configurable via the Workspace panel's configurator. Each widget -/// shows a label + value and can be toggled, repositioned, and reordered. - -open Model -open Msg -open Tea.Html - -/// Render a single status bar widget. -let renderWidget = (widget: statusWidget, model: model): Tea_Vdom.t => { - let value = switch widget.kind { - | ActivePanel => - switch model.panelSwitcher.activePanel { - | Some(id) => PanelRegistry.panelName(id) - | None => "Core Panes" - } - | WorkspaceMode => - switch model.workspace.mode { - | RhodiumMode => "Rhodium" - | EverythingMode => "Everything" - | CodeMode => "Code" - | BespokeMode => "Bespoke" - } - | SessionProtection => - switch model.workspace.protection { - | Open => "Open" - | ReadOnly => "RO" - | Sandboxed => "Sand" - | LanguageLocked(_) => "LangLock" - | TranspilationGuarded => "TGuard" - | ProductionGated => "ProdGate" - } - | ExecutionMode => - switch model.workspace.executionMode { - | Live => "Live" - | DryRun => "DryRun" - | Simulation => "Sim" - | Emulation => "Emu" - } - | CpuUsage => - switch model.statusBar.systemInfo { - | Some(info) => Float.toFixed(info.cpuUsage, ~digits=0) ++ "%" - | None => "--" - } - | MemoryUsage => - switch model.statusBar.systemInfo { - | Some(info) => StatusBarEngine.formatBytes(info.memoryUsed) - | None => "--" - } - | DiskUsage => - switch model.statusBar.systemInfo { - | Some(info) => StatusBarEngine.formatBytes(info.diskUsed) - | None => "--" - } - | RepoInfo => - switch model.repoLoader.currentRepo { - | Some(repo) => repo.name - | None => "No repo" - } - | ProviderStatus => - if model.ai.loading { - "AI..." - } else if model.ai.broadcastMode { - "Broadcast" - } else { - Int.toString(Array.length(model.ai.providers)) ++ " providers" - } - | TaskProgress => "0/0" - | WatcherRate => Int.toString(model.watcher.eventCount) ++ " events" - | SessionUptime => - switch model.statusBar.systemInfo { - | Some(info) => StatusBarEngine.formatUptime(info.uptimeSeconds) - | None => "--" - } - | ActiveAgents => "0 agents" - | UndoRedoStatus => - Int.toString(Array.length(model.undoStack)) ++ - "/" ++ - Int.toString(Array.length(model.redoStack)) - | CustomWidget(label) => label - } - - div( - list{ - Attrs.class_( - "flex items-center gap-1 px-2 py-0.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800/50 rounded cursor-default transition-colors", - ), - Attrs.title(widget.label ++ ": " ++ value), - }, - list{ - div(list{Attrs.class_("text-gray-600")}, list{text(widget.label)}), - div(list{Attrs.class_("text-gray-300")}, list{text(value)}), - }, - ) -} - -/// Render the full status bar. -let view = (model: model): Tea_Vdom.t => { - if !model.statusBar.visible { - noNode - } else { - let leftWidgets = StatusBarEngine.widgetsForPosition(model.statusBar.widgets, Left) - let centerWidgets = StatusBarEngine.widgetsForPosition(model.statusBar.widgets, Center) - let rightWidgets = StatusBarEngine.widgetsForPosition(model.statusBar.widgets, Right) - - div( - list{ - Attrs.class_( - "h-6 bg-gray-900 border-t border-gray-800 flex items-center justify-between px-2 relative z-20 shrink-0", - ), - Attrs.role("status"), - Attrs.ariaLabel("PanLL status bar"), - }, - list{ - // Left section - div( - list{Attrs.class_("flex items-center gap-1")}, - Array.map(leftWidgets, w => renderWidget(w, model))->List.fromArray, - ), - // Center section - div( - list{Attrs.class_("flex items-center gap-1")}, - Array.map(centerWidgets, w => renderWidget(w, model))->List.fromArray, - ), - // Right section - div( - list{Attrs.class_("flex items-center gap-1")}, - Array.map(rightWidgets, w => renderWidget(w, model))->List.fromArray, - ), - }, - ) - } -} diff --git a/src/components/StrategyDrift.affine b/src/components/StrategyDrift.affine new file mode 100644 index 00000000..703b118f --- /dev/null +++ b/src/components/StrategyDrift.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module StrategyDrift; + +// TODO: Complete semantic implementation diff --git a/src/components/StrategyDrift.res b/src/components/StrategyDrift.res deleted file mode 100644 index b5f9b031..00000000 --- a/src/components/StrategyDrift.res +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL StrategyDrift Component — visualises the learning loop's -/// top-prover-per-class recommendations, the PROVEN/SANCTIFY certificate -/// landscape, and strategy-shift events. -/// -/// Data sources (all polled on a timer): -/// GET http://localhost:8080/api/v1/proof_attempts/certificates -/// GET http://localhost:8080/api/v1/proof_attempts/coverage -/// Hypatia.Rules.StrategyDrift.snapshot/0 (via HTTP bridge — TODO) -/// -/// Layout: three stacked panels -/// 1. Certs Grid — 11 classes × N provers, cert-status colouring -/// 2. Coverage Bars — per-class n_repos × n_provers × n_attempts -/// 3. Drift Events — append-only log of shift events - -open Tea.Html - -// ── Shared types ─────────────────────────────────────────────────────── - -type certStatus = Proven | Pending | Sanctified | Unknown - -type proverCert = { - obligation_class: string, - prover_used: string, - success_rate: float, - total_attempts: int, - status: certStatus, -} - -type classCoverage = { - obligation_class: string, - n_repos: int, - n_provers: int, - n_attempts: int, - n_success: int, -} - -type driftEvent = { - timestamp: string, - obligation_class: string, - old_top: string, - new_top: string, - candidates_requeued: int, -} - -type strategyDriftModel = { - certs: array, - coverage: array, - drift_events: array, - last_refresh: string, - error: option, -} - -// ── Render helpers ───────────────────────────────────────────────────── - -let statusBadge = (status: certStatus): Tea_Vdom.t<'msg> => { - let (color, label) = switch status { - | Proven => ("text-green-400 border-green-500", "PROVEN") - | Sanctified => ("text-amber-300 border-amber-400 border-2", "SANCTIFIED") - | Pending => ("text-gray-400 border-gray-600", "pending") - | Unknown => ("text-gray-500 border-gray-700", "?") - } - span( - list{Attrs.class_("inline-block px-2 py-1 text-xs font-mono border " ++ color)}, - list{text(label)}, - ) -} - -let certCell = (cert: proverCert): Tea_Vdom.t<'msg> => { - let rate_pct = Js.Float.toFixedWithPrecision(cert.success_rate *. 100.0, ~digits=0) - let intensity = if cert.success_rate > 0.9 { - "bg-green-900/40" - } else if cert.success_rate > 0.5 { - "bg-yellow-900/40" - } else { - "bg-red-900/40" - } - div( - list{Attrs.class_("p-2 border border-gray-700 " ++ intensity)}, - list{ - div( - list{Attrs.class_("text-sm font-mono text-gray-200")}, - list{text(cert.prover_used)}, - ), - div( - list{Attrs.class_("text-xs font-mono text-gray-400")}, - list{text(rate_pct ++ "% · n=" ++ Belt.Int.toString(cert.total_attempts))}, - ), - statusBadge(cert.status), - }, - ) -} - -let certsGrid = (certs: array): Tea_Vdom.t<'msg> => { - let grouped = Belt.Array.reduce(certs, Js.Dict.empty(), (acc, cert) => { - let key = cert.obligation_class - let existing = switch Js.Dict.get(acc, key) { - | Some(arr) => arr - | None => [] - } - Js.Dict.set(acc, key, Belt.Array.concat(existing, [cert])) - acc - }) - let classes = Js.Dict.keys(grouped) - - div( - list{Attrs.class_("mb-4")}, - list{ - h3( - list{Attrs.class_("text-lg font-bold text-gray-200 mb-2")}, - list{text("Certificates Grid")}, - ), - div( - list{Attrs.class_("space-y-2")}, - Belt.Array.map(classes, class_name => { - let class_certs = switch Js.Dict.get(grouped, class_name) { - | Some(arr) => arr - | None => [] - } - div( - list{Attrs.class_("border-l-2 border-blue-500 pl-2")}, - list{ - div( - list{Attrs.class_("text-sm font-mono text-gray-300 mb-1")}, - list{text(class_name)}, - ), - div( - list{Attrs.class_("flex gap-2 flex-wrap")}, - Belt.Array.map(class_certs, certCell)->Belt.List.fromArray, - ), - }, - ) - })->Belt.List.fromArray, - ), - }, - ) -} - -let coverageBar = (cov: classCoverage): Tea_Vdom.t<'msg> => { - let bar_width = Js.Math.min_int(cov.n_attempts * 2, 400) - let bar_width_str = Belt.Int.toString(bar_width) ++ "px" - div( - list{Attrs.class_("flex items-center gap-3 py-1")}, - list{ - span( - list{Attrs.class_("text-xs font-mono text-gray-400 w-24")}, - list{text(cov.obligation_class)}, - ), - div( - list{ - Attrs.class_("h-4 bg-blue-900/60 border border-blue-700"), - Attrs.style("width", bar_width_str), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs font-mono text-gray-400")}, - list{ - text( - "n=" ++ - Belt.Int.toString(cov.n_attempts) ++ - " · " ++ - Belt.Int.toString(cov.n_repos) ++ - " repos · " ++ - Belt.Int.toString(cov.n_provers) ++ - " provers", - ), - }, - ), - }, - ) -} - -let coverageBars = (coverage: array): Tea_Vdom.t<'msg> => { - div( - list{Attrs.class_("mb-4")}, - list{ - h3( - list{Attrs.class_("text-lg font-bold text-gray-200 mb-2")}, - list{text("Cross-Repo Coverage")}, - ), - div(list{Attrs.class_("space-y-1")}, Belt.Array.map(coverage, coverageBar)->Belt.List.fromArray), - }, - ) -} - -let driftEventRow = (event: driftEvent): Tea_Vdom.t<'msg> => { - div( - list{Attrs.class_("flex gap-3 py-1 text-xs font-mono border-b border-gray-800")}, - list{ - span(list{Attrs.class_("text-gray-500 w-40")}, list{text(event.timestamp)}), - span(list{Attrs.class_("text-blue-300 w-24")}, list{text(event.obligation_class)}), - span( - list{Attrs.class_("text-gray-400")}, - list{text(event.old_top ++ " → ")}, - ), - span(list{Attrs.class_("text-green-400")}, list{text(event.new_top)}), - span( - list{Attrs.class_("text-amber-300 ml-auto")}, - list{ - text("re-queued " ++ Belt.Int.toString(event.candidates_requeued)), - }, - ), - }, - ) -} - -let driftEventsPanel = (events: array): Tea_Vdom.t<'msg> => { - div( - list{}, - list{ - h3( - list{Attrs.class_("text-lg font-bold text-gray-200 mb-2")}, - list{text("Strategy Drift Events")}, - ), - if Array.length(events) == 0 { - div( - list{Attrs.class_("text-sm text-gray-500 italic")}, - list{text("No shifts detected yet.")}, - ) - } else { - div(list{Attrs.class_("space-y-0")}, Belt.Array.map(events, driftEventRow)->Belt.List.fromArray) - }, - }, - ) -} - -// ── Top-level view ───────────────────────────────────────────────────── - -let view = (model: strategyDriftModel): Tea_Vdom.t<'msg> => { - div( - list{Attrs.class_("strategy-drift-panel p-4 bg-gray-900 text-gray-200 min-h-full")}, - list{ - div( - list{Attrs.class_("flex items-baseline justify-between mb-4")}, - list{ - h2( - list{Attrs.class_("text-xl font-bold")}, - list{text("Strategy Drift")}, - ), - span( - list{Attrs.class_("text-xs font-mono text-gray-500")}, - list{text("refreshed: " ++ model.last_refresh)}, - ), - }, - ), - switch model.error { - | Some(err) => - div( - list{Attrs.class_("mb-4 p-2 border border-red-700 bg-red-900/40 text-red-300 text-xs")}, - list{text("error: " ++ err)}, - ) - | None => noNode - }, - certsGrid(model.certs), - coverageBars(model.coverage), - driftEventsPanel(model.drift_events), - }, - ) -} - -// ── Decoders (JSON from verisim-api) ─────────────────────────────────── - -let certStatusOfString = (s: string): certStatus => { - switch s { - | "proven" => Proven - | "sanctified" => Sanctified - | "pending" => Pending - | _ => Unknown - } -} diff --git a/src/components/SystemUpdate.affine b/src/components/SystemUpdate.affine new file mode 100644 index 00000000..26e82ef6 --- /dev/null +++ b/src/components/SystemUpdate.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module SystemUpdate; + +// TODO: Complete semantic implementation diff --git a/src/components/SystemUpdate.res b/src/components/SystemUpdate.res deleted file mode 100644 index 63fa1498..00000000 --- a/src/components/SystemUpdate.res +++ /dev/null @@ -1,422 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL System Update Component — system component update management panel. -/// -/// Renders a three-section view: -/// 1. Summary bar: total components, up-to-date, updates available, failed -/// 2. Component table: grouped by category with version info and status badges -/// 3. Action bar: Check All, Update All, asdf Details, Logs -/// -/// Data flows through SystemUpdateCmd → Gossamer backend → shell commands -/// for rpm-ostree, flatpak, asdf, cargo, deno, fwupd. - -open Model -open Msg -open Tea.Html - -// --------------------------------------------------------------------------- -// Summary bar -// --------------------------------------------------------------------------- - -/// Render a single metric card in the summary bar. -let renderMetricCard = (label: string, value: int, color: string): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex flex-col items-center p-3 rounded-lg bg-gray-900/60 min-w-[100px]"), - Attrs.role("status"), - Attrs.ariaLabel(`${label}: ${Int.toString(value)}`), - }, - list{ - span( - list{Attrs.class_(`text-2xl font-bold ${color}`)}, - list{text(Int.toString(value))}, - ), - span( - list{Attrs.class_("text-xs text-gray-400 mt-1")}, - list{text(label)}, - ), - }, - ) -} - -/// Render the summary bar with aggregate metrics. -let renderSummaryBar = (summary: SystemUpdateModule.updateSummary): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex gap-4 p-4 border-b border-gray-800"), - Attrs.role("region"), - Attrs.ariaLabel("Update summary"), - }, - list{ - renderMetricCard("Total", summary.totalComponents, "text-gray-200"), - renderMetricCard("Up to date", summary.upToDate, "text-green-400"), - renderMetricCard("Updates", summary.updatesAvailable, "text-amber-400"), - renderMetricCard("Failed", summary.failed, "text-red-400"), - renderMetricCard("Updating", summary.updating, "text-blue-400"), - }, - ) -} - -// --------------------------------------------------------------------------- -// Status badge -// --------------------------------------------------------------------------- - -/// Render a status badge with appropriate color. -let renderStatusBadge = (status: SystemUpdateModule.updateStatus): Tea_Vdom.t => { - let color = SystemUpdateModule.statusColor(status) - let label = SystemUpdateModule.statusLabel(status) - - span( - list{ - Attrs.class_("inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium"), - Attrs.prop("style", `background-color: ${color}20; color: ${color}; border: 1px solid ${color}40`), - Attrs.ariaLabel(`Status: ${label}`), - }, - list{text(label)}, - ) -} - -// --------------------------------------------------------------------------- -// Component row -// --------------------------------------------------------------------------- - -/// Render a single component row in the table. -let renderComponentRow = (component: SystemUpdateModule.component): Tea_Vdom.t => { - let canUpdate = switch component.status { - | UpdateAvailable(_) => true - | _ => false - } - - div( - list{ - Attrs.class_( - "flex items-center gap-4 p-3 border-b border-gray-800/50 hover:bg-gray-900/40 transition-colors", - ), - Attrs.role("row"), - }, - list{ - // Name - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(component.name)}), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text(`Managed by ${component.managed_by}`)}, - ), - }, - ), - // Current version - div( - list{Attrs.class_("w-28 text-right")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-300")}, - list{text(component.currentVersion)}, - ), - }, - ), - // Latest version - div( - list{Attrs.class_("w-28 text-right")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-400")}, - list{ - text( - switch component.latestVersion { - | Some(v) => v - | None => "-" - }, - ), - }, - ), - }, - ), - // Status badge - div(list{Attrs.class_("w-40 text-center")}, list{renderStatusBadge(component.status)}), - // Update button - div( - list{Attrs.class_("w-20 text-right")}, - list{ - if canUpdate { - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-amber-600 hover:bg-amber-500 text-white transition-colors", - ), - Events.onClick(SystemUpdate(ApplyComponent(component.id))), - Attrs.ariaLabel(`Update ${component.name}`), - }, - list{text("Update")}, - ) - } else { - text("") - }, - }, - ), - }, - ) -} - -// --------------------------------------------------------------------------- -// Category group -// --------------------------------------------------------------------------- - -/// Render a category header and its components. -let renderCategoryGroup = ( - category: SystemUpdateModule.componentCategory, - components: array, -): Tea_Vdom.t => { - let label = SystemUpdateModule.categoryLabel(category) - let count = Array.length(components) - - div( - list{ - Attrs.class_("mb-4"), - Attrs.role("group"), - Attrs.ariaLabel(`${label} (${Int.toString(count)} components)`), - }, - list{ - // Category header - div( - list{Attrs.class_("flex items-center gap-2 px-3 py-2 bg-gray-900/80 rounded-t")}, - list{ - span(list{Attrs.class_("text-sm font-semibold text-gray-300")}, list{text(label)}), - span( - list{Attrs.class_("text-xs text-gray-500 px-2 py-0.5 rounded-full bg-gray-800")}, - list{text(Int.toString(count))}, - ), - }, - ), - // Component rows - div( - list{Attrs.class_("border border-gray-800/50 rounded-b")}, - list{ - ...Array.map(components, renderComponentRow)->List.fromArray - }, - ), - }, - ) -} - -// --------------------------------------------------------------------------- -// Component table -// --------------------------------------------------------------------------- - -/// Render the full component table, grouped by category. -let renderComponentTable = (components: array): Tea_Vdom.t => { - // Group components by category - let categories: array = [ - BaseOS, - Firmware, - Toolchain, - Runtime, - PackageManager, - Desktop, - ] - - div( - list{ - Attrs.class_("p-4 overflow-y-auto"), - Attrs.role("table"), - Attrs.ariaLabel("System components"), - }, - list{ - // Column headers - div( - list{ - Attrs.class_("flex items-center gap-4 px-3 py-2 mb-2 text-xs text-gray-500 uppercase tracking-wider"), - Attrs.role("row"), - }, - list{ - div(list{Attrs.class_("flex-1")}, list{text("Component")}), - div(list{Attrs.class_("w-28 text-right")}, list{text("Current")}), - div(list{Attrs.class_("w-28 text-right")}, list{text("Latest")}), - div(list{Attrs.class_("w-40 text-center")}, list{text("Status")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Action")}), - }, - ), - // Category groups - ...Array.map(categories, category => { - let filtered = Array.filter(components, c => c.category == category) - if Array.length(filtered) > 0 { - renderCategoryGroup(category, filtered) - } else { - text("") - } - })->List.fromArray, - }, - ) -} - -// --------------------------------------------------------------------------- -// Action bar -// --------------------------------------------------------------------------- - -/// Render the action bar with Check All, Update All, and utility buttons. -let renderActionBar = (isLoading: bool): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex items-center gap-3 p-4 border-t border-gray-800 bg-gray-950/60"), - Attrs.role("toolbar"), - Attrs.ariaLabel("Update actions"), - }, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-blue-600 hover:bg-blue-500 text-white transition-colors disabled:opacity-50", - ), - Events.onClick(SystemUpdate(CheckAll)), - KeyboardNav.onActivate(SystemUpdate(CheckAll)), - Attrs.disabled(isLoading), - Attrs.ariaLabel("Check all components for updates"), - }, - list{text(isLoading ? "Checking..." : "Check All")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-amber-600 hover:bg-amber-500 text-white transition-colors disabled:opacity-50", - ), - Events.onClick(SystemUpdate(ApplyAll)), - KeyboardNav.onActivate(SystemUpdate(ApplyAll)), - Attrs.disabled(isLoading), - Attrs.ariaLabel("Apply all available updates"), - }, - list{text("Update All")}, - ), - div(list{Attrs.class_("flex-1")}, list{}), - button( - list{ - Attrs.class_( - "px-3 py-2 text-sm rounded bg-gray-800 hover:bg-gray-700 text-gray-300 transition-colors", - ), - Events.onClick(SystemUpdate(AsdfStatus)), - KeyboardNav.onActivate(SystemUpdate(AsdfStatus)), - Attrs.ariaLabel("View asdf plugin details"), - }, - list{text("asdf Details")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-2 text-sm rounded bg-gray-800 hover:bg-gray-700 text-gray-300 transition-colors", - ), - Events.onClick(SystemUpdate(ToggleShowLogs)), - KeyboardNav.onActivate(SystemUpdate(ToggleShowLogs)), - Attrs.ariaLabel("View update history"), - }, - list{text("Logs")}, - ), - }, - ) -} - -// --------------------------------------------------------------------------- -// Log viewer -// --------------------------------------------------------------------------- - -/// Render the log viewer (collapsible). -let renderLogViewer = ( - logs: array<{.."timestamp": string, "summary": string}>, - visible: bool, -): Tea_Vdom.t => { - if !visible { - text("") - } else { - div( - list{ - Attrs.class_("p-4 border-t border-gray-800 bg-gray-950/80 max-h-64 overflow-y-auto"), - Attrs.role("log"), - Attrs.ariaLabel("Update log history"), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - span( - list{Attrs.class_("text-sm font-semibold text-gray-300")}, - list{text("Update History")}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-300"), - Events.onClick(SystemUpdate(ToggleShowLogs)), - KeyboardNav.onActivate(SystemUpdate(ToggleShowLogs)), - }, - list{text("Close")}, - ), - }, - ), - ...Array.map(logs, entry => - div( - list{Attrs.class_("flex gap-3 py-1 text-xs border-b border-gray-800/30")}, - list{ - span( - list{Attrs.class_("text-gray-500 w-32 shrink-0 font-mono")}, - list{text(entry["timestamp"])}, - ), - span(list{Attrs.class_("text-gray-400")}, list{text(entry["summary"])}), - }, - ) - )->List.fromArray, - }, - ) - } -} - -// --------------------------------------------------------------------------- -// Root view -// --------------------------------------------------------------------------- - -/// Main view for the System Update panel. -/// Renders summary bar, component table, action bar, and optional log viewer. -let view = (model: model): Tea_Vdom.t => { - let su = model.systemUpdate - let summary = su.summary - let components = su.components - let isLoading = su.loading - let logs = su.logs - let showLogs = su.showLogs - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100"), - Attrs.role("region"), - Attrs.ariaLabel("System Update Panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center gap-3 px-4 py-3 border-b border-gray-800")}, - list{ - span( - list{Attrs.class_("text-lg font-bold text-gray-100")}, - list{text("System Update")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `${Int.toString(summary.totalComponents)} components across ${Int.toString(6)} managers`, - ), - }, - ), - }, - ), - // Summary metrics - renderSummaryBar(summary), - // Component table (scrollable) - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - list{renderComponentTable(components)}, - ), - // Log viewer (toggled) - renderLogViewer(logs, showLogs), - // Action bar (fixed at bottom) - renderActionBar(isLoading), - }, - ) -} diff --git a/src/components/TangleViz.affine b/src/components/TangleViz.affine new file mode 100644 index 00000000..cbafa911 --- /dev/null +++ b/src/components/TangleViz.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TangleViz; + +// TODO: Complete semantic implementation diff --git a/src/components/TangleViz.res b/src/components/TangleViz.res deleted file mode 100644 index 7d423950..00000000 --- a/src/components/TangleViz.res +++ /dev/null @@ -1,1240 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL TangleViz Component — view layer for the topological programming panel. -/// -/// Renders a full-screen overlay with: -/// - Tangle source input area -/// - Braid word display (algebraic notation with Unicode) -/// - Interactive SVG braid diagram with over/under crossings -/// - Knot invariant selector and result display -/// - Example braid quick-select buttons -/// - View mode tabs (Braid / Knot / Algebraic) -/// -/// SVG rendering draws N horizontal strands with crossings at each generator. -/// Over-crossings use solid lines; under-crossings use a gap (white break). -/// Strands are colour-coded for visual strand tracking. - -open Model -open Msg -open Tea.Html - -// ════════════════════════════════════════════════════════════════════════ -// View Mode Tabs -// ════════════════════════════════════════════════════════════════════════ - -/// Render a single view mode tab button. -let renderViewTab = (mode: TangleVizModel.tangleViewMode, isActive: bool): Tea_Vdom.t => { - let activeClass = isActive - ? "border-indigo-500 text-indigo-300 bg-gray-800/50" - : "border-transparent text-gray-500 hover:text-gray-300 hover:border-gray-600" - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium border-b-2 cursor-pointer transition-colors ${activeClass}`, - ), - Attrs.role("tab"), - Events.onClick(TangleViz(SetViewMode(mode))), - }, - list{text(TangleVizEngine.viewModeLabel(mode))}, - ) -} - -/// Render the view mode tab bar. -let renderViewTabBar = (activeMode: TangleVizModel.tangleViewMode): Tea_Vdom.t => { - div( - list{ - Attrs.class_("flex border-b border-gray-800 overflow-x-auto"), - Attrs.role("tablist"), - Attrs.ariaLabel("Topology view modes"), - }, - TangleVizEngine.allViewModes - ->Array.map(mode => renderViewTab(mode, mode === activeMode)) - ->List.fromArray, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Source Input Area -// ════════════════════════════════════════════════════════════════════════ - -/// Render the Tangle source code input area. -let renderSourceInput = ( - inputText: string, - parsedProgram: option, -): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Tangle Source")}, - ), - textarea( - list{ - Attrs.class_( - "w-full h-24 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono placeholder-gray-600 focus:border-indigo-500 focus:outline-none resize-y", - ), - Attrs.placeholder("Enter Tangle source code or use an example below..."), - Attrs.value(inputText), - Events.onInput(text => TangleViz(SetInputText(text))), - }, - list{}, - ), - // Parse status indicator - switch parsedProgram { - | None => - div(list{Attrs.class_("mt-1 text-xs text-gray-600")}, list{text("No input parsed yet")}) - | Some(ParsedOk) => - div(list{Attrs.class_("mt-1 text-xs text-emerald-400")}, list{text("Parsed successfully")}) - | Some(ParseFailed(err)) => - div(list{Attrs.class_("mt-1 text-xs text-red-400")}, list{text(`Parse error: ${err}`)}) - }, - // Parse button - div( - list{Attrs.class_("mt-2 flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-indigo-600 text-white rounded hover:bg-indigo-500 transition-colors", - ), - Events.onClick(TangleViz(ParseInput)), - KeyboardNav.onActivate(TangleViz(ParseInput)), - }, - list{text("Parse")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 transition-colors", - ), - Events.onClick(TangleViz(ClearAll)), - KeyboardNav.onActivate(TangleViz(ClearAll)), - }, - list{text("Clear")}, - ), - }, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Example Braid Quick-Select -// ════════════════════════════════════════════════════════════════════════ - -/// Render the example braid quick-select buttons. -let renderExamples = (): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Example Braids")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - TangleVizEngine.exampleBraids() - ->Array.map(ex => - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-800 text-gray-300 rounded border border-gray-700 hover:bg-gray-700 hover:border-gray-600 transition-colors", - ), - Attrs.title(ex.description), - Events.onClick(TangleViz(LoadExample(ex.generators))), - }, - list{text(ex.name)}, - ) - ) - ->List.fromArray, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Braid Word Display (Algebraic Notation) -// ════════════════════════════════════════════════════════════════════════ - -/// Render the algebraic braid word with Unicode notation. -let renderBraidWord = ( - generators: array, - strandCount: int, -): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Braid Word")}, - ), - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - // The braid word in Unicode - div( - list{Attrs.class_("text-lg font-mono text-indigo-300")}, - list{text(TangleVizEngine.braidWordToString(generators))}, - ), - // Strand count badge - div( - list{Attrs.class_("text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded")}, - list{text(`${Int.toString(strandCount)} strands`)}, - ), - // Crossing count badge - div( - list{Attrs.class_("text-xs text-gray-500 bg-gray-800 px-2 py-1 rounded")}, - list{text(`${Int.toString(Array.length(generators))} crossings`)}, - ), - }, - ), - // Individual generators as clickable chips - if Array.length(generators) > 0 { - div( - list{Attrs.class_("mt-2 flex flex-wrap gap-1")}, - generators - ->Array.mapWithIndex((gen, idx) => { - let colour = if gen.exponent > 0 { - "bg-emerald-900/50 text-emerald-300 border-emerald-700" - } else { - "bg-red-900/50 text-red-300 border-red-700" - } - span( - list{ - Attrs.class_(`text-xs px-2 py-0.5 rounded border font-mono ${colour}`), - Attrs.title( - `Generator ${Int.toString(idx + 1)}: ${TangleVizEngine.generatorLabel(gen)}`, - ), - }, - list{text(TangleVizEngine.generatorLabel(gen))}, - ) - }) - ->List.fromArray, - ) - } else { - noNode - }, - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// SVG Braid Diagram -// ════════════════════════════════════════════════════════════════════════ - -/// Render the SVG braid diagram. -/// -/// Layout: N horizontal strands flow left-to-right. At each generator σᵢ, -/// strands i and i+1 cross. Positive crossings: strand i goes over (drawn -/// on top). Negative crossings: strand i goes under (drawn with a gap). -/// -/// The strands maintain their physical position tracking through crossings -/// so colours follow the actual strand paths. -let renderBraidSvg = ( - generators: array, - strandCount: int, -): Tea_Vdom.t => { - let crossingW = TangleVizEngine.crossingWidth - let strandSp = TangleVizEngine.strandSpacing - let leftM = TangleVizEngine.svgLeftMargin - let topM = TangleVizEngine.svgTopMargin - - let numCrossings = Array.length(generators) - let svgWidth = leftM *. 2.0 +. Int.toFloat(numCrossings + 1) *. crossingW - let svgHeight = topM *. 2.0 +. Int.toFloat(strandCount - 1) *. strandSp - - // Track which physical strand is at each vertical position. - // strandAt[position] = original strand index (for colour). - let strandAt = Array.fromInitializer(~length=strandCount, i => i) - - // Build path segments for each strand through each crossing column. - // We collect SVG elements: lines for straight segments, paths for crossings. - let elements: array> = [] - - // Draw initial left-side strand labels - for pos in 0 to strandCount - 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let colour = TangleVizEngine.strandColour(pos) - let _ = - elements->Array.push( - Tea_Svg.text'( - list{ - Tea_Svg.Attrs.class_("text-xs"), - Tea_Svg.Attrs.x(Float.toString(leftM -. 20.0)), - Tea_Svg.Attrs.y(Float.toString(y +. 4.0)), - Tea_Svg.Attrs.fill(colour), - Tea_Svg.Attrs.textAnchor("middle"), - }, - list{Tea.Html.text(Int.toString(pos + 1))}, - ), - ) - } - - // For each crossing column, draw the crossing and straight-through strands - for col in 0 to numCrossings - 1 { - let gen = generators->Array.getUnsafe(col) - let crossIdx = gen.index - 1 // Convert 1-based to 0-based position - let x1 = leftM +. Int.toFloat(col) *. crossingW - let x2 = leftM +. Int.toFloat(col + 1) *. crossingW - - // Draw straight-through strands (those not involved in this crossing) - for pos in 0 to strandCount - 1 { - if pos !== crossIdx && pos !== crossIdx + 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let origStrand = strandAt->Array.getUnsafe(pos) - let colour = TangleVizEngine.strandColour(origStrand) - let _ = - elements->Array.push( - Tea_Svg.line( - list{ - Tea_Svg.Attrs.x1(Float.toString(x1)), - Tea_Svg.Attrs.y1(Float.toString(y)), - Tea_Svg.Attrs.x2(Float.toString(x2)), - Tea_Svg.Attrs.y2(Float.toString(y)), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - }, - list{}, - ), - ) - } - } - - // Draw the crossing between positions crossIdx and crossIdx+1 - if crossIdx >= 0 && crossIdx + 1 < strandCount { - let yTop = topM +. Int.toFloat(crossIdx) *. strandSp - let yBot = topM +. Int.toFloat(crossIdx + 1) *. strandSp - let origTop = strandAt->Array.getUnsafe(crossIdx) - let origBot = strandAt->Array.getUnsafe(crossIdx + 1) - let colourTop = TangleVizEngine.strandColour(origTop) - let colourBot = TangleVizEngine.strandColour(origBot) - - // Compute cubic bezier control points for smooth crossing - let mx = (x1 +. x2) /. 2.0 - - if gen.exponent > 0 { - // Positive crossing: top strand goes OVER - // Draw under-strand first (with gap), then over-strand - // Under strand: bottom→top, drawn with a gap in the middle - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yBot)} C ${Float.toString( - mx, - )} ${Float.toString(yBot)}, ${Float.toString(mx)} ${Float.toString( - yTop, - )}, ${Float.toString(x2)} ${Float.toString(yTop)}`, - ), - Tea_Svg.Attrs.stroke(colourBot), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.strokeDasharray("20 12 20 0"), - }, - list{}, - ), - ) - // Over strand: top→bottom, solid line on top - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yTop)} C ${Float.toString( - mx, - )} ${Float.toString(yTop)}, ${Float.toString(mx)} ${Float.toString( - yBot, - )}, ${Float.toString(x2)} ${Float.toString(yBot)}`, - ), - Tea_Svg.Attrs.stroke(colourTop), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - }, - list{}, - ), - ) - } else { - // Negative crossing: top strand goes UNDER - // Draw over-strand first (bottom→top, solid), then under-strand (top→bottom, gapped) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yTop)} C ${Float.toString( - mx, - )} ${Float.toString(yTop)}, ${Float.toString(mx)} ${Float.toString( - yBot, - )}, ${Float.toString(x2)} ${Float.toString(yBot)}`, - ), - Tea_Svg.Attrs.stroke(colourTop), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.strokeDasharray("20 12 20 0"), - }, - list{}, - ), - ) - // Over strand: bottom→top, solid line on top - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yBot)} C ${Float.toString( - mx, - )} ${Float.toString(yBot)}, ${Float.toString(mx)} ${Float.toString( - yTop, - )}, ${Float.toString(x2)} ${Float.toString(yTop)}`, - ), - Tea_Svg.Attrs.stroke(colourBot), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - }, - list{}, - ), - ) - } - - // Swap the strand tracking - let tmp = strandAt->Array.getUnsafe(crossIdx) - let _ = strandAt->Array.set(crossIdx, strandAt->Array.getUnsafe(crossIdx + 1)) - let _ = strandAt->Array.set(crossIdx + 1, tmp) - } - } - - // Draw final right-side straight segments from last crossing to edge - let xFinal = leftM +. Int.toFloat(numCrossings) *. crossingW - let xEnd = xFinal +. crossingW *. 0.5 - for pos in 0 to strandCount - 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let origStrand = strandAt->Array.getUnsafe(pos) - let colour = TangleVizEngine.strandColour(origStrand) - let _ = - elements->Array.push( - Tea_Svg.line( - list{ - Tea_Svg.Attrs.x1(Float.toString(xFinal)), - Tea_Svg.Attrs.y1(Float.toString(y)), - Tea_Svg.Attrs.x2(Float.toString(xEnd)), - Tea_Svg.Attrs.y2(Float.toString(y)), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - }, - list{}, - ), - ) - } - - // Also draw initial left-side straight segments from edge to first crossing - let xStart = leftM -. crossingW *. 0.3 - for pos in 0 to strandCount - 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let colour = TangleVizEngine.strandColour(pos) - let _ = - elements->Array.push( - Tea_Svg.line( - list{ - Tea_Svg.Attrs.x1(Float.toString(xStart)), - Tea_Svg.Attrs.y1(Float.toString(y)), - Tea_Svg.Attrs.x2(Float.toString(leftM)), - Tea_Svg.Attrs.y2(Float.toString(y)), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - }, - list{}, - ), - ) - } - - div( - list{Attrs.class_("p-4 flex-1 overflow-auto"), Attrs.ariaLabel("Braid diagram visualization")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Braid Diagram")}, - ), - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-4 overflow-x-auto")}, - list{ - if numCrossings === 0 && strandCount <= 2 { - div( - list{Attrs.class_("text-gray-600 text-sm text-center py-8")}, - list{text("Select an example or enter a braid word to visualize")}, - ) - } else { - Tea_Svg.svg( - list{ - Tea_Svg.Attrs.class_("block mx-auto"), - Tea_Svg.Attrs.viewBox( - `0 0 ${Float.toString(svgWidth)} ${Float.toString(svgHeight)}`, - ), - Tea_Svg.Attrs.width( - Float.toString(Float.fromInt(Math.Int.min(Float.toInt(svgWidth), 800))), - ), - Tea_Svg.Attrs.height(Float.toString(svgHeight)), - }, - // Background - list{ - Tea_Svg.rect( - list{ - Tea_Svg.Attrs.x("0"), - Tea_Svg.Attrs.y("0"), - Tea_Svg.Attrs.width(Float.toString(svgWidth)), - Tea_Svg.Attrs.height(Float.toString(svgHeight)), - Tea_Svg.Attrs.fill("#0a0a0f"), - Tea_Svg.Attrs.rx("8"), - }, - list{}, - ), - ...elements->List.fromArray, - }, - ) - }, - }, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Knot Diagram (Braid Closure) -// ════════════════════════════════════════════════════════════════════════ - -/// Render the knot diagram view as braid closure. -/// -/// Draws the braid diagram horizontally, then connects each strand's -/// right endpoint back to its corresponding left endpoint with semicircular -/// arcs on the right and left sides. This produces the standard braid -/// closure, turning the braid into a knot or link diagram. -let renderKnotDiagram = ( - generators: array, - strandCount: int, -): Tea_Vdom.t => { - let crossingW = TangleVizEngine.crossingWidth - let strandSp = TangleVizEngine.strandSpacing - let topM = TangleVizEngine.svgTopMargin - - let numCrossings = Array.length(generators) - - // Extra horizontal margin for the closure arcs on each side - let arcMargin = 60.0 - let leftM = TangleVizEngine.svgLeftMargin +. arcMargin - - let braidWidth = Int.toFloat(numCrossings + 1) *. crossingW - let svgWidth = leftM +. braidWidth +. arcMargin +. 20.0 - let svgHeight = topM *. 2.0 +. Int.toFloat(strandCount - 1) *. strandSp +. 40.0 - - // Track which physical strand is at each vertical position through crossings - let strandAt = Array.fromInitializer(~length=strandCount, i => i) - - let elements: array> = [] - - // Draw initial left-side strand segments (from left arc join to first crossing) - let xStart = leftM - for pos in 0 to strandCount - 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let colour = TangleVizEngine.strandColour(pos) - let _ = - elements->Array.push( - Tea_Svg.line( - list{ - Tea_Svg.Attrs.x1(Float.toString(xStart)), - Tea_Svg.Attrs.y1(Float.toString(y)), - Tea_Svg.Attrs.x2(Float.toString(xStart +. crossingW *. 0.3)), - Tea_Svg.Attrs.y2(Float.toString(y)), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - }, - list{}, - ), - ) - } - - // Draw crossings (same logic as braid view) - for col in 0 to numCrossings - 1 { - let gen = generators->Array.getUnsafe(col) - let crossIdx = gen.index - 1 - let x1 = leftM +. crossingW *. 0.3 +. Int.toFloat(col) *. crossingW - let x2 = leftM +. crossingW *. 0.3 +. Int.toFloat(col + 1) *. crossingW - - // Straight-through strands - for pos in 0 to strandCount - 1 { - if pos !== crossIdx && pos !== crossIdx + 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let origStrand = strandAt->Array.getUnsafe(pos) - let colour = TangleVizEngine.strandColour(origStrand) - let _ = - elements->Array.push( - Tea_Svg.line( - list{ - Tea_Svg.Attrs.x1(Float.toString(x1)), - Tea_Svg.Attrs.y1(Float.toString(y)), - Tea_Svg.Attrs.x2(Float.toString(x2)), - Tea_Svg.Attrs.y2(Float.toString(y)), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - }, - list{}, - ), - ) - } - } - - // Draw crossing between positions crossIdx and crossIdx+1 - if crossIdx >= 0 && crossIdx + 1 < strandCount { - let yTop = topM +. Int.toFloat(crossIdx) *. strandSp - let yBot = topM +. Int.toFloat(crossIdx + 1) *. strandSp - let origTop = strandAt->Array.getUnsafe(crossIdx) - let origBot = strandAt->Array.getUnsafe(crossIdx + 1) - let colourTop = TangleVizEngine.strandColour(origTop) - let colourBot = TangleVizEngine.strandColour(origBot) - let mx = (x1 +. x2) /. 2.0 - - if gen.exponent > 0 { - // Under strand (gapped) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yBot)} C ${Float.toString( - mx, - )} ${Float.toString(yBot)}, ${Float.toString(mx)} ${Float.toString( - yTop, - )}, ${Float.toString(x2)} ${Float.toString(yTop)}`, - ), - Tea_Svg.Attrs.stroke(colourBot), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.strokeDasharray("20 12 20 0"), - }, - list{}, - ), - ) - // Over strand (solid) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yTop)} C ${Float.toString( - mx, - )} ${Float.toString(yTop)}, ${Float.toString(mx)} ${Float.toString( - yBot, - )}, ${Float.toString(x2)} ${Float.toString(yBot)}`, - ), - Tea_Svg.Attrs.stroke(colourTop), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - }, - list{}, - ), - ) - } else { - // Under strand (gapped) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yTop)} C ${Float.toString( - mx, - )} ${Float.toString(yTop)}, ${Float.toString(mx)} ${Float.toString( - yBot, - )}, ${Float.toString(x2)} ${Float.toString(yBot)}`, - ), - Tea_Svg.Attrs.stroke(colourTop), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.strokeDasharray("20 12 20 0"), - }, - list{}, - ), - ) - // Over strand (solid) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(x1)} ${Float.toString(yBot)} C ${Float.toString( - mx, - )} ${Float.toString(yBot)}, ${Float.toString(mx)} ${Float.toString( - yTop, - )}, ${Float.toString(x2)} ${Float.toString(yTop)}`, - ), - Tea_Svg.Attrs.stroke(colourBot), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - }, - list{}, - ), - ) - } - - // Swap strand tracking - let tmp = strandAt->Array.getUnsafe(crossIdx) - let _ = strandAt->Array.set(crossIdx, strandAt->Array.getUnsafe(crossIdx + 1)) - let _ = strandAt->Array.set(crossIdx + 1, tmp) - } - } - - // Draw right-side straight segments from last crossing to arc start - let xBraidEnd = leftM +. crossingW *. 0.3 +. Int.toFloat(numCrossings) *. crossingW - let xRightArc = xBraidEnd +. crossingW *. 0.3 - for pos in 0 to strandCount - 1 { - let y = topM +. Int.toFloat(pos) *. strandSp - let origStrand = strandAt->Array.getUnsafe(pos) - let colour = TangleVizEngine.strandColour(origStrand) - let _ = - elements->Array.push( - Tea_Svg.line( - list{ - Tea_Svg.Attrs.x1(Float.toString(xBraidEnd)), - Tea_Svg.Attrs.y1(Float.toString(y)), - Tea_Svg.Attrs.x2(Float.toString(xRightArc)), - Tea_Svg.Attrs.y2(Float.toString(y)), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - }, - list{}, - ), - ) - } - - // Draw closure arcs: connect right-side strand endpoints back to left-side. - // After all crossings, strandAt[pos] tells us which original strand is at - // vertical position `pos` on the right side. - // On the left side, original strand `i` is at position `i`. - // We connect: right position `pos` (original strand strandAt[pos]) - // to left position strandAt[pos] (where that original strand starts). - // - // Right arcs go from right endpoint to a point far right, then back. - // Left arcs go from left endpoint to a point far left, then back. - // We draw them as semicircular arcs on the right and left sides. - for pos in 0 to strandCount - 1 { - let origStrand = strandAt->Array.getUnsafe(pos) - let colour = TangleVizEngine.strandColour(origStrand) - let yRight = topM +. Int.toFloat(pos) *. strandSp - let yLeft = topM +. Int.toFloat(origStrand) *. strandSp - - if pos === origStrand { - // Strand returns to the same vertical position: draw arcs on right and left - // Right arc: semicircle from right endpoint going right and looping back - let arcRadius = 15.0 +. Int.toFloat(pos) *. 5.0 - // Right semicircular arc (goes out right and comes back to same y) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(xRightArc)} ${Float.toString(yRight)} A ${Float.toString( - arcRadius, - )} ${Float.toString(arcRadius)} 0 1 1 ${Float.toString( - xRightArc, - )} ${Float.toString(yRight -. 0.01)}`, - ), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.5"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.opacity("0.0"), - }, - list{}, - ), - ) - // Actually, for same-position strands: draw a full right-side arc out and - // a left-side arc out. The braid closure connects right pos to left pos - // by going around the outside. - // Right arc: from (xRightArc, yRight) curving right - // Left arc: from (xStart, yLeft) curving left - // Connect them with top/bottom paths - let xRight = xRightArc +. arcMargin *. 0.7 - let xLeft = xStart -. arcMargin *. 0.7 - // Top path: right endpoint -> right arc point -> top -> left arc point -> left endpoint - let yMid = (yRight +. yLeft) /. 2.0 - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(xRightArc)} ${Float.toString(yRight)} C ${Float.toString( - xRight, - )} ${Float.toString(yRight)}, ${Float.toString(xRight)} ${Float.toString( - yMid, - )}, ${Float.toString(xRight)} ${Float.toString(yMid -. strandSp *. 1.5)}`, - ), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.0"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.opacity("0.6"), - }, - list{}, - ), - ) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(xStart)} ${Float.toString(yLeft)} C ${Float.toString( - xLeft, - )} ${Float.toString(yLeft)}, ${Float.toString(xLeft)} ${Float.toString( - yMid, - )}, ${Float.toString(xLeft)} ${Float.toString(yMid -. strandSp *. 1.5)}`, - ), - Tea_Svg.Attrs.stroke(colour), - Tea_Svg.Attrs.strokeWidth("2.0"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.opacity("0.6"), - }, - list{}, - ), - ) - } else { - // Strand ends at a different vertical position: draw closure arcs - // connecting right `pos` to left `origStrand`. - // Use a wide arc on the right side if going down, left side if going up. - let colour2 = TangleVizEngine.strandColour(origStrand) - let yMid = (yRight +. yLeft) /. 2.0 - let spread = Float.fromInt(Math.Int.abs(pos - origStrand)) *. 8.0 +. 20.0 - - // Right-side closure arc: from right endpoint, curve outward, to a midpoint - let xOutRight = xRightArc +. spread - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(xRightArc)} ${Float.toString(yRight)} C ${Float.toString( - xOutRight, - )} ${Float.toString(yRight)}, ${Float.toString(xOutRight)} ${Float.toString( - yMid, - )}, ${Float.toString(xRightArc +. spread *. 0.5)} ${Float.toString(yMid)}`, - ), - Tea_Svg.Attrs.stroke(colour2), - Tea_Svg.Attrs.strokeWidth("2.0"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.opacity("0.6"), - }, - list{}, - ), - ) - - // Left-side closure arc: from midpoint, curve outward to left endpoint - let xOutLeft = xStart -. spread - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(xStart -. spread *. 0.5)} ${Float.toString( - yMid, - )} C ${Float.toString(xOutLeft)} ${Float.toString(yMid)}, ${Float.toString( - xOutLeft, - )} ${Float.toString(yLeft)}, ${Float.toString(xStart)} ${Float.toString(yLeft)}`, - ), - Tea_Svg.Attrs.stroke(colour2), - Tea_Svg.Attrs.strokeWidth("2.0"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.opacity("0.6"), - }, - list{}, - ), - ) - - // Top/bottom connecting segment (dashed, shows the closure path) - let _ = - elements->Array.push( - Tea_Svg.path( - list{ - Tea_Svg.Attrs.d( - `M ${Float.toString(xRightArc +. spread *. 0.5)} ${Float.toString( - yMid, - )} L ${Float.toString(xStart -. spread *. 0.5)} ${Float.toString(yMid)}`, - ), - Tea_Svg.Attrs.stroke(colour2), - Tea_Svg.Attrs.strokeWidth("1.5"), - Tea_Svg.Attrs.fill("none"), - Tea_Svg.Attrs.opacity("0.35"), - Tea_Svg.Attrs.strokeDasharray("4 3"), - }, - list{}, - ), - ) - } - } - - div( - list{ - Attrs.class_("p-4 flex-1 overflow-auto"), - Attrs.ariaLabel("Knot diagram visualization (braid closure)"), - }, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Knot Diagram (Braid Closure)")}, - ), - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-4 overflow-x-auto")}, - list{ - if numCrossings === 0 && strandCount <= 2 { - div( - list{Attrs.class_("text-gray-600 text-sm text-center py-8")}, - list{text("Select an example or enter a braid word to visualize its closure")}, - ) - } else { - div( - list{}, - list{ - // Info bar - div( - list{Attrs.class_("flex items-center gap-4 mb-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${Int.toString(strandCount)}-strand braid with ${Int.toString( - numCrossings, - )} crossings`, - ), - }, - ), - div( - list{Attrs.class_("text-xs font-mono text-indigo-300")}, - list{text(TangleVizEngine.braidWordToString(generators))}, - ), - }, - ), - // SVG closure diagram - Tea_Svg.svg( - list{ - Tea_Svg.Attrs.class_("block mx-auto"), - Tea_Svg.Attrs.viewBox( - `0 0 ${Float.toString(svgWidth)} ${Float.toString(svgHeight)}`, - ), - Tea_Svg.Attrs.width( - Float.toString(Float.fromInt(Math.Int.min(Float.toInt(svgWidth), 900))), - ), - Tea_Svg.Attrs.height(Float.toString(svgHeight)), - }, - list{ - // Background - Tea_Svg.rect( - list{ - Tea_Svg.Attrs.x("0"), - Tea_Svg.Attrs.y("0"), - Tea_Svg.Attrs.width(Float.toString(svgWidth)), - Tea_Svg.Attrs.height(Float.toString(svgHeight)), - Tea_Svg.Attrs.fill("#0a0a0f"), - Tea_Svg.Attrs.rx("8"), - }, - list{}, - ), - ...elements->List.fromArray, - }, - ), - // Legend - div( - list{Attrs.class_("mt-2 text-[10px] text-gray-600 text-center")}, - list{ - text( - "Solid lines = braid crossings | Curved lines = closure arcs connecting strand endpoints", - ), - }, - ), - }, - ) - }, - }, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Algebraic View -// ════════════════════════════════════════════════════════════════════════ - -/// Render the algebraic view — braid group notation and relations. -let renderAlgebraicView = ( - generators: array, - strandCount: int, -): Tea_Vdom.t => { - let writhe = TangleVizEngine.computeWrithe(generators) - div( - list{Attrs.class_("p-4 flex-1")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Algebraic View")}, - ), - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-6 space-y-4")}, - list{ - // Braid group header - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`Braid group B${TangleVizEngine.toSubscript(strandCount)}`)}, - ), - // Braid word - div( - list{Attrs.class_("text-xl font-mono text-indigo-300")}, - list{text(TangleVizEngine.braidWordToString(generators))}, - ), - // Properties - div( - list{Attrs.class_("border-t border-gray-800 pt-4 space-y-2")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`Word length: ${Int.toString(Array.length(generators))}`)}, - ), - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`Writhe: ${Int.toString(writhe)}`)}, - ), - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{text(`Strand count: ${Int.toString(strandCount)}`)}, - ), - // Braid group relations - div( - list{Attrs.class_("border-t border-gray-800 pt-3 mt-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-600 mb-1")}, - list{text("Braid group relations:")}, - ), - div( - list{Attrs.class_("text-xs font-mono text-gray-500 space-y-1")}, - list{ - div( - list{}, - list{ - text( - "\xcf\x83\xe2\x82\x96\xcf\x83\xe2\x82\x97 = \xcf\x83\xe2\x82\x97\xcf\x83\xe2\x82\x96 when |i-j| \xe2\x89\xa5 2", - ), - }, - ), // σᵢσⱼ = σⱼσᵢ - div( - list{}, - list{ - text( - "\xcf\x83\xe2\x82\x96\xcf\x83\xe2\x82\x97\xcf\x83\xe2\x82\x96 = \xcf\x83\xe2\x82\x97\xcf\x83\xe2\x82\x96\xcf\x83\xe2\x82\x97 when |i-j| = 1", - ), - }, - ), // σᵢσⱼσᵢ = σⱼσᵢσⱼ - }, - ), - }, - ), - }, - ), - }, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Invariant Selector and Result -// ════════════════════════════════════════════════════════════════════════ - -/// Render the invariant selector and computation result. -let renderInvariants = ( - generators: array, - selectedInvariant: option, - invariantResult: option, -): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4 border-t border-gray-800")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-500 uppercase tracking-wider mb-2")}, - list{text("Knot Invariants")}, - ), - // Invariant buttons - div( - list{Attrs.class_("flex flex-wrap gap-2 mb-3")}, - TangleVizEngine.allInvariants - ->Array.map(inv => { - let isSelected = selectedInvariant === Some(inv) - let activeClass = isSelected - ? "bg-indigo-600 text-white border-indigo-500" - : "bg-gray-800 text-gray-400 border-gray-700 hover:bg-gray-700 hover:text-gray-300" - button( - list{ - Attrs.class_(`px-3 py-1.5 text-xs rounded border transition-colors ${activeClass}`), - Events.onClick(TangleViz(SelectInvariant(inv))), - }, - list{text(TangleVizEngine.invariantLabel(inv))}, - ) - }) - ->List.fromArray, - ), - // Compute button - switch selectedInvariant { - | None => noNode - | Some(_) => - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-emerald-600 text-white rounded hover:bg-emerald-500 transition-colors", - ), - Events.onClick(TangleViz(ComputeInvariant)), - KeyboardNav.onActivate(TangleViz(ComputeInvariant)), - }, - list{text("Compute")}, - ), - // Result display - switch invariantResult { - | None => noNode - | Some(result) => - div( - list{ - Attrs.class_( - "text-sm font-mono text-emerald-300 bg-gray-900 px-3 py-2 rounded border border-gray-800", - ), - }, - list{text(result)}, - ) - }, - }, - ) - }, - // Disabled state when no generators - if Array.length(generators) === 0 { - div( - list{Attrs.class_("mt-2 text-xs text-gray-600")}, - list{text("Load a braid word to compute invariants")}, - ) - } else { - noNode - }, - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Header -// ════════════════════════════════════════════════════════════════════════ - -/// Render the panel header with title and close button. -let renderHeader = (): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center justify-between px-6 py-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("Tangle Viz")}), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Topological Programming Visualizer")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Error Display -// ════════════════════════════════════════════════════════════════════════ - -/// Render an error banner if present. -let renderError = (error: option): Tea_Vdom.t => { - switch error { - | None => noNode - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-4 py-2 bg-red-900/30 border border-red-800 rounded text-sm text-red-300", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("ml-3 text-xs text-red-400 hover:text-red-200 underline"), - Events.onClick(TangleViz(DismissError)), - KeyboardNav.onActivate(TangleViz(DismissError)), - }, - list{text("dismiss")}, - ), - }, - ) - } -} - -// ════════════════════════════════════════════════════════════════════════ -// Main View -// ════════════════════════════════════════════════════════════════════════ - -/// Main TangleViz panel view — full-screen overlay. -let view = (tv: tangleVizState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Tangle Viz panel"), - }, - list{ - // Header - renderHeader(), - // Error banner - renderError(tv.error), - // View mode tabs - renderViewTabBar(tv.viewMode), - // Two-column layout: left = input/controls, right = visualization - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Left sidebar: source input, examples, invariants - div( - list{Attrs.class_("w-96 flex-shrink-0 border-r border-gray-800 overflow-y-auto")}, - list{ - renderSourceInput(tv.inputText, tv.parsedProgram), - renderExamples(), - renderBraidWord(tv.braidWord, tv.strandCount), - renderInvariants(tv.braidWord, tv.selectedInvariant, tv.invariantResult), - }, - ), - // Right main area: visualization based on view mode - div( - list{Attrs.class_("flex-1 overflow-auto")}, - list{ - switch tv.viewMode { - | BraidDiagram => renderBraidSvg(tv.braidWord, tv.strandCount) - | KnotDiagram => renderKnotDiagram(tv.braidWord, tv.strandCount) - | AlgebraicView => renderAlgebraicView(tv.braidWord, tv.strandCount) - }, - }, - ), - }, - ), - }, - ) -} diff --git a/src/components/TeamDashboard.affine b/src/components/TeamDashboard.affine new file mode 100644 index 00000000..fe1af2c5 --- /dev/null +++ b/src/components/TeamDashboard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TeamDashboard; + +// TODO: Complete semantic implementation diff --git a/src/components/TeamDashboard.res b/src/components/TeamDashboard.res deleted file mode 100644 index e0ea0415..00000000 --- a/src/components/TeamDashboard.res +++ /dev/null @@ -1,284 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL TeamDashboard — team member presence, activity feed, and progress overview. -/// Viewer clade panel for team collaboration visibility. -/// -/// Four tabs: Team (member cards with status dots), Activity (feed), -/// Progress (overview), and Schedule (placeholder). - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tailwind colour class for status dot. -let statusDotColour = (status: memberStatus): string => - switch status { - | MemberOnline => "bg-emerald-400" - | MemberBusy => "bg-amber-400" - | MemberAway => "bg-gray-400" - | MemberOffline => "bg-gray-600" - } - -/// Label for member status. -let statusLabel = (status: memberStatus): string => - switch status { - | MemberOnline => "Online" - | MemberBusy => "Busy" - | MemberAway => "Away" - | MemberOffline => "Offline" - } - -/// Tab bar rendering. -let renderTabs = (active: teamDashboardTab): Tea_Vdom.t => { - let tabs = TeamDashboardEngine.allTabs - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-3 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(TeamDashboard(SetTdTab(tab))), - }, - list{text(TeamDashboardEngine.tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Team tab: member cards with presence indicators and current task. -let renderTeamTab = (state: teamDashboardState): Tea_Vdom.t => { - let filtered = TeamDashboardEngine.filterMembers(state.members, state.filter) - let onlineCount = TeamDashboardEngine.countOnline(state.members) - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, - list{text("No team members loaded.")}, - ) - } else { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-1")}, - list{ - text( - `${Int.toString(onlineCount)} of ${Int.toString(Array.length(state.members))} online`, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3 max-h-96 overflow-y-auto")}, - filtered - ->Array.map(member => { - div( - list{Attrs.class_("bg-gray-800 rounded p-3 border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-2")}, - list{ - div( - list{ - Attrs.class_(`w-2.5 h-2.5 rounded-full ${statusDotColour(member.status)}`), - }, - list{}, - ), - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(member.name)}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text(`${member.role} - ${statusLabel(member.status)}`)}, - ), - switch member.currentTask { - | Some(task) => - div( - list{Attrs.class_("text-xs text-gray-400 bg-gray-900 rounded px-2 py-1 mt-1")}, - list{text(task)}, - ) - | None => noNode - }, - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } -} - -/// Activity tab: chronological feed of team actions. -let renderActivityTab = (state: teamDashboardState): Tea_Vdom.t => { - let recent = TeamDashboardEngine.recentActivity(state.activity, 50) - if Array.length(recent) === 0 { - div(list{Attrs.class_("p-4 text-gray-500 text-sm italic")}, list{text("No recent activity.")}) - } else { - div( - list{Attrs.class_("flex flex-col gap-1 p-4 max-h-96 overflow-y-auto")}, - recent - ->Array.map(entry => { - div( - list{Attrs.class_("flex items-start gap-2 p-2 bg-gray-800 rounded")}, - list{ - div(list{Attrs.class_("w-1.5 h-1.5 rounded-full bg-cyan-500 mt-1.5")}, list{}), - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300")}, - list{ - span(list{Attrs.class_("font-medium text-gray-200")}, list{text(entry.actor)}), - text(` ${entry.action} `), - span(list{Attrs.class_("text-cyan-400")}, list{text(entry.target)}), - }, - ), - div(list{Attrs.class_("text-xs text-gray-600")}, list{text(entry.timestamp)}), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - } -} - -/// Progress tab: overview of team progress metrics. -let renderProgressTab = (state: teamDashboardState): Tea_Vdom.t => { - let onlineCount = TeamDashboardEngine.countOnline(state.members) - let totalMembers = Array.length(state.members) - let activityCount = Array.length(state.activity) - div( - list{Attrs.class_("flex flex-col gap-4 p-4")}, - list{ - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 border border-gray-700 text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-emerald-400")}, - list{text(Int.toString(onlineCount))}, - ), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text("Online")}), - }, - ), - div( - list{Attrs.class_("bg-gray-800 rounded p-4 border border-gray-700 text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-gray-300")}, - list{text(Int.toString(totalMembers))}, - ), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text("Team Size")}), - }, - ), - div( - list{Attrs.class_("bg-gray-800 rounded p-4 border border-gray-700 text-center")}, - list{ - div( - list{Attrs.class_("text-2xl font-light text-cyan-400")}, - list{text(Int.toString(activityCount))}, - ), - div(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{text("Activities")}), - }, - ), - }, - ), - }, - ) -} - -/// Schedule tab: placeholder for future calendar integration. -let renderScheduleTab = (_state: teamDashboardState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4")}, - list{ - div( - list{Attrs.class_("bg-gray-800 rounded p-4 h-48 flex items-center justify-center")}, - list{ - span( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Schedule integration coming soon.")}, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function for the Team Dashboard panel. -let view = (state: teamDashboardState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabTeam => renderTeamTab(state) - | TabActivity => renderActivityTab(state) - | TabProgress => renderProgressTab(state) - | TabSchedule => renderScheduleTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Team Dashboard")}, - ), - // Filter input - input( - list{ - Attrs.class_( - "bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-gray-300 w-48 placeholder-gray-600", - ), - Attrs.placeholder("Filter members..."), - Attrs.value(state.filter), - Events.onInput(value => TeamDashboard(SetTdFilter(value))), - }, - list{}, - ), - }, - ), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/Tentacles.affine b/src/components/Tentacles.affine new file mode 100644 index 00000000..1ca0805b --- /dev/null +++ b/src/components/Tentacles.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tentacles; + +// TODO: Complete semantic implementation diff --git a/src/components/Tentacles.res b/src/components/Tentacles.res deleted file mode 100644 index cc3d10ec..00000000 --- a/src/components/Tentacles.res +++ /dev/null @@ -1,631 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Tentacles — panel component for the 7-Tentacles compiler agent orchestra. -/// -/// Renders four category tabs: Agent (single-agent 3-panel view), Orchestra -/// (7-agent grid), Stage (progressive reveal config), Progress (stats dashboard). -/// Uses Tea_Html (no JSX) and Tailwind CSS, consistent with all PanLL panels. - -open Model -open Msg -open TentaclesEngine -open Tea.Html - -/// Render a category tab button. -let renderTab = (label: string, active: bool, cat: tentaclesCategory): Tea_Vdom.t => { - let baseClass = "px-3 py-1.5 text-xs rounded-t border-b-2 transition-colors cursor-pointer" - let cls = active - ? `${baseClass} text-cyan-300 border-cyan-400 bg-gray-800` - : `${baseClass} text-gray-500 border-transparent hover:text-gray-300` - button( - list{ - Attrs.class_(cls), - Events.onClick(Tentacles(SetTentaclesCategory(cat))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Render an OODA phase indicator. -let renderOodaIndicator = (currentPhase: oodaPhase): Tea_Vdom.t => { - let phases = [(Observe, "O"), (Orient, "R"), (Decide, "D"), (Act, "A")] - div( - list{Attrs.class_("flex gap-1")}, - phases - ->Array.map(((phase, letter)) => { - let cls = - phase == currentPhase - ? "w-6 h-6 rounded-full bg-cyan-500 text-gray-950 flex items-center justify-center text-xs font-bold" - : "w-6 h-6 rounded-full bg-gray-700 text-gray-400 flex items-center justify-center text-xs" - div(list{Attrs.class_(cls)}, list{text(letter)}) - }) - ->List.fromArray, - ) -} - -/// Render a constraint card for Panel-L feed. -let renderConstraint = (c: tentacleConstraint): Tea_Vdom.t => { - let statusCls = c.satisfied ? "text-green-400" : "text-amber-400" - let statusText = c.satisfied ? "SAT" : "UNSAT" - div( - list{Attrs.class_("p-2 mb-1 rounded bg-gray-800/60 border border-gray-700/50")}, - list{ - div( - list{Attrs.class_("flex justify-between items-center")}, - list{ - span(list{Attrs.class_("text-xs text-gray-300")}, list{text(c.label)}), - span(list{Attrs.class_(`text-xs font-mono ${statusCls}`)}, list{text(statusText)}), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 font-mono mt-0.5 truncate")}, - list{text(c.expression)}, - ), - }, - ) -} - -/// Render a reasoning entry for Panel-N feed. -let renderReasoning = (r: reasoningEntry): Tea_Vdom.t => { - let phaseColour = switch r.phase { - | Observe => "text-blue-400" - | Orient => "text-yellow-400" - | Decide => "text-orange-400" - | Act => "text-green-400" - } - div( - list{Attrs.class_("p-2 mb-1 rounded bg-gray-800/40 border-l-2 border-gray-600")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_(`text-xs font-mono ${phaseColour}`)}, - list{text(oodaIcon(r.phase))}, - ), - span(list{Attrs.class_("text-xs text-gray-300")}, list{text(r.summary)}), - }, - ), - }, - ) -} - -/// Render a validated result card for Panel-W feed. -let renderResult = (r: validatedResult): Tea_Vdom.t => { - let verifiedCls = r.verified ? "text-green-400" : "text-red-400" - let verifiedText = r.verified ? "VERIFIED" : "UNVERIFIED" - div( - list{Attrs.class_("p-2 mb-1 rounded bg-gray-800/60 border border-gray-700/50")}, - list{ - div( - list{Attrs.class_("flex justify-between items-center mb-1")}, - list{ - span(list{Attrs.class_("text-xs font-medium text-gray-200")}, list{text(r.title)}), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span(list{Attrs.class_(`text-xs ${verifiedCls}`)}, list{text(verifiedText)}), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Float.toFixed(r.confidence *. 100.0, ~digits=0) ++ "%")}, - ), - }, - ), - }, - ), - div(list{Attrs.class_("text-xs text-gray-400 font-mono truncate")}, list{text(r.content)}), - }, - ) -} - -/// Render a single agent card (used in both AgentView and Orchestra). -let renderAgentCard = (agent: tentacleAgentState, isSelected: bool, compact: bool): Tea_Vdom.t< - msg, -> => { - let borderCls = tentacleBorderClass(agent.id) - let bgCls = tentacleBgClass(agent.id) - let textCls = tentacleTextClass(agent.id) - let selectedBorder = isSelected ? borderCls : "border-gray-700" - let name = agentDisplayName(agent) - - if compact { - // Compact mode: small coloured dot with status - div( - list{ - Attrs.class_( - `p-2 rounded-lg border ${selectedBorder} ${bgCls} cursor-pointer hover:brightness-110 transition-all`, - ), - Events.onClick(Tentacles(SelectAgent(agent.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div( - list{ - Attrs.class_( - `w-3 h-3 rounded-full ${tentacleBgClass(agent.id)} border ${borderCls}`, - ), - }, - list{}, - ), - span( - list{Attrs.class_(`text-xs ${textCls} font-medium`)}, - list{text(tentacleShortLabel(agent.id))}, - ), - if agent.busy { - span(list{Attrs.class_("text-xs text-cyan-400 animate-pulse")}, list{text("...")}) - } else { - noNode - }, - }, - ), - }, - ) - } else { - // Full card: name, role, OODA, status - div( - list{ - Attrs.class_( - `p-3 rounded-lg border ${selectedBorder} ${bgCls} cursor-pointer hover:brightness-110 transition-all`, - ), - Events.onClick(Tentacles(SelectAgent(agent.id))), - }, - list{ - // Header: name + busy indicator - div( - list{Attrs.class_("flex justify-between items-center mb-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - div(list{Attrs.class_(`w-3 h-3 rounded-full border ${borderCls}`)}, list{}), - span(list{Attrs.class_(`text-sm font-medium ${textCls}`)}, list{text(name)}), - }, - ), - if agent.busy { - span(list{Attrs.class_("text-xs text-cyan-400 animate-pulse")}, list{text("WORKING")}) - } else { - span(list{Attrs.class_("text-xs text-gray-600")}, list{text("IDLE")}) - }, - }, - ), - // Role - div(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text(agent.compilerRole)}), - // OODA indicator - renderOodaIndicator(agent.currentPhase), - // Catchphrase - div( - list{Attrs.class_("text-xs text-gray-500 italic mt-2 truncate")}, - list{text(agent.personality.catchphrase)}, - ), - // Stats row - div( - list{Attrs.class_("flex gap-3 mt-2 text-xs text-gray-500")}, - list{ - span( - list{}, - list{text(Int.toString(Array.length(agent.constraints)) ++ " constraints")}, - ), - span(list{}, list{text(Int.toString(Array.length(agent.results)) ++ " results")}), - }, - ), - // Error indicator - switch agent.lastError { - | Some(err) => - div( - list{ - Attrs.class_( - "mt-2 p-1.5 rounded bg-red-900/30 border border-red-700/50 text-xs text-red-400 truncate", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - }, - ) - } -} - -/// Render the Agent View tab — single agent with 3-panel breakdown. -let renderAgentView = (state: tentaclesState): Tea_Vdom.t => { - let agent = findAgent(state.agents, state.selectedAgent) - switch agent { - | None => div(list{Attrs.class_("p-4 text-gray-500 text-sm")}, list{text("No agent selected")}) - | Some(a) => { - let textCls = tentacleTextClass(a.id) - let borderCls = tentacleBorderClass(a.id) - div( - list{Attrs.class_("flex flex-col h-full")}, - list{ - // Agent selector strip - div( - list{Attrs.class_("flex gap-1 px-3 py-2 border-b border-gray-800")}, - allTentacles - ->Array.map(id => { - let isSelected = id == state.selectedAgent - let cls = isSelected - ? `px-2 py-1 text-xs rounded cursor-pointer ${tentacleBgClass( - id, - )} ${tentacleTextClass(id)} border ${tentacleBorderClass(id)}` - : "px-2 py-1 text-xs rounded cursor-pointer text-gray-500 hover:text-gray-300 border border-transparent" - button( - list{Attrs.class_(cls), Events.onClick(Tentacles(SelectAgent(id)))}, - list{text(tentacleShortLabel(id))}, - ) - }) - ->List.fromArray, - ), - // Agent header - div( - list{Attrs.class_("px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex justify-between items-center")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div(list{Attrs.class_(`w-4 h-4 rounded-full border-2 ${borderCls}`)}, list{}), - span( - list{Attrs.class_(`text-lg font-medium ${textCls}`)}, - list{text(agentDisplayName(a))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("(" ++ a.compilerRole ++ ")")}, - ), - }, - ), - renderOodaIndicator(a.currentPhase), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 italic mt-1")}, - list{text(a.personality.catchphrase)}, - ), - }, - ), - // Three-column layout: Constraints | Reasoning | Results - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Panel-L: Constraints - div( - list{Attrs.class_("flex-1 overflow-auto border-r border-gray-800 p-3")}, - list{ - div( - list{ - Attrs.class_( - "text-xs text-gray-500 font-medium mb-2 uppercase tracking-wider", - ), - }, - list{text("Constraints (L)")}, - ), - if Array.length(a.constraints) == 0 { - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("No active constraints")}, - ) - } else { - div(list{}, a.constraints->Array.map(renderConstraint)->List.fromArray) - }, - }, - ), - // Panel-N: Reasoning - div( - list{Attrs.class_("flex-1 overflow-auto border-r border-gray-800 p-3")}, - list{ - div( - list{ - Attrs.class_( - "text-xs text-gray-500 font-medium mb-2 uppercase tracking-wider", - ), - }, - list{text("Reasoning (N)")}, - ), - if Array.length(a.reasoning) == 0 { - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("No reasoning entries")}, - ) - } else { - div(list{}, a.reasoning->Array.map(renderReasoning)->List.fromArray) - }, - }, - ), - // Panel-W: Results - div( - list{Attrs.class_("flex-1 overflow-auto p-3")}, - list{ - div( - list{ - Attrs.class_( - "text-xs text-gray-500 font-medium mb-2 uppercase tracking-wider", - ), - }, - list{text("Results (W)")}, - ), - if Array.length(a.results) == 0 { - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("No validated results")}, - ) - } else { - div(list{}, a.results->Array.map(renderResult)->List.fromArray) - }, - }, - ), - }, - ), - }, - ) - } - } -} - -/// Render the Orchestra tab — all 7 agents in a grid. -let renderOrchestra = (state: tentaclesState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4")}, - list{ - // Header with compact toggle - div( - list{Attrs.class_("flex justify-between items-center mb-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300 font-medium")}, - list{text("Agent Orchestra")}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-300 cursor-pointer"), - Events.onClick(Tentacles(ToggleOrchestraCompact)), - KeyboardNav.onActivate(Tentacles(ToggleOrchestraCompact)), - }, - list{text(state.orchestraCompact ? "Expand" : "Compact")}, - ), - }, - ), - // Agent grid - div( - list{ - Attrs.class_( - state.orchestraCompact - ? "grid grid-cols-7 gap-2" - : "grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3", - ), - }, - state.agents - ->Array.map(a => renderAgentCard(a, a.id == state.selectedAgent, state.orchestraCompact)) - ->List.fromArray, - ), - // Summary stats - div( - list{Attrs.class_("mt-4 flex gap-6 text-xs text-gray-500 border-t border-gray-800 pt-3")}, - list{ - span(list{}, list{text(Int.toString(busyCount(state.agents)) ++ " active")}), - span(list{}, list{text(Int.toString(errorCount(state.agents)) ++ " errors")}), - span(list{}, list{text(Int.toString(totalConstraints(state.agents)) ++ " constraints")}), - span(list{}, list{text(Int.toString(totalResults(state.agents)) ++ " results")}), - if state.ffiConnected { - span(list{Attrs.class_("text-green-500")}, list{text("FFI Connected")}) - } else { - span(list{Attrs.class_("text-gray-600")}, list{text("FFI Disconnected")}) - }, - }, - ), - }, - ) -} - -/// Render the Stage Config tab. -let renderStageConfig = (state: tentaclesState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300 font-medium mb-4")}, - list{text("Progressive Reveal Stage")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-4")}, - list{ - text( - "Set the global stage to control which compiler concepts are revealed to all agents.", - ), - }, - ), - // Stage selector - div( - list{Attrs.class_("flex flex-col gap-3")}, - allStages - ->Array.map(stage => { - let isActive = stage == state.globalStage - let cls = isActive - ? "p-3 rounded-lg border border-cyan-500 bg-cyan-900/20 cursor-pointer" - : "p-3 rounded-lg border border-gray-700 bg-gray-800/40 cursor-pointer hover:border-gray-500" - div( - list{Attrs.class_(cls), Events.onClick(Tentacles(SetGlobalStage(stage)))}, - list{ - div( - list{Attrs.class_("flex justify-between items-center")}, - list{ - span( - list{ - Attrs.class_( - isActive ? "text-sm text-cyan-300 font-medium" : "text-sm text-gray-300", - ), - }, - list{text(stageLabel(stage))}, - ), - if isActive { - span(list{Attrs.class_("text-xs text-cyan-400")}, list{text("ACTIVE")}) - } else { - noNode - }, - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{ - text( - switch stage { - | Cuttle => "Introductory stage. Game-like interactions, gentle metaphors, hidden compiler concepts." - | Squidlet => "Intermediate stage. Pattern-matching challenges, revealed connections between games and code." - | Duet => "Paired reasoning stage. Two agents collaborate, showing how compiler subsystems interact." - | Octopus => "Full access stage. Direct compiler subsystem interaction, formal methods, proof obligations." - }, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Render the Progress tab. -let renderProgress = (state: tentaclesState): Tea_Vdom.t => { - div( - list{Attrs.class_("p-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-300 font-medium mb-4")}, - list{text("Progress Dashboard")}, - ), - // Per-agent stats - div( - list{Attrs.class_("flex flex-col gap-2")}, - state.agents - ->Array.map(a => { - let textCls = tentacleTextClass(a.id) - let bgCls = tentacleBgClass(a.id) - let constraintCount = Array.length(a.constraints) - let resultCount = Array.length(a.results) - let verifiedCount = a.results->Array.filter(r => r.verified)->Array.length - div( - list{Attrs.class_(`p-3 rounded-lg ${bgCls} border border-gray-700/50`)}, - list{ - div( - list{Attrs.class_("flex justify-between items-center mb-1")}, - list{ - span( - list{Attrs.class_(`text-sm font-medium ${textCls}`)}, - list{text(agentDisplayName(a))}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(tentacleRole(a.id))}), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-xs text-gray-500")}, - list{ - span(list{}, list{text(Int.toString(constraintCount) ++ " constraints")}), - span(list{}, list{text(Int.toString(resultCount) ++ " results")}), - span(list{}, list{text(Int.toString(verifiedCount) ++ " verified")}), - span(list{}, list{text("Stage: " ++ stageLabel(a.stage))}), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - // FFI status - div( - list{Attrs.class_("mt-4 p-3 rounded-lg bg-gray-800/40 border border-gray-700/50")}, - list{ - div( - list{Attrs.class_("flex justify-between items-center")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400")}, list{text("ECHIDNA FFI Bridge")}), - if state.ffiConnected { - span(list{Attrs.class_("text-xs text-green-400")}, list{text("Connected")}) - } else { - span(list{Attrs.class_("text-xs text-gray-600")}, list{text("Disconnected")}) - }, - }, - ), - switch state.ffiError { - | Some(err) => div(list{Attrs.class_("mt-1 text-xs text-red-400")}, list{text(err)}) - | None => noNode - }, - div( - list{Attrs.class_("mt-2")}, - list{ - button( - list{ - Attrs.class_("text-xs text-cyan-400 hover:text-cyan-300 cursor-pointer"), - Events.onClick(Tentacles(CheckFfiBridge)), - KeyboardNav.onActivate(Tentacles(CheckFfiBridge)), - }, - list{text("Check Connection")}, - ), - }, - ), - }, - ), - }, - ) -} - -/// Main view for the Tentacles panel. -let view = (state: tentaclesState): Tea_Vdom.t => { - div( - list{Attrs.class_("fixed inset-0 z-40 bg-gray-950/95 flex flex-col")}, - list{ - // Panel header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text("7-Tentacles")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Compiler Agent Orchestra")}, - ), - }, - ), - // Close button - button( - list{ - Attrs.class_("text-gray-500 hover:text-gray-300 text-xs cursor-pointer"), - Events.onClick(PanelSwitcher(TogglePanel(PanelTentacles))), - }, - list{text("[X]")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 pt-2 border-b border-gray-800")}, - allCategories - ->Array.map(cat => renderTab(categoryLabel(cat), cat == state.activeCategory, cat)) - ->List.fromArray, - ), - // Content area - div( - list{Attrs.class_("flex-1 overflow-auto")}, - list{ - switch state.activeCategory { - | AgentView => renderAgentView(state) - | Orchestra => renderOrchestra(state) - | StageConfig => renderStageConfig(state) - | Progress => renderProgress(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/Tsdm.affine b/src/components/Tsdm.affine new file mode 100644 index 00000000..119f030f --- /dev/null +++ b/src/components/Tsdm.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Tsdm; + +// TODO: Complete semantic implementation diff --git a/src/components/Tsdm.res b/src/components/Tsdm.res deleted file mode 100644 index 141a713a..00000000 --- a/src/components/Tsdm.res +++ /dev/null @@ -1,516 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL TSDM Panel — Triaxial Software Development Methodology directive. -/// -/// A directive panel: users reorder axes, tier priorities, and cleanup steps. -/// Other panels read the active directive when presenting and sequencing work. -/// Displays aggregated work items classified by TSDM axes and tiers. -/// -/// Two columns: Axis/tier ordering (left), Work items (right). - -open Msg -open TsdmModel -open Tea.Html - -/// Display label for an axis. -let axisLabel = (axis: axisId): string => - switch axis { - | AxisScope => "Scope" - | AxisMaintenance => "Maintenance" - | AxisAudit => "Audit" - } - -/// Colour class for an axis. -let axisColour = (axis: axisId): string => - switch axis { - | AxisScope => "text-emerald-400" - | AxisMaintenance => "text-amber-400" - | AxisAudit => "text-cyan-400" - } - -/// Display label for a scope tier. -let scopeTierLabel = (tier: scopeTier): string => - switch tier { - | Must => "Must" - | Intend => "Intend" - | Like => "Like" - } - -/// Display label for a maintenance tier. -let maintenanceTierLabel = (tier: maintenanceTier): string => - switch tier { - | Corrective => "Corrective" - | Adaptive => "Adaptive" - | Perfective => "Perfective" - } - -/// Display label for an audit tier. -let auditTierLabel = (tier: auditTier): string => - switch tier { - | Systems => "Systems" - | Compliance => "Compliance" - | Effects => "Effects" - } - -/// Display label for a cleanup step. -let cleanupStepLabel = (step: cleanupStep): string => - switch step { - | RootCleanup => "Root Cleanup" - | StaleWorkCull => "Stale Work Cull" - | DocsSyncHumanMachine => "Docs Sync (Human + Machine)" - | ComplianceAudit => "Compliance Audit" - | EffectsAudit => "Effects Audit" - | ReleaseSummary => "Release Summary" - | NextActions => "Next Actions" - } - -/// Render a numbered, reorderable list item with up/down arrows. -let orderItem = ( - index: int, - lbl: string, - colour: string, - total: int, - upMsg: msg, - downMsg: msg, -): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2 py-1.5 px-3 bg-gray-800/50 rounded mb-1")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 font-mono w-5")}, - list{text(Int.toString(index + 1))}, - ), - span(list{Attrs.class_(`flex-1 text-sm font-mono ${colour}`)}, list{text(lbl)}), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-200 disabled:opacity-30 px-1"), - Attrs.disabled(index == 0), - Events.onClick(upMsg), - }, - list{text("^")}, - ), - button( - list{ - Attrs.class_("text-xs text-gray-500 hover:text-gray-200 disabled:opacity-30 px-1"), - Attrs.disabled(index == total - 1), - Events.onClick(downMsg), - }, - list{text("v")}, - ), - }, - ) -} - -/// Render a toggleable cleanup step. -let cleanupItem = (step: cleanupStep, enabled: bool): Tea_Vdom.t => { - label( - list{ - Attrs.class_("flex items-center gap-2 py-1 px-3 cursor-pointer hover:bg-gray-800/30 rounded"), - }, - list{ - input( - list{ - Attrs.type_("checkbox"), - Attrs.checked(enabled), - Attrs.class_("w-3.5 h-3.5 accent-cyan-500"), - Events.onClick(Tsdm(ToggleCleanupStep(step))), - }, - list{}, - ), - span( - list{ - Attrs.class_( - `text-sm font-mono ${enabled ? "text-gray-200" : "text-gray-500 line-through"}`, - ), - }, - list{text(cleanupStepLabel(step))}, - ), - }, - ) -} - -/// Render a work item row. -let workItemRow = (item: tsdmWorkItem): Tea_Vdom.t => { - let axisColourClass = axisColour(item.axis) - let doneClass = item.done ? "opacity-40" : "" - div( - list{Attrs.class_(`flex items-center gap-3 py-1.5 px-3 border-b border-gray-700 ${doneClass}`)}, - list{ - span( - list{Attrs.class_(`text-xs font-mono font-bold ${axisColourClass} min-w-[50px]`)}, - list{text(axisLabel(item.axis))}, - ), - span( - list{Attrs.class_("text-xs text-gray-500 font-mono min-w-[70px]")}, - list{ - text( - switch (item.scopeTier, item.maintenanceTier, item.auditTier) { - | (Some(t), _, _) => scopeTierLabel(t) - | (_, Some(t), _) => maintenanceTierLabel(t) - | (_, _, Some(t)) => auditTierLabel(t) - | (None, None, None) => "-" - }, - ), - }, - ), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{div(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(item.title)})}, - ), - span(list{Attrs.class_("text-xs text-gray-600 font-mono")}, list{text(item.sourcePanel)}), - }, - ) -} - -/// Sort work items according to current TSDM directive. -let sortByDirective = ( - items: array, - axisOrder: array, - showCompleted: bool, -): array => { - items - ->Array.filter(item => showCompleted || !item.done) - ->Array.toSorted((a, b) => { - // Primary sort: axis position in axisOrder - let posA = axisOrder->Array.findIndex(ax => ax == a.axis)->Int.toFloat - let posB = axisOrder->Array.findIndex(ax => ax == b.axis)->Int.toFloat - posA -. posB - }) -} - -/// Render an axis filter button. -let axisFilterBtn = ( - lbl: string, - filterVal: option, - activeFilter: option, -): Tea_Vdom.t => { - let active = activeFilter == filterVal - button( - list{ - Attrs.class_( - `px-2 py-0.5 rounded font-mono text-xs ${active - ? "bg-indigo-600 text-white" - : "bg-gray-700 text-gray-400 hover:bg-gray-600"}`, - ), - Events.onClick(Tsdm(SetAxisFilter(filterVal))), - }, - list{text(lbl)}, - ) -} - -/// Main panel view. -let view = (state: tsdmState): Tea_Vdom.t => { - let sorted = sortByDirective(state.workItems, state.axisOrder, state.showCompleted) - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100 overflow-hidden")}, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-3 bg-gray-800 border-b border-gray-700", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span(list{Attrs.class_("text-lg font-bold text-indigo-400")}, list{text("TSDM")}), - span( - list{ - Attrs.class_("text-xs text-gray-500 font-mono px-2 py-0.5 rounded bg-gray-700"), - }, - list{text("directive")}, - ), - if state.locked { - span( - list{ - Attrs.class_( - "text-xs text-amber-400 font-mono px-2 py-0.5 rounded bg-gray-700", - ), - }, - list{text("LOCKED")}, - ) - } else { - noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded font-mono ${state.locked - ? "bg-amber-700 hover:bg-amber-600 text-white" - : "bg-gray-700 hover:bg-gray-600 text-gray-300"}`, - ), - Events.onClick(Tsdm(ToggleLock)), - KeyboardNav.onActivate(Tsdm(ToggleLock)), - }, - list{text(state.locked ? "unlock" : "lock")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded bg-indigo-700 hover:bg-indigo-600 text-white font-mono", - ), - Events.onClick(Tsdm(ResetToDefaults)), - KeyboardNav.onActivate(Tsdm(ResetToDefaults)), - }, - list{text("reset")}, - ), - }, - ), - }, - ), - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left column: Axis ordering - div( - list{Attrs.class_("w-64 border-r border-gray-700 overflow-y-auto p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 font-bold mb-2 uppercase")}, - list{text("Axis Execution Order")}, - ), - div( - list{}, - state.axisOrder - ->Array.mapWithIndex((axis, i) => - orderItem( - i, - axisLabel(axis), - axisColour(axis), - Array.length(state.axisOrder), - Tsdm(MoveAxisUp(i)), - Tsdm(MoveAxisDown(i)), - ) - ) - ->List.fromArray, - ), - // Scope tiers - div( - list{Attrs.class_("text-xs text-gray-500 font-bold mt-4 mb-2 uppercase")}, - list{text("Scope Tiers")}, - ), - div( - list{}, - state.scopeOrder - ->Array.mapWithIndex((tier, i) => - orderItem( - i, - scopeTierLabel(tier), - "text-emerald-300", - Array.length(state.scopeOrder), - Tsdm(MoveScopeTierUp(i)), - Tsdm(MoveScopeTierDown(i)), - ) - ) - ->List.fromArray, - ), - // Maintenance tiers - div( - list{Attrs.class_("text-xs text-gray-500 font-bold mt-4 mb-2 uppercase")}, - list{text("Maintenance Tiers")}, - ), - div( - list{}, - state.maintenanceOrder - ->Array.mapWithIndex((tier, i) => - orderItem( - i, - maintenanceTierLabel(tier), - "text-amber-300", - Array.length(state.maintenanceOrder), - Tsdm(MoveMaintenanceTierUp(i)), - Tsdm(MoveMaintenanceTierDown(i)), - ) - ) - ->List.fromArray, - ), - // Audit tiers - div( - list{Attrs.class_("text-xs text-gray-500 font-bold mt-4 mb-2 uppercase")}, - list{text("Audit Tiers")}, - ), - div( - list{}, - state.auditOrder - ->Array.mapWithIndex((tier, i) => - orderItem( - i, - auditTierLabel(tier), - "text-cyan-300", - Array.length(state.auditOrder), - Tsdm(MoveAuditTierUp(i)), - Tsdm(MoveAuditTierDown(i)), - ) - ) - ->List.fromArray, - ), - // Cleanup steps - div( - list{Attrs.class_("text-xs text-gray-500 font-bold mt-4 mb-2 uppercase")}, - list{text("Cleanup Steps")}, - ), - div( - list{}, - state.cleanupSteps - ->Array.map(step => { - let enabled = state.cleanupEnabled->Array.includes(step) - cleanupItem(step, enabled) - }) - ->List.fromArray, - ), - // Tooling - div( - list{Attrs.class_("text-xs text-gray-500 font-bold mt-4 mb-2 uppercase")}, - list{text("Tooling")}, - ), - div( - list{Attrs.class_("text-xs text-gray-400 px-3 py-1")}, - list{ - div(list{}, list{text(`Compliance: ${state.auditTooling.complianceTool}`)}), - div(list{}, list{text(`Effects: ${state.auditTooling.effectsTool}`)}), - }, - ), - }, - ), - // Right column: Work items - div( - list{Attrs.class_("flex-1 flex flex-col overflow-hidden")}, - list{ - // Filter bar - div( - list{ - Attrs.class_( - "flex items-center gap-2 px-3 py-2 border-b border-gray-700 text-xs", - ), - }, - list{ - // Axis filter buttons - axisFilterBtn("All", None, state.axisFilter), - div( - list{Attrs.class_("flex gap-1")}, - state.axisOrder - ->Array.map(axis => - axisFilterBtn(axisLabel(axis), Some(axis), state.axisFilter) - ) - ->List.fromArray, - ), - label( - list{Attrs.class_("flex items-center gap-1 text-gray-400 cursor-pointer ml-2")}, - list{ - input( - list{ - Attrs.type_("checkbox"), - Attrs.checked(state.showCompleted), - Attrs.class_("w-3 h-3 accent-indigo-500"), - Events.onClick(Tsdm(ToggleShowCompleted)), - KeyboardNav.onActivate(Tsdm(ToggleShowCompleted)), - }, - list{}, - ), - text("Done"), - }, - ), - input( - list{ - Attrs.type_("text"), - Attrs.class_( - "ml-auto w-40 bg-gray-800 text-sm text-gray-200 px-2 py-0.5 rounded border border-gray-600 font-mono", - ), - Attrs.placeholder("Search..."), - Attrs.value(state.searchText), - Events.onInput(v => Tsdm(SetTsdmSearch(v))), - }, - list{}, - ), - }, - ), - // Work items list - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - { - let filtered = switch state.axisFilter { - | None => sorted - | Some(axis) => sorted->Array.filter(item => item.axis == axis) - } - let searched = - state.searchText == "" - ? filtered - : filtered->Array.filter(item => - String.includes( - String.toLowerCase(item.title), - String.toLowerCase(state.searchText), - ) - ) - if Array.length(searched) == 0 { - list{ - div( - list{ - Attrs.class_( - "flex items-center justify-center h-32 text-gray-500 text-sm", - ), - }, - list{ - text( - if Array.length(state.workItems) == 0 { - "No work items. Consumer panels will populate items as they run." - } else { - "No items match the current filter." - }, - ), - }, - ), - } - } else { - searched->Array.map(workItemRow)->List.fromArray - } - }, - ), - // Footer - div( - list{ - Attrs.class_( - "flex items-center justify-between px-3 py-2 bg-gray-800 border-t border-gray-700 text-xs text-gray-500", - ), - }, - list{ - span( - list{}, - list{ - text( - `${Int.toString(Array.length(state.workItems))} items (${Int.toString( - state.workItems->Array.filter(i => i.done)->Array.length, - )} done)`, - ), - }, - ), - span(list{}, list{text("TSDM 1.0 — Scope > Maintenance > Audit")}), - }, - ), - }, - ), - }, - ), - // Error display - switch state.lastError { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 border-t border-red-700 text-red-300 text-sm"), - }, - list{text(err)}, - ) - | None => noNode - }, - }, - ) -} diff --git a/src/components/TypeLL.affine b/src/components/TypeLL.affine new file mode 100644 index 00000000..2181ecda --- /dev/null +++ b/src/components/TypeLL.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TypeLL; + +// TODO: Complete semantic implementation diff --git a/src/components/TypeLL.res b/src/components/TypeLL.res deleted file mode 100644 index 5edc95a0..00000000 --- a/src/components/TypeLL.res +++ /dev/null @@ -1,1306 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL TypeLL Component — the verification kernel panel. -/// -/// Four tabs: Checker, Explorer, Refinement, Guide. -/// View layer selector (RAW/FOLDED/GLYPHED/WYSIWYG) controls how type -/// information is presented — progressive disclosure from expert to learner. -/// -/// The evangeliser philosophy ("Celebrate good, minimize bad, show better") -/// drives the narrative feedback on every type check result. - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Shared sub-views -// ============================================================================ - -/// Render the view layer selector (RAW → WYSIWYG). -let renderViewLayerSelector = (active: viewLayer): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1 bg-gray-900 rounded-lg p-1")}, - TypeLLEngine.allViewLayers - ->Array.map(vl => { - let isActive = vl === active - let colour = TypeLLEngine.viewLayerColour(vl) - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded transition-colors ${isActive - ? colour ++ " ring-1 ring-gray-600" - : "text-gray-600 hover:text-gray-400"}`, - ), - Attrs.ariaLabel(TypeLLEngine.viewLayerDescription(vl)), - Events.onClick(TypeLL(SetViewLayer(vl))), - }, - list{text(TypeLLEngine.viewLayerLabel(vl))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a type feature badge with glyph. -let renderFeatureBadge = (f: typeFeature, viewLayer: viewLayer): Tea_Vdom.t => { - let glyph = TypeLLEngine.featureGlyph(f) - let tierColour = TypeLLEngine.tierColour(TypeLLEngine.featureTier(f)) - switch viewLayer { - | Raw => - span( - list{Attrs.class_(`px-1.5 py-0.5 text-xs rounded ${tierColour}`)}, - list{text(TypeLLEngine.featureCode(f))}, - ) - | Folded => - span( - list{Attrs.class_(`px-2 py-0.5 text-xs rounded ${tierColour}`)}, - list{text(TypeLLEngine.featureLabel(f))}, - ) - | Glyphed => - span( - list{ - Attrs.class_(`px-2 py-0.5 text-xs rounded ${tierColour}`), - Attrs.ariaLabel(glyph.meaning), - }, - list{text(`${glyph.symbol} ${glyph.label}`)}, - ) - | Wysiwyg => - div( - list{Attrs.class_(`px-3 py-1.5 text-xs rounded-lg ${tierColour} border border-gray-700`)}, - list{ - div( - list{Attrs.class_("font-medium")}, - list{text(`${glyph.symbol} ${TypeLLEngine.featureLabel(f)}`)}, - ), - div(list{Attrs.class_("text-gray-500 mt-0.5")}, list{text(glyph.meaning)}), - }, - ) - } -} - -/// Render an evangeliser narrative block. -let renderNarrative = (narrative: typeNarrative): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4 space-y-3")}, - list{ - if narrative.celebrate !== "" { - div( - list{Attrs.class_("flex items-start gap-2")}, - list{ - span( - list{Attrs.class_("text-emerald-400 text-sm font-medium shrink-0")}, - list{text("Celebrate")}, - ), - span(list{Attrs.class_("text-sm text-gray-300")}, list{text(narrative.celebrate)}), - }, - ) - } else { - noNode - }, - if narrative.minimize !== "" { - div( - list{Attrs.class_("flex items-start gap-2")}, - list{ - span( - list{Attrs.class_("text-amber-400 text-sm font-medium shrink-0")}, - list{text("Note")}, - ), - span(list{Attrs.class_("text-sm text-gray-400")}, list{text(narrative.minimize)}), - }, - ) - } else { - noNode - }, - if narrative.showBetter !== "" { - div( - list{Attrs.class_("flex items-start gap-2")}, - list{ - span( - list{Attrs.class_("text-cyan-400 text-sm font-medium shrink-0")}, - list{text("Better")}, - ), - span(list{Attrs.class_("text-sm text-gray-300")}, list{text(narrative.showBetter)}), - }, - ) - } else { - noNode - }, - if narrative.safety !== "" { - div( - list{Attrs.class_("flex items-start gap-2")}, - list{ - span( - list{Attrs.class_("text-violet-400 text-sm font-medium shrink-0")}, - list{text("Safety")}, - ), - span(list{Attrs.class_("text-sm text-gray-300")}, list{text(narrative.safety)}), - }, - ) - } else { - noNode - }, - }, - ) -} - -/// Render a type check result. -let renderCheckResult = ( - result: typeCheckResult, - viewLayer: viewLayer, - narrative: option, -): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-4")}, - list{ - // Status + type signature - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3 mb-3")}, - list{ - span( - list{ - Attrs.class_( - if result.valid { - "text-emerald-400 text-sm font-medium" - } else { - "text-red-400 text-sm font-medium" - }, - ), - }, - list{ - text( - if result.valid { - "Well-typed" - } else { - "Type error" - }, - ), - }, - ), - span( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded ${TypeLLEngine.tierColour(result.maxTier)}`, - ), - }, - list{text(TypeLLEngine.tierLabel(result.maxTier))}, - ), - }, - ), - // Type signature with view layer formatting - pre( - list{ - Attrs.class_( - "font-mono text-sm text-gray-200 bg-gray-950 rounded p-3 whitespace-pre-wrap", - ), - }, - list{ - text( - TypeLLEngine.formatSignature( - result.typeSignature, - viewLayer, - result.activeFeatures, - ), - ), - }, - ), - if result.explanation !== "" { - div(list{Attrs.class_("mt-2 text-sm text-gray-400")}, list{text(result.explanation)}) - } else { - noNode - }, - }, - ), - // Active features - if result.activeFeatures->Array.length > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - result.activeFeatures->Array.map(f => renderFeatureBadge(f, viewLayer))->List.fromArray, - ) - } else { - noNode - }, - // Proof obligations - if result.proofObligations->Array.length > 0 { - div( - list{Attrs.class_("bg-gray-900 border border-violet-700/50 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-violet-400 mb-2 font-medium")}, - list{text("Proof Obligations")}, - ), - div( - list{Attrs.class_("space-y-1")}, - result.proofObligations - ->Array.map(po => - div(list{Attrs.class_("text-sm text-gray-300 font-mono")}, list{text(po)}) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Effects - if result.effects->Array.length > 0 { - div( - list{Attrs.class_("bg-gray-900 border border-amber-700/50 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-amber-400 mb-2 font-medium")}, - list{text("Effects")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - result.effects - ->Array.map(e => - span( - list{Attrs.class_("px-2 py-0.5 text-xs rounded bg-amber-900/30 text-amber-300")}, - list{text(e)}, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Linearity issues - if result.linearityIssues->Array.length > 0 { - div( - list{Attrs.class_("bg-gray-900 border border-red-700/50 rounded-lg p-4")}, - list{ - div( - list{Attrs.class_("text-xs text-red-400 mb-2 font-medium")}, - list{text("Linearity Issues")}, - ), - div( - list{Attrs.class_("space-y-1")}, - result.linearityIssues - ->Array.map(li => div(list{Attrs.class_("text-sm text-red-300")}, list{text(li)})) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Evangeliser narrative - switch narrative { - | Some(n) => renderNarrative(n) - | None => noNode - }, - }, - ) -} - -/// Render category tabs. -let renderTabs = (active: typellCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - TypeLLEngine.allCategories - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-cyan-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(TypeLL(SetTlCategory(tab))), - }, - list{text(TypeLLEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main view for the TypeLL panel. -let view = (tl: typellState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("TypeLL verification kernel panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("TypeLL")}), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Verification Kernel")}), - if tl.serverConnected { - span(list{Attrs.class_("text-xs text-emerald-500")}, list{text("connected")}) - } else { - span(list{Attrs.class_("text-xs text-amber-500")}, list{text("offline")}) - }, - if tl.serviceActive { - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(tl.queriesServed)} queries served`)}, - ) - } else { - noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - renderViewLayerSelector(tl.activeViewLayer), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(tl.activeCategory), - switch tl.activeCategory { - // ── Checker Tab ── - | TlChecker => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text( - "Enter an expression to type-check. TypeLL detects dependent, linear, affine, session, and refinement types automatically.", - ), - }, - ), - textarea( - list{ - Attrs.class_( - "w-full h-40 bg-gray-900 border border-gray-700 rounded-lg p-4 font-mono text-sm text-gray-200 resize-none focus:border-cyan-500 focus:outline-none", - ), - Attrs.value(tl.checkerInput), - Attrs.placeholder("e.g., fun (n : Nat) => Vec n Int"), - Events.onInput(v => TypeLL(UpdateCheckerInput(v))), - }, - list{}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-cyan-600 text-white rounded hover:bg-cyan-500", - ), - Events.onClick(TypeLL(RunCheck)), - KeyboardNav.onActivate(TypeLL(RunCheck)), - }, - list{text("Check Types")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(TypeLL(RunInfer)), - KeyboardNav.onActivate(TypeLL(RunInfer)), - }, - list{text("Infer Type")}, - ), - }, - ), - switch tl.lastCheckResult { - | Some(result) => renderCheckResult(result, tl.activeViewLayer, tl.lastNarrative) - | None => noNode - }, - }, - ) - // ── Explorer Tab ── - | TlExplorer => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("flex gap-3 items-center")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 font-mono", - ), - Attrs.placeholder("Search signatures..."), - Attrs.value(tl.signatureFilter), - Events.onInput(v => TypeLL(SetSignatureFilter(v))), - }, - list{}, - ), - // Tier filter buttons - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${tl.tierFilter === None - ? "bg-gray-700 text-gray-200" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(TypeLL(SetTierFilter(None))), - }, - list{text("All")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${tl.tierFilter === Some(TierCore) - ? TypeLLEngine.tierColour(TierCore) - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(TypeLL(SetTierFilter(Some(TierCore)))), - }, - list{text("Core")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${tl.tierFilter === Some(TierAdvanced) - ? TypeLLEngine.tierColour(TierAdvanced) - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(TypeLL(SetTierFilter(Some(TierAdvanced)))), - }, - list{text("Advanced")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${tl.tierFilter === Some(TierResearch) - ? TypeLLEngine.tierColour(TierResearch) - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(TypeLL(SetTierFilter(Some(TierResearch)))), - }, - list{text("Research")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-cyan-600 text-white rounded hover:bg-cyan-500", - ), - Events.onClick(TypeLL(LoadSignatures)), - KeyboardNav.onActivate(TypeLL(LoadSignatures)), - }, - list{text("Load")}, - ), - }, - ), - // Signature list - if tl.signatures->Array.length === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{text("No signatures loaded. Connect to TypeLL server and click Load.")}, - ) - } else { - let filtered = - tl.signatures - ->TypeLLEngine.filterByTier(tl.tierFilter) - ->TypeLLEngine.filterBySearch(tl.signatureFilter) - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - filtered - ->Array.map(sig => - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-3 border-b border-gray-800 hover:bg-gray-900/50", - ), - }, - list{ - span( - list{ - Attrs.class_( - `px-1.5 py-0.5 text-xs rounded ${TypeLLEngine.tierColour( - sig.tier, - )}`, - ), - }, - list{text(TypeLLEngine.tierLabel(sig.tier))}, - ), - span( - list{Attrs.class_("text-sm font-mono text-cyan-400")}, - list{text(sig.name)}, - ), - span( - list{ - Attrs.class_("text-sm font-mono text-gray-400 flex-1 truncate"), - }, - list{text(sig.signature)}, - ), - span( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(sig.module_)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - // Universes - if tl.universes->Array.length > 0 { - div( - list{Attrs.class_("mt-6")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-3")}, - list{text("Type Universes")}, - ), - div( - list{Attrs.class_("space-y-2")}, - tl.universes - ->Array.map(u => - div( - list{Attrs.class_("flex items-center gap-3 text-sm")}, - list{ - span( - list{Attrs.class_("font-mono text-violet-400 w-8 text-right")}, - list{text(Int.toString(u.level))}, - ), - span( - list{Attrs.class_("font-mono text-gray-200")}, - list{text(u.name)}, - ), - span(list{Attrs.class_("text-gray-500")}, list{text(u.description)}), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - }, - ) - // ── Refinement Tab ── - | TlRefinement => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text( - "Narrow a type with refinement constraints. TypeLL checks satisfiability and consistency.", - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - div( - list{Attrs.class_("space-y-2")}, - list{ - label(list{Attrs.class_("text-xs text-gray-500")}, list{text("Base Type")}), - input( - list{ - Attrs.class_( - "w-full bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono placeholder-gray-600", - ), - Attrs.placeholder("e.g., Int"), - Attrs.value(tl.refinementSpec), - Events.onInput(v => TypeLL(UpdateRefinementSpec(v))), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - label( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Constraints (one per line)")}, - ), - textarea( - list{ - Attrs.class_( - "w-full h-20 bg-gray-900 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 font-mono resize-none placeholder-gray-600", - ), - Attrs.placeholder("x > 0\nx < 256"), - Attrs.value(tl.refinementConstraints), - Events.onInput(v => TypeLL(UpdateRefinementConstraints(v))), - }, - list{}, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm bg-violet-600 text-white rounded hover:bg-violet-500", - ), - Events.onClick(TypeLL(RunRefine)), - KeyboardNav.onActivate(TypeLL(RunRefine)), - }, - list{text("Apply Refinement")}, - ), - switch tl.lastRefinement { - | Some(ref) => - div( - list{ - Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-4 space-y-3"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{ - Attrs.class_( - if ref.consistent { - "text-emerald-400 text-sm" - } else { - "text-red-400 text-sm" - }, - ), - }, - list{ - text( - if ref.consistent { - "Consistent" - } else { - "Inconsistent" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-4")}, - list{ - div( - list{}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text("Base Type")}, - ), - pre( - list{ - Attrs.class_( - "font-mono text-sm text-gray-300 bg-gray-950 rounded p-2", - ), - }, - list{text(ref.baseType)}, - ), - }, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1")}, - list{text("Refined Type")}, - ), - pre( - list{ - Attrs.class_( - "font-mono text-sm text-cyan-300 bg-gray-950 rounded p-2", - ), - }, - list{text(ref.refinedType)}, - ), - }, - ), - }, - ), - }, - ) - | None => noNode - }, - }, - ) - // ── Discipline Tab ── - | TlDiscipline => - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text( - "Type discipline modes control the default type system behaviour per module. Affine by default (like Rust), with opt-in linear, dependent, refined, or unrestricted modes.", - ), - }, - ), - // Default discipline selector - div( - list{Attrs.class_("p-3 bg-gray-900 border border-gray-700 rounded-lg")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wide mb-2")}, - list{text("Default Discipline")}, - ), - div( - list{Attrs.class_("flex flex-wrap gap-2")}, - TypeLLEngine.allDisciplines - ->Array.map(d => { - let isActive = d === tl.defaultDiscipline - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded transition-colors ${isActive - ? TypeLLEngine.disciplineColour(d) ++ " font-medium" - : "text-gray-500 hover:text-gray-300 bg-gray-800"}`, - ), - Events.onClick(TypeLL(SetDefaultDiscipline(d))), - }, - list{text(TypeLLEngine.disciplineDirective(d))}, - ) - }) - ->List.fromArray, - ), - }, - ), - // Active declarations - div( - list{Attrs.class_("p-3 bg-gray-900 border border-gray-700 rounded-lg")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wide mb-2")}, - list{ - text( - "Module Declarations (" ++ - Int.toString(Array.length(tl.disciplineDeclarations)) ++ ")", - ), - }, - ), - if Array.length(tl.disciplineDeclarations) === 0 { - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{ - text( - "No module-level discipline declarations yet. Modules inherit the default.", - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - tl.disciplineDeclarations - ->Array.map(decl => - div( - list{ - Attrs.class_( - "flex items-center justify-between px-2 py-1 bg-gray-800/40 rounded text-xs", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-300 font-mono")}, - list{text(decl.scope)}, - ), - span( - list{ - Attrs.class_( - TypeLLEngine.disciplineColour( - decl.discipline, - ) ++ " px-2 py-0.5 rounded", - ), - }, - list{text(TypeLLEngine.disciplineDirective(decl.discipline))}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ), - // QTT quantifier reference - div( - list{Attrs.class_("p-3 bg-gray-900 border border-gray-700 rounded-lg")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wide mb-2")}, - list{text("QTT Usage Quantifiers")}, - ), - div( - list{Attrs.class_("grid grid-cols-3 gap-3")}, - list{ - div( - list{Attrs.class_("text-center p-2 bg-gray-800/40 rounded")}, - list{ - div( - list{Attrs.class_("text-lg font-mono text-purple-400")}, - list{text("0")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Erased at runtime")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("Proof-only witness")}, - ), - }, - ), - div( - list{Attrs.class_("text-center p-2 bg-gray-800/40 rounded")}, - list{ - div( - list{Attrs.class_("text-lg font-mono text-red-400")}, - list{text("1")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Exactly once")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("Linear consumption")}, - ), - }, - ), - div( - list{Attrs.class_("text-center p-2 bg-gray-800/40 rounded")}, - list{ - div( - list{Attrs.class_("text-lg font-mono text-green-400")}, - list{text("w")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-500")}, - list{text("Unrestricted")}, - ), - div( - list{Attrs.class_("text-[10px] text-gray-600")}, - list{text("Standard FP")}, - ), - }, - ), - }, - ), - }, - ), - // Unified analysis result (if available) - switch tl.lastUnifiedAnalysis { - | Some(analysis) => - div( - list{Attrs.class_("p-3 bg-gray-900 border border-gray-700 rounded-lg")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wide mb-2")}, - list{text("Last Unified Analysis")}, - ), - div( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(TypeLLEngine.unifiedAnalysisSummary(analysis))}, - ), - }, - ) - | None => noNode - }, - }, - ) - // ── Guide Tab ── - | TlGuide => - div( - list{Attrs.class_("space-y-8 max-w-3xl")}, - list{ - // Tier 1: Core - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs font-medium rounded ${TypeLLEngine.tierColour( - TierCore, - )}`, - ), - }, - list{text("Tier 1: Core")}, - ), - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{text("Essential type safety")}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3")}, - TypeLLEngine.coreFeatures - ->Array.map(f => { - let glyph = TypeLLEngine.featureGlyph(f) - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("font-mono text-emerald-400")}, - list{text(glyph.symbol)}, - ), - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(TypeLLEngine.featureLabel(f))}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(glyph.meaning)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Tier 2: Advanced - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs font-medium rounded ${TypeLLEngine.tierColour( - TierAdvanced, - )}`, - ), - }, - list{text("Tier 2: Advanced")}, - ), - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{text("Precision resource management")}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3")}, - TypeLLEngine.advancedFeatures - ->Array.map(f => { - let glyph = TypeLLEngine.featureGlyph(f) - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("font-mono text-blue-400")}, - list{text(glyph.symbol)}, - ), - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(TypeLLEngine.featureLabel(f))}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(glyph.meaning)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Tier 3: Research - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs font-medium rounded ${TypeLLEngine.tierColour( - TierResearch, - )}`, - ), - }, - list{text("Tier 3: Research")}, - ), - span( - list{Attrs.class_("text-sm text-gray-400")}, - list{text("Frontier type theory")}, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3")}, - TypeLLEngine.researchFeatures - ->Array.map(f => { - let glyph = TypeLLEngine.featureGlyph(f) - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2 mb-1")}, - list{ - span( - list{Attrs.class_("font-mono text-purple-400")}, - list{text(glyph.symbol)}, - ), - span( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text(TypeLLEngine.featureLabel(f))}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(glyph.meaning)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - // Cross-panel integration table - div( - list{Attrs.class_("space-y-3")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-200")}, - list{text("Cross-Panel Integration")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{ - text( - "TypeLL provides type intelligence to every panel, not just this one.", - ), - }, - ), - div( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded-lg overflow-hidden", - ), - }, - list{ - div( - list{Attrs.class_("divide-y divide-gray-800")}, - list{ - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-40 text-cyan-400")}, - list{text("VeriSimDB")}, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text("VCL-total 10-level type safety (supersedes VCL-DT)")}, - ), - }, - ), - div( - list{ - Attrs.class_("p-2 text-xs bg-gray-950 border-l-2 border-cyan-700"), - }, - list{ - div( - list{Attrs.class_("text-cyan-500 font-bold mb-1")}, - list{text("VCL-total Safety Levels")}, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-1 text-gray-500")}, - list{ - span(list{}, list{text("L1 Parse-time")}), - span(list{}, list{text("L2 Schema-binding")}), - span(list{}, list{text("L3 Type-compatible")}), - span(list{}, list{text("L4 Null-safety")}), - span(list{}, list{text("L5 Injection-proof")}), - span(list{}, list{text("L6 Result-type")}), - span( - list{Attrs.class_("text-amber-500")}, - list{text("L7 Cardinality")}, - ), - span( - list{Attrs.class_("text-amber-500")}, - list{text("L8 Effect-tracking")}, - ), - span( - list{Attrs.class_("text-amber-500")}, - list{text("L9 Temporal")}, - ), - span( - list{Attrs.class_("text-amber-500")}, - list{text("L10 Linearity")}, - ), - }, - ), - div( - list{Attrs.class_("mt-1 text-gray-600 italic")}, - list{text("Amber = research-identified (Idris2 verified)")}, - ), - }, - ), - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-40 text-cyan-400")}, - list{text("Protocol-Squisher")}, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text("Schema type compatibility, adapter type safety")}, - ), - }, - ), - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-40 text-cyan-400")}, - list{text("My-Lang")}, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text("Full type checking across Solo/Duet/Ensemble/Me")}, - ), - }, - ), - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-40 text-cyan-400")}, - list{text("Anti-Crash")}, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{text("Type-level validation before token acceptance")}, - ), - }, - ), - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-40 text-cyan-400")}, - list{text("Pane-L")}, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{ - text("Type constraints as first-class symbolic constraints"), - }, - ), - }, - ), - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span(list{Attrs.class_("w-40 text-cyan-400")}, list{text("BoJ")}), - span( - list{Attrs.class_("text-gray-400")}, - list{text("Cartridge ABI type checking (Idris2 formal specs)")}, - ), - }, - ), - div( - list{Attrs.class_("flex p-2 text-xs")}, - list{ - span( - list{Attrs.class_("w-40 text-cyan-400")}, - list{text("ECHIDNA")}, - ), - span( - list{Attrs.class_("text-gray-400")}, - list{ - text( - "Proof obligation dispatch — TypeLL generates, ECHIDNA proves", - ), - }, - ), - }, - ), - }, - ), - }, - ), - }, - ), - }, - ) - }, - // Loading/error - if tl.loading { - div( - list{Attrs.class_("mt-4 text-gray-400 text-sm"), Attrs.role("status")}, - list{text("Processing...")}, - ) - } else { - noNode - }, - switch tl.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/TypingBridge.affine b/src/components/TypingBridge.affine new file mode 100644 index 00000000..71a142ce --- /dev/null +++ b/src/components/TypingBridge.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module TypingBridge; + +// TODO: Complete semantic implementation diff --git a/src/components/TypingBridge.res b/src/components/TypingBridge.res deleted file mode 100644 index c9d05cc4..00000000 --- a/src/components/TypingBridge.res +++ /dev/null @@ -1,360 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Typing Bridge Component — TypeLL type constraints for IDApTIK game -/// state. Displays constraint lists, inference results, a typed configuration -/// editor, and diagnostic messages. - -open Model -open Msg -open Tea.Html - -/// Render a severity badge for a type constraint. -let severityBadge = (sev: constraintSeverity): Tea_Vdom.t => { - let (color, label) = switch sev { - | ConstraintError => ("bg-red-700 text-red-100", "Error") - | ConstraintWarning => ("bg-yellow-700 text-yellow-100", "Warn") - | ConstraintInfo => ("bg-blue-700 text-blue-100", "Info") - } - span(list{Attrs.class_("px-2 py-0.5 text-xs rounded font-mono " ++ color)}, list{text(label)}) -} - -/// Render an inference status indicator. -let inferenceStatusBadge = (status: inferenceStatus): Tea_Vdom.t => { - let (color, label) = switch status { - | InferenceSuccess => ("text-green-400", "OK") - | InferencePartial => ("text-yellow-400", "Partial") - | InferenceFailed => ("text-red-400", "Failed") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Main view function for the Typing Bridge panel. -let view = (state: typingBridgeState): Tea_Vdom.t => { - let satisfiedCount = state.constraints->Array.filter(c => c.satisfied)->Array.length - let totalCount = Array.length(state.constraints) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Typing Bridge — TypeLL Type Constraints"), - }, - list{ - // Header row - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-cyan-300")}, - list{text("Typing Bridge")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - Int.toString(satisfiedCount) ++ "/" ++ Int.toString(totalCount) ++ " satisfied", - ), - }, - ), - if state.running { - span( - list{Attrs.class_("text-xs text-yellow-400 animate-pulse")}, - list{text("Checking...")}, - ) - } else { - Tea_Html.noNode - }, - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs bg-cyan-800 hover:bg-cyan-700 text-white rounded"), - Events.onClick(TypingBridge(TbStarted)), - KeyboardNav.onActivate(TypingBridge(TbStarted)), - }, - list{text("Run Type Check")}, - ), - }, - ), - // Tab bar - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Constraints { - "bg-cyan-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(TypingBridge(SetTbTab(Constraints))), - }, - list{text("Constraints")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Inference { - "bg-cyan-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(TypingBridge(SetTbTab(Inference))), - }, - list{text("Inference")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Editor { - "bg-cyan-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(TypingBridge(SetTbTab(Editor))), - }, - list{text("Editor")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if state.activeTab == Diagnostics { - "bg-cyan-700 text-white" - } else { - "bg-gray-800 text-gray-400 hover:text-gray-200" - }, - ), - Events.onClick(TypingBridge(SetTbTab(Diagnostics))), - }, - list{text("Diagnostics")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200 flex justify-between items-center", - ), - }, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 text-xs ml-2"), - Events.onClick(TypingBridge(DismissTbError)), - KeyboardNav.onActivate(TypingBridge(DismissTbError)), - }, - list{text("Dismiss")}, - ), - }, - ) - | None => Tea_Html.noNode - }, - // Content area - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-4")}, - list{ - switch state.activeTab { - | Constraints => - div( - list{}, - state.constraints - ->Array.map(c => - div( - list{Attrs.class_("flex items-center gap-3 py-2 border-b border-gray-800/50")}, - list{ - span( - list{ - Attrs.class_( - "w-3 h-3 rounded-full " ++ if c.satisfied { - "bg-green-500" - } else { - "bg-red-500" - }, - ), - }, - list{}, - ), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm font-mono text-gray-200")}, - list{text(c.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(c.targetPath ++ " : " ++ c.typeExpression)}, - ), - }, - ), - severityBadge(c.severity), - }, - ) - ) - ->List.fromArray, - ) - | Inference => - div( - list{}, - state.inferenceResults - ->Array.map(r => - div( - list{Attrs.class_("py-2 border-b border-gray-800/50")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-200")}, - list{text(r.targetPath)}, - ), - inferenceStatusBadge(r.status), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(Float.toFixed(r.inferenceTimeMs, ~digits=1) ++ "ms")}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-cyan-400 font-mono mt-1")}, - list{text(r.inferredType)}, - ), - if Array.length(r.suggestions) > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mt-1")}, - r.suggestions - ->Array.map(s => - span( - list{ - Attrs.class_("px-2 py-0.5 text-xs bg-gray-800 text-gray-400 rounded"), - }, - list{text(s)}, - ) - ) - ->List.fromArray, - ) - } else { - Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ) - | Editor => - div( - list{}, - state.configFields - ->Array.map(f => - div( - list{Attrs.class_("flex items-center gap-3 py-2 border-b border-gray-800/50")}, - list{ - span( - list{ - Attrs.class_( - "w-2 h-2 rounded-full " ++ if f.valid { - "bg-green-500" - } else { - "bg-red-500" - }, - ), - }, - list{}, - ), - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - div( - list{Attrs.class_("text-sm font-mono text-gray-300")}, - list{text(f.path)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Expected: " ++ f.expectedType)}, - ), - }, - ), - span( - list{Attrs.class_("text-sm font-mono text-cyan-300")}, - list{text(f.currentValue)}, - ), - switch f.validationMessage { - | Some(msg_text) => - span(list{Attrs.class_("text-xs text-red-400")}, list{text(msg_text)}) - | None => Tea_Html.noNode - }, - }, - ) - ) - ->List.fromArray, - ) - | Diagnostics => - div( - list{}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-3")}, - list{ - text( - Int.toString(totalCount) ++ - " constraints, " ++ - Int.toString(satisfiedCount) ++ - " satisfied, " ++ - Int.toString(totalCount - satisfiedCount) ++ " violations", - ), - }, - ), - div( - list{}, - state.constraints - ->Array.filter(c => !c.satisfied) - ->Array.map(c => - div( - list{ - Attrs.class_( - "px-3 py-2 mb-2 bg-red-900/30 border border-red-800/50 rounded", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - severityBadge(c.severity), - span( - list{Attrs.class_("text-sm text-red-200 font-mono")}, - list{text(c.name)}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400 mt-1")}, - list{text(c.description)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text(c.targetPath ++ " : " ++ c.typeExpression)}, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ), - }, - ) -} diff --git a/src/components/Ums.affine b/src/components/Ums.affine new file mode 100644 index 00000000..1c28a472 --- /dev/null +++ b/src/components/Ums.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Ums; + +// TODO: Complete semantic implementation diff --git a/src/components/Ums.res b/src/components/Ums.res deleted file mode 100644 index f59152c9..00000000 --- a/src/components/Ums.res +++ /dev/null @@ -1,862 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Universal Modding Studio Component — view for the unified hub -/// orchestrating IDApTIK game content creation. Project browser, ABI -/// validator, template gallery, asset pipeline, distribution manager, -/// and modding API reference. - -open Model -open Msg -open Tea.Html - -/// Render a category tab button for the UMS panel. -let renderTab = (label: string, cat: umsCategory, active: umsCategory): Tea_Vdom.t => { - let isActive = cat === active - let cls = isActive - ? "px-3 py-1.5 text-xs font-medium bg-gray-700 text-white rounded" - : "px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 rounded cursor-pointer" - button(list{Attrs.class_(cls), Events.onClick(Ums(SetUmsCategory(cat)))}, list{text(label)}) -} - -/// Render a project card with name, stats, and validation badge. -let renderProjectCard = (project: modProject, isSelected: bool): Tea_Vdom.t => { - let borderCls = if isSelected { - "border-cyan-400" - } else { - "border-gray-700" - } - let validBadge = if project.validated { - "text-emerald-400" - } else { - "text-gray-500" - } - div( - list{ - Attrs.class_( - `p-3 bg-gray-800 rounded border ${borderCls} cursor-pointer hover:border-gray-500`, - ), - Events.onClick(Ums(SelectProject(project.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-100")}, list{text(project.name)}), - span( - list{Attrs.class_(`text-xs ${validBadge}`)}, - list{ - text( - if project.validated { - "Validated" - } else { - "Unvalidated" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400 mb-2 line-clamp-2")}, - list{text(project.description)}, - ), - div( - list{Attrs.class_("flex items-center gap-3 text-xs")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text(`v${project.version}`)}), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(project.levelCount)} levels`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(project.puzzleCount)} puzzles`)}, - ), - span( - list{Attrs.class_("text-gray-500")}, - list{text(`${Int.toString(project.assetCount)} assets`)}, - ), - }, - ), - }, - ) -} - -/// Render the projects list view with filter, stats, and project grid. -let renderProjects = (state: umsState): Tea_Vdom.t => { - let filtered = UmsEngine.filterProjects(state.projects, state.filterText) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter bar - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Filter projects..."), - Attrs.value(state.filterText), - Events.onInput(text => Ums(SetUmsFilter(text))), - }, - list{}, - ), - }, - ), - // Stats - div( - list{Attrs.class_("flex items-center gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text(`${Int.toString(Array.length(filtered))} projects`)}), - span( - list{}, - list{ - text(`${Int.toString(UmsEngine.validatedProjectCount(state.projects))} validated`), - }, - ), - }, - ), - // Project cards - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No projects found. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(Ums(CreateProject(""))), - }, - list{text("Create a new project")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-3")}, - filtered - ->Array.map(p => renderProjectCard(p, state.selectedProjectId === Some(p.id))) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render a single ABI validation result row. -let renderValidationRow = (result: abiValidationResult): Tea_Vdom.t => { - let statusCls = UmsEngine.validationStatusColour(result) - let proofItem = (label: string, passed: bool) => { - let cls = if passed { - "text-emerald-400" - } else { - "text-red-400" - } - span(list{Attrs.class_(`text-xs ${cls}`)}, list{text(label)}) - } - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-2")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text(`Level: ${result.levelId}`)}), - span( - list{Attrs.class_(`text-xs ${statusCls}`)}, - list{text(UmsEngine.validationStatusLabel(result))}, - ), - }, - ), - div( - list{Attrs.class_("flex flex-wrap gap-3")}, - list{ - proofItem("Guards-in-Zones", result.guardsInZones), - proofItem("Defence-Targets", result.defenceTargetsValid), - proofItem("Zones-Ordered", result.zonesOrdered), - proofItem("PBX-Consistent", result.pbxConsistent), - proofItem("Devices-Exist", result.devicesExist), - }, - ), - if Array.length(result.errors) > 0 { - div( - list{Attrs.class_("mt-2 space-y-1")}, - result.errors - ->Array.map(err => div(list{Attrs.class_("text-xs text-red-300")}, list{text(err)})) - ->List.fromArray, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the ABI validator view with results and "Validate All" button. -let renderAbiValidator = (state: umsState): Tea_Vdom.t => { - let passedCount = state.validationResults->Array.filter(r => r.allPassed)->Array.length - let totalCount = Array.length(state.validationResults) - div( - list{Attrs.class_("space-y-3")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(Ums(ValidateAll)), - KeyboardNav.onActivate(Ums(ValidateAll)), - }, - list{text("Validate All Levels")}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${Int.toString(passedCount)}/${Int.toString(totalCount)} levels pass all proofs`, - ), - }, - ), - }, - ), - // Validation results - if totalCount === 0 { - div( - list{ - Attrs.class_( - "text-center text-gray-500 text-sm py-8 border border-dashed border-gray-700 rounded", - ), - }, - list{text("No validation results — click 'Validate All Levels' to run ABI proofs")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.validationResults - ->Array.map(renderValidationRow) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render a template card in the template gallery. -let renderTemplateCard = (tmpl: modTemplate): Tea_Vdom.t => { - let catCls = UmsEngine.templateCategoryColour(tmpl.category) - div( - list{ - Attrs.class_( - "p-3 bg-gray-800 rounded border border-gray-700 hover:border-gray-500 cursor-pointer", - ), - Events.onClick(Ums(InstantiateTemplate(tmpl.id))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-100")}, list{text(tmpl.name)}), - span( - list{Attrs.class_(`text-xs ${catCls}`)}, - list{text(UmsEngine.templateCategoryLabel(tmpl.category))}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-400 mb-2 line-clamp-2")}, - list{text(tmpl.description)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`Difficulty: ${tmpl.difficulty}`)}, - ), - }, - ) -} - -/// Render the templates browser with category filter. -let renderTemplates = (state: umsState): Tea_Vdom.t => { - let filtered = UmsEngine.filterTemplates(state.templates, state.filterText) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter bar - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Filter templates..."), - Attrs.value(state.filterText), - Events.onInput(text => Ums(SetUmsFilter(text))), - }, - list{}, - ), - }, - ), - // Stats - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(Array.length(filtered))} templates available`)}, - ), - // Template cards - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No templates found. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(Ums(LoadTemplates)), - KeyboardNav.onActivate(Ums(LoadTemplates)), - }, - list{text("Load templates")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-2 lg:grid-cols-3 gap-3")}, - filtered->Array.map(renderTemplateCard)->List.fromArray, - ) - }, - }, - ) -} - -/// Render the asset grid with type filter and size stats. -let renderAssets = (state: umsState): Tea_Vdom.t => { - let filtered = UmsEngine.filterAssets(state.assets, state.filterText) - let totalSize = filtered->Array.reduce(0, (acc, a) => acc + a.sizeBytes) - div( - list{Attrs.class_("space-y-3")}, - list{ - // Filter bar and import button - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Filter assets..."), - Attrs.value(state.filterText), - Events.onInput(text => Ums(SetUmsFilter(text))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(Ums(ImportAsset(""))), - }, - list{text("Import Asset")}, - ), - }, - ), - // Stats with type counts - div( - list{Attrs.class_("flex items-center gap-4 text-xs text-gray-400")}, - list{ - span(list{}, list{text(`${Int.toString(Array.length(filtered))} assets`)}), - span(list{}, list{text(`${Int.toString(totalSize / 1024)}KB total`)}), - ...UmsEngine.allAssetTypes - ->Array.map(at => { - let count = UmsEngine.countByAssetType(state.assets, at) - if count > 0 { - span( - list{Attrs.class_(UmsEngine.assetTypeColour(at))}, - list{text(`${Int.toString(count)} ${UmsEngine.assetTypeLabel(at)}`)}, - ) - } else { - noNode - } - }) - ->List.fromArray, - }, - ), - // Asset grid - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No assets loaded. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(Ums(LoadAssets)), - KeyboardNav.onActivate(Ums(LoadAssets)), - }, - list{text("Load assets")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("grid grid-cols-3 lg:grid-cols-4 gap-2")}, - filtered - ->Array.map(asset => { - let typeCls = UmsEngine.assetTypeColour(asset.assetType) - div( - list{Attrs.class_("p-2 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-100 font-medium truncate")}, - list{text(asset.name)}, - ), - div( - list{Attrs.class_(`text-xs ${typeCls}`)}, - list{text(UmsEngine.assetTypeLabel(asset.assetType))}, - ), - div( - list{Attrs.class_("text-xs text-gray-600")}, - list{text(`${Int.toString(asset.sizeBytes / 1024)}KB`)}, - ), - if Array.length(asset.usedIn) > 0 { - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{text(`Used in ${Int.toString(Array.length(asset.usedIn))} places`)}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render a distribution target row. -let renderDistributionTarget = (target: distributionTarget): Tea_Vdom.t => { - let platCls = UmsEngine.platformColour(target.platform) - div( - list{Attrs.class_("flex items-center gap-3 p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - span( - list{Attrs.class_(`text-sm font-medium ${platCls}`)}, - list{text(UmsEngine.platformLabel(target.platform))}, - ), - span(list{Attrs.class_("text-xs text-gray-400 flex-1 truncate")}, list{text(target.url)}), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(`v${target.version}`)}), - if target.lastPublished !== "" { - span(list{Attrs.class_("text-xs text-gray-600")}, list{text(target.lastPublished)}) - } else { - span(list{Attrs.class_("text-xs text-gray-600")}, list{text("Never published")}) - }, - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-purple-700 text-white rounded hover:bg-purple-600 cursor-pointer", - ), - Events.onClick(Ums(PublishMod)), - KeyboardNav.onActivate(Ums(PublishMod)), - }, - list{text("Publish")}, - ), - }, - ) -} - -/// Render the distribution view with publish targets and version. -let renderDistribution = (state: umsState): Tea_Vdom.t => { - div( - list{Attrs.class_("space-y-3")}, - list{ - // Header with selected project info - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200")}, list{text("Distribution Targets")}), - switch state.selectedProjectId { - | Some(id) => - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(`Project: ${id}`)}) - | None => - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("No project selected")}) - }, - }, - ), - // Targets - if Array.length(state.distributionTargets) === 0 { - div( - list{ - Attrs.class_( - "text-center text-gray-500 text-sm py-8 border border-dashed border-gray-700 rounded", - ), - }, - list{text("No distribution targets configured")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.distributionTargets->Array.map(renderDistributionTarget)->List.fromArray, - ) - }, - }, - ) -} - -/// Render the searchable API reference documentation. -let renderApiReference = (state: umsState): Tea_Vdom.t => { - let filtered = if state.filterText === "" { - state.apiEntries - } else { - let lower = String.toLowerCase(state.filterText) - state.apiEntries->Array.filter(e => - String.includes(String.toLowerCase(e.name), lower) || - String.includes(String.toLowerCase(e.description), lower) || - String.includes(String.toLowerCase(e.category), lower) - ) - } - div( - list{Attrs.class_("space-y-3")}, - list{ - // Search bar - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - input( - list{ - Attrs.class_( - "flex-1 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded text-sm text-gray-200 placeholder-gray-500", - ), - Attrs.placeholder("Search API reference..."), - Attrs.value(state.filterText), - Events.onInput(text => Ums(SetUmsFilter(text))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer", - ), - Events.onClick(Ums(LoadApiReference)), - KeyboardNav.onActivate(Ums(LoadApiReference)), - }, - list{text("Refresh")}, - ), - }, - ), - // Stats - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(`${Int.toString(Array.length(filtered))} API entries`)}, - ), - // API entries - if Array.length(filtered) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 text-sm py-8")}, - list{ - text("No API entries found. "), - button( - list{ - Attrs.class_("text-cyan-400 hover:text-cyan-300 underline cursor-pointer"), - Events.onClick(Ums(LoadApiReference)), - KeyboardNav.onActivate(Ums(LoadApiReference)), - }, - list{text("Load API reference")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - filtered - ->Array.map(entry => - div( - list{Attrs.class_("p-3 bg-gray-800 rounded border border-gray-700")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-sm font-medium text-cyan-400 font-mono")}, - list{text(entry.name)}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(entry.category)}), - }, - ), - div( - list{Attrs.class_("text-xs text-amber-400 font-mono mb-1")}, - list{text(entry.signature)}, - ), - div( - list{Attrs.class_("text-xs text-gray-400 mb-2")}, - list{text(entry.description)}, - ), - if entry.example !== "" { - div( - list{Attrs.class_("p-2 bg-gray-900 rounded text-xs text-gray-300 font-mono")}, - list{text(entry.example)}, - ) - } else { - noNode - }, - div( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{text(`Since: ${entry.since}`)}, - ), - }, - ) - ) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Main view function for the Universal Modding Studio panel. -/// Render a Level Architect summary card showing entity count and validation status. -let renderLevelArchitectSummary = (la: levelArchitectState): Tea_Vdom.t => { - let entityCount = Array.length(la.entities) - let validationBadge = switch la.umsValidation { - | Some(v) if v.allPassed => - span(list{Attrs.class_("text-xs text-emerald-400")}, list{text("ABI: ALL PASSED")}) - | Some(_) => span(list{Attrs.class_("text-xs text-red-400")}, list{text("ABI: HAS FAILURES")}) - | None => span(list{Attrs.class_("text-xs text-gray-500")}, list{text("ABI: Not validated")}) - } - div( - list{Attrs.class_("p-3 bg-gray-800/50 rounded border border-gray-700/50")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-1")}, - list{ - span( - list{Attrs.class_("text-xs font-medium text-gray-300")}, - list{text("Level Architect")}, - ), - button( - list{ - Attrs.class_("text-xs text-cyan-400 hover:text-cyan-300 cursor-pointer"), - Events.onClick(Ums(NavigateToPanel(PanelLevelArchitect))), - }, - list{text("Open")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3 text-xs")}, - list{ - span( - list{Attrs.class_("text-gray-400")}, - list{text(`${Int.toString(entityCount)} entities`)}, - ), - span(list{Attrs.class_("text-gray-400")}, list{text(`${la.levelName}`)}), - validationBadge, - }, - ), - }, - ) -} - -/// Main view function for the Universal Modding Studio panel. -/// Accepts both UMS state and Level Architect state for cross-panel data display. -let view = (state: umsState, ~levelArchitect: levelArchitectState): Tea_Vdom.t => { - /// Selected project name for the header subtitle. - let projectName = switch state.selectedProjectId { - | Some(id) => - state.projects - ->Array.find(p => p.id === id) - ->Option.map(p => p.name) - ->Option.getOr("Unknown Project") - | None => "No Project Selected" - } - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Universal Modding Studio panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{Attrs.class_("text-lg font-semibold text-gray-100")}, - list{text("Universal Modding Studio")}, - ), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(projectName)}), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer", - ), - Events.onClick(Ums(CreateProject(""))), - }, - list{text("New Project")}, - ), - button( - list{ - Attrs.class_( - if state.bojRouting { - "px-2 py-1 text-xs bg-cyan-700 text-white rounded hover:bg-cyan-600 cursor-pointer" - } else { - "px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600 cursor-pointer" - }, - ), - Events.onClick(Ums(ToggleUmsBojRouting)), - KeyboardNav.onActivate(Ums(ToggleUmsBojRouting)), - }, - list{text("BoJ Routing")}, - ), - }, - ), - }, - ), - // Category tabs - div( - list{Attrs.class_("flex items-center gap-1 px-4 py-2 border-b border-gray-800")}, - list{ - renderTab("Projects", UmsProjects, state.activeCategory), - renderTab("ABI Validator", UmsAbiValidator, state.activeCategory), - renderTab("Templates", UmsTemplates, state.activeCategory), - renderTab("Assets", UmsAssets, state.activeCategory), - renderTab("Distribution", UmsDistribution, state.activeCategory), - renderTab("API Reference", UmsApiReference, state.activeCategory), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 p-2 bg-red-900/50 border border-red-700 rounded text-xs text-red-300", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - text(err), - button( - list{ - Attrs.class_("text-red-400 hover:text-red-200 cursor-pointer"), - Events.onClick(Ums(DismissUmsError)), - KeyboardNav.onActivate(Ums(DismissUmsError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Loading - if state.loading { - div( - list{Attrs.class_("px-4 py-2 text-xs text-cyan-400 animate-pulse")}, - list{text("Loading UMS data...")}, - ) - } else { - noNode - }, - // Level Architect cross-panel summary - div( - list{Attrs.class_("px-4 py-2 border-b border-gray-800/50")}, - list{renderLevelArchitectSummary(levelArchitect)}, - ), - // Cross-panel navigation — quick-launch related eNSAID panels - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-2 border-b border-gray-800/50")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500 mr-1")}, list{text("Open:")}), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-cyan-400 rounded hover:bg-gray-700 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelLevelArchitect))), - }, - list{text("Level Architect")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-cyan-400 rounded hover:bg-gray-700 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelDlcWorkshop))), - }, - list{text("DLC Workshop")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-cyan-400 rounded hover:bg-gray-700 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelGamePreview))), - }, - list{text("Game Preview")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-cyan-400 rounded hover:bg-gray-700 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelVmInspector))), - }, - list{text("VM Inspector")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-cyan-400 rounded hover:bg-gray-700 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelBuildDashboard))), - }, - list{text("Build Dashboard")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-cyan-400 rounded hover:bg-gray-700 cursor-pointer", - ), - Events.onClick(Ums(NavigateToPanel(PanelReleaseManager))), - }, - list{text("Release Manager")}, - ), - }, - ), - // Main content - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch state.activeCategory { - | UmsProjects => renderProjects(state) - | UmsAbiValidator => renderAbiValidator(state) - | UmsTemplates => renderTemplates(state) - | UmsAssets => renderAssets(state) - | UmsDistribution => renderDistribution(state) - | UmsApiReference => renderApiReference(state) - }, - }, - ), - }, - ) -} diff --git a/src/components/UnitTestRunner.affine b/src/components/UnitTestRunner.affine new file mode 100644 index 00000000..341c9533 --- /dev/null +++ b/src/components/UnitTestRunner.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module UnitTestRunner; + +// TODO: Complete semantic implementation diff --git a/src/components/UnitTestRunner.res b/src/components/UnitTestRunner.res deleted file mode 100644 index 276d599e..00000000 --- a/src/components/UnitTestRunner.res +++ /dev/null @@ -1,392 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL UnitTestRunner — interactive test execution dashboard with coverage -/// heatmaps, run history, and diff-aware filtering for IDApTIK game testing. -/// -/// Renders four tabs: test results (pass/fail list with durations), module -/// coverage (percentage bars with heatmap colouring), run history (summary -/// cards), and diff-aware mode (only changed-module tests). The Run/Stop -/// header buttons dispatch lifecycle messages and a spinner overlays when -/// tests are in flight. - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Tab label lookup for unitTestTab variants. -let tabLabel = (tab: unitTestTab): string => - switch tab { - | TabTestResults => "Results" - | TabCoverage => "Coverage" - | TabHistory => "History" - | TabDiffAware => "Diff-Aware" - } - -/// Render the tab bar. Active tab gets a cyan bottom border; others are -/// ghost buttons with hover highlight. -let renderTabs = (active: unitTestTab): Tea_Vdom.t => { - let tabs: array = [TabTestResults, TabCoverage, TabHistory, TabDiffAware] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-cyan-400 border-b-2 border-cyan-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900 cursor-pointer"}`, - ), - Events.onClick(UnitTestRunner(SetUtrTab(tab))), - }, - list{text(tabLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Status icon for a single test case result. -let statusIcon = (status: testCaseStatus): Tea_Vdom.t => - switch status { - | TestPending => span(list{Attrs.class_("text-gray-500 text-xs font-mono")}, list{text("--")}) - | TestRunning => - span(list{Attrs.class_("text-amber-400 text-xs font-mono animate-pulse")}, list{text("...")}) - | TestPassed(_) => - span(list{Attrs.class_("text-emerald-400 text-xs font-mono")}, list{text("OK")}) - | TestFailed(_, _) => - span(list{Attrs.class_("text-red-400 text-xs font-mono")}, list{text("FAIL")}) - | TestSkipped(_) => - span(list{Attrs.class_("text-blue-400 text-xs font-mono")}, list{text("SKIP")}) - } - -/// Duration display (right-aligned, dimmed). -let durationDisplay = (status: testCaseStatus): Tea_Vdom.t => - switch status { - | TestPassed(ms) => - span( - list{Attrs.class_("text-gray-500 text-xs font-mono")}, - list{text(`${Float.toString(ms)}ms`)}, - ) - | TestFailed(_, ms) => - span( - list{Attrs.class_("text-gray-500 text-xs font-mono")}, - list{text(`${Float.toString(ms)}ms`)}, - ) - | _ => noNode - } - -// ========================================================================= -// Tab content views -// ========================================================================= - -/// Test results tab: scrollable list of test cases with pass/fail icons, -/// suite grouping, test name, and duration. -let renderResultsTab = (state: unitTestRunnerState): Tea_Vdom.t => { - let count = Array.length(state.results) - let passed = - state.results - ->Array.filter(r => - switch r.status { - | TestPassed(_) => true - | _ => false - } - ) - ->Array.length - let failed = - state.results - ->Array.filter(r => - switch r.status { - | TestFailed(_, _) => true - | _ => false - } - ) - ->Array.length - - div( - list{Attrs.class_("flex flex-col gap-3 p-4")}, - list{ - // Summary counts - div( - list{Attrs.class_("flex gap-4 text-sm")}, - list{ - span(list{Attrs.class_("text-gray-400")}, list{text(`Total: ${Int.toString(count)}`)}), - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`Passed: ${Int.toString(passed)}`)}, - ), - span(list{Attrs.class_("text-red-400")}, list{text(`Failed: ${Int.toString(failed)}`)}), - }, - ), - // Result rows - div( - list{Attrs.class_("flex flex-col gap-1 max-h-96 overflow-y-auto")}, - state.results - ->Array.map(result => { - div( - list{ - Attrs.class_( - "flex items-center justify-between gap-3 px-3 py-2 bg-gray-800 rounded text-sm", - ), - }, - list{ - statusIcon(result.status), - span( - list{Attrs.class_("text-gray-500 font-mono text-xs min-w-24")}, - list{text(result.suiteName)}, - ), - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(result.testName)}), - durationDisplay(result.status), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Coverage tab: module heatmap with percentage bars. Colour graduates from -/// red (< 40%) through amber (40-70%) to emerald (> 70%). -let renderCoverageTab = (state: unitTestRunnerState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-1")}, - list{text("Module Coverage Heatmap")}, - ), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-96 overflow-y-auto")}, - state.coverage - ->Array.map(mod => { - let pct = mod.coveragePercent - let pctStr = Float.toFixed(pct, ~digits=1) - let barColour = if pct < 40.0 { - "bg-red-500" - } else if pct < 70.0 { - "bg-amber-500" - } else { - "bg-emerald-500" - } - let widthPct = Int.toString(Int.fromFloat(pct)) - div( - list{Attrs.class_("bg-gray-800 rounded p-2")}, - list{ - div( - list{Attrs.class_("flex justify-between text-xs mb-1")}, - list{ - span(list{Attrs.class_("text-gray-300 font-mono")}, list{text(mod.moduleName)}), - span( - list{Attrs.class_("text-gray-500")}, - list{ - text( - `${Int.toString(mod.testedFunctions)}/${Int.toString( - mod.totalFunctions, - )} (${pctStr}%)`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("w-full h-2 bg-gray-700 rounded overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - `h-full ${barColour} transition-all duration-300 w-[${widthPct}%]`, - ), - }, - list{}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// History tab: summary cards for previous test runs. -let renderHistoryTab = (state: unitTestRunnerState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-2 p-4")}, - list{ - h3(list{Attrs.class_("text-sm font-medium text-gray-300 mb-1")}, list{text("Run History")}), - div( - list{Attrs.class_("flex flex-col gap-2 max-h-96 overflow-y-auto")}, - state.history - ->Array.map(run => { - let allPassed = run.failed === 0 - let borderCls = allPassed ? "border-emerald-700" : "border-red-700" - div( - list{Attrs.class_(`bg-gray-800 rounded p-3 border ${borderCls}`)}, - list{ - div( - list{Attrs.class_("flex justify-between text-xs text-gray-400 mb-1")}, - list{ - span(list{}, list{text(run.timestamp)}), - span(list{}, list{text(`${Float.toFixed(run.durationMs, ~digits=0)}ms`)}), - }, - ), - div( - list{Attrs.class_("flex gap-4 text-sm")}, - list{ - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`${Int.toString(run.passed)} passed`)}, - ), - span( - list{Attrs.class_("text-red-400")}, - list{text(`${Int.toString(run.failed)} failed`)}, - ), - span( - list{Attrs.class_("text-blue-400")}, - list{text(`${Int.toString(run.skipped)} skipped`)}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -/// Diff-aware tab: toggle and filtered results for changed modules only. -let renderDiffAwareTab = (state: unitTestRunnerState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex flex-col gap-3 p-4")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium cursor-pointer ${state.diffAwareOnly - ? "bg-cyan-700 text-white" - : "bg-gray-700 text-gray-400"}`, - ), - Events.onClick(UnitTestRunner(ToggleDiffAware)), - KeyboardNav.onActivate(UnitTestRunner(ToggleDiffAware)), - }, - list{text(state.diffAwareOnly ? "Diff-Aware: ON" : "Diff-Aware: OFF")}, - ), - span( - list{Attrs.class_("text-gray-500 text-xs")}, - list{text("Only run tests for modules changed since last commit")}, - ), - }, - ), - div( - list{Attrs.class_("text-sm text-gray-400")}, - list{ - text(`${Int.toString(Array.length(state.results))} test(s) matched by diff-aware filter`), - }, - ), - }, - ) -} - -// ========================================================================= -// Running spinner overlay -// ========================================================================= - -/// Full-width pulsing indicator shown when a test run is in progress. -let runningSpinner = (running: bool): Tea_Vdom.t => { - if running { - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-2 bg-gray-800 border-b border-gray-700")}, - list{ - div(list{Attrs.class_("w-3 h-3 bg-amber-400 rounded-full animate-pulse")}, list{}), - span(list{Attrs.class_("text-sm text-amber-300")}, list{text("Tests running...")}), - }, - ) - } else { - noNode - } -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Primary view function dispatching tab content based on active tab. -let view = (state: unitTestRunnerState): Tea_Vdom.t => { - let content = switch state.activeTab { - | TabTestResults => renderResultsTab(state) - | TabCoverage => renderCoverageTab(state) - | TabHistory => renderHistoryTab(state) - | TabDiffAware => renderDiffAwareTab(state) - } - - div( - list{Attrs.class_("flex flex-col h-full bg-gray-900 text-gray-100")}, - list{ - // Header with title and Run/Stop buttons - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - h2( - list{Attrs.class_("text-lg font-semibold text-cyan-300")}, - list{text("Unit Test Runner")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-emerald-700 text-white rounded hover:bg-emerald-600 cursor-pointer font-medium", - ), - Events.onClick(UnitTestRunner(RunAllTests)), - KeyboardNav.onActivate(UnitTestRunner(RunAllTests)), - }, - list{text("Run")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-red-700 text-white rounded hover:bg-red-600 cursor-pointer font-medium", - ), - Events.onClick(UnitTestRunner(StopTests)), - KeyboardNav.onActivate(UnitTestRunner(StopTests)), - }, - list{text("Stop")}, - ), - }, - ), - }, - ), - // Running indicator - runningSpinner(state.running), - // Error display - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 text-red-300 text-sm border-b border-red-800"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeTab), - // Tab content - div(list{Attrs.class_("flex-1 overflow-y-auto")}, list{content}), - }, - ) -} diff --git a/src/components/Vab.affine b/src/components/Vab.affine new file mode 100644 index 00000000..7fed3815 --- /dev/null +++ b/src/components/Vab.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Vab; + +// TODO: Complete semantic implementation diff --git a/src/components/Vab.res b/src/components/Vab.res deleted file mode 100644 index 3ae4cd5b..00000000 --- a/src/components/Vab.res +++ /dev/null @@ -1,919 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL VAB (Verified Assembly Building) Component. -/// -/// KSP Vehicle Assembly Building-inspired panel for composing verified server -/// components from the proven-servers catalog. The visual design closely mirrors -/// KSP's iconic VAB: green toolbar and category tabs, orange selection accents, -/// industrial steel rack rails, staging indicators, and part stats. -/// -/// Layout: -/// +================================================================+ -/// | [KSP-GREEN TOOLBAR] "Server Name" [Clear] [Launch] [Close] | -/// +==+===========================+=================================+ -/// | | Component Grid | Server Rack (Assembly Area) | -/// |G | +------+ +------+ | ┌─────────────────────────────┐ | -/// |R | | part | | part | | │ ● [1] proven-tls 443 │ | -/// |E | | :443 | | ✓ | | │ ● [2] proven-httpd 80,443 │ | -/// |E | +------+ +------+ | │ ● [3] proven-dbconn 5432 │ | -/// |N | +------+ +------+ | │ · ─ ─ empty slot ─ ─ · │ | -/// | | | part | | part | | │ · ─ ─ empty slot ─ ─ · │ | -/// |C | |:50051| | ✓ | | └─────────────────────────────┘ | -/// |A | +------+ +------+ | | -/// |T | | ⚠ Missing: proven-socket | -/// |S | | ⚠ No audit — ops not logged | -/// +==+===========================+=================================+ -/// | [STATS] RU: 7 Ports: 5 ✓HTTP ✓DB ✗Email ⚠Audit ✗Cache | -/// +================================================================+ -/// -/// Colour scheme: KSP VAB (green #4a7c40 toolbar, orange #e8721c accents, -/// industrial steel #2e2e2e/#3a3a3a, green #5a9e50 verified, red #cc3333 errors). - -open Msg -open Model -open Tea.Html - -// =========================================================================== -// Category Sidebar (KSP-green vertical tab strip) -// =========================================================================== - -/// Render a single category icon button in the KSP-green sidebar. -/// Active tab uses the bright green gradient; inactive uses darker green. -let renderCategoryButton = (cat: vabCategory, isActive: bool): Tea_Vdom.t => { - let activeClass = isActive ? "vab-sidebar-btn-active" : "vab-sidebar-btn" - - button( - list{ - Attrs.class_( - `w-11 h-11 flex items-center justify-center text-xs font-bold rounded ${activeClass}`, - ), - Attrs.style("color", isActive ? "white" : "#8ab580"), - Attrs.title(VabCatalog.categoryName(cat)), - Attrs.ariaLabel(`Select ${VabCatalog.categoryName(cat)} category`), - Attrs.ariaPressed(isActive), - Events.onClick(Vab(SelectCategory(cat))), - }, - list{text(VabCatalog.categoryIcon(cat))}, - ) -} - -/// Render the full vertical category sidebar (11 icons) with KSP green styling. -let renderCategorySidebar = (selectedCategory: vabCategory): Tea_Vdom.t => { - let categories: array = [ - VabCore, - VabNetwork, - VabDns, - VabWeb, - VabIot, - VabEmail, - VabSecurity, - VabData, - VabApplication, - VabInfrastructure, - VabConnectors, - ] - - div( - list{ - Attrs.class_("w-14 vab-sidebar flex flex-col gap-1 p-1.5 overflow-y-auto"), - Attrs.ariaLabel("Component categories"), - Attrs.role("tablist"), - }, - List.concat( - // "PARTS" label at top (like KSP) - list{ - div( - list{Attrs.class_("text-center py-1 mb-1")}, - list{ - span( - list{ - Attrs.class_("text-[8px] font-bold tracking-widest uppercase"), - Attrs.style("color", "#6ab35e"), - }, - list{text("PARTS")}, - ), - }, - ), - }, - categories - ->Array.map(cat => renderCategoryButton(cat, cat === selectedCategory)) - ->List.fromArray, - ), - ) -} - -// =========================================================================== -// Top Toolbar (KSP-green gradient bar) -// =========================================================================== - -/// Render the KSP-green top toolbar with server name and action buttons. -/// Mimics KSP's green gradient toolbar with orange-accented buttons. -let renderTopBar = (server: assembledServer): Tea_Vdom.t => { - let componentCount = Array.length(server.components) - - div( - list{Attrs.class_("flex items-center gap-3 px-4 py-2.5 vab-toolbar")}, - list{ - // VAB icon/title - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_("text-sm font-bold"), - Attrs.style("color", "#b8e6b0"), - Attrs.style("text-shadow", "0 1px 2px rgba(0,0,0,0.4)"), - }, - list{text("VAB")}, - ), - div( - list{ - Attrs.class_("w-px h-5"), - Attrs.style("background-color", "rgba(255,255,255,0.2)"), - }, - list{}, - ), - }, - ), - // Server name input - div( - list{Attrs.class_("flex-1 flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_("text-[10px] font-bold uppercase tracking-wider"), - Attrs.style("color", "#8ab580"), - }, - list{text("Server:")}, - ), - input( - list{ - Attrs.class_( - "rounded px-3 py-1 text-sm text-white w-64 focus:outline-none font-mono", - ), - Attrs.style("background-color", "rgba(0,0,0,0.3)"), - Attrs.style("border", "1px solid rgba(255,255,255,0.15)"), - Attrs.value(server.name), - Attrs.placeholder("Untitled Server"), - Events.onInput(value => Vab(RenameServer(value))), - }, - list{}, - ), - span( - list{ - Attrs.class_("text-[10px] font-mono"), - Attrs.style("color", "rgba(255,255,255,0.4)"), - }, - list{text(`${Int.toString(componentCount)} parts`)}, - ), - }, - ), - // Action buttons (KSP-style chunky buttons) - button( - list{ - Attrs.class_( - "px-3 py-1.5 rounded text-xs font-bold uppercase tracking-wide transition-colors", - ), - Attrs.style("background-color", "rgba(0,0,0,0.25)"), - Attrs.style("border", "1px solid rgba(255,255,255,0.15)"), - Attrs.style("color", "#ccc"), - Events.onClick(Vab(ClearAssembly)), - KeyboardNav.onActivate(Vab(ClearAssembly)), - Attrs.ariaLabel("Clear all components from server"), - }, - list{text("Clear")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1.5 rounded text-xs font-bold uppercase tracking-wide transition-colors", - ), - Attrs.style("background-color", "rgba(232,114,28,0.8)"), - Attrs.style("border", "1px solid #e8721c"), - Attrs.style("color", "white"), - Attrs.style("text-shadow", "0 1px 1px rgba(0,0,0,0.3)"), - Events.onClick(Vab(ToggleVab)), - KeyboardNav.onActivate(Vab(ToggleVab)), - Attrs.ariaLabel("Close VAB panel"), - }, - list{text("Close")}, - ), - }, - ) -} - -// =========================================================================== -// Component Grid (Part Picker) -// =========================================================================== - -/// Render a single part card in the KSP-style component grid. -/// Shows part name, port badges, verified indicator, rack unit size, -/// and dependency count — mimicking KSP's part tooltip. -let renderComponentCard = (comp: vabComponent, isAssembled: bool, isHovered: bool): Tea_Vdom.t< - msg, -> => { - let cardClass = if isAssembled { - "vab-part vab-part-installed" - } else if isHovered { - "vab-part" - } else { - "vab-part" - } - - let hoverBorder = if isHovered && !isAssembled { - "border-color: #e8721c;" - } else { - "" - } - - div( - list{ - Attrs.class_(`${cardClass} p-2.5 cursor-pointer`), - Attrs.style("style", hoverBorder), - Events.onClick( - if isAssembled { - Vab(RemoveComponent(comp.id)) - } else { - Vab(AddComponent(comp.id)) - }, - ), - Events.onMouseEnter(Vab(HoverComponent(Some(comp.id)))), - Events.onMouseLeave(Vab(HoverComponent(None))), - Attrs.title(comp.description), - Attrs.ariaLabel( - if isAssembled { - `Remove ${comp.name} from server` - } else { - `Add ${comp.name} to server` - }, - ), - }, - list{ - // Header: short name + verified checkmark - div( - list{Attrs.class_("flex items-center justify-between mb-1.5")}, - list{ - span( - list{ - Attrs.class_("text-xs font-bold truncate"), - Attrs.style( - "color", - if isAssembled { - "#6ab35e" - } else { - "#ddd" - }, - ), - }, - list{text(comp.shortName)}, - ), - // Green verified tick (KSP science-unlock style) - span(list{Attrs.class_("vab-verified text-xs font-bold")}, list{text("V")}), - }, - ), - // Port badges (orange-tinted) - if Array.length(comp.ports) > 0 { - div( - list{Attrs.class_("flex flex-wrap gap-1 mb-1.5")}, - comp.ports - ->Array.map(port => - span( - list{ - Attrs.class_("text-[9px] px-1.5 py-0.5 rounded font-mono font-bold"), - Attrs.style("background-color", "rgba(232,114,28,0.15)"), - Attrs.style("color", "#e8a050"), - Attrs.style("border", "1px solid rgba(232,114,28,0.25)"), - }, - list{text(Int.toString(port))}, - ) - ) - ->List.fromArray, - ) - } else { - noNode - }, - // Stats row: rack units + dep count (like KSP's mass/cost) - div( - list{Attrs.class_("flex items-center justify-between mt-1")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-[9px] font-mono"), Attrs.style("color", "#888")}, - list{text(`${Int.toString(comp.rackUnits)}U`)}, - ), - if Array.length(comp.dependencies) > 0 { - span( - list{Attrs.class_("text-[9px] font-mono"), Attrs.style("color", "#e8721c")}, - list{text(`${Int.toString(Array.length(comp.dependencies))} deps`)}, - ) - } else { - span( - list{Attrs.class_("text-[9px] font-mono"), Attrs.style("color", "#555")}, - list{text("no deps")}, - ) - }, - }, - ), - // Installed indicator or +add - if isAssembled { - span( - list{Attrs.class_("text-[9px] font-bold"), Attrs.style("color", "#5a9e50")}, - list{text("INSTALLED")}, - ) - } else { - span( - list{Attrs.class_("text-[9px] font-bold"), Attrs.style("color", "#888")}, - list{text("+ADD")}, - ) - }, - }, - ), - }, - ) -} - -/// Render the component grid for the selected category. -/// Filter bar and sort buttons use KSP-green/orange styling. -let renderComponentGrid = ( - catalog: array, - selectedCategory: vabCategory, - sortBy: vabSortBy, - filterText: string, - assembledIds: array, - hoveredComponent: option, -): Tea_Vdom.t => { - // Filter by category - let categoryComponents = VabCatalog.getByCategory(catalog, selectedCategory) - - // Apply text filter - let filtered = if filterText === "" { - categoryComponents - } else { - let lower = String.toLowerCase(filterText) - Array.filter(categoryComponents, c => - String.includes(String.toLowerCase(c.name), lower) || - String.includes(String.toLowerCase(c.shortName), lower) || - String.includes(String.toLowerCase(c.description), lower) - ) - } - - // Apply sort - let sorted = switch sortBy { - | SortByName => - Array.toSorted(filtered, (a, b) => a.name < b.name ? -1.0 : a.name > b.name ? 1.0 : 0.0) - | SortByPorts => - Array.toSorted(filtered, (a, b) => - Float.fromInt(Array.length(a.ports)) -. Float.fromInt(Array.length(b.ports)) - ) - | SortByDeps => - Array.toSorted(filtered, (a, b) => - Float.fromInt(Array.length(a.dependencies)) -. Float.fromInt(Array.length(b.dependencies)) - ) - } - - // Sort button helper - let sortBtn = (label: string, sortVal: vabSortBy) => { - let isActive = sortBy === sortVal - button( - list{ - Attrs.class_( - "px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wide transition-colors", - ), - Attrs.style( - "background-color", - if isActive { - "rgba(232,114,28,0.8)" - } else { - "rgba(255,255,255,0.05)" - }, - ), - Attrs.style( - "color", - if isActive { - "white" - } else { - "#888" - }, - ), - Attrs.style( - "border", - if isActive { - "1px solid #e8721c" - } else { - "1px solid rgba(255,255,255,0.08)" - }, - ), - Events.onClick(Vab(SetSortBy(sortVal))), - }, - list{text(label)}, - ) - } - - div( - list{ - Attrs.class_("flex-1 flex flex-col overflow-hidden"), - Attrs.style("background-color", "#1e1e1e"), - }, - list{ - // Filter bar - div( - list{ - Attrs.class_("flex items-center gap-2 px-3 py-2"), - Attrs.style("background-color", "#252525"), - Attrs.style("border-bottom", "1px solid #333"), - }, - list{ - input( - list{ - Attrs.class_( - "flex-1 rounded px-2 py-1 text-xs text-gray-300 focus:outline-none font-mono", - ), - Attrs.style("background-color", "rgba(0,0,0,0.3)"), - Attrs.style("border", "1px solid #444"), - Attrs.placeholder("Search parts..."), - Attrs.value(filterText), - Events.onInput(value => Vab(SetFilterText(value))), - }, - list{}, - ), - sortBtn("A-Z", SortByName), - sortBtn("Ports", SortByPorts), - sortBtn("Deps", SortByDeps), - }, - ), - // Category header - div( - list{ - Attrs.class_("px-3 py-1.5 flex items-center gap-2"), - Attrs.style("border-bottom", "1px solid #2a2a2a"), - }, - list{ - span( - list{ - Attrs.class_("text-[10px] font-bold uppercase tracking-widest"), - Attrs.style("color", "#6ab35e"), - }, - list{text(VabCatalog.categoryName(selectedCategory))}, - ), - span( - list{Attrs.class_("text-[10px] font-mono"), Attrs.style("color", "#555")}, - list{text(`${Int.toString(Array.length(sorted))} parts`)}, - ), - }, - ), - // Component grid - div( - list{ - Attrs.class_("flex-1 overflow-y-auto px-3 pb-3 pt-2"), - Attrs.role("list"), - Attrs.ariaLabel("Available components"), - }, - list{ - div( - list{Attrs.class_("grid grid-cols-3 gap-2")}, - sorted - ->Array.map(comp => { - let isAssembled = Array.some(assembledIds, id => id === comp.id) - let isHovered = hoveredComponent === Some(comp.id) - renderComponentCard(comp, isAssembled, isHovered) - }) - ->List.fromArray, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Server Rack (Assembly Area — industrial steel with rail mounts) -// =========================================================================== - -/// Render a single component in the server rack as a mounted rack unit. -/// Uses KSP-style staging numbers (orange), green/red LEDs, and -/// industrial steel appearance. Components with missing deps get red LED. -let renderRackUnit = ( - comp: vabComponent, - index: int, - warnings: array, - _catalog: array, -): Tea_Vdom.t => { - // Check if this component has any critical warnings - let hasCritical = Array.some(warnings, w => - switch w { - | MissingRequired(compId, _) => compId === comp.id - | PortConflict(_, a, b) => a === comp.id || b === comp.id - | _ => false - } - ) - - let unitClass = hasCritical ? "vab-rack-unit vab-rack-unit-error" : "vab-rack-unit" - - div( - list{ - Attrs.class_( - `${unitClass} flex items-center gap-2 px-2 py-2 rounded-sm transition-all group`, - ), - Attrs.role("listitem"), - }, - list{ - // Mounting bolt (left) - div(list{Attrs.class_("vab-bolt flex-shrink-0")}, list{}), - // Staging number (KSP orange) - div(list{Attrs.class_("vab-stage flex-shrink-0")}, list{text(Int.toString(index + 1))}), - // LED indicator (green=ok, red=missing deps) - div( - list{ - Attrs.class_(hasCritical ? "vab-led-red flex-shrink-0" : "vab-led-green flex-shrink-0"), - }, - list{}, - ), - // Component name - span( - list{ - Attrs.class_("flex-1 text-xs font-mono font-bold"), - Attrs.style( - "color", - if hasCritical { - "#cc6666" - } else { - "#ddd" - }, - ), - }, - list{text(comp.shortName)}, - ), - // Rack unit size - span( - list{Attrs.class_("text-[9px] font-mono"), Attrs.style("color", "#666")}, - list{text(`${Int.toString(comp.rackUnits)}U`)}, - ), - // Port display (orange-tinted) - if Array.length(comp.ports) > 0 { - span( - list{Attrs.class_("text-[10px] font-mono font-bold"), Attrs.style("color", "#e8a050")}, - list{text(comp.ports->Array.map(p => Int.toString(p))->Array.join(","))}, - ) - } else { - noNode - }, - // Remove button (visible on hover — red X) - button( - list{ - Attrs.class_( - "opacity-0 group-hover:opacity-100 w-5 h-5 flex items-center justify-center rounded text-xs font-bold transition-all", - ), - Attrs.style("color", "#cc3333"), - Events.onClick(Vab(RemoveComponent(comp.id))), - Attrs.ariaLabel(`Remove ${comp.name} from rack`), - }, - list{text("X")}, - ), - // Mounting bolt (right) - div(list{Attrs.class_("vab-bolt flex-shrink-0")}, list{}), - }, - ) -} - -/// Render an empty rack slot with mounting hole pattern. -let renderEmptySlot = (_index: int): Tea_Vdom.t => { - div( - list{Attrs.class_("vab-rack-empty flex items-center gap-2 px-2 py-2 rounded-sm opacity-40")}, - list{ - div(list{Attrs.class_("vab-bolt flex-shrink-0")}, list{}), - div( - list{Attrs.class_("flex-1 text-center")}, - list{ - span( - list{Attrs.class_("text-[10px] font-mono"), Attrs.style("color", "#333")}, - list{text("--- empty slot ---")}, - ), - }, - ), - div(list{Attrs.class_("vab-bolt flex-shrink-0")}, list{}), - }, - ) -} - -/// Render the server rack — the right-hand assembly area. -/// Industrial steel appearance with mounting rails, staging numbers, -/// LED indicators, and KSP-style dependency warnings below. -let renderAssemblyRack = ( - server: assembledServer, - catalog: array, - warnings: array, -): Tea_Vdom.t => { - let assembledComponents = Array.filterMap(server.components, id => - VabCatalog.findById(catalog, id) - ) - let componentCount = Array.length(assembledComponents) - - // Calculate total rack units - let totalRU = Array.reduce(assembledComponents, 0, (acc, comp) => acc + comp.rackUnits) - - // Calculate empty slots (minimum 6 visible rack slots) - let totalSlots = if componentCount < 6 { - 6 - } else { - componentCount + 2 - } - let emptySlots = totalSlots - componentCount - - div( - list{ - Attrs.class_("w-[400px] flex flex-col overflow-hidden"), - Attrs.style("background-color", "#181818"), - Attrs.style("border-left", "2px solid #333"), - }, - list{ - // Rack header with KSP-style label - div( - list{ - Attrs.class_("px-3 py-2 flex items-center justify-between"), - Attrs.style("background-color", "#222"), - Attrs.style("border-bottom", "2px solid #333"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{ - Attrs.class_("text-xs font-bold uppercase tracking-widest"), - Attrs.style("color", "#6ab35e"), - }, - list{text("Assembly")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span( - list{ - Attrs.class_("text-[10px] font-mono font-bold"), - Attrs.style("color", "#e8721c"), - }, - list{text(`${Int.toString(componentCount)} parts`)}, - ), - span( - list{Attrs.class_("text-[10px] font-mono"), Attrs.style("color", "#666")}, - list{text(`${Int.toString(totalRU)} RU`)}, - ), - }, - ), - }, - ), - // Rack body (with rail mounts and grid background) - div( - list{ - Attrs.class_("flex-1 overflow-y-auto vab-rack vab-rack-rails px-3 py-2"), - Attrs.role("list"), - Attrs.ariaLabel("Assembled server components"), - }, - list{ - div( - list{Attrs.class_("flex flex-col gap-1")}, - List.concat( - // Assembled components (with staging numbers) - assembledComponents - ->Array.mapWithIndex((comp, i) => renderRackUnit(comp, i, warnings, catalog)) - ->List.fromArray, - // Empty slots - Array.make(~length=emptySlots, 0) - ->Array.mapWithIndex((_, i) => renderEmptySlot(componentCount + i)) - ->List.fromArray, - ), - ), - }, - ), - // Warning list below rack (KSP-style klaxon warnings) - if Array.length(warnings) > 0 { - div( - list{ - Attrs.class_("px-3 py-2 max-h-36 overflow-y-auto"), - Attrs.style("background-color", "rgba(30,10,10,0.6)"), - Attrs.style("border-top", "2px solid #cc3333"), - Attrs.role("alert"), - Attrs.ariaLive("polite"), - }, - list{ - div( - list{ - Attrs.class_("text-[10px] font-bold uppercase tracking-widest mb-1.5"), - Attrs.style("color", "#cc3333"), - }, - list{text("WARNINGS")}, - ), - div( - list{Attrs.class_("flex flex-col gap-1")}, - warnings - ->Array.map(w => { - let severity = VabEngine.warningSeverity(w) - let colourClass = switch severity { - | "error" => "vab-warning-error" - | "warning" => "vab-warning-caution" - | _ => "vab-warning-info" - } - let iconText = switch severity { - | "error" => "!!" - | "warning" => "!~" - | _ => "??" - } - div( - list{Attrs.class_(`flex items-start gap-1.5 text-[10px] ${colourClass}`)}, - list{ - span(list{Attrs.class_("font-bold font-mono")}, list{text(iconText)}), - span( - list{Attrs.class_("font-mono")}, - list{text(VabEngine.warningLabel(w, catalog))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else if componentCount > 0 { - div( - list{ - Attrs.class_("px-3 py-2"), - Attrs.style("background-color", "rgba(10,30,10,0.4)"), - Attrs.style("border-top", "2px solid #3a6332"), - }, - list{ - div( - list{ - Attrs.class_("text-[10px] font-bold font-mono"), - Attrs.style("color", "#5a9e50"), - }, - list{text("ALL CHECKS PASSED — Ready for deployment")}, - ), - }, - ) - } else { - div( - list{Attrs.class_("px-3 py-2"), Attrs.style("border-top", "1px solid #2a2a2a")}, - list{ - div( - list{Attrs.class_("text-[10px] font-mono"), Attrs.style("color", "#555")}, - list{text("Click parts to add to rack")}, - ), - }, - ) - }, - }, - ) -} - -// =========================================================================== -// Status Bar (Capabilities — KSP stats instrument panel) -// =========================================================================== - -/// Render the bottom capability status bar — dark instrument panel style. -/// Green badges for CAN DO, muted badges for CANNOT, orange for warnings. -/// Warning counts displayed as KSP-style mission readiness indicators. -let renderStatusBar = ( - capabilities: array, - warnings: array, -): Tea_Vdom.t => { - let (reqCount, recCount, secCount) = VabEngine.countWarnings(warnings) - let totalWarnings = reqCount + recCount + secCount - - div( - list{ - Attrs.class_("flex items-center gap-2 px-4 py-2 vab-stats overflow-x-auto"), - Attrs.role("status"), - Attrs.ariaLabel("Server capabilities"), - }, - list{ - // Mission readiness — warning summary counts - if totalWarnings > 0 { - div( - list{ - Attrs.class_("flex items-center gap-2 mr-3 pr-3"), - Attrs.style("border-right", "1px solid #444"), - }, - list{ - span( - list{ - Attrs.class_("text-[10px] font-bold uppercase tracking-wider mr-1"), - Attrs.style("color", "#cc3333"), - }, - list{text("HOLD")}, - ), - if reqCount > 0 { - span( - list{Attrs.class_("text-[10px] font-mono font-bold vab-warning-error")}, - list{text(`${Int.toString(reqCount)} critical`)}, - ) - } else { - noNode - }, - if secCount > 0 { - span( - list{Attrs.class_("text-[10px] font-mono font-bold vab-warning-caution")}, - list{text(`${Int.toString(secCount)} security`)}, - ) - } else { - noNode - }, - if recCount > 0 { - span( - list{Attrs.class_("text-[10px] font-mono vab-warning-info")}, - list{text(`${Int.toString(recCount)} advisory`)}, - ) - } else { - noNode - }, - }, - ) - } else { - div( - list{ - Attrs.class_("flex items-center gap-1 mr-3 pr-3"), - Attrs.style("border-right", "1px solid #444"), - }, - list{ - span( - list{ - Attrs.class_("text-[10px] font-bold uppercase tracking-wider"), - Attrs.style("color", "#5a9e50"), - }, - list{text("GO")}, - ), - }, - ) - }, - // Capability badges (green=yes, gray=no, orange=warning) - div( - list{Attrs.class_("flex items-center gap-1 flex-wrap")}, - capabilities - ->Array.map(cap => { - let (label, badgeClass, icon) = switch cap { - | CanDo(name) => (name, "vab-cap-yes", "V") - | CannotDo(name) => (name, "vab-cap-no", "X") - | WarningCap(name) => (name, "vab-cap-warn", "!") - } - span( - list{ - Attrs.class_( - `inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[9px] font-mono font-bold ${badgeClass}`, - ), - Attrs.title(label), - }, - list{span(list{}, list{text(icon)}), span(list{}, list{text(label)})}, - ) - }) - ->List.fromArray, - ), - }, - ) -} - -// =========================================================================== -// Main View -// =========================================================================== - -/// The main VAB panel view, rendered as a full-screen overlay. -/// Composes: KSP-green toolbar + green category sidebar + part grid + -/// industrial assembly rack + instrument panel status bar. -let view = (vab: vabState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 flex flex-col z-40"), - Attrs.style("background-color", "#1a1a1a"), - Attrs.role("dialog"), - Attrs.ariaLabel("Verified Assembly Building — Server Component Composer"), - KeyboardUtil.onEscape(Vab(ToggleVab)), - }, - list{ - // Top toolbar — KSP green gradient - renderTopBar(vab.server), - // Main content — sidebar + grid + rack - div( - list{Attrs.class_("flex-1 flex overflow-hidden")}, - list{ - // Left: KSP-green category sidebar - renderCategorySidebar(vab.selectedCategory), - // Centre: component grid (part picker) - renderComponentGrid( - vab.catalog, - vab.selectedCategory, - vab.sortBy, - vab.filterText, - vab.server.components, - vab.hoveredComponent, - ), - // Right: industrial server rack - renderAssemblyRack(vab.server, vab.catalog, vab.warnings), - }, - ), - // Bottom: capability instrument panel - renderStatusBar(vab.capabilities, vab.warnings), - }, - ) -} diff --git a/src/components/ValenceShell.affine b/src/components/ValenceShell.affine new file mode 100644 index 00000000..5074a4af --- /dev/null +++ b/src/components/ValenceShell.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ValenceShell; + +// TODO: Complete semantic implementation diff --git a/src/components/ValenceShell.res b/src/components/ValenceShell.res deleted file mode 100644 index 30acc078..00000000 --- a/src/components/ValenceShell.res +++ /dev/null @@ -1,847 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Valence Shell Component — renders the embedded terminal panel. -/// -/// The Valence Shell is a full-screen overlay panel providing: -/// -/// 1. **Terminal** — PTY-backed terminal emulator with Claude Code integration. -/// When the xterm.js widget is wired (Phase 2), the terminal area will be -/// replaced by the real PTY. For now, it renders a styled output buffer -/// with an input line and command history navigation. -/// -/// 2. **Recordings** — Browse, replay, export, and delete asciinema .cast -/// session recordings. Each recording shows duration, size, and creation date. -/// -/// 3. **Checkpoints** — Valence filesystem save/restore points with formal -/// reversibility proofs. Create, list, and restore checkpoints. -/// -/// 4. **History** — Command history with timestamps and reversibility markers. -/// -/// 5. **Settings** — Shell backend selection, approval gate configuration, -/// split view toggle, and IDApTIK-specific completions. -/// -/// Collaborative features: -/// - **Approval gate**: When enabled, commands queue for parent review before -/// execution. The child types, the parent approves — every command becomes -/// a teaching moment. -/// - **Session recording**: Record terminal sessions to .cast files for replay, -/// sharing, and teaching. -/// - **Screenshots**: Capture terminal state to the PanLL Capture panel. - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helper renderers -// ========================================================================= - -/// Render the category tab bar. -let renderTabs = (active: valenceShellCategory): Tea_Vdom.t => { - let tabs: array = [ - ShellTerminal, - ShellRecordings, - ShellCheckpoints, - ShellHistory, - ShellSettings, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - let label = ValenceShellEngine.categoryLabel(tab) - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-emerald-400 border-b-2 border-emerald-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900"}`, - ), - Events.onClick(ValenceShell(SetShellCategory(tab))), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a single terminal output line. -let renderOutputLine = (line: terminalLine): Tea_Vdom.t => { - let colour = line.isStdout ? "text-gray-200" : "text-red-400" - div( - list{Attrs.class_(`font-mono text-sm ${colour} whitespace-pre-wrap px-4 py-0.5`)}, - list{text(line.content)}, - ) -} - -/// Render the terminal view — output buffer + input line + toolbar. -let renderTerminal = (state: valenceShellState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex flex-col bg-gray-950")}, - list{ - // Backend status bar - div( - list{ - Attrs.class_( - "flex items-center justify-between px-4 py-2 bg-gray-900/50 border-b border-gray-800", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Connection indicator - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${state.ptyConnected ? "bg-emerald-400" : "bg-gray-600"}`, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(ValenceShellEngine.backendLabel(state.backend))}, - ), - // CWD display - span(list{Attrs.class_("text-xs text-gray-500 font-mono")}, list{text(state.cwd)}), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Recording indicator - switch state.recording { - | RecordingActive(_) => - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div( - list{Attrs.class_("w-2 h-2 rounded-full bg-red-500 animate-pulse")}, - list{}, - ), - span(list{Attrs.class_("text-xs text-red-400")}, list{text("REC")}), - button( - list{ - Attrs.class_("text-xs text-gray-400 hover:text-gray-200 px-2 py-1"), - Events.onClick(ValenceShell(StopRecordingSession)), - KeyboardNav.onActivate(ValenceShell(StopRecordingSession)), - }, - list{text("Stop")}, - ), - }, - ) - | RecordingPaused(_) => - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full bg-yellow-500")}, list{}), - span(list{Attrs.class_("text-xs text-yellow-400")}, list{text("PAUSED")}), - }, - ) - | RecordingIdle => - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300 px-2 py-1 rounded bg-gray-800", - ), - Events.onClick(ValenceShell(StartRecordingSession)), - KeyboardNav.onActivate(ValenceShell(StartRecordingSession)), - }, - list{text("Record")}, - ) - }, - // Screenshot button - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300 px-2 py-1 rounded bg-gray-800", - ), - Events.onClick(ValenceShell(ScreenshotTerminal)), - KeyboardNav.onActivate(ValenceShell(ScreenshotTerminal)), - }, - list{text("Screenshot")}, - ), - // Approval gate indicator - switch state.approvalGate { - | GateDisabled => noNode - | GateEnabled => - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full bg-amber-400")}, list{}), - span(list{Attrs.class_("text-xs text-amber-400")}, list{text("GATE ON")}), - }, - ) - | GateLearning => - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div(list{Attrs.class_("w-2 h-2 rounded-full bg-blue-400")}, list{}), - span(list{Attrs.class_("text-xs text-blue-400")}, list{text("LEARNING")}), - }, - ) - }, - // Claude Code indicator - if state.claudeCodeActive { - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span( - list{Attrs.class_("text-xs text-purple-400 font-medium")}, - list{text("Claude")}, - ), - }, - ) - } else { - noNode - }, - }, - ), - }, - ), - // Terminal output area - div( - list{ - Attrs.class_("flex-1 overflow-auto bg-gray-950 py-2"), - Attrs.id("valence-terminal-output"), - }, - if Array.length(state.outputBuffer) === 0 { - list{ - div( - list{Attrs.class_("px-4 py-8 text-center")}, - list{ - div(list{Attrs.class_("text-gray-600 text-sm mb-2")}, list{text("Valence Shell")}), - div( - list{Attrs.class_("text-gray-700 text-xs")}, - list{ - text( - state.valenceAvailable - ? "Formally verified reversible shell ready." - : "System shell mode (install valence-shell for reversible ops).", - ), - }, - ), - div( - list{Attrs.class_("text-gray-700 text-xs mt-1")}, - list{text("Type a command or run `claude` to start Claude Code.")}, - ), - }, - ), - } - } else { - state.outputBuffer->Array.map(renderOutputLine)->List.fromArray - }, - ), - // Pending commands (approval gate) - if Array.length(state.pendingCommands) > 0 { - div( - list{Attrs.class_("border-t border-amber-800/50 bg-amber-950/20 px-4 py-2")}, - list{ - div( - list{Attrs.class_("text-xs text-amber-400 mb-2 font-medium")}, - list{ - text( - `${Int.toString( - Array.length(state.pendingCommands), - )} command(s) awaiting approval`, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - state.pendingCommands - ->Array.mapWithIndex((cmd, idx) => { - div( - list{ - Attrs.class_("flex items-center justify-between bg-gray-900 rounded px-3 py-1"), - }, - list{ - span( - list{Attrs.class_("font-mono text-sm text-amber-200")}, - list{text(cmd.command)}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "text-xs px-2 py-1 rounded bg-emerald-800 text-emerald-200 hover:bg-emerald-700", - ), - Events.onClick(ValenceShell(ApproveCommand(idx))), - }, - list{text("Approve")}, - ), - button( - list{ - Attrs.class_( - "text-xs px-2 py-1 rounded bg-red-800 text-red-200 hover:bg-red-700", - ), - Events.onClick(ValenceShell(RejectCommand(idx))), - }, - list{text("Reject")}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - } else { - noNode - }, - // Input line - div( - list{ - Attrs.class_("border-t border-gray-800 bg-gray-900 px-4 py-2 flex items-center gap-2"), - }, - list{ - span(list{Attrs.class_("text-emerald-400 font-mono text-sm")}, list{text("$")}), - input( - list{ - Attrs.class_( - "flex-1 bg-transparent text-gray-200 font-mono text-sm outline-none placeholder-gray-600", - ), - Attrs.type_("text"), - Attrs.value(state.inputLine), - Attrs.placeholder("Type a command..."), - Events.onInput(value => ValenceShell(UpdateInput(value))), - KeyboardUtil.onEnterOrSpace(ValenceShell(SubmitInput)), - Attrs.id("valence-shell-input"), - Attrs.ariaLabel("Shell command input"), - }, - list{}, - ), - }, - ), - // Completions popup - if ( - state.completionsVisible && - Array.length(ValenceShellEngine.filterCompletions(state.inputLine, state.completions)) > 0 - ) { - let filtered = ValenceShellEngine.filterCompletions(state.inputLine, state.completions) - div( - list{ - Attrs.class_( - "absolute bottom-16 left-4 right-16 bg-gray-800 border border-gray-700 rounded shadow-lg max-h-48 overflow-auto z-50", - ), - }, - filtered - ->Array.map(completion => { - button( - list{ - Attrs.class_( - "block w-full text-left px-3 py-1.5 font-mono text-sm text-gray-300 hover:bg-gray-700 hover:text-gray-100", - ), - Events.onClick(ValenceShell(SelectCompletion(completion))), - }, - list{text(completion)}, - ) - }) - ->List.fromArray, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the recordings browser tab. -let renderRecordings = (state: valenceShellState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Session Recordings")}, - ), - button( - list{ - Attrs.class_( - "text-xs text-gray-400 hover:text-gray-200 px-3 py-1 rounded bg-gray-800", - ), - Events.onClick(ValenceShell(LoadRecordings)), - KeyboardNav.onActivate(ValenceShell(LoadRecordings)), - }, - list{text("Refresh")}, - ), - }, - ), - if Array.length(state.recordings) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No recordings yet. Start one from the Terminal tab.")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.recordings - ->Array.map(recording => { - div( - list{ - Attrs.class_( - "flex items-center justify-between bg-gray-900 rounded-lg px-4 py-3 border border-gray-800", - ), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text(recording.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{ - text( - `${Float.toString(recording.durationSecs)}s | ${Int.toString( - recording.sizeBytes, - )} bytes`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - "text-xs text-gray-400 hover:text-gray-200 px-2 py-1 bg-gray-800 rounded", - ), - Events.onClick(ValenceShell(ExportRecordingAs(recording.id, "html"))), - }, - list{text("Export HTML")}, - ), - button( - list{ - Attrs.class_( - "text-xs text-red-400 hover:text-red-300 px-2 py-1 bg-gray-800 rounded", - ), - Events.onClick(ValenceShell(DeleteRecordingById(recording.id))), - }, - list{text("Delete")}, - ), - }, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the checkpoints tab. -let renderCheckpoints = (state: valenceShellState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-4")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Valence Filesystem Checkpoints")}, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - `text-xs px-3 py-1 rounded ${state.valenceAvailable - ? "bg-emerald-800 text-emerald-200 hover:bg-emerald-700" - : "bg-gray-800 text-gray-600 cursor-not-allowed"}`, - ), - Events.onClick(ValenceShell(CreateCheckpointWithLabel("manual"))), - }, - list{text("Create Checkpoint")}, - ), - button( - list{ - Attrs.class_( - "text-xs text-gray-400 hover:text-gray-200 px-3 py-1 rounded bg-gray-800", - ), - Events.onClick(ValenceShell(LoadCheckpoints)), - KeyboardNav.onActivate(ValenceShell(LoadCheckpoints)), - }, - list{text("Refresh")}, - ), - }, - ), - }, - ), - if !state.valenceAvailable { - div( - list{Attrs.class_("bg-amber-950/30 border border-amber-800/30 rounded-lg p-4 mb-4")}, - list{ - div( - list{Attrs.class_("text-amber-400 text-sm font-medium mb-1")}, - list{text("Valence shell not installed")}, - ), - div( - list{Attrs.class_("text-amber-500/70 text-xs")}, - list{ - text("Checkpoints require the Valence shell binary for reversible filesystem ops."), - }, - ), - }, - ) - } else { - noNode - }, - if Array.length(state.checkpoints) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No checkpoints. Create one to save the current filesystem state.")}, - ) - } else { - div( - list{Attrs.class_("space-y-2")}, - state.checkpoints - ->Array.map(cp => { - div( - list{ - Attrs.class_( - "flex items-center justify-between bg-gray-900 rounded-lg px-4 py-3 border border-gray-800", - ), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200 font-medium")}, - list{text(cp.label)}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-1")}, - list{ - text(`${Int.toString(cp.opsSinceCheckpoint)} ops since this checkpoint`), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "text-xs text-blue-400 hover:text-blue-300 px-3 py-1 bg-gray-800 rounded", - ), - Events.onClick(ValenceShell(RestoreCheckpointById(cp.id))), - }, - list{text("Restore")}, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the command history tab. -let renderHistory = (state: valenceShellState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-4")}, - list{text("Command History")}, - ), - if Array.length(state.commandHistory) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No commands executed yet.")}, - ) - } else { - div( - list{Attrs.class_("space-y-1")}, - state.commandHistory - ->Array.mapWithIndex((cmd, idx) => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 font-mono text-sm px-3 py-1.5 rounded hover:bg-gray-900", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-600 w-8 text-right")}, - list{text(Int.toString(idx + 1))}, - ), - span(list{Attrs.class_("text-gray-300 flex-1")}, list{text(cmd)}), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the settings tab. -let renderSettings = (state: valenceShellState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6 space-y-6")}, - list{ - // Shell backend section - div( - list{Attrs.class_("space-y-2")}, - list{ - h3(list{Attrs.class_("text-sm font-medium text-gray-300")}, list{text("Shell Backend")}), - div( - list{Attrs.class_("flex items-center gap-3 bg-gray-900 rounded-lg px-4 py-3")}, - list{ - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${state.valenceAvailable - ? "bg-emerald-400" - : "bg-gray-600"}`, - ), - }, - list{}, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-sm text-gray-200")}, - list{text(ValenceShellEngine.backendLabel(state.backend))}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{ - text( - state.valenceAvailable - ? "Valence shell provides formally verified reversible filesystem operations." - : "Install valence-shell for reversible ops, MAA audit trail, and checkpoints.", - ), - }, - ), - }, - ), - }, - ), - }, - ), - // Approval gate section - div( - list{Attrs.class_("space-y-2")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Approval Gate (Collaborative Mode)")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mb-2")}, - list{ - text( - "When enabled, commands must be approved before execution. Ideal for parent-child collaborative sessions.", - ), - }, - ), - div( - list{Attrs.class_("flex gap-2")}, - list{ - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${state.approvalGate === GateDisabled - ? "bg-gray-700 text-gray-200" - : "bg-gray-900 text-gray-500"}`, - ), - Events.onClick(ValenceShell(SetApprovalGate(GateDisabled))), - }, - list{text("Disabled")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${state.approvalGate === GateEnabled - ? "bg-amber-800 text-amber-200" - : "bg-gray-900 text-gray-500"}`, - ), - Events.onClick(ValenceShell(SetApprovalGate(GateEnabled))), - }, - list{text("Enabled")}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${state.approvalGate === GateLearning - ? "bg-blue-800 text-blue-200" - : "bg-gray-900 text-gray-500"}`, - ), - Events.onClick(ValenceShell(SetApprovalGate(GateLearning))), - }, - list{text("Learning")}, - ), - }, - ), - }, - ), - // Split view section - div( - list{Attrs.class_("flex items-center justify-between bg-gray-900 rounded-lg px-4 py-3")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-sm text-gray-200")}, list{text("Split View")}), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text("Show two terminal instances side by side.")}, - ), - }, - ), - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded ${state.splitView - ? "bg-emerald-800 text-emerald-200" - : "bg-gray-800 text-gray-500"}`, - ), - Events.onClick(ValenceShell(ToggleSplitView)), - KeyboardNav.onActivate(ValenceShell(ToggleSplitView)), - }, - list{text(state.splitView ? "On" : "Off")}, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Render the Valence Shell panel as a full-screen overlay. -let view = (state: valenceShellState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/98 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("Valence Shell — embedded terminal with Claude Code integration"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - // Terminal icon placeholder (text-based for now) - div( - list{ - Attrs.class_("w-6 h-6 rounded bg-emerald-900 flex items-center justify-center"), - }, - list{ - span(list{Attrs.class_("text-emerald-400 text-xs font-bold")}, list{text(">_")}), - }, - ), - div( - list{}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Valence Shell")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Embedded terminal with Claude Code integration")}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Claude Code quick-launch button - if !state.claudeCodeActive { - button( - list{ - Attrs.class_( - "text-xs px-3 py-1.5 rounded bg-purple-900 text-purple-200 hover:bg-purple-800", - ), - Events.onClick(ValenceShell(LaunchClaudeCode)), - KeyboardNav.onActivate(ValenceShell(LaunchClaudeCode)), - }, - list{text("Launch Claude")}, - ) - } else { - noNode - }, - // Close button - button( - list{ - Attrs.class_( - "text-gray-500 hover:text-gray-300 px-3 py-1.5 text-sm rounded bg-gray-800 hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{Attrs.class_("px-4 py-2 bg-red-950 border-b border-red-900")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-red-400 text-sm")}, list{text(err)}), - button( - list{ - Attrs.class_("text-red-500 hover:text-red-400 text-xs"), - Events.onClick(ValenceShell(DismissError)), - KeyboardNav.onActivate(ValenceShell(DismissError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeCategory), - // Content area — switches on active category - switch state.activeCategory { - | ShellTerminal => renderTerminal(state) - | ShellRecordings => renderRecordings(state) - | ShellCheckpoints => renderCheckpoints(state) - | ShellHistory => renderHistory(state) - | ShellSettings => renderSettings(state) - }, - }, - ) -} diff --git a/src/components/VerificationDashboard.affine b/src/components/VerificationDashboard.affine new file mode 100644 index 00000000..a466706b --- /dev/null +++ b/src/components/VerificationDashboard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VerificationDashboard; + +// TODO: Complete semantic implementation diff --git a/src/components/VerificationDashboard.res b/src/components/VerificationDashboard.res deleted file mode 100644 index 42c8fbbc..00000000 --- a/src/components/VerificationDashboard.res +++ /dev/null @@ -1,731 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL VerificationDashboard Component — proof/test/benchmark status panel. -/// -/// Five tabs: Summary, By Language, Proofs, Benchmarks, Fuzzing. -/// Shows aggregated and per-language verification status across all -/// nextgen-languages repos. - -open Model -open Msg -open Tea.Html - -// ============================================================================ -// Shared sub-views -// ============================================================================ - -/// Render category tabs. -let renderTabs = (active: verificationDashboardCategory): Tea_Vdom.t => { - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 mb-4"), Attrs.role("tablist")}, - VerificationDashboardEngine.allCategories - ->Array.map(tab => { - let isActive = tab === active - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm rounded-t transition-colors ${isActive - ? "bg-gray-800 text-gray-200 border-b-2 border-violet-500" - : "text-gray-500 hover:text-gray-300"}`, - ), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Events.onClick(VerificationDashboard(SetVdCategory(tab))), - }, - list{text(VerificationDashboardEngine.categoryLabel(tab))}, - ) - }) - ->List.fromArray, - ) -} - -/// Render a summary stat card. -let renderStatCard = (label: string, value: string, colour: string): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 border border-gray-700 rounded-lg p-3 text-center")}, - list{ - div(list{Attrs.class_(`text-2xl font-mono ${colour}`)}, list{text(value)}), - div(list{Attrs.class_("text-[10px] text-gray-500")}, list{text(label)}), - }, - ) -} - -/// Render a proof system badge. -let renderProofBadge = (ps: VerificationDashboardModel.proofSystem): Tea_Vdom.t => { - span( - list{ - Attrs.class_( - `px-1.5 py-0.5 text-[10px] rounded ${VerificationDashboardEngine.proofSystemColour(ps)}`, - ), - }, - list{text(VerificationDashboardEngine.proofSystemCode(ps))}, - ) -} - -/// Render a language verification row. -let renderLanguageRow = (status: VerificationDashboardModel.languageVerificationStatus): Tea_Vdom.t< - msg, -> => { - let rate = VerificationDashboardEngine.passRate(status) - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 border-b border-gray-800 hover:bg-gray-900/50 text-xs", - ), - Events.onClick(VerificationDashboard(SelectVdLanguage(Some(status.name)))), - }, - list{ - div(list{Attrs.class_("w-28 text-gray-200 font-medium")}, list{text(status.name)}), - div( - list{Attrs.class_("w-16 text-right text-gray-300 font-mono")}, - list{text(Int.toString(status.totalTests))}, - ), - div( - list{Attrs.class_("w-16 text-right text-emerald-400 font-mono")}, - list{text(Int.toString(status.passingTests))}, - ), - div( - list{Attrs.class_("w-16 text-right text-red-400 font-mono")}, - list{text(Int.toString(status.failingTests))}, - ), - div( - list{ - Attrs.class_( - `w-14 text-right font-mono ${VerificationDashboardEngine.passRateColour(rate)}`, - ), - }, - list{text(Int.toString(rate) ++ "%")}, - ), - div( - list{ - Attrs.class_( - `w-14 text-right font-mono ${VerificationDashboardEngine.admittedColour( - status.admittedCount, - )}`, - ), - }, - list{text(Int.toString(status.admittedCount))}, - ), - div( - list{Attrs.class_("w-14 text-right text-violet-400 font-mono")}, - list{text(Int.toString(status.provedCount))}, - ), - div( - list{Attrs.class_("w-20 flex gap-0.5")}, - status.proofSystems->Array.map(renderProofBadge)->List.fromArray, - ), - div( - list{ - Attrs.class_( - `w-16 text-center ${VerificationDashboardEngine.conformanceColour(status.conformance)}`, - ), - }, - list{text(VerificationDashboardEngine.conformanceLabel(status.conformance))}, - ), - div( - list{Attrs.class_("w-10 text-center text-gray-500")}, - list{ - text( - if status.fuzzing !== None { - "Yes" - } else { - "-" - }, - ), - }, - ), - }, - ) -} - -// ============================================================================ -// Main View -// ============================================================================ - -/// Main view for the VerificationDashboard panel. -let view = (vd: verificationDashboardState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel("VerificationDashboard panel"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between p-4 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Verification Dashboard")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text("Proof / Test / Benchmark Status")}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - input( - list{ - Attrs.class_( - "bg-gray-900 border border-gray-700 rounded px-3 py-1 text-sm text-gray-200 placeholder-gray-600 w-48", - ), - Attrs.placeholder("Filter languages..."), - Attrs.value(vd.filterText), - Events.onInput(v => VerificationDashboard(SetVdFilter(v))), - }, - list{}, - ), - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded ${vd.showDebtOnly - ? "bg-amber-600 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(VerificationDashboard(ToggleDebtOnly)), - KeyboardNav.onActivate(VerificationDashboard(ToggleDebtOnly)), - }, - list{text("Debt Only")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 text-sm bg-gray-800 text-gray-300 rounded hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Content - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - renderTabs(vd.activeCategory), - { - let data = VerificationDashboardEngine.allLanguageStatuses - let filtered = { - let searched = VerificationDashboardEngine.filterBySearch(data, vd.filterText) - if vd.showDebtOnly { - VerificationDashboardEngine.filterDebtOnly(searched) - } else { - searched - } - } - let sorted = VerificationDashboardEngine.sortLanguages(filtered, vd.sortBy) - switch vd.activeCategory { - // ── Summary Tab ── - | VdSummary => { - let summary = VerificationDashboardEngine.computeSummary(data) - div( - list{Attrs.class_("space-y-6")}, - list{ - // Stats grid - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - renderStatCard( - "Languages", - Int.toString(summary.totalLanguages), - "text-teal-400", - ), - renderStatCard( - "Total Tests", - Int.toString(summary.totalTests), - "text-cyan-400", - ), - renderStatCard( - "Passing", - Int.toString(summary.totalPassing), - "text-emerald-400", - ), - renderStatCard( - "Failing", - Int.toString(summary.totalFailing), - if summary.totalFailing > 0 { - "text-red-400" - } else { - "text-emerald-400" - }, - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-4 gap-3")}, - list{ - renderStatCard( - "Avg Pass Rate", - Int.toString(summary.avgPassRate) ++ "%", - VerificationDashboardEngine.passRateColour(summary.avgPassRate), - ), - renderStatCard( - "Proved", - Int.toString(summary.totalProved), - "text-violet-400", - ), - renderStatCard( - "Admitted/Sorry", - Int.toString(summary.totalAdmitted), - VerificationDashboardEngine.admittedColour(summary.totalAdmitted), - ), - renderStatCard( - "Full Conformance", - Int.toString(summary.languagesFullConformance) ++ - "/" ++ - Int.toString(summary.totalLanguages), - "text-emerald-400", - ), - }, - ), - div( - list{Attrs.class_("grid grid-cols-2 gap-3")}, - list{ - renderStatCard( - "With Fuzzing", - Int.toString(summary.languagesWithFuzzing), - "text-amber-400", - ), - renderStatCard( - "Formal Proofs", - Int.toString(summary.totalProved) ++ - " proved, " ++ - Int.toString(summary.totalAdmitted) ++ " admitted", - "text-violet-400", - ), - }, - ), - }, - ) - } - // ── By Language Tab ── - | VdByLanguage => - div( - list{Attrs.class_("space-y-4")}, - list{ - // Sort controls - div( - list{Attrs.class_("flex gap-2 items-center mb-2")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Sort by:")}), - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded ${vd.sortBy === VdSortByName - ? "bg-violet-600 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(VerificationDashboard(SetVdSort(VdSortByName))), - }, - list{text("Name")}, - ), - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded ${vd.sortBy === VdSortByTests - ? "bg-violet-600 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(VerificationDashboard(SetVdSort(VdSortByTests))), - }, - list{text("Tests")}, - ), - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded ${vd.sortBy === VdSortByPassRate - ? "bg-violet-600 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(VerificationDashboard(SetVdSort(VdSortByPassRate))), - }, - list{text("Pass Rate")}, - ), - button( - list{ - Attrs.class_( - `px-2 py-1 text-xs rounded ${vd.sortBy === VdSortByAdmitted - ? "bg-violet-600 text-white" - : "bg-gray-800 text-gray-400"}`, - ), - Events.onClick(VerificationDashboard(SetVdSort(VdSortByAdmitted))), - }, - list{text("Admitted")}, - ), - }, - ), - // Table - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - // Header - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 border-b border-gray-700 text-[10px] text-gray-500 uppercase tracking-wide", - ), - }, - list{ - div(list{Attrs.class_("w-28")}, list{text("Language")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Total")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Pass")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Fail")}), - div(list{Attrs.class_("w-14 text-right")}, list{text("Rate")}), - div(list{Attrs.class_("w-14 text-right")}, list{text("Admit")}), - div(list{Attrs.class_("w-14 text-right")}, list{text("Proved")}), - div(list{Attrs.class_("w-20")}, list{text("Provers")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Conf")}), - div(list{Attrs.class_("w-10 text-center")}, list{text("Fuzz")}), - }, - ), - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - sorted->Array.map(renderLanguageRow)->List.fromArray, - ), - }, - ), - }, - ) - // ── Proofs Tab ── - | VdProofs => { - let withProofs = data->Array.filter(s => s.provedCount > 0 || s.admittedCount > 0) - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text( - "Formal verification status — proofs discharged vs admitted/sorry across all languages.", - ), - }, - ), - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 border-b border-gray-700 text-[10px] text-gray-500 uppercase tracking-wide", - ), - }, - list{ - div(list{Attrs.class_("w-28")}, list{text("Language")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Proved")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Admitted")}), - div(list{Attrs.class_("w-24")}, list{text("Systems")}), - div(list{Attrs.class_("flex-1")}, list{text("Status")}), - }, - ), - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - withProofs - ->Array.map(s => { - let status = if s.admittedCount === 0 { - "Clean — all proofs discharged" - } else { - Int.toString( - s.admittedCount, - ) ++ " admitted — formal verification debt" - } - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 border-b border-gray-800 text-xs", - ), - }, - list{ - div( - list{Attrs.class_("w-28 text-gray-200 font-medium")}, - list{text(s.name)}, - ), - div( - list{Attrs.class_("w-20 text-right text-violet-400 font-mono")}, - list{text(Int.toString(s.provedCount))}, - ), - div( - list{ - Attrs.class_( - `w-20 text-right font-mono ${VerificationDashboardEngine.admittedColour( - s.admittedCount, - )}`, - ), - }, - list{text(Int.toString(s.admittedCount))}, - ), - div( - list{Attrs.class_("w-24 flex gap-0.5")}, - s.proofSystems->Array.map(renderProofBadge)->List.fromArray, - ), - div( - list{ - Attrs.class_( - `flex-1 ${if s.admittedCount === 0 { - "text-emerald-400" - } else { - "text-amber-400" - }}`, - ), - }, - list{text(status)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - if Array.length(withProofs) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{text("No languages have formal proofs yet.")}, - ) - } else { - noNode - }, - }, - ) - } - // ── Benchmarks Tab ── - | VdBenchmarks => { - let benchmarks = VerificationDashboardEngine.allBenchmarks() - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text("Performance benchmark results across all language implementations."), - }, - ), - if Array.length(benchmarks) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{text("No benchmark results available.")}, - ) - } else { - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 border-b border-gray-700 text-[10px] text-gray-500 uppercase tracking-wide", - ), - }, - list{ - div(list{Attrs.class_("w-28")}, list{text("Language")}), - div(list{Attrs.class_("w-40")}, list{text("Benchmark")}), - div(list{Attrs.class_("w-24 text-right")}, list{text("Mean (ms)")}), - div(list{Attrs.class_("w-24 text-right")}, list{text("StdDev")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Iters")}), - div(list{Attrs.class_("w-16 text-center")}, list{text("Regr?")}), - }, - ), - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - benchmarks - ->Array.map(b => - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 border-b border-gray-800 text-xs", - ), - }, - list{ - div( - list{Attrs.class_("w-28 text-gray-200 font-medium")}, - list{text(b.language)}, - ), - div( - list{Attrs.class_("w-40 text-gray-300 font-mono")}, - list{text(b.name)}, - ), - div( - list{Attrs.class_("w-24 text-right text-cyan-400 font-mono")}, - list{text(Float.toFixed(b.meanMs, ~digits=1))}, - ), - div( - list{Attrs.class_("w-24 text-right text-gray-500 font-mono")}, - list{text(Float.toFixed(b.stddevMs, ~digits=1))}, - ), - div( - list{Attrs.class_("w-20 text-right text-gray-400 font-mono")}, - list{text(Int.toString(b.iterations))}, - ), - div( - list{ - Attrs.class_( - `w-16 text-center ${if b.regression { - "text-red-400" - } else { - "text-gray-600" - }}`, - ), - }, - list{ - text( - if b.regression { - "Yes" - } else { - "-" - }, - ), - }, - ), - }, - ) - ) - ->List.fromArray, - ), - }, - ) - }, - }, - ) - } - // ── Fuzzing Tab ── - | VdFuzzing => { - let fuzzData = VerificationDashboardEngine.allFuzzingCoverage() - div( - list{Attrs.class_("space-y-4")}, - list{ - div( - list{Attrs.class_("text-sm text-gray-400 mb-2")}, - list{ - text( - "Fuzzing coverage across language implementations. Only languages with active fuzz targets are shown.", - ), - }, - ), - if Array.length(fuzzData) === 0 { - div( - list{Attrs.class_("text-center text-gray-500 mt-8")}, - list{text("No fuzzing coverage data available.")}, - ) - } else { - div( - list{Attrs.class_("border border-gray-700 rounded-lg overflow-hidden")}, - list{ - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 bg-gray-800/50 border-b border-gray-700 text-[10px] text-gray-500 uppercase tracking-wide", - ), - }, - list{ - div(list{Attrs.class_("w-28")}, list{text("Language")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Targets")}), - div( - list{Attrs.class_("w-24 text-right")}, - list{text("Lines Covered")}, - ), - div(list{Attrs.class_("w-24 text-right")}, list{text("Total Lines")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Cover%")}), - div(list{Attrs.class_("w-16 text-right")}, list{text("Crashes")}), - div(list{Attrs.class_("w-20 text-right")}, list{text("Hours")}), - }, - ), - div( - list{Attrs.class_("max-h-96 overflow-y-auto")}, - fuzzData - ->Array.map(f => { - let coverPct = if f.totalLines > 0 { - f.linesCovered * 100 / f.totalLines - } else { - 0 - } - div( - list{ - Attrs.class_( - "flex items-center gap-3 p-2 border-b border-gray-800 text-xs", - ), - }, - list{ - div( - list{Attrs.class_("w-28 text-gray-200 font-medium")}, - list{text(f.language)}, - ), - div( - list{Attrs.class_("w-16 text-right text-gray-300 font-mono")}, - list{text(Int.toString(f.targets))}, - ), - div( - list{ - Attrs.class_("w-24 text-right text-emerald-400 font-mono"), - }, - list{text(Int.toString(f.linesCovered))}, - ), - div( - list{Attrs.class_("w-24 text-right text-gray-400 font-mono")}, - list{text(Int.toString(f.totalLines))}, - ), - div( - list{ - Attrs.class_( - `w-16 text-right font-mono ${VerificationDashboardEngine.passRateColour( - coverPct, - )}`, - ), - }, - list{text(Int.toString(coverPct) ++ "%")}, - ), - div( - list{ - Attrs.class_( - `w-16 text-right font-mono ${if f.crashesFound > 0 { - "text-red-400" - } else { - "text-emerald-400" - }}`, - ), - }, - list{text(Int.toString(f.crashesFound))}, - ), - div( - list{Attrs.class_("w-20 text-right text-gray-400 font-mono")}, - list{text(Float.toFixed(f.fuzzHours, ~digits=1))}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ) - }, - }, - ) - } - } - }, - // Error display - switch vd.error { - | Some(e) => - div( - list{ - Attrs.class_( - "mt-4 p-3 bg-red-900/30 border border-red-700 rounded text-sm text-red-300", - ), - Attrs.role("alert"), - }, - list{text(e)}, - ) - | None => noNode - }, - }, - ), - }, - ) -} diff --git a/src/components/VerisimdbFeeds.affine b/src/components/VerisimdbFeeds.affine new file mode 100644 index 00000000..a52a07b7 --- /dev/null +++ b/src/components/VerisimdbFeeds.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VerisimdbFeeds; + +// TODO: Complete semantic implementation diff --git a/src/components/VerisimdbFeeds.res b/src/components/VerisimdbFeeds.res deleted file mode 100644 index ee0ef10b..00000000 --- a/src/components/VerisimdbFeeds.res +++ /dev/null @@ -1,263 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL VeriSimDB Feeds Component — cross-repo analytics health and flow. -/// -/// Two-column layout: left sidebar with feed list, right content with -/// detail view showing feed health, record count, and throughput. - -open Model -open Msg -open Tea.Html - -/// Render a feed health badge. -let healthBadge = (health: feedHealth): Tea_Vdom.t => { - let (color, label) = switch health { - | FeedHealthy => ("text-green-400", "Healthy") - | FeedStale => ("text-amber-400", "Stale") - | FeedError(reason) => ("text-red-400", "Error: " ++ reason) - | FeedUnknown => ("text-gray-500", "Unknown") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(label)}) -} - -/// Render a feed row in the sidebar. -let feedRow = (feed: dataFeed, selected: bool): Tea_Vdom.t => { - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-2 border-b border-gray-800 hover:bg-gray-800/60 transition-colors " ++ if ( - selected - ) { - "bg-gray-800/80 border-l-2 border-l-blue-500" - } else { - "" - }, - ), - Events.onClick(VerisimdbFeeds(SelectFeed(feed.feedId))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(feed.name)}), - healthBadge(feed.health), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 mt-0.5")}, - list{text(`${Int.toString(feed.recordCount)} records`)}, - ), - }, - ) -} - -/// Render a tab button. -let tabBtn = (current: verisimdbFeedsTab, target: verisimdbFeedsTab, label: string): Tea_Vdom.t< - msg, -> => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(VerisimdbFeeds(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the VeriSimDB Feeds panel. -let view = (state: verisimdbFeedsState): Tea_Vdom.t => { - let healthy = VerisimdbFeedsEngine.healthyFeedCount(state.feeds) - let total = Array.length(state.feeds) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("VeriSimDB Feeds — Data Feed Health and Flow"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-cyan-300")}, - list{text("VeriSimDB Feeds")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(`${Int.toString(healthy)}/${Int.toString(total)} healthy`)}, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600"), - Events.onClick(VerisimdbFeeds(CheckFeeds)), - KeyboardNav.onActivate(VerisimdbFeeds(CheckFeeds)), - }, - list{ - text( - if state.checking { - "Checking..." - } else { - "Check Health" - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - VerisimdbFeedsEngine.allTabs - ->Array.map(t => tabBtn(state.activeTab, t, VerisimdbFeedsEngine.tabLabel(t))) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar - div( - list{Attrs.class_("w-64 border-r border-gray-800 overflow-y-auto")}, - state.feeds - ->Array.map(f => feedRow(f, state.selectedFeed == Some(f.feedId))) - ->List.fromArray, - ), - // Right content - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.selectedFeed { - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a feed to view details")}, - ) - | Some(feedId) => - switch state.feeds->Array.find(f => f.feedId == feedId) { - | None => div(list{}, list{text("Feed not found")}) - | Some(feed) => - div( - list{}, - list{ - h3( - list{Attrs.class_("text-md font-semibold text-gray-200 mb-3")}, - list{text(feed.name)}, - ), - div( - list{Attrs.class_("space-y-2")}, - list{ - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Source:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300")}, - list{text(feed.source)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Health:")}, - ), - healthBadge(feed.health), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Records:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300")}, - list{text(Int.toString(feed.recordCount))}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Throughput:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300")}, - list{text(`${Float.toFixed(feed.throughput, ~digits=1)} rec/day`)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-xs text-gray-500 w-28")}, - list{text("Last Update:")}, - ), - span( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(feed.lastUpdate)}, - ), - }, - ), - }, - ), - }, - ) - } - }, - }, - ), - }, - ), - // Footer - div( - list{Attrs.class_("px-4 py-2 border-t border-gray-800 text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - VerisimdbFeedsEngine.totalRecords(state.feeds), - )} total records across all feeds`, - ), - }, - ), - }, - ) -} diff --git a/src/components/Vexometer.affine b/src/components/Vexometer.affine new file mode 100644 index 00000000..ff5f7ba0 --- /dev/null +++ b/src/components/Vexometer.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Vexometer; + +// TODO: Complete semantic implementation diff --git a/src/components/Vexometer.res b/src/components/Vexometer.res deleted file mode 100644 index e1dd27f4..00000000 --- a/src/components/Vexometer.res +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Vexometer Component -/// -/// Measures and displays the "Friction of Things" - the cognitive load -/// on the Operator. Triggers anti-inflammatory UI adjustments when -/// vexation exceeds thresholds. - -open Model -open Msg -open Tea.Html - -/// Vexation level thresholds -let lowThreshold = 0.3 -let mediumThreshold = 0.5 -let highThreshold = 0.7 -let criticalThreshold = 0.9 - -/// Get colour based on vexation level -let getVexationColour = (index: float): string => { - if index >= criticalThreshold { - "text-red-400" - } else if index >= highThreshold { - "text-orange-400" - } else if index >= mediumThreshold { - "text-amber-400" - } else if index >= lowThreshold { - "text-yellow-400" - } else { - "text-emerald-400" - } -} - -/// Get bar colour based on vexation level -let getBarColour = (index: float): string => { - if index >= criticalThreshold { - "bg-red-500" - } else if index >= highThreshold { - "bg-orange-500" - } else if index >= mediumThreshold { - "bg-amber-500" - } else if index >= lowThreshold { - "bg-yellow-500" - } else { - "bg-emerald-500" - } -} - -/// Get status text based on vexation level -let getStatusText = (index: float, antiInflammatory: bool, inertia: bool): string => { - if inertia { - "Inertia Detected" - } else if antiInflammatory { - "Anti-Inflammatory Active" - } else if index >= criticalThreshold { - "Critical Vexation" - } else if index >= highThreshold { - "High Friction" - } else if index >= mediumThreshold { - "Moderate Friction" - } else if index >= lowThreshold { - "Mild Friction" - } else { - "Stable Co-Orbit" - } -} - -/// Render the expanded vexometer panel -let renderExpandedView = (state: vexometerState): Tea_Vdom.t => { - let indexPercent = Int.toString(Int.fromFloat(state.index *. 100.0)) - let barWidth = indexPercent ++ "%" - let barColour = getBarColour(state.index) - let textColour = getVexationColour(state.index) - let statusText = getStatusText(state.index, state.antiInflammatoryActive, state.inertiaDetected) - - div( - list{ - Attrs.class_( - "fixed bottom-4 right-4 w-64 bg-gray-900 border border-gray-700 rounded-lg p-4 shadow-xl", - ), - Attrs.role("meter"), - Attrs.ariaLabel("Vexation Index"), - Attrs.ariaLive("polite"), - Attrs.ariaValueNow(state.index *. 100.0), - Attrs.ariaValueMin(0.0), - Attrs.ariaValueMax(100.0), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - div( - list{Attrs.class_("text-sm font-semibold text-gray-300")}, - list{text("Vexation Index")}, - ), - div( - list{Attrs.class_(`text-lg font-bold ${textColour}`)}, - list{text(indexPercent ++ "%")}, - ), - }, - ), - // Progress bar - div( - list{Attrs.class_("h-3 bg-gray-800 rounded-full overflow-hidden mb-3")}, - list{ - div( - list{ - Attrs.class_(`h-full ${barColour} transition-all duration-500`), - Attrs.style("width", barWidth), - }, - list{}, - ), - }, - ), - // Status - div(list{Attrs.class_(`text-xs ${textColour} mb-3`)}, list{text(statusText)}), - // Metrics - div( - list{Attrs.class_("grid grid-cols-2 gap-2 text-xs")}, - list{ - div( - list{Attrs.class_("bg-gray-800/50 p-2 rounded")}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Cancellations")}), - div( - list{Attrs.class_("text-amber-300 font-mono")}, - list{text(Int.toString(state.recentCancellations))}, - ), - }, - ), - div( - list{Attrs.class_("bg-gray-800/50 p-2 rounded")}, - list{ - div(list{Attrs.class_("text-gray-500")}, list{text("Corrections")}), - div( - list{Attrs.class_("text-amber-300 font-mono")}, - list{text(Int.toString(state.recentCorrections))}, - ), - }, - ), - }, - ), - // Anti-inflammatory indicator - if state.antiInflammatoryActive { - div( - list{ - Attrs.class_( - "mt-3 p-2 bg-indigo-900/30 border border-indigo-700/50 rounded text-xs text-indigo-300", - ), - }, - list{text("Environment simplified to reduce friction")}, - ) - } else { - noNode - }, - // Inertia breaker prompt - if state.inertiaDetected { - div( - list{ - Attrs.class_( - "mt-3 p-2 bg-amber-900/30 border border-amber-700/50 rounded text-xs text-amber-300", - ), - }, - list{text("Stasis detected. Consider: What's the smallest next step?")}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the compact vexometer indicator -let renderCompactView = (state: vexometerState): Tea_Vdom.t => { - let indexPercent = Int.toString(Int.fromFloat(state.index *. 100.0)) - let barWidth = indexPercent ++ "%" - let barColour = getBarColour(state.index) - - div( - list{ - Attrs.class_("fixed bottom-4 right-4 w-32"), - Attrs.role("meter"), - Attrs.ariaLabel("Vexation Index"), - Attrs.ariaValueNow(state.index *. 100.0), - Attrs.ariaValueMin(0.0), - Attrs.ariaValueMax(100.0), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-1 flex items-center justify-between")}, - list{ - text("Vexation"), - div(list{Attrs.class_("text-gray-400")}, list{text(indexPercent ++ "%")}), - }, - ), - div( - list{Attrs.class_("h-2 bg-gray-800 rounded-full overflow-hidden")}, - list{ - div( - list{ - Attrs.class_(`h-full ${barColour} transition-all duration-300`), - Attrs.style("width", barWidth), - }, - list{}, - ), - }, - ), - }, - ) -} - -/// Main Vexometer view -let view = (state: vexometerState, expanded: bool): Tea_Vdom.t => { - if expanded { - renderExpandedView(state) - } else { - renderCompactView(state) - } -} diff --git a/src/components/VexometerFriction.affine b/src/components/VexometerFriction.affine new file mode 100644 index 00000000..999d2ed3 --- /dev/null +++ b/src/components/VexometerFriction.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VexometerFriction; + +// TODO: Complete semantic implementation diff --git a/src/components/VexometerFriction.res b/src/components/VexometerFriction.res deleted file mode 100644 index 28c9f5dc..00000000 --- a/src/components/VexometerFriction.res +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Vexometer Friction Component — irritation surface measurements across tools. -/// -/// Two-column layout: left sidebar with tool list (sorted by friction), -/// right content with ISA dimension detail view and trend indicators. - -open Model -open Msg -open Tea.Html - -/// Render a friction trend indicator. -let trendIndicator = (trend: frictionTrend): Tea_Vdom.t => { - let (color, arrow) = switch trend { - | Improving => ("text-green-400", "v") - | Stable => ("text-gray-400", "-") - | Worsening => ("text-red-400", "^") - | NoData => ("text-gray-600", "?") - } - span(list{Attrs.class_("text-xs font-mono " ++ color)}, list{text(arrow)}) -} - -/// Render a tool row in the sidebar. -let toolRow = (tool: toolFrictionProfile, selected: bool): Tea_Vdom.t => { - let scoreColor = if tool.overallScore < 3.0 { - "text-green-400" - } else if tool.overallScore < 6.0 { - "text-amber-400" - } else { - "text-red-400" - } - button( - list{ - Attrs.class_( - "w-full text-left px-3 py-2 border-b border-gray-800 hover:bg-gray-800/60 transition-colors " ++ if ( - selected - ) { - "bg-gray-800/80 border-l-2 border-l-blue-500" - } else { - "" - }, - ), - Events.onClick(VexometerFriction(SelectTool(tool.toolName))), - }, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-200 truncate")}, list{text(tool.toolName)}), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - span( - list{Attrs.class_("text-xs font-mono " ++ scoreColor)}, - list{text(Float.toFixed(tool.overallScore, ~digits=1))}, - ), - trendIndicator(tool.trend), - }, - ), - }, - ), - }, - ) -} - -/// Render an ISA dimension bar. -let dimensionBar = (dim: isaDimension): Tea_Vdom.t => { - let barColor = if dim.score < 3.0 { - "bg-green-500" - } else if dim.score < 6.0 { - "bg-amber-500" - } else { - "bg-red-500" - } - let pctWidth = Float.toFixed(dim.score *. 10.0, ~digits=1) - div( - list{Attrs.class_("flex items-center gap-2 py-1")}, - list{ - span(list{Attrs.class_("text-xs text-gray-400 w-28 shrink-0")}, list{text(dim.name)}), - div( - list{Attrs.class_("flex-1 bg-gray-800 rounded-full h-2")}, - list{ - div( - list{ - Attrs.class_(`${barColor} h-2 rounded-full transition-all duration-300`), - Attrs.style("width", `${pctWidth}%`), - }, - list{}, - ), - }, - ), - span( - list{Attrs.class_("text-xs text-gray-500 w-8 text-right")}, - list{text(Float.toFixed(dim.score, ~digits=1))}, - ), - span( - list{Attrs.class_("text-xs text-gray-600 w-10 text-right")}, - list{text(`(${Int.toString(dim.sampleCount)})`)}, - ), - }, - ) -} - -/// Render a tab button. -let tabBtn = ( - current: vexometerFrictionTab, - target: vexometerFrictionTab, - label: string, -): Tea_Vdom.t => { - let active = current == target - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded " ++ if active { - "bg-blue-600 text-white" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }, - ), - Events.onClick(VexometerFriction(SetTab(target))), - Attrs.role("tab"), - Attrs.ariaSelected(active), - }, - list{text(label)}, - ) -} - -/// Main view function for the Vexometer Friction panel. -let view = (state: vexometerFrictionState): Tea_Vdom.t => { - let avgFriction = VexometerFrictionEngine.averageFriction(state.tools) - let total = Array.length(state.tools) - - div( - list{ - Attrs.class_("flex flex-col h-full bg-gray-950 text-gray-100 overflow-hidden"), - Attrs.role("region"), - Attrs.ariaLabel("Vexometer Friction — Irritation Surface Measurements"), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-2 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - h2( - list{Attrs.class_("text-lg font-bold text-orange-300")}, - list{text("Vexometer Friction")}, - ), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - `Avg: ${Float.toFixed(avgFriction, ~digits=1)}/10.0 | ${Int.toString( - total, - )} tools`, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_("px-3 py-1 text-xs rounded bg-green-700 text-white hover:bg-green-600"), - Events.onClick(VexometerFriction(MeasureAll)), - KeyboardNav.onActivate(VexometerFriction(MeasureAll)), - }, - list{ - text( - if state.measuring { - "Measuring..." - } else { - "Measure All" - }, - ), - }, - ), - }, - ), - // Tabs - div( - list{Attrs.class_("flex gap-1 px-4 py-2 border-b border-gray-800"), Attrs.role("tablist")}, - VexometerFrictionEngine.allTabs - ->Array.map(t => tabBtn(state.activeTab, t, VexometerFrictionEngine.tabLabel(t))) - ->List.fromArray, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "mx-4 mt-2 px-3 py-2 bg-red-900/50 border border-red-700 rounded text-sm text-red-200", - ), - }, - list{text(err)}, - ) - | None => noNode - }, - // Two-column layout - div( - list{Attrs.class_("flex flex-1 overflow-hidden")}, - list{ - // Left sidebar — tool list sorted by friction - div( - list{Attrs.class_("w-64 border-r border-gray-800 overflow-y-auto")}, - VexometerFrictionEngine.sortByFriction(state.tools) - ->Array.map(t => toolRow(t, state.selectedTool == Some(t.toolName))) - ->List.fromArray, - ), - // Right content — ISA dimension detail - div( - list{Attrs.class_("flex-1 overflow-y-auto px-4 py-2")}, - list{ - switch state.selectedTool { - | None => - div( - list{Attrs.class_("flex items-center justify-center h-full text-gray-600")}, - list{text("Select a tool to view friction dimensions")}, - ) - | Some(name) => - switch state.tools->Array.find(t => t.toolName == name) { - | None => div(list{}, list{text("Tool not found")}) - | Some(tool) => - div( - list{}, - list{ - div( - list{Attrs.class_("flex items-center justify-between mb-3")}, - list{ - h3( - list{Attrs.class_("text-md font-semibold text-gray-200")}, - list{text(tool.toolName)}, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - span( - list{Attrs.class_("text-sm font-mono text-gray-300")}, - list{text(`${Float.toFixed(tool.overallScore, ~digits=1)}/10.0`)}, - ), - trendIndicator(tool.trend), - span( - list{Attrs.class_("text-xs text-gray-500")}, - list{text(VexometerFrictionEngine.trendLabel(tool.trend))}, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("space-y-1")}, - tool.dimensions->Array.map(d => dimensionBar(d))->List.fromArray, - ), - div( - list{Attrs.class_("mt-3 text-xs text-gray-600")}, - list{text(`Last measured: ${tool.lastMeasured}`)}, - ), - }, - ) - } - }, - }, - ), - }, - ), - // Footer - div( - list{Attrs.class_("px-4 py-2 border-t border-gray-800 text-xs text-gray-500")}, - list{ - text( - `${Int.toString( - state.tools->Array.filter(t => t.trend == Worsening)->Array.length, - )} tools worsening`, - ), - }, - ), - }, - ) -} diff --git a/src/components/VideoCoordination.affine b/src/components/VideoCoordination.affine new file mode 100644 index 00000000..b98ac978 --- /dev/null +++ b/src/components/VideoCoordination.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VideoCoordination; + +// TODO: Complete semantic implementation diff --git a/src/components/VideoCoordination.res b/src/components/VideoCoordination.res deleted file mode 100644 index 016276e8..00000000 --- a/src/components/VideoCoordination.res +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// VideoCoordination Component — view rendering for the video transfer dashboard. -/// -/// Features: -/// - Batch progress tracking -/// - Quota usage visualisation (Daily 750GB limit) -/// - ARIA-live regions for status updates - -open Tea.Html -open VideoCoordinationModel -open VideoCoordinationEngine - -let viewBatch = batch => { - div( - list{Attrs.class_("p-4 bg-gray-900 rounded-lg border border-gray-800 mb-4")}, - list{ - div( - list{Attrs.class_("flex justify-between items-center mb-2")}, - list{ - span(list{Attrs.class_("text-sm font-medium text-gray-300")}, list{text(batch.source)}), - span( - list{ - Attrs.class_( - switch batch.status { - | "Active" => "text-blue-400" - | "Completed" => "text-green-400" - | _ => "text-gray-500" - }, - ), - }, - list{text(batch.status)}, - ), - }, - ), - // Progress Bar - div( - list{Attrs.class_("w-full bg-gray-800 rounded-full h-2 mb-2")}, - list{ - div( - list{ - Attrs.class_("bg-blue-600 h-2 rounded-full transition-all duration-500"), - Attrs.style("width", Float.toString(batch->batchProgress) ++ "%"), - Attrs.role("progressbar"), - Attrs.ariaValueNow(batch->batchProgress), - Attrs.ariaValueMin(0.0), - Attrs.ariaValueMax(100.0), - }, - list{}, - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-500 flex justify-between")}, - list{ - text( - batch.processedFiles->Belt.Int.toString ++ - (" / " ++ - batch.totalFiles->Belt.Int.toString), - ), - text(batch.failedFiles > 0 ? batch.failedFiles->Belt.Int.toString ++ " failed" : ""), - }, - ), - }, - ) -} - -let view = (state: videoCoordinationState) => { - div( - list{ - Attrs.class_("h-full flex flex-col bg-gray-950 p-6"), - Attrs.role("region"), - Attrs.ariaLabel("Video Transfer Coordination"), - }, - list{ - // Header - div( - list{Attrs.class_("flex justify-between items-end mb-8")}, - list{ - div( - list{}, - list{ - h2( - list{Attrs.class_("text-2xl font-bold text-gray-100 mb-1")}, - list{text("Video Coordination")}, - ), - p( - list{Attrs.class_("text-sm text-gray-500")}, - list{text("Orchestrating Drive to Photos migration")}, - ), - }, - ), - // Quota Indicator - div( - list{Attrs.class_("text-right")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider")}, - list{text("Daily Quota")}, - ), - div( - list{Attrs.class_("text-lg font-mono text-gray-300")}, - list{text("750 GB Limit")}, - ), - }, - ), - }, - ), - // Active Batches - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - state.activeBatches->List.length == 0 - ? list{ - div( - list{Attrs.class_("text-center text-gray-600 mt-20")}, - list{text("No active transfers. Start a batch via CLI or Provisioner.")}, - ), - } - : state.activeBatches->List.map(viewBatch), - ), - }, - ) -} diff --git a/src/components/VmInspector.affine b/src/components/VmInspector.affine new file mode 100644 index 00000000..b7bc9358 --- /dev/null +++ b/src/components/VmInspector.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VmInspector; + +// TODO: Complete semantic implementation diff --git a/src/components/VmInspector.res b/src/components/VmInspector.res deleted file mode 100644 index 553efaf6..00000000 --- a/src/components/VmInspector.res +++ /dev/null @@ -1,832 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL VM Inspector Component — renders the reversible VM visual debugger. -/// -/// The Debugger tab shows stack, memory, instruction listing, and step -/// controls (forward AND backward). The Timeline tab provides a scrubber -/// over execution history. Call Graph shows subroutine relationships. -/// Port I/O monitors SEND/RECV buffers. Statistics shows per-instruction -/// and per-tier execution counts. - -open Model -open Msg -open Tea.Html - -// ========================================================================= -// Helpers -// ========================================================================= - -/// Render the category tab bar. -let renderTabs = (active: vmInspectorCategory): Tea_Vdom.t => { - let tabs: array = [ - InspectorDebugger, - InspectorTimeline, - InspectorCallGraph, - InspectorPortIO, - InspectorStatistics, - ] - div( - list{Attrs.class_("flex gap-1 border-b border-gray-800 px-4")}, - tabs - ->Array.map(tab => { - let isActive = tab === active - let label = VmInspectorEngine.categoryLabel(tab) - button( - list{ - Attrs.class_( - `px-4 py-2 text-sm font-medium transition-colors rounded-t ${isActive - ? "bg-gray-800 text-orange-400 border-b-2 border-orange-400" - : "text-gray-500 hover:text-gray-300 hover:bg-gray-900"}`, - ), - Events.onClick(VmInspector(SetInspectorCategory(tab))), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ) -} - -/// Render the step controls toolbar. -let renderStepControls = (state: vmInspectorState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Step backward - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs rounded bg-amber-800 text-amber-200 hover:bg-amber-700 font-medium", - ), - Events.onClick(VmInspector(StepBackward)), - KeyboardNav.onActivate(VmInspector(StepBackward)), - }, - list{text("Step Back")}, - ), - // Step forward - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs rounded bg-emerald-800 text-emerald-200 hover:bg-emerald-700 font-medium", - ), - Events.onClick(VmInspector(StepForward)), - KeyboardNav.onActivate(VmInspector(StepForward)), - }, - list{text("Step Forward")}, - ), - // Run to breakpoint - button( - list{ - Attrs.class_( - `px-3 py-1.5 text-xs rounded font-medium ${state.running - ? "bg-red-800 text-red-200 hover:bg-red-700" - : "bg-blue-800 text-blue-200 hover:bg-blue-700"}`, - ), - Events.onClick( - VmInspector( - if state.running { - PauseVm - } else { - RunVm - }, - ), - ), - }, - list{ - text( - if state.running { - "Pause" - } else { - "Run" - }, - ), - }, - ), - // Reset - button( - list{ - Attrs.class_("px-3 py-1.5 text-xs rounded bg-gray-800 text-gray-400 hover:text-gray-200"), - Events.onClick(VmInspector(ResetVm)), - KeyboardNav.onActivate(VmInspector(ResetVm)), - }, - list{text("Reset")}, - ), - // Step counter - div( - list{Attrs.class_("ml-4 flex items-center gap-2")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("Step:")}), - span( - list{Attrs.class_("text-xs text-gray-300 font-mono")}, - list{text(Int.toString(state.totalSteps))}, - ), - span(list{Attrs.class_("text-xs text-gray-500 ml-2")}, list{text("PC:")}), - span( - list{Attrs.class_("text-xs text-orange-400 font-mono")}, - list{text(Int.toString(state.pc))}, - ), - }, - ), - }, - ) -} - -/// Render the stack visualisation. -let renderStack = (stack: array): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2 font-medium")}, - list{text(`Stack (${Int.toString(Array.length(stack))})`)}, - ), - if Array.length(stack) === 0 { - div(list{Attrs.class_("text-gray-600 text-xs italic")}, list{text("(empty)")}) - } else { - div( - list{Attrs.class_("space-y-0.5")}, - // Show stack top-first (reversed) - stack - ->Array.toReversed - ->Array.mapWithIndex((value, idx) => { - div( - list{ - Attrs.class_( - `flex items-center gap-2 font-mono text-sm px-2 py-0.5 rounded ${idx === 0 - ? "bg-orange-900/30 text-orange-300" - : "text-gray-300"}`, - ), - }, - list{ - span( - list{Attrs.class_("text-gray-600 w-6 text-right text-xs")}, - list{ - text( - if idx === 0 { - "TOS" - } else { - Int.toString(Array.length(stack) - 1 - idx) - }, - ), - }, - ), - span(list{}, list{text(Int.toString(value))}), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the memory grid. -let renderMemory = (memory: array): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2 font-medium")}, - list{text(`Memory (${Int.toString(Array.length(memory))} cells)`)}, - ), - if Array.length(memory) === 0 { - div(list{Attrs.class_("text-gray-600 text-xs italic")}, list{text("No memory allocated")}) - } else { - div( - list{Attrs.class_("grid grid-cols-8 gap-0.5")}, - memory - ->Array.map(cell => { - let bgClass = if cell.recentWrite { - "bg-red-900/50" - } else if cell.recentRead { - "bg-blue-900/50" - } else if cell.value !== 0 { - "bg-gray-800" - } else { - "bg-gray-900" - } - div( - list{ - Attrs.class_(`${bgClass} rounded px-1 py-0.5 text-center font-mono text-xs`), - Attrs.title( - `Address: ${Int.toString(cell.address)}, Value: ${Int.toString(cell.value)}`, - ), - }, - list{ - span( - list{ - Attrs.class_( - if cell.value !== 0 { - "text-gray-200" - } else { - "text-gray-700" - }, - ), - }, - list{text(Int.toString(cell.value))}, - ), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the instruction listing. -let renderInstructions = (instructions: array, pc: int): Tea_Vdom.t => { - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-3")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 mb-2 font-medium")}, - list{text(`Instructions (${Int.toString(Array.length(instructions))})`)}, - ), - if Array.length(instructions) === 0 { - div(list{Attrs.class_("text-gray-600 text-xs italic")}, list{text("No program loaded")}) - } else { - div( - list{Attrs.class_("space-y-0.5 max-h-64 overflow-auto")}, - instructions - ->Array.map(instr => { - let isCurrentPc = instr.index === pc - let tierColour = VmInspectorEngine.tierColour(instr.tier) - div( - list{ - Attrs.class_( - `flex items-center gap-2 font-mono text-sm px-2 py-0.5 rounded cursor-pointer hover:bg-gray-800 ${isCurrentPc - ? "bg-orange-900/40 border-l-2 border-orange-400" - : ""}`, - ), - Events.onClick(VmInspector(ToggleBreakpoint(instr.index))), - }, - list{ - // Breakpoint indicator - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${instr.hasBreakpoint - ? "bg-red-500" - : "bg-transparent"}`, - ), - }, - list{}, - ), - // Address - span( - list{Attrs.class_("text-gray-600 w-8 text-right text-xs")}, - list{text(Int.toString(instr.index))}, - ), - // Tier badge - span( - list{Attrs.class_(`text-xs ${tierColour} w-6`)}, - list{text(VmInspectorEngine.tierShortLabel(instr.tier))}, - ), - // Mnemonic - span( - list{ - Attrs.class_( - if isCurrentPc { - "text-orange-300 font-bold" - } else { - "text-gray-300" - }, - ), - }, - list{text(instr.mnemonic)}, - ), - // Execution count - if instr.executionCount > 0 { - span( - list{Attrs.class_("text-gray-600 text-xs ml-auto")}, - list{text(`x${Int.toString(instr.executionCount)}`)}, - ) - } else { - noNode - }, - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the main debugger view — stack + memory + instructions + controls. -let renderDebugger = (state: vmInspectorState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex flex-col")}, - list{ - // Step controls toolbar - div( - list{ - Attrs.class_( - "px-4 py-2 bg-gray-900/50 border-b border-gray-800 flex items-center justify-between", - ), - }, - list{ - renderStepControls(state), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Connection status - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - div( - list{ - Attrs.class_( - `w-2 h-2 rounded-full ${switch state.connection { - | VmLiveConnection => "bg-emerald-400" - | VmFileConnection(_) => "bg-blue-400" - | VmDisconnected => "bg-gray-600" - }}`, - ), - }, - list{}, - ), - span( - list{Attrs.class_("text-xs text-gray-400")}, - list{text(VmInspectorEngine.connectionLabel(state.connection))}, - ), - }, - ), - // Export snapshot button - button( - list{ - Attrs.class_( - "text-xs text-gray-500 hover:text-gray-300 px-2 py-1 rounded bg-gray-800", - ), - Events.onClick(VmInspector(ExportSnapshot)), - KeyboardNav.onActivate(VmInspector(ExportSnapshot)), - }, - list{text("Export State")}, - ), - // Multi-VM toggle - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded ${state.multiVmView - ? "bg-purple-800 text-purple-200" - : "bg-gray-800 text-gray-500"}`, - ), - Events.onClick(VmInspector(ToggleMultiVm)), - KeyboardNav.onActivate(VmInspector(ToggleMultiVm)), - }, - list{text("Multi-VM")}, - ), - // BoJ routing toggle - button( - list{ - Attrs.class_( - if state.bojRouting { - "px-3 py-1.5 text-xs bg-blue-700 text-white rounded" - } else { - "px-3 py-1.5 text-xs bg-gray-700 text-gray-300 rounded hover:bg-gray-600" - }, - ), - Attrs.ariaLabel( - if state.bojRouting { - "Disable BoJ routing" - } else { - "Enable BoJ routing" - }, - ), - Events.onClick(VmInspector(ToggleVmBojRouting)), - KeyboardNav.onActivate(VmInspector(ToggleVmBojRouting)), - }, - list{ - text( - if state.bojRouting { - "BoJ On" - } else { - "BoJ" - }, - ), - }, - ), - }, - ), - }, - ), - // Three-column layout: Stack | Instructions | Memory - div( - list{Attrs.class_("flex-1 overflow-auto p-4 grid grid-cols-3 gap-4")}, - list{ - renderStack(state.stack), - renderInstructions(state.instructions, state.pc), - renderMemory(state.memory), - }, - ), - }, - ) -} - -/// Render the execution timeline tab. -let renderTimeline = (state: vmInspectorState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-4")}, - list{text(`Execution Timeline (${Int.toString(Array.length(state.history))} snapshots)`)}, - ), - if Array.length(state.history) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No execution history. Step through instructions to build the timeline.")}, - ) - } else { - div( - list{}, - list{ - // Timeline scrubber - div( - list{Attrs.class_("mb-4")}, - list{ - div( - list{ - Attrs.class_("flex items-center justify-between text-xs text-gray-500 mb-1"), - }, - list{ - text("Step 0"), - text(`Step ${Int.toString(Array.length(state.history) - 1)}`), - }, - ), - div( - list{Attrs.class_("h-2 bg-gray-800 rounded-full relative")}, - list{ - div( - list{ - Attrs.class_("absolute top-0 left-0 h-2 bg-orange-500 rounded-full"), - Attrs.style( - "width", - `${if Array.length(state.history) > 0 { - Float.toString( - Int.toFloat(state.timelinePosition) /. - Int.toFloat(Array.length(state.history) - 1) *. 100.0, - ) - } else { - "0" - }}%`, - ), - }, - list{}, - ), - }, - ), - }, - ), - // Current snapshot details - switch state.history->Array.get(state.timelinePosition) { - | Some(snapshot) => - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-4")}, - list{ - div( - list{Attrs.class_("grid grid-cols-3 gap-4 text-sm")}, - list{ - div( - list{}, - list{ - div(list{Attrs.class_("text-gray-500 text-xs mb-1")}, list{text("Step")}), - div( - list{Attrs.class_("font-mono text-gray-200")}, - list{text(Int.toString(snapshot.step))}, - ), - }, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs mb-1")}, - list{text("Instruction")}, - ), - div( - list{Attrs.class_("font-mono text-orange-300")}, - list{text(snapshot.instructionMnemonic)}, - ), - }, - ), - div( - list{}, - list{ - div( - list{Attrs.class_("text-gray-500 text-xs mb-1")}, - list{text("Stack")}, - ), - div( - list{Attrs.class_("font-mono text-gray-200")}, - list{text(VmInspectorEngine.formatStack(snapshot.stack))}, - ), - }, - ), - }, - ), - }, - ) - | None => noNode - }, - // Navigation buttons - div( - list{Attrs.class_("flex items-center justify-center gap-4 mt-4")}, - list{ - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-gray-800 text-gray-300 hover:bg-gray-700", - ), - Events.onClick(VmInspector(SeekTimeline(0))), - }, - list{text("Start")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-gray-800 text-gray-300 hover:bg-gray-700", - ), - Events.onClick( - VmInspector( - SeekTimeline( - if state.timelinePosition > 0 { - state.timelinePosition - 1 - } else { - 0 - }, - ), - ), - ), - }, - list{text("Prev")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-gray-800 text-gray-300 hover:bg-gray-700", - ), - Events.onClick(VmInspector(SeekTimeline(state.timelinePosition + 1))), - }, - list{text("Next")}, - ), - button( - list{ - Attrs.class_( - "px-4 py-2 text-sm rounded bg-gray-800 text-gray-300 hover:bg-gray-700", - ), - Events.onClick(VmInspector(SeekTimeline(Array.length(state.history) - 1))), - }, - list{text("End")}, - ), - }, - ), - }, - ) - }, - }, - ) -} - -/// Render the port I/O tab. -let renderPortIO = (state: vmInspectorState): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-4")}, - list{text(`Port I/O (${Int.toString(Array.length(state.portLog))} entries)`)}, - ), - if Array.length(state.portLog) === 0 { - div( - list{Attrs.class_("text-center text-gray-600 text-sm py-8")}, - list{text("No port I/O recorded. Execute SEND/RECV instructions.")}, - ) - } else { - div( - list{Attrs.class_("space-y-0.5")}, - state.portLog - ->Array.map(entry => { - div( - list{ - Attrs.class_( - "flex items-center gap-3 font-mono text-sm px-3 py-1 rounded bg-gray-900/50", - ), - }, - list{ - span( - list{Attrs.class_("text-gray-600 w-12 text-right text-xs")}, - list{text(`@${Int.toString(entry.atStep)}`)}, - ), - span( - list{ - Attrs.class_( - if entry.isSend { - "text-red-400 w-12" - } else { - "text-emerald-400 w-12" - }, - ), - }, - list{ - text( - if entry.isSend { - "SEND" - } else { - "RECV" - }, - ), - }, - ), - span( - list{Attrs.class_("text-gray-500 w-12")}, - list{text(`port ${Int.toString(entry.port)}`)}, - ), - span(list{Attrs.class_("text-gray-300")}, list{text(Int.toString(entry.value))}), - }, - ) - }) - ->List.fromArray, - ) - }, - }, - ) -} - -/// Render the statistics tab — placeholder for charts. -let renderStatistics = (state: vmInspectorState): Tea_Vdom.t => { - let tiers: array = [ - TierArithmetic, - TierConditionals, - TierStackMemory, - TierSubroutines, - TierIO, - ] - div( - list{Attrs.class_("flex-1 overflow-auto p-6")}, - list{ - h3( - list{Attrs.class_("text-sm font-medium text-gray-300 mb-4")}, - list{text("Execution Statistics")}, - ), - // Tier usage cards - div( - list{Attrs.class_("grid grid-cols-5 gap-3 mb-6")}, - tiers - ->Array.mapWithIndex((tier, idx) => { - let count = switch state.tierCounts->Array.get(idx) { - | Some(c) => c - | None => 0 - } - let colour = VmInspectorEngine.tierColour(tier) - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-3 text-center")}, - list{ - div( - list{Attrs.class_(`text-xs ${colour} mb-1`)}, - list{text(VmInspectorEngine.tierShortLabel(tier))}, - ), - div( - list{Attrs.class_("text-lg font-bold text-gray-200")}, - list{text(Int.toString(count))}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-1")}, - list{text(VmInspectorEngine.tierLabel(tier))}, - ), - }, - ) - }) - ->List.fromArray, - ), - // Total steps - div( - list{Attrs.class_("bg-gray-900 rounded-lg border border-gray-800 p-4")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-sm text-gray-400")}, list{text("Total Steps")}), - span( - list{Attrs.class_("text-xl font-bold text-gray-200 font-mono")}, - list{text(Int.toString(state.totalSteps))}, - ), - }, - ), - }, - ), - }, - ) -} - -// ========================================================================= -// Main view -// ========================================================================= - -/// Render the VM Inspector panel as a full-screen overlay. -let view = (state: vmInspectorState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/98 z-40 flex flex-col"), - Attrs.role("dialog"), - Attrs.ariaLabel( - "VM Inspector — reversible VM visual debugger with step forward and backward", - ), - }, - list{ - // Header - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div( - list{ - Attrs.class_("w-6 h-6 rounded bg-orange-900 flex items-center justify-center"), - }, - list{ - span(list{Attrs.class_("text-orange-400 text-xs font-bold")}, list{text("VM")}), - }, - ), - div( - list{}, - list{ - h2( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("VM Inspector")}, - ), - div( - list{Attrs.class_("text-xs text-gray-500")}, - list{ - text( - "Reversible VM debugger — 23 instructions, 5 tiers, step forward and backward", - ), - }, - ), - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "text-gray-500 hover:text-gray-300 px-3 py-1.5 text-sm rounded bg-gray-800 hover:bg-gray-700", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - // Error banner - switch state.error { - | Some(err) => - div( - list{Attrs.class_("px-4 py-2 bg-red-950 border-b border-red-900")}, - list{ - div( - list{Attrs.class_("flex items-center justify-between")}, - list{ - span(list{Attrs.class_("text-red-400 text-sm")}, list{text(err)}), - button( - list{ - Attrs.class_("text-red-500 hover:text-red-400 text-xs"), - Events.onClick(VmInspector(DismissVmError)), - KeyboardNav.onActivate(VmInspector(DismissVmError)), - }, - list{text("Dismiss")}, - ), - }, - ), - }, - ) - | None => noNode - }, - // Tab bar - renderTabs(state.activeCategory), - // Content - switch state.activeCategory { - | InspectorDebugger => renderDebugger(state) - | InspectorTimeline => renderTimeline(state) - | InspectorCallGraph => - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Call graph visualisation — coming in Phase 2.")}, - ), - }, - ) - | InspectorPortIO => renderPortIO(state) - | InspectorStatistics => renderStatistics(state) - }, - }, - ) -} diff --git a/src/components/VoiceTag.affine b/src/components/VoiceTag.affine new file mode 100644 index 00000000..d5f041e7 --- /dev/null +++ b/src/components/VoiceTag.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module VoiceTag; + +// TODO: Complete semantic implementation diff --git a/src/components/VoiceTag.res b/src/components/VoiceTag.res deleted file mode 100644 index 70252bcf..00000000 --- a/src/components/VoiceTag.res +++ /dev/null @@ -1,506 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Code MRI — VoiceTag Component (Layer 0) -/// -/// View layer for the tag management panel. Renders: -/// - Tag list with type badges, line ranges, and attribution -/// - Voice input controls (start/stop listening, transcript display) -/// - Tag creation form (keyboard fallback for non-voice input) -/// - Filter controls (by type, resolved/unresolved) -/// - Summary bar (total tags, unresolved count, AI vs human ratio) -/// -/// The component is designed as an ambient sidebar or panel overlay, -/// depending on how the user configures their layout. It always shows -/// the tags for the currently active file. -/// -/// STANDALONE-FIRST: This UI is one consumer of the .mri.json format. -/// The same tags can be viewed/edited by CLI tools, VS Code extensions, -/// or any JSON-aware tool. PanLL adds voice input and agentic integration. - -open Model -open Msg -open Tea.Html - -// =========================================================================== -// Tag type badge — coloured pill showing tag category -// =========================================================================== - -/// Render a tag type badge (coloured pill). -let renderTagBadge = (tagType: mriTagType): Tea_Vdom.t => { - let (bg, text_, border) = VoiceTagEngine.tagTypeColour(tagType) - let label = VoiceTagEngine.tagTypeShort(tagType) - let isModal = VoiceTagEngine.isModalTag(tagType) - span( - list{ - Attrs.class_( - `text-xs px-1.5 py-0.5 rounded border ${bg} ${text_} ${border} ${isModal - ? "ring-1 ring-pink-500/30" - : ""}`, - ), - Attrs.title( - VoiceTagEngine.tagTypeLabel(tagType) ++ ( - isModal ? " (modal — affects system behaviour)" : "" - ), - ), - }, - list{text(label)}, - ) -} - -// =========================================================================== -// Attribution display — who created the tag and how -// =========================================================================== - -/// Render attribution info (agent + method). -let renderAttribution = (attr: mriAttribution): Tea_Vdom.t => { - let methodStr = VoiceTagEngine.methodLabel(attr.method) - let isAi = attr.agent !== "human" - let agentColour = isAi ? "text-amber-400" : "text-emerald-400" - span( - list{Attrs.class_("text-xs text-gray-500 flex items-center gap-1")}, - list{ - span(list{Attrs.class_(agentColour)}, list{text(attr.agent)}), - span(list{Attrs.class_("text-gray-700")}, list{text("·")}), - span(list{}, list{text(methodStr)}), - }, - ) -} - -// =========================================================================== -// Single tag row -// =========================================================================== - -/// Render a single tag in the tag list. -let renderTag = (tag: mriTag, isSelected: bool): Tea_Vdom.t => { - let selectedClass = isSelected - ? "bg-gray-800/70 border-indigo-600" - : "border-gray-800/50 hover:bg-gray-800/30" - let resolvedClass = tag.resolved ? "opacity-60" : "" - let lineStr = - tag.startLine === tag.endLine - ? `L${Int.toString(tag.startLine)}` - : `L${Int.toString(tag.startLine)}-${Int.toString(tag.endLine)}` - - div( - list{ - Attrs.class_( - `flex items-center gap-2 px-3 py-2 border-b ${selectedClass} ${resolvedClass} transition-colors cursor-pointer`, - ), - Attrs.ariaLabel(VoiceTagEngine.tagAriaLabel(tag)), - Events.onClick(VoiceTag(SelectTag(Some(tag.id)))), - }, - list{ - // Tag number (for voice reference: "show tag 3") - span( - list{Attrs.class_("text-xs text-gray-600 w-6 text-right font-mono")}, - list{text(`#${Int.toString(tag.id)}`)}, - ), - // Tag type badge - renderTagBadge(tag.tagType), - // Line range - span(list{Attrs.class_("text-xs text-gray-500 font-mono w-16")}, list{text(lineStr)}), - // Message (if any) - div( - list{Attrs.class_("flex-1 min-w-0")}, - list{ - switch tag.message { - | Some(m) => - span(list{Attrs.class_("text-sm text-gray-300 truncate block")}, list{text(m)}) - | None => - span(list{Attrs.class_("text-sm text-gray-600 italic")}, list{text("(no message)")}) - }, - }, - ), - // Attribution - renderAttribution(tag.attribution), - // Resolved indicator - if tag.resolved { - span( - list{Attrs.class_("text-xs text-emerald-600"), Attrs.title("Resolved")}, - list{text("R")}, - ) - } else { - noNode - }, - // Actions: resolve, delete - div( - list{Attrs.class_("flex items-center gap-1 ml-2")}, - list{ - if !tag.resolved { - button( - list{ - Attrs.class_( - "text-xs px-1.5 py-0.5 text-gray-500 hover:text-emerald-400 hover:bg-gray-800 rounded transition-colors", - ), - Attrs.title("Resolve tag"), - Events.onClick(VoiceTag(ResolveTagById(tag.id))), - }, - list{text("R")}, - ) - } else { - noNode - }, - button( - list{ - Attrs.class_( - "text-xs px-1.5 py-0.5 text-gray-500 hover:text-red-400 hover:bg-gray-800 rounded transition-colors", - ), - Attrs.title("Delete tag"), - Events.onClick(VoiceTag(DeleteTagById(tag.id))), - }, - list{text("X")}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Voice controls — start/stop listening, status indicator -// =========================================================================== - -/// Render voice input controls. -let renderVoiceControls = (voice: voiceState): Tea_Vdom.t => { - let (statusText, statusClass, buttonLabel, buttonAction) = switch voice { - | VoiceOff => ("Off", "text-gray-600", "Start Voice", VoiceTag(StartVoice)) - | VoiceListening => ( - "Listening...", - "text-emerald-400 animate-pulse", - "Stop", - VoiceTag(StopVoice), - ) - | VoiceProcessing(transcript) => ( - `Processing: "${transcript}"`, - "text-amber-400", - "Cancel", - VoiceTag(StopVoice), - ) - | VoiceError(err) => (`Error: ${err}`, "text-red-400", "Retry", VoiceTag(StartVoice)) - } - - div( - list{Attrs.class_("flex items-center gap-3 px-4 py-2 border-b border-gray-800 bg-gray-900/50")}, - list{ - // Voice status indicator - div( - list{Attrs.class_("flex items-center gap-2 flex-1")}, - list{ - // Mic icon (simple text indicator) - span( - list{ - Attrs.class_( - switch voice { - | VoiceListening => "text-emerald-400 text-lg" - | _ => "text-gray-600 text-lg" - }, - ), - }, - list{text("M")}, - ), - span(list{Attrs.class_(`text-xs ${statusClass}`)}, list{text(statusText)}), - }, - ), - // Voice toggle button - button( - list{ - Attrs.class_( - "px-3 py-1 text-xs rounded transition-colors " ++ - switch voice { - | VoiceListening => "bg-red-900/50 text-red-300 hover:bg-red-800/50 border border-red-700" - | _ => "bg-gray-800 text-gray-300 hover:bg-gray-700 border border-gray-700" - }, - ), - Events.onClick(buttonAction), - Attrs.ariaLabel(buttonLabel), - }, - list{text(buttonLabel)}, - ), - }, - ) -} - -// =========================================================================== -// Summary bar — aggregate stats for the current file -// =========================================================================== - -/// Render the summary statistics bar. -let renderSummary = (summary: mriFileSummary): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "flex items-center gap-4 px-4 py-2 border-b border-gray-800 text-xs text-gray-500", - ), - }, - list{ - span(list{}, list{text(`${Int.toString(summary.totalTags)} tags`)}), - span(list{Attrs.class_("text-gray-700")}, list{text("|")}), - span( - list{Attrs.class_(summary.unresolvedTags > 0 ? "text-amber-400" : "")}, - list{text(`${Int.toString(summary.unresolvedTags)} open`)}, - ), - span(list{Attrs.class_("text-gray-700")}, list{text("|")}), - span(list{}, list{text(`${Int.toString(summary.humanTagCount)} human`)}), - span(list{}, list{text(`${Int.toString(summary.aiTagCount)} AI`)}), - if summary.careOnRegions > 0 { - span( - list{Attrs.class_("text-pink-400")}, - list{text(`${Int.toString(summary.careOnRegions)} care-on`)}, - ) - } else { - noNode - }, - if summary.ecoModeRegions > 0 { - span( - list{Attrs.class_("text-emerald-400")}, - list{text(`${Int.toString(summary.ecoModeRegions)} eco`)}, - ) - } else { - noNode - }, - }, - ) -} - -// =========================================================================== -// Filter controls — type filter, show resolved toggle -// =========================================================================== - -/// Render the filter bar. -let renderFilters = (filterType: option, showResolved: bool): Tea_Vdom.t => { - div( - list{Attrs.class_("flex items-center gap-2 px-4 py-2 border-b border-gray-800")}, - list{ - // Type filter buttons - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded transition-colors ${filterType === None - ? "bg-indigo-900/50 text-indigo-300 border border-indigo-700" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(VoiceTag(SetFilterType(None))), - }, - list{text("All")}, - ), - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded transition-colors ${filterType === Some(Todo) - ? "bg-blue-900/50 text-blue-300 border border-blue-700" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(VoiceTag(SetFilterType(Some(Todo)))), - }, - list{text("TODO")}, - ), - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded transition-colors ${filterType === Some(Fixme) - ? "bg-red-900/50 text-red-300 border border-red-700" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(VoiceTag(SetFilterType(Some(Fixme)))), - }, - list{text("FIXME")}, - ), - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded transition-colors ${filterType === Some(Refactor) - ? "bg-amber-900/50 text-amber-300 border border-amber-700" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(VoiceTag(SetFilterType(Some(Refactor)))), - }, - list{text("REFACTOR")}, - ), - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded transition-colors ${filterType === Some(Review) - ? "bg-cyan-900/50 text-cyan-300 border border-cyan-700" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(VoiceTag(SetFilterType(Some(Review)))), - }, - list{text("REVIEW")}, - ), - // Spacer - div(list{Attrs.class_("flex-1")}, list{}), - // Show resolved toggle - button( - list{ - Attrs.class_( - `text-xs px-2 py-1 rounded transition-colors ${showResolved - ? "bg-gray-700 text-gray-300" - : "text-gray-500 hover:text-gray-300"}`, - ), - Events.onClick(VoiceTag(ToggleShowResolved)), - KeyboardNav.onActivate(VoiceTag(ToggleShowResolved)), - }, - list{text(showResolved ? "Hide Resolved" : "Show Resolved")}, - ), - }, - ) -} - -// =========================================================================== -// Header — file name, controls, close button -// =========================================================================== - -/// Render the panel header. -let renderHeader = (currentFile: option): Tea_Vdom.t => { - let fileName = switch currentFile { - | Some(f) => f - | None => "(no file)" - } - div( - list{Attrs.class_("flex items-center justify-between px-4 py-3 border-b border-gray-800")}, - list{ - // Title - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - div(list{Attrs.class_("text-lg font-medium text-gray-200")}, list{text("Code MRI")}), - span(list{Attrs.class_("text-xs text-gray-500")}, list{text("VoiceTag")}), - span( - list{Attrs.class_("text-xs text-gray-600 font-mono truncate max-w-xs")}, - list{text(fileName)}, - ), - }, - ), - // Controls - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Load tags for current file - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-xs bg-gray-800 text-gray-300 rounded hover:bg-gray-700 border border-gray-700 transition-colors", - ), - Events.onClick(VoiceTag(LoadFileTags)), - KeyboardNav.onActivate(VoiceTag(LoadFileTags)), - Attrs.title("Reload tags from .mri.json"), - }, - list{text("Reload")}, - ), - // Close button - button( - list{ - Attrs.class_( - "px-3 py-1.5 text-sm text-gray-400 hover:text-gray-200 bg-gray-800 rounded hover:bg-gray-700 transition-colors", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Empty state — no tags yet -// =========================================================================== - -/// Render the empty state when no tags exist. -let renderEmpty = (): Tea_Vdom.t => { - div( - list{Attrs.class_("flex-1 flex items-center justify-center")}, - list{ - div( - list{Attrs.class_("text-center max-w-sm")}, - list{ - div(list{Attrs.class_("text-gray-500 text-lg mb-2")}, list{text("No tags yet")}), - div( - list{Attrs.class_("text-gray-600 text-sm mb-4")}, - list{ - text( - "Use voice commands or the keyboard to annotate code regions. Tags are saved as .mri.json sidecars — portable, no PanLL required.", - ), - }, - ), - div( - list{Attrs.class_("text-xs text-gray-700 space-y-1")}, - list{ - div(list{}, list{text("Voice: \"line 24 to 34 tag todo fix this later\"")}), - div(list{}, list{text("Voice: \"tag fixme needs error handling\"")}), - div(list{}, list{text("Voice: \"resolve tag 3\"")}), - }, - ), - }, - ), - }, - ) -} - -// =========================================================================== -// Main view — full-screen panel overlay -// =========================================================================== - -/// Main VoiceTag panel view — renders as a full-screen overlay. -/// -/// Layout: -/// Header (title, file name, reload, close) -/// Voice controls (mic toggle, status, transcript) -/// Summary bar (tag counts, human/AI ratio) -/// Filter bar (type filter, resolved toggle) -/// Tag list (scrollable) -let view = (vt: voiceTagState): Tea_Vdom.t => { - // Apply filters to the tag list. - let filteredTags = { - let byType = switch vt.filterType { - | Some(t) => VoiceTagEngine.filterByType(vt.tags, t) - | None => vt.tags - } - if vt.showResolved { - byType - } else { - VoiceTagEngine.filterUnresolved(byType) - } - } - - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 flex flex-col"), - Attrs.ariaLabel("Code MRI VoiceTag panel"), - }, - list{ - // Header - renderHeader(vt.currentFile), - // Voice controls - renderVoiceControls(vt.voice), - // Summary bar - renderSummary(vt.summary), - // Filter bar - renderFilters(vt.filterType, vt.showResolved), - // Error display - switch vt.error { - | Some(err) => - div( - list{ - Attrs.class_("px-4 py-2 bg-red-900/30 border-b border-red-800 text-xs text-red-400"), - }, - list{text(err)}, - ) - | None => noNode - }, - // Tag list (scrollable) - if Array.length(filteredTags) === 0 { - renderEmpty() - } else { - div( - list{Attrs.class_("flex-1 overflow-y-auto")}, - filteredTags - ->Array.map(tag => renderTag(tag, vt.selectedTagId === Some(tag.id))) - ->List.fromArray, - ) - }, - }, - ) -} diff --git a/src/components/WiringInspector.affine b/src/components/WiringInspector.affine new file mode 100644 index 00000000..316bfcc6 --- /dev/null +++ b/src/components/WiringInspector.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module WiringInspector; + +// TODO: Complete semantic implementation diff --git a/src/components/WiringInspector.res b/src/components/WiringInspector.res deleted file mode 100644 index f7f4b08a..00000000 --- a/src/components/WiringInspector.res +++ /dev/null @@ -1,799 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Constraint Audit Dashboard — Phase 5 Audit & Operator Trust. -/// -/// Upgraded from the original Wiring Inspector panel to a full audit -/// dashboard. Shows lifecycle state distribution, health scores, -/// bottleneck analysis, and per-state panel breakdowns using PCC -/// constraint data. -/// -/// Tabs: Overview | By State | Bottlenecks | History -/// -/// Layout: -/// - Header: title, health badge, Run Audit button, Close button -/// - Tab bar: four audit tabs with active indicator -/// - Content: tab-specific sub-views -/// -/// Uses Tea_Html pattern (NOT JSX): div(list{attrs}, list{children}) - -open Model -open Msg -open Tea.Html - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Health score badge -// ════════════════════════════════════════════════════════════════════════ - -/// Render the health score as a coloured badge. -let renderHealthBadge = (dist: stateDistribution): Tea_Vdom.t => { - let score = WiringInspectorEngine.healthScore(dist) - let colorClass = WiringInspectorEngine.healthScoreColor(score) - let bgClass = WiringInspectorEngine.healthScoreBgColor(score) - span( - list{ - Attrs.class_(`text-sm font-medium px-3 py-1 rounded border ${colorClass} ${bgClass}`), - Attrs.ariaLabel(`Health score: ${Int.toString(score)} percent`), - }, - list{text(`${Int.toString(score)}% healthy`)}, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Repairability badge -// ════════════════════════════════════════════════════════════════════════ - -/// Render a repairability badge. -let repairBadge = (r: WiringInspectorModel.repairability): Tea_Vdom.t => - span( - list{ - Attrs.class_( - `text-xs px-1.5 py-0.5 rounded ${WiringInspectorEngine.repairabilityColor( - r, - )} bg-gray-800 border border-gray-700`, - ), - }, - list{text(WiringInspectorEngine.repairabilityLabel(r))}, - ) - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Header bar -// ════════════════════════════════════════════════════════════════════════ - -/// Render the top header with title, health badge, timestamp, and action buttons. -let renderHeader = (state: wiringInspectorState): Tea_Vdom.t => - div( - list{ - Attrs.class_("flex items-center justify-between px-6 py-4 border-b border-gray-800"), - Attrs.role("banner"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - span( - list{Attrs.class_("text-lg font-medium text-gray-200")}, - list{text("Constraint Audit Dashboard")}, - ), - // Health score badge (only when results exist) - if Array.length(state.results) > 0 { - renderHealthBadge(state.distribution) - } else { - noNode - }, - // Last run timestamp - switch state.lastRunAt { - | Some(ts) => - span(list{Attrs.class_("text-xs text-gray-500")}, list{text(`Last run: ${ts}`)}) - | None => noNode - }, - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - // Run Audit button - button( - list{ - Attrs.class_( - "px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-500 transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed", - ), - Events.onClick(WiringInspector(RunVerification)), - KeyboardNav.onActivate(WiringInspector(RunVerification)), - Attrs.disabled(state.loading), - Attrs.ariaLabel("Run PCC audit against all panel contracts"), - }, - list{ - text( - if state.loading { - "Auditing..." - } else { - "Run Audit" - }, - ), - }, - ), - // Close button - button( - list{ - Attrs.class_( - "px-3 py-2 bg-gray-800 text-gray-300 rounded hover:bg-gray-700 transition-colors text-sm", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - Attrs.ariaLabel("Close Constraint Audit Dashboard"), - }, - list{text("Close")}, - ), - }, - ), - }, - ) - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Tab bar -// ════════════════════════════════════════════════════════════════════════ - -/// Render the tab bar with four audit tabs. -let renderTabBar = (activeTab: auditTab): Tea_Vdom.t => { - let allTabs: array = [Overview, ByState, Bottlenecks, History] - div( - list{ - Attrs.class_("flex border-b border-gray-800 px-6"), - Attrs.role("tablist"), - Attrs.ariaLabel("Audit dashboard tabs"), - }, - allTabs - ->Array.map(tab => { - let isActive = tab == activeTab - let label = WiringInspectorEngine.tabLabel(tab) - button( - list{ - Attrs.class_( - `px-4 py-2.5 text-sm font-medium transition-colors border-b-2 -mb-px ${if isActive { - "text-indigo-400 border-indigo-500" - } else { - "text-gray-500 border-transparent hover:text-gray-300 hover:border-gray-600" - }}`, - ), - Events.onClick(WiringInspector(SetAuditTab(tab))), - Attrs.role("tab"), - Attrs.ariaSelected(isActive), - Attrs.ariaLabel(`${label} tab`), - }, - list{text(label)}, - ) - }) - ->List.fromArray, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: State distribution bar -// ════════════════════════════════════════════════════════════════════════ - -/// Render a single segment of the stacked state distribution bar. -let renderSegment = ( - label: string, - count: int, - total: int, - colorClass: string, - bgClass: string, -): Tea_Vdom.t => { - if count == 0 || total == 0 { - noNode - } else { - let pct = Int.toFloat(count) *. 100.0 /. Int.toFloat(total) - let widthStr = Float.toString(pct) - div( - list{ - Attrs.class_( - `${bgClass} flex items-center justify-center py-2 text-xs font-medium ${colorClass} overflow-hidden`, - ), - Attrs.style("width", `${widthStr}%`), - Attrs.ariaLabel(`${label}: ${Int.toString(count)} panels`), - }, - list{ - if pct > 8.0 { - text(`${label} ${Int.toString(count)}`) - } else if pct > 3.0 { - text(Int.toString(count)) - } else { - noNode - }, - }, - ) - } -} - -/// Render the full horizontal stacked distribution bar. -let renderDistributionBar = (dist: stateDistribution): Tea_Vdom.t => - div( - list{ - Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4 mb-4"), - Attrs.ariaLabel("Panel state distribution"), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-3")}, - list{text("State Distribution")}, - ), - div( - list{Attrs.class_("flex rounded overflow-hidden border border-gray-700")}, - list{ - renderSegment( - "Releasable", - dist.releasable, - dist.total, - "text-green-300", - "bg-green-800/60", - ), - renderSegment("Viable", dist.viable, dist.total, "text-cyan-300", "bg-cyan-800/60"), - renderSegment("Wired", dist.wired, dist.total, "text-blue-300", "bg-blue-800/60"), - renderSegment("Draft", dist.draft, dist.total, "text-yellow-300", "bg-yellow-800/60"), - renderSegment("Broken", dist.broken, dist.total, "text-red-300", "bg-red-800/60"), - }, - ), - // Legend - div( - list{Attrs.class_("flex gap-4 mt-3 text-xs text-gray-500 flex-wrap")}, - list{ - span( - list{}, - list{ - span( - list{Attrs.class_("inline-block w-2 h-2 rounded-full bg-green-500 mr-1")}, - list{}, - ), - text(`Releasable (${Int.toString(dist.releasable)})`), - }, - ), - span( - list{}, - list{ - span( - list{Attrs.class_("inline-block w-2 h-2 rounded-full bg-cyan-500 mr-1")}, - list{}, - ), - text(`Viable (${Int.toString(dist.viable)})`), - }, - ), - span( - list{}, - list{ - span( - list{Attrs.class_("inline-block w-2 h-2 rounded-full bg-blue-500 mr-1")}, - list{}, - ), - text(`Wired (${Int.toString(dist.wired)})`), - }, - ), - span( - list{}, - list{ - span( - list{Attrs.class_("inline-block w-2 h-2 rounded-full bg-yellow-500 mr-1")}, - list{}, - ), - text(`Draft (${Int.toString(dist.draft)})`), - }, - ), - span( - list{}, - list{ - span(list{Attrs.class_("inline-block w-2 h-2 rounded-full bg-red-500 mr-1")}, list{}), - text(`Broken (${Int.toString(dist.broken)})`), - }, - ), - }, - ), - }, - ) - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Quick stats grid -// ════════════════════════════════════════════════════════════════════════ - -/// Render a single quick stat card. -let statCard = (label: string, count: int, colorClass: string): Tea_Vdom.t => - div( - list{ - Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4 flex-1"), - Attrs.role("status"), - Attrs.ariaLabel(`${label}: ${Int.toString(count)}`), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-1")}, - list{text(label)}, - ), - div(list{Attrs.class_(`text-2xl font-light ${colorClass}`)}, list{text(Int.toString(count))}), - }, - ) - -/// Render the four quick stat cards. -let renderQuickStats = (dist: stateDistribution): Tea_Vdom.t => - div( - list{Attrs.class_("grid grid-cols-4 gap-3 mb-4")}, - list{ - statCard("Total Panels", dist.total, "text-gray-200"), - statCard("Releasable", dist.releasable, "text-green-400"), - statCard("Need Tests", dist.wired, "text-blue-400"), - statCard("Need Attention", dist.draft + dist.broken, "text-yellow-400"), - }, - ) - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Bottleneck row -// ════════════════════════════════════════════════════════════════════════ - -/// Render a single bottleneck row. -let renderBottleneckRow = (bn: bottleneck): Tea_Vdom.t => - div( - list{ - Attrs.class_( - "flex items-center gap-3 py-2 px-3 rounded hover:bg-gray-800/40 text-sm border-b border-gray-800/50", - ), - }, - list{ - // Panel name - span(list{Attrs.class_("text-gray-300 font-medium w-32 shrink-0")}, list{text(bn.panelId)}), - // Obligation kind - span(list{Attrs.class_("text-xs text-gray-600 w-20 shrink-0")}, list{text(`[${bn.kind}]`)}), - // Blocked count - span( - list{Attrs.class_("text-red-400 font-mono text-xs w-20 shrink-0")}, - list{text(`blocks ${Int.toString(bn.blockedCount)}`)}, - ), - // Repairability badge - repairBadge(bn.repairability), - // Message - span(list{Attrs.class_("text-gray-400 text-xs flex-1 truncate")}, list{text(bn.message)}), - // File - switch bn.file { - | Some(f) => - span(list{Attrs.class_("text-gray-500 text-xs font-mono truncate max-w-48")}, list{text(f)}) - | None => noNode - }, - }, - ) - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Top 5 bottlenecks card -// ════════════════════════════════════════════════════════════════════════ - -/// Render the top 5 bottlenecks card for the overview tab. -let renderTopBottlenecks = (bottlenecks: array): Tea_Vdom.t => { - let top5 = WiringInspectorEngine.topBottlenecks(bottlenecks, 5) - div( - list{ - Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-4 mb-4"), - Attrs.ariaLabel("Top 5 bottlenecks"), - }, - list{ - div( - list{Attrs.class_("text-xs text-gray-500 uppercase tracking-wider mb-3")}, - list{text("Top 5 Bottlenecks")}, - ), - if Array.length(top5) > 0 { - div(list{}, top5->Array.map(renderBottleneckRow)->List.fromArray) - } else { - div( - list{Attrs.class_("text-gray-600 text-sm py-4 text-center")}, - list{text("No bottlenecks found")}, - ) - }, - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Overview tab -// ════════════════════════════════════════════════════════════════════════ - -/// Render the Overview tab content. -let renderOverviewTab = (state: wiringInspectorState): Tea_Vdom.t => { - let dist = state.distribution - let score = WiringInspectorEngine.healthScore(dist) - let colorClass = WiringInspectorEngine.healthScoreColor(score) - div( - list{Attrs.role("tabpanel"), Attrs.ariaLabel("Overview tab content")}, - list{ - // Large health score display - div( - list{Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg p-6 mb-4 text-center")}, - list{ - div( - list{Attrs.class_(`text-5xl font-light ${colorClass} mb-2`)}, - list{text(`${Int.toString(score)}%`)}, - ), - div( - list{Attrs.class_("text-gray-500 text-sm uppercase tracking-wider")}, - list{text("Healthy")}, - ), - }, - ), - // State distribution bar - renderDistributionBar(dist), - // Top 5 bottlenecks - renderTopBottlenecks(state.bottlenecks), - // Quick stats - renderQuickStats(dist), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: By State tab -// ════════════════════════════════════════════════════════════════════════ - -/// Render a single panel row within a state section. -let renderStatePanelRow = (v: panelVerification): Tea_Vdom.t => - div( - list{Attrs.class_("flex items-center gap-3 py-2 px-3 text-sm hover:bg-gray-800/40 rounded")}, - list{ - // Panel name - span(list{Attrs.class_("text-gray-200 font-medium w-40 shrink-0")}, list{text(v.panelId)}), - // Obligation summary - span( - list{Attrs.class_("text-gray-500 text-xs w-24 shrink-0")}, - list{text(`${Int.toString(v.satisfied)}/${Int.toString(v.total)} satisfied`)}, - ), - // Primary bottleneck - switch v.primaryBottleneck { - | Some(bn) => - span(list{Attrs.class_("text-red-400 text-xs")}, list{text(`Bottleneck: ${bn}`)}) - | None => noNode - }, - // Next requirement - switch v.policy.nextRequirement { - | Some(req) => - span(list{Attrs.class_("text-amber-400 text-xs ml-auto")}, list{text(`Next: ${req}`)}) - | None => noNode - }, - }, - ) - -/// Render a collapsible section for a single lifecycle state. -let renderStateSection = ( - state: panelState, - panels: array, - isExpanded: bool, -): Tea_Vdom.t => { - let label = WiringInspectorEngine.stateLabel(state) - let colorClass = WiringInspectorEngine.stateColor(state) - let bgClass = WiringInspectorEngine.stateBgColor(state) - let borderClass = WiringInspectorEngine.stateBorderColor(state) - let count = Array.length(panels) - div( - list{Attrs.class_("mb-2 rounded-lg overflow-hidden border border-gray-800")}, - list{ - // Section header — clickable to expand/collapse - button( - list{ - Attrs.class_( - `w-full text-left px-4 py-3 flex items-center justify-between ${bgClass} hover:opacity-90 transition-opacity`, - ), - Events.onClick(WiringInspector(ToggleStateSection(state))), - Attrs.ariaExpanded(isExpanded), - Attrs.ariaLabel(`${label} section, ${Int.toString(count)} panels`), - Attrs.role("button"), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-3")}, - list{ - span(list{Attrs.class_(`font-medium ${colorClass}`)}, list{text(label)}), - span( - list{ - Attrs.class_(`text-xs px-2 py-0.5 rounded border ${borderClass} ${colorClass}`), - }, - list{text(Int.toString(count))}, - ), - }, - ), - span( - list{Attrs.class_("text-gray-500 text-sm")}, - list{ - text( - if isExpanded { - "[-]" - } else { - "[+]" - }, - ), - }, - ), - }, - ), - // Expanded panel list - if isExpanded && count > 0 { - div( - list{Attrs.class_("bg-gray-950/40 px-2 py-1 space-y-0.5")}, - panels->Array.map(renderStatePanelRow)->List.fromArray, - ) - } else if isExpanded { - div( - list{Attrs.class_("bg-gray-950/40 px-4 py-3 text-gray-600 text-sm text-center")}, - list{text("No panels in this state")}, - ) - } else { - noNode - }, - }, - ) -} - -/// Render the By State tab with five collapsible sections. -let renderByStateTab = (state: wiringInspectorState): Tea_Vdom.t => { - let results = state.results - let allStates: array = [Releasable, Viable, Wired, Draft, Broken] - div( - list{Attrs.role("tabpanel"), Attrs.ariaLabel("By State tab content")}, - allStates - ->Array.map(panelState => { - let panels = WiringInspectorEngine.panelsByState(results, panelState) - let sectionId = WiringInspectorEngine.stateLabel(panelState) - let isExpanded = state.selectedPanel == Some(sectionId) - renderStateSection(panelState, panels, isExpanded) - }) - ->List.fromArray, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: Bottlenecks tab -// ════════════════════════════════════════════════════════════════════════ - -/// Render the full bottleneck table header. -let renderBottleneckHeader = (): Tea_Vdom.t => - div( - list{ - Attrs.class_( - "flex items-center gap-3 py-2 px-3 text-xs text-gray-500 uppercase tracking-wider border-b border-gray-700 font-medium", - ), - Attrs.role("row"), - }, - list{ - span(list{Attrs.class_("w-32 shrink-0")}, list{text("Panel")}), - span(list{Attrs.class_("w-40 shrink-0")}, list{text("Obligation")}), - span(list{Attrs.class_("w-20 shrink-0")}, list{text("Kind")}), - span(list{Attrs.class_("w-20 shrink-0")}, list{text("Blocked")}), - span(list{Attrs.class_("w-16 shrink-0")}, list{text("Repair")}), - span(list{Attrs.class_("flex-1")}, list{text("Message")}), - span(list{Attrs.class_("w-48 shrink-0")}, list{text("File")}), - }, - ) - -/// Render a full bottleneck table row (more detailed than the overview row). -let renderFullBottleneckRow = (bn: bottleneck): Tea_Vdom.t => - div( - list{ - Attrs.class_( - "flex items-center gap-3 py-2 px-3 text-sm hover:bg-gray-800/40 border-b border-gray-800/50", - ), - Attrs.role("row"), - }, - list{ - span(list{Attrs.class_("text-gray-300 font-medium w-32 shrink-0")}, list{text(bn.panelId)}), - span( - list{Attrs.class_("text-gray-200 font-mono text-xs w-40 shrink-0 truncate")}, - list{text(bn.obligationId)}, - ), - span(list{Attrs.class_("text-gray-600 text-xs w-20 shrink-0")}, list{text(bn.kind)}), - span( - list{Attrs.class_("text-red-400 font-mono text-xs w-20 shrink-0")}, - list{text(Int.toString(bn.blockedCount))}, - ), - span(list{Attrs.class_("w-16 shrink-0")}, list{repairBadge(bn.repairability)}), - span(list{Attrs.class_("text-gray-400 text-xs flex-1 truncate")}, list{text(bn.message)}), - switch bn.file { - | Some(f) => - span( - list{Attrs.class_("text-gray-500 text-xs font-mono w-48 shrink-0 truncate")}, - list{text(f)}, - ) - | None => span(list{Attrs.class_("w-48 shrink-0")}, list{}) - }, - }, - ) - -/// Render the filter bar for the bottleneck table. -let renderBottleneckFilterBar = (state: wiringInspectorState): Tea_Vdom.t => { - let kindChip = (label: string, value: option, isActive: bool) => - button( - list{ - Attrs.class_( - `px-3 py-1 text-xs rounded-full border transition-colors ${if isActive { - "bg-indigo-600 border-indigo-500 text-white" - } else { - "bg-gray-800 border-gray-700 text-gray-400 hover:bg-gray-700" - }}`, - ), - Events.onClick(WiringInspector(SetFilterStatus(value))), - Attrs.ariaLabel(`Filter by: ${label}`), - }, - list{text(label)}, - ) - div( - list{Attrs.class_("flex items-center gap-2 mb-3 flex-wrap")}, - list{ - span(list{Attrs.class_("text-xs text-gray-500 mr-1")}, list{text("Filter:")}), - kindChip("All", None, state.filterStatus == None), - kindChip("Registry", Some("registry"), state.filterStatus == Some("registry")), - kindChip("Model", Some("model"), state.filterStatus == Some("model")), - kindChip("Msg", Some("msg"), state.filterStatus == Some("msg")), - kindChip("View", Some("view"), state.filterStatus == Some("view")), - kindChip("Test", Some("test"), state.filterStatus == Some("test")), - span(list{Attrs.class_("border-l border-gray-700 h-4 mx-1")}, list{}), - kindChip("Safe", Some("safe"), state.filterStatus == Some("safe")), - kindChip("Unsafe", Some("unsafe"), state.filterStatus == Some("unsafe")), - kindChip("Manual", Some("manual"), state.filterStatus == Some("manual")), - }, - ) -} - -/// Apply bottleneck filters to the full list. -let filterBottlenecks = (bottlenecks: array, filterStatus: option): array< - bottleneck, -> => - switch filterStatus { - | None => bottlenecks - | Some("safe") => bottlenecks->Array.filter(bn => bn.repairability == Safe) - | Some("unsafe") => bottlenecks->Array.filter(bn => bn.repairability == Unsafe) - | Some("manual") => bottlenecks->Array.filter(bn => bn.repairability == Manual) - | Some(kind) => bottlenecks->Array.filter(bn => bn.kind == kind) - } - -/// Render the Bottlenecks tab with the full table. -let renderBottlenecksTab = (state: wiringInspectorState): Tea_Vdom.t => { - let filtered = filterBottlenecks(state.bottlenecks, state.filterStatus) - div( - list{Attrs.role("tabpanel"), Attrs.ariaLabel("Bottlenecks tab content")}, - list{ - renderBottleneckFilterBar(state), - div( - list{ - Attrs.class_("bg-gray-900/60 border border-gray-800 rounded-lg overflow-hidden"), - Attrs.role("table"), - Attrs.ariaLabel("Bottleneck obligations table"), - }, - list{ - renderBottleneckHeader(), - if Array.length(filtered) > 0 { - div( - list{Attrs.role("rowgroup")}, - filtered->Array.map(renderFullBottleneckRow)->List.fromArray, - ) - } else { - div( - list{Attrs.class_("text-gray-600 text-sm py-8 text-center")}, - list{text("No bottlenecks match the current filter")}, - ) - }, - }, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-2 text-right")}, - list{ - text( - `${Int.toString(Array.length(filtered))} of ${Int.toString( - Array.length(state.bottlenecks), - )} bottlenecks shown`, - ), - }, - ), - }, - ) -} - -// ════════════════════════════════════════════════════════════════════════ -// Sub-view: History tab (placeholder) -// ════════════════════════════════════════════════════════════════════════ - -/// Render the History tab placeholder. -let renderHistoryTab = (): Tea_Vdom.t => - div( - list{ - Attrs.class_("text-center py-16"), - Attrs.role("tabpanel"), - Attrs.ariaLabel("History tab content"), - }, - list{ - div(list{Attrs.class_("text-gray-500 text-lg mb-2")}, list{text("Coming soon")}), - div( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Audit history tracking will show how panel health evolves over time.")}, - ), - }, - ) - -// ════════════════════════════════════════════════════════════════════════ -// Main view -// ════════════════════════════════════════════════════════════════════════ - -/// Main Constraint Audit Dashboard panel view (Phase 5). -let view = (state: wiringInspectorState): Tea_Vdom.t => - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950 overflow-auto z-50"), - Attrs.role("region"), - Attrs.ariaLabel("Constraint Audit Dashboard"), - }, - list{ - // Header bar - renderHeader(state), - // Tab bar - renderTabBar(state.activeTab), - // Content area - div( - list{Attrs.class_("max-w-6xl mx-auto px-6 py-6")}, - list{ - // Error banner - switch state.error { - | Some(err) => - div( - list{ - Attrs.class_( - "bg-red-900/30 border border-red-800 text-red-300 rounded-lg p-4 mb-4", - ), - Attrs.role("alert"), - }, - list{ - div(list{Attrs.class_("font-medium mb-1")}, list{text("Audit Error")}), - div(list{Attrs.class_("text-sm")}, list{text(err)}), - }, - ) - | None => noNode - }, - // Loading spinner - if state.loading { - div( - list{ - Attrs.class_("flex items-center justify-center py-12"), - Attrs.role("status"), - Attrs.ariaLabel("Audit in progress"), - }, - list{ - div( - list{ - Attrs.class_( - "animate-spin w-8 h-8 border-2 border-gray-700 border-t-indigo-500 rounded-full", - ), - }, - list{}, - ), - span( - list{Attrs.class_("ml-3 text-gray-400")}, - list{text("Running constraint audit...")}, - ), - }, - ) - } else if Array.length(state.results) == 0 { - // Empty state - div( - list{Attrs.class_("text-center py-16")}, - list{ - div( - list{Attrs.class_("text-gray-500 text-lg mb-2")}, - list{text("No audit results")}, - ), - div( - list{Attrs.class_("text-gray-600 text-sm")}, - list{text("Click \"Run Audit\" to check all panel contracts with PCC.")}, - ), - }, - ) - } else { - // Tab content - switch state.activeTab { - | Overview => renderOverviewTab(state) - | ByState => renderByStateTab(state) - | Bottlenecks => renderBottlenecksTab(state) - | History => renderHistoryTab() - } - }, - }, - ), - }, - ) diff --git a/src/components/Wizard.affine b/src/components/Wizard.affine new file mode 100644 index 00000000..b463cc80 --- /dev/null +++ b/src/components/Wizard.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Wizard; + +// TODO: Complete semantic implementation diff --git a/src/components/Wizard.res b/src/components/Wizard.res deleted file mode 100644 index 7a0d5cad..00000000 --- a/src/components/Wizard.res +++ /dev/null @@ -1,672 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Wizard Component — Guided plugin/panel creation wizard. - -open Model -open Msg -open Tea.Html - -let tierToString = (tier: ProvisionerModel.pluginTrustTier): string => { - switch tier { - | Teranga => "Teranga" - | Shield => "Shield" - | Ayo => "Ayo" - } -} - -/// Render type selection step -let renderSelectType = (state: WizardModel.wizardState): Tea_Vdom.t => { - div(list{}, list{ - h3(list{Attrs.class_("text-xl font-semibold mb-4 text-gray-200")}, list{text("What would you like to create?")}), - p(list{Attrs.class_("text-gray-400 mb-6")}, list{text("Choose whether to create a panel or a plugin")}), - - div(list{Attrs.class_("grid grid-cols-2 gap-4")}, list{ - // Panel option - button( - list{ - Attrs.class_( - `p-6 border-2 rounded-lg transition-all ${state.creationType === Some(WizardModel.CreatingPanel) ? "border-indigo-500 bg-indigo-900" : "border-gray-700 hover:border-gray-600"}` - ), - Events.onClick(Wizard(SetCreationType(WizardModel.CreatingPanel))), - }, - list{ - h4(list{Attrs.class_("text-lg font-semibold mb-2")}, list{text("Panel")}), - p(list{Attrs.class_("text-gray-400 text-sm")}, list{text("Create a new UI panel for PanLL")}), - } - ), - - // Plugin option - button( - list{ - Attrs.class_( - `p-6 border-2 rounded-lg transition-all ${state.creationType === Some(WizardModel.CreatingPlugin) ? "border-indigo-500 bg-indigo-900" : "border-gray-700 hover:border-gray-600"}` - ), - Events.onClick(Wizard(SetCreationType(WizardModel.CreatingPlugin))), - }, - list{ - h4(list{Attrs.class_("text-lg font-semibold mb-2")}, list{text("Plugin")}), - p(list{Attrs.class_("text-gray-400 text-sm")}, list{text("Create a new plugin cartridge")}), - } - ), - }), - }) -} - -/// Render capability selection step -let renderChooseCapabilities = (state: WizardModel.wizardState): Tea_Vdom.t => { - let categories = WizardModel.getCapabilityCategories() - let requiredDeps = WizardModel.getRequiredDependencies(state) - - div( - list{Attrs.class_("space-y-6")}, - List.concat( - list{ - h3(list{Attrs.class_("text-xl font-semibold text-gray-200")}, list{text("Select Capabilities")}), - p(list{Attrs.class_("text-gray-400")}, list{text("Choose what your component can do")}), - // Required dependencies warning - if requiredDeps->Array.length > 0 { - div(list{Attrs.class_("p-3 bg-yellow-900/30 rounded-lg")}, list{ - h4(list{Attrs.class_("font-semibold text-yellow-300 mb-2")}, list{text("Required Dependencies")}), - p(list{Attrs.class_("text-yellow-200 text-sm")}, list{ - text("The following capabilities require additional dependencies:"), - }), - ul( - list{Attrs.class_("list-disc list-inside mt-2 text-yellow-100 text-sm")}, - requiredDeps->Array.map(depId => li(list{}, list{text(depId)}))->List.fromArray - ), - }) - } else { - noNode - }, - }, - // Capability categories - categories->Array.map(category => { - let categoryCaps = WizardModel.getCapabilitiesByCategory(category) - - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("text-lg font-semibold mb-3 text-gray-200")}, list{text(category)}), - div( - list{Attrs.class_("grid grid-cols-1 md:grid-cols-2 gap-3")}, - categoryCaps->Array.map(cap => { - let isSelected = WizardModel.isCapabilitySelected(cap.id, state) - let hasDeps = cap.requiredDependencies->Array.length > 0 - - button( - list{ - Attrs.class_( - `p-3 border rounded transition-all flex flex-col ${isSelected ? "border-indigo-500 bg-indigo-900/30" : "border-gray-600 hover:border-gray-500"}` - ), - Events.onClick(Wizard(ToggleCapability(cap.id))), - }, - list{ - div(list{Attrs.class_("flex justify-between items-start")}, list{ - div(list{}, list{ - h5(list{Attrs.class_("font-medium text-gray-200")}, list{text(cap.name)}), - p(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{text(cap.description)}), - }), - if hasDeps { - span(list{ - Attrs.class_("text-xs bg-yellow-600 text-yellow-100 px-2 py-1 rounded-full ml-2") - }, list{text(`${cap.requiredDependencies->Array.length->Int.toString} deps`)}) - } else { - noNode - } - }), - if isSelected { - div(list{Attrs.class_("text-xs text-green-400 mt-2 self-end")}, list{text("✓ Selected")}) - } else { - noNode - } - } - ) - })->List.fromArray - ) - }) - })->List.fromArray - ) - ) -} - -/// Render dependency configuration step -let renderConfigureDependencies = (state: WizardModel.wizardState): Tea_Vdom.t => { - let requiredDeps = WizardModel.getRequiredDependencies(state) - let satisfiedDeps = state.dependencies->Array.map(dep => dep.pluginId) - - div(list{Attrs.class_("space-y-6")}, list{ - // Step header - h3(list{Attrs.class_("text-xl font-semibold text-gray-200")}, list{text("Configure Dependencies")}), - p(list{Attrs.class_("text-gray-400")}, list{text("Set up required dependencies for selected capabilities")}), - - // Dependency status - div(list{Attrs.class_("flex gap-4")}, list{ - div(list{Attrs.class_("flex-1")}, list{ - div(list{Attrs.class_("p-4 border border-gray-700 rounded-lg")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-2")}, list{text("Required Dependencies")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-3")}, list{ - text(`${requiredDeps->Array.length->Int.toString} dependencies needed`) - }), - if requiredDeps->Array.length > 0 { - ul( - list{Attrs.class_("space-y-2 text-sm")}, - requiredDeps->Array.map(depId => { - let isSatisfied = satisfiedDeps->Array.includes(depId) - li(list{ - Attrs.class_(`flex items-center ${isSatisfied ? "text-green-400" : "text-red-400"}`) - }, list{ - if isSatisfied { - span(list{Attrs.class_("mr-2")}, list{text("✓")}) - } else { - span(list{Attrs.class_("mr-2")}, list{text("✗")}) - }, - text(depId) - }) - })->List.fromArray - ) - } else { - p(list{Attrs.class_("text-green-400 text-sm")}, list{text("No dependencies required")}) - } - }) - }), - div(list{Attrs.class_("flex-1")}, list{ - div(list{Attrs.class_("p-4 border border-gray-700 rounded-lg")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-2")}, list{text("Configured Dependencies")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-3")}, list{ - text(`${state.dependencies->Array.length->Int.toString} dependencies configured`) - }), - if state.dependencies->Array.length > 0 { - table(list{Attrs.class_("w-full text-sm")}, list{ - thead(list{}, list{ - tr(list{}, list{ - th(list{Attrs.class_("text-left text-gray-400 pb-2")}, list{text("Plugin")}), - th(list{Attrs.class_("text-left text-gray-400 pb-2")}, list{text("Version")}), - th(list{Attrs.class_("text-left text-gray-400 pb-2")}, list{text("Tier")}), - }) - }), - tbody( - list{}, - state.dependencies->Array.map(dep => { - tr(list{Attrs.class_("border-t border-gray-800")}, list{ - td(list{Attrs.class_("py-2 text-gray-200")}, list{text(dep.pluginId)}), - td(list{Attrs.class_("py-2")}, list{ - input( - list{ - Attrs.class_("w-24 px-2 py-1 bg-gray-800 border border-gray-600 rounded text-sm"), - Attrs.value(dep.version), - Events.onInput(newVersion => Wizard(AddDependency(dep.pluginId, newVersion))), - }, - list{} - ) - }), - td(list{Attrs.class_("py-2")}, list{ - select( - list{ - Attrs.class_("px-2 py-1 bg-gray-800 border border-gray-600 rounded text-sm"), - Attrs.value(tierToString(dep.tier)), - }, - list{ - option'(list{Attrs.value("Teranga")}, list{text("Teranga")}), - option'(list{Attrs.value("Shield")}, list{text("Shield")}), - option'(list{Attrs.value("Ayo")}, list{text("Ayo")}), - } - ) - }) - }) - })->List.fromArray - ) - }) - } else { - p(list{Attrs.class_("text-gray-400 text-sm")}, list{text("No dependencies configured")}) - } - }) - }) - }), - - // Add dependency form - if requiredDeps->Array.length > 0 && !WizardModel.areDependenciesSatisfied(state) { - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-3")}, list{text("Add Missing Dependency")}), - - div(list{Attrs.class_("space-y-3")}, list{ - // Plugin selection - div(list{}, list{ - label(list{Attrs.class_("block text-sm text-gray-400 mb-1")}, list{text("Plugin")}), - select( - list{ - Attrs.class_("w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-sm"), - }, - List.concat( - list{option'(list{Attrs.value("")}, list{text("Select plugin...")})}, - requiredDeps->Array.map(depId => - option'(list{Attrs.value(depId)}, list{text(depId)}) - )->List.fromArray - ) - ) - }), - - // Version selection - div(list{}, list{ - label(list{Attrs.class_("block text-sm text-gray-400 mb-1")}, list{text("Version")}), - input( - list{ - Attrs.class_("w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-sm"), - Attrs.placeholder("1.0.0"), - }, - list{} - ) - }), - - // Trust tier - div(list{}, list{ - label(list{Attrs.class_("block text-sm text-gray-400 mb-1")}, list{text("Trust Tier")}), - select( - list{Attrs.class_("w-full px-3 py-2 bg-gray-800 border border-gray-600 rounded text-sm")}, - list{ - option'(list{Attrs.value("Ayo")}, list{text("Ayo (Community)")}), - option'(list{Attrs.value("Shield")}, list{text("Shield (Security)")}), - option'(list{Attrs.value("Teranga")}, list{text("Teranga (Core)")}), - } - ) - }), - - button( - list{ - Attrs.class_("mt-3 px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition"), - Attrs.disabled(!WizardModel.canProceed(state)), - }, - list{text("Add Dependency")} - ) - }) - }) - } else { - noNode - } - }) -} - -/// Render security setup step -let renderSetupSecurity = (state: WizardModel.wizardState): Tea_Vdom.t => { - div(list{Attrs.class_("space-y-6")}, list{ - // Step header - h3(list{Attrs.class_("text-xl font-semibold text-gray-200")}, list{text("Setup Security")}), - p(list{Attrs.class_("text-gray-400")}, list{text("Configure security and sandboxing for your component")}), - - // Trust tier selection - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-3")}, list{text("Trust Tier")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-3")}, list{ - text("Select the appropriate trust level for your component") - }), - - div(list{Attrs.class_("space-y-3")}, list{ - // Teranga (Core) - button( - list{ - Attrs.class_( - `w-full p-4 border-2 rounded-lg text-left transition-all ${state.securityConfig.trustTier === Teranga ? "border-yellow-500 bg-yellow-900/30" : "border-gray-600 hover:border-gray-500"}` - ), - Events.onClick(Wizard(SetTrustTier(Teranga))), - }, - list{ - div(list{Attrs.class_("flex justify-between items-center")}, list{ - div(list{}, list{ - h5(list{Attrs.class_("font-medium text-gray-200")}, list{text("Teranga - Core")}), - p(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{ - text("Core plugins, always available, high trust") - }), - }), - if state.securityConfig.trustTier === Teranga { - span(list{Attrs.class_("text-green-400")}, list{text("✓")}) - } else { - noNode - } - }) - } - ), - - // Shield (Security) - button( - list{ - Attrs.class_( - `w-full p-4 border-2 rounded-lg text-left transition-all ${state.securityConfig.trustTier === Shield ? "border-red-500 bg-red-900/30" : "border-gray-600 hover:border-gray-500"}` - ), - Events.onClick(Wizard(SetTrustTier(Shield))), - }, - list{ - div(list{Attrs.class_("flex justify-between items-center")}, list{ - div(list{}, list{ - h5(list{Attrs.class_("font-medium text-gray-200")}, list{text("Shield - Security")}), - p(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{ - text("Security-critical plugins, elevated trust") - }), - }), - if state.securityConfig.trustTier === Shield { - span(list{Attrs.class_("text-green-400")}, list{text("✓")}) - } else { - noNode - } - }) - } - ), - - // Ayo (Community) - button( - list{ - Attrs.class_( - `w-full p-4 border-2 rounded-lg text-left transition-all ${state.securityConfig.trustTier === Ayo ? "border-blue-500 bg-blue-900/30" : "border-gray-600 hover:border-gray-500"}` - ), - Events.onClick(Wizard(SetTrustTier(Ayo))), - }, - list{ - div(list{Attrs.class_("flex justify-between items-center")}, list{ - div(list{}, list{ - h5(list{Attrs.class_("font-medium text-gray-200")}, list{text("Ayo - Community")}), - p(list{Attrs.class_("text-xs text-gray-400 mt-1")}, list{ - text("Community plugins, standard trust") - }), - }), - if state.securityConfig.trustTier === Ayo { - span(list{Attrs.class_("text-green-400")}, list{text("✓")}) - } else { - noNode - } - }) - } - ), - }) - }), - - // Sandbox policy - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-3")}, list{text("Sandbox Policy")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-3")}, list{ - text("Configure resource access permissions") - }), - - div(list{Attrs.class_("space-y-4")}, list{ - // Network access - div(list{Attrs.class_("flex items-center justify-between p-3 border border-gray-700 rounded")}, list{ - div(list{}, list{ - h5(list{Attrs.class_("font-medium text-gray-200")}, list{text("Network Access")}), - p(list{Attrs.class_("text-xs text-gray-400")}, list{text("Allow outgoing network requests")}), - }), - label(list{Attrs.class_("relative inline-flex items-center cursor-pointer")}, list{ - input(list{ - Attrs.type_("checkbox"), - Attrs.class_("sr-only peer"), - Attrs.checked(state.securityConfig.networkAccess), - Events.onChange(_ => Wizard(ToggleNetworkAccess(!state.securityConfig.networkAccess))), - }, list{}), - div(list{Attrs.class_("w-11 h-6 bg-gray-600 rounded-full peer peer-checked:bg-indigo-600")}, list{}), - }) - }), - - // Filesystem access - div(list{Attrs.class_("flex items-center justify-between p-3 border border-gray-700 rounded")}, list{ - div(list{}, list{ - h5(list{Attrs.class_("font-medium text-gray-200")}, list{text("Filesystem Access")}), - p(list{Attrs.class_("text-xs text-gray-400")}, list{text("Allow filesystem read/write operations")}), - }), - label(list{Attrs.class_("relative inline-flex items-center cursor-pointer")}, list{ - input(list{ - Attrs.type_("checkbox"), - Attrs.class_("sr-only peer"), - Attrs.checked(state.securityConfig.filesystemAccess), - Events.onChange(_ => Wizard(ToggleFilesystemAccess(!state.securityConfig.filesystemAccess))), - }, list{}), - div(list{Attrs.class_("w-11 h-6 bg-gray-600 rounded-full peer peer-checked:bg-indigo-600")}, list{}), - }) - }), - }) - }), - - // Capability restrictions placeholder - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-3")}, list{text("Allowed Capabilities")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-3")}, list{ - text("Specify which capabilities this component can use") - }), - p(list{Attrs.class_("text-gray-500 text-sm")}, list{ - text("Capability restriction UI will be implemented here") - }) - }), - }) -} - -/// Render review and generate step -let renderReviewAndGenerate = (state: WizardModel.wizardState): Tea_Vdom.t => { - let capabilityCount = state.selectedCapabilities->Array.length - let dependencyCount = state.dependencies->Array.length - let isPanel = state.creationType === Some(WizardModel.CreatingPanel) - let validationErrors = WizardModel.validateWizardConfig(state) - - div(list{Attrs.class_("space-y-6")}, list{ - // Step header - h3(list{Attrs.class_("text-xl font-semibold text-gray-200")}, list{text("Review & Generate")}), - p(list{Attrs.class_("text-gray-400")}, list{ - text(`Review your ${isPanel ? "panel" : "plugin"} configuration before generation`) - }), - - // Summary cards - div(list{Attrs.class_("grid grid-cols-1 md:grid-cols-3 gap-4")}, list{ - // Type card - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-2")}, list{text("Type")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{text(isPanel ? "Panel" : "Plugin")}), - p(list{Attrs.class_("text-xs text-gray-500")}, list{text(isPanel ? "UI component" : "Backend cartridge")}), - }), - - // Capabilities card - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-2")}, list{text("Capabilities")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{ - text(`${capabilityCount->Int.toString} capabilities selected`) - }), - if capabilityCount > 0 { - div(list{}, list{ - ul( - list{Attrs.class_("text-xs text-gray-500 mt-2 space-y-1 list-disc list-inside")}, - state.selectedCapabilities - ->Array.slice(~start=0, ~end=3) - ->Array.map(capId => { - let name = WizardModel.capabilityRegistry - ->Array.find(c => c.id === capId) - ->Option.mapOr(capId, c => c.name) - li(list{}, list{text(name)}) - }) - ->List.fromArray - ), - if capabilityCount > 3 { - p(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{ - text(`... and ${Int.toString(capabilityCount - 3)} more`) - }) - } else { - noNode - } - }) - } else { - p(list{Attrs.class_("text-xs text-gray-500 mt-2")}, list{text("No capabilities selected")}) - } - }), - - // Dependencies card - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-2")}, list{text("Dependencies")}), - p(list{Attrs.class_("text-xs text-gray-400 mb-2")}, list{ - text(`${dependencyCount->Int.toString} dependencies configured`) - }), - if dependencyCount > 0 { - div(list{}, list{ - ul( - list{Attrs.class_("text-xs text-gray-500 mt-2 space-y-1 list-disc list-inside")}, - state.dependencies - ->Array.slice(~start=0, ~end=3) - ->Array.map(dep => li(list{}, list{text(`${dep.pluginId} v${dep.version}`)}) ) - ->List.fromArray - ), - if dependencyCount > 3 { - p(list{Attrs.class_("text-xs text-gray-500 mt-1")}, list{ - text(`... and ${Int.toString(dependencyCount - 3)} more`) - }) - } else { - noNode - } - }) - } else { - p(list{Attrs.class_("text-xs text-green-400 mt-2")}, list{text("No dependencies required")}) - } - }), - }), - - // Security card - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-2")}, list{text("Security")}), - div(list{Attrs.class_("space-y-3")}, list{ - div(list{}, list{ - p(list{Attrs.class_("text-xs text-gray-400")}, list{text("Trust Tier")}), - p(list{Attrs.class_("font-medium text-gray-200")}, list{ - text(switch state.securityConfig.trustTier { - | Teranga => "Teranga (Core)" - | Shield => "Shield (Security)" - | Ayo => "Ayo (Community)" - }) - }) - }), - div(list{}, list{ - p(list{Attrs.class_("text-xs text-gray-400")}, list{text("Network Access")}), - p(list{Attrs.class_("font-medium text-gray-200")}, list{ - text(state.securityConfig.networkAccess ? "Enabled" : "Disabled") - }) - }), - div(list{}, list{ - p(list{Attrs.class_("text-xs text-gray-400")}, list{text("Filesystem Access")}), - p(list{Attrs.class_("font-medium text-gray-200")}, list{ - text(state.securityConfig.filesystemAccess ? "Enabled" : "Disabled") - }) - }), - }) - }), - - // Validation status - div(list{Attrs.class_("border border-gray-700 rounded-lg p-4")}, list{ - h4(list{Attrs.class_("font-semibold text-gray-200 mb-3")}, list{text("Validation")}), - if validationErrors->Array.length === 0 { - div(list{Attrs.class_("text-green-400")}, list{ - span(list{Attrs.class_("mr-2")}, list{text("✓")}), - text("All checks passed - ready to generate!") - }) - } else { - div(list{}, list{ - div(list{Attrs.class_("text-red-400")}, list{ - span(list{Attrs.class_("mr-2")}, list{text("✗")}), - text("Please fix the following issues:") - }), - ul( - list{Attrs.class_("text-red-300 text-sm mt-2 list-disc list-inside")}, - validationErrors->Array.map(error => li(list{}, list{text(error)}))->List.fromArray - ), - }) - } - }), - - // Generate button - button( - list{ - Attrs.class_("mt-6 w-full px-6 py-3 bg-green-600 text-white rounded hover:bg-green-700 transition disabled:opacity-50 disabled:cursor-not-allowed"), - Attrs.disabled(validationErrors->Array.length > 0 || state.generating), - Events.onClick(Wizard(StartGeneration)), - }, - list{ - text(state.generating ? "Generating..." : "Generate Component") - } - ), - }) -} - -/// Dispatch to the correct step renderer -let renderStepContent = (state: WizardModel.wizardState): Tea_Vdom.t => { - switch state.currentStep { - | SelectType => renderSelectType(state) - | ChooseCapabilities => renderChooseCapabilities(state) - | ConfigureDependencies => renderConfigureDependencies(state) - | SetupSecurity => renderSetupSecurity(state) - | ReviewAndGenerate => renderReviewAndGenerate(state) - } -} - -/// Render the wizard component -let renderWizard = (state: model): Tea_Vdom.t => { - let wizardState = state.wizard - - div( - list{Attrs.class_("wizard-container p-6 bg-gray-900 rounded-lg max-w-2xl mx-auto")}, - list{ - // Header - div(list{Attrs.class_("flex justify-between items-center mb-6")}, list{ - h2(list{Attrs.class_("text-2xl font-bold text-gray-200")}, list{text("Plugin/Panel Creation Wizard")}), - button( - list{ - Attrs.class_("px-4 py-2 bg-gray-700 text-white rounded hover:bg-gray-600 transition"), - Events.onClick(Wizard(ResetWizard)), - }, - list{text("Reset")} - ), - }), - - // Step indicator - div( - list{Attrs.class_("flex gap-4 mb-6")}, - [ - WizardModel.SelectType, - WizardModel.ChooseCapabilities, - WizardModel.ConfigureDependencies, - WizardModel.SetupSecurity, - WizardModel.ReviewAndGenerate, - ]->Array.map(step => { - let isCurrent = wizardState.currentStep === step - let isCompleted = WizardModel.stepIndex(step) < WizardModel.stepIndex(wizardState.currentStep) - - div( - list{ - Attrs.class_( - `flex items-center gap-2 ${isCurrent ? "text-indigo-400" : isCompleted ? "text-green-400" : "text-gray-500"}` - ), - }, - list{ - div(list{Attrs.class_("w-6 h-6 rounded-full border-2 flex items-center justify-center text-xs")}, list{ - text(isCompleted ? "✓" : Int.toString(WizardModel.stepIndex(step) + 1)) - }), - text(WizardModel.stepLabel(step)), - } - ) - })->List.fromArray - ), - - // Step content - div(list{Attrs.class_("border-t border-gray-700 pt-6")}, list{ - renderStepContent(wizardState) - }), - - // Navigation - div(list{Attrs.class_("flex justify-between mt-8")}, list{ - button( - list{ - Attrs.class_("px-4 py-2 bg-gray-700 text-white rounded hover:bg-gray-600 transition disabled:opacity-50 disabled:cursor-not-allowed"), - Attrs.disabled(wizardState.currentStep === WizardModel.SelectType), - Events.onClick(Wizard(PreviousStep)), - }, - list{text("Back")} - ), - button( - list{ - Attrs.class_("px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed"), - Attrs.disabled(!WizardModel.canProceed(wizardState)), - Events.onClick(Wizard(NextStep)), - }, - list{text(WizardModel.isLastStep(wizardState.currentStep) ? "Finish" : "Next")} - ), - }), - } - ) -} - -let view = renderWizard diff --git a/src/components/Workspace.affine b/src/components/Workspace.affine new file mode 100644 index 00000000..eee74842 --- /dev/null +++ b/src/components/Workspace.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Workspace; + +// TODO: Complete semantic implementation diff --git a/src/components/Workspace.res b/src/components/Workspace.res deleted file mode 100644 index 4772ab2b..00000000 --- a/src/components/Workspace.res +++ /dev/null @@ -1,841 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Workspace Panel — the configurator for panel arrangements, groups, -/// sessions, modes, keybindings, and protection levels (DD-024, DD-025). -/// -/// This is the "ribbon designer" for PanLL — like Directory Opus's toolbar -/// editor or Word's ribbon customiser, but for the entire workspace layout. - -open Model -open Msg -open Tea.Html - -/// Render a mode badge showing the current workspace mode. -let renderModeBadge = (mode: workspaceMode): Tea_Vdom.t => { - let (label, colour) = switch mode { - | RhodiumMode => ("Rhodium", "bg-yellow-700") - | EverythingMode => ("Everything", "bg-purple-700") - | CodeMode => ("Code", "bg-blue-700") - | BespokeMode => ("Bespoke", "bg-teal-700") - } - button( - list{ - Attrs.class_( - `px-3 py-1 rounded text-xs font-medium ${colour} text-white hover:opacity-80 transition-opacity`, - ), - Attrs.title("Click to cycle workspace mode: Rhodium > Everything > Code > Bespoke"), - Events.onClick(Workspace(CycleWorkspaceMode)), - KeyboardNav.onActivate(Workspace(CycleWorkspaceMode)), - }, - list{text(label)}, - ) -} - -/// Render the protection level indicator. -let renderProtectionBadge = (protection: sessionProtection): Tea_Vdom.t => { - let (label, colour) = switch protection { - | Open => ("Open", "bg-green-700") - | ReadOnly => ("Read-Only", "bg-red-700") - | Sandboxed => ("Sandboxed", "bg-orange-700") - | LanguageLocked(_) => ("Lang-Locked", "bg-amber-700") - | TranspilationGuarded => ("Transpile-Guard", "bg-indigo-700") - | ProductionGated => ("Prod-Gated", "bg-rose-700") - } - div( - list{ - Attrs.class_(`px-2 py-0.5 rounded text-xs ${colour} text-white`), - Attrs.title("Session protection level — controls what mutations are allowed"), - }, - list{text(label)}, - ) -} - -/// Render the execution mode indicator. -let renderExecutionBadge = (mode: executionMode): Tea_Vdom.t => { - let (label, colour) = switch mode { - | Live => ("Live", "bg-green-600") - | DryRun => ("Dry Run", "bg-yellow-600") - | Simulation => ("Simulation", "bg-cyan-600") - | Emulation => ("Emulation", "bg-violet-600") - } - div( - list{ - Attrs.class_(`px-2 py-0.5 rounded text-xs ${colour} text-white`), - Attrs.title( - "Execution mode — Live applies changes, Dry Run previews them, Simulation uses mock data", - ), - }, - list{text(label)}, - ) -} - -/// Render a configurator section with title and items. -let renderConfigSection = ( - title: string, - tooltip: string, - items: list>, -): Tea_Vdom.t => { - div( - list{Attrs.class_("mb-6")}, - list{ - div( - list{ - Attrs.class_("text-sm font-medium text-gray-400 mb-2 border-b border-gray-800 pb-1"), - Attrs.title(tooltip), - }, - list{text(title)}, - ), - div(list{Attrs.class_("space-y-2")}, items), - }, - ) -} - -/// Render a selectable option button — highlights when active. -let renderOption = (label: string, description: string, isActive: bool, onClick: msg): Tea_Vdom.t< - msg, -> => { - let activeClass = if isActive { - "border-indigo-500 bg-indigo-950/50 text-indigo-300" - } else { - "border-gray-800 bg-gray-900/50 text-gray-400 hover:border-gray-600" - } - button( - list{ - Attrs.class_(`w-full text-left p-3 rounded border ${activeClass} transition-colors`), - Attrs.title(description), - Attrs.ariaLabel(`${label}: ${description}`), - Events.onClick(onClick), - }, - list{ - div(list{Attrs.class_("text-xs font-medium")}, list{text(label)}), - div(list{Attrs.class_("text-xs text-gray-600 mt-0.5")}, list{text(description)}), - }, - ) -} - -/// Render an arrangement card — shows name, active state, load/delete buttons. -let renderArrangementCard = (arr: arrangement, isActive: bool): Tea_Vdom.t => { - let activeClass = if isActive { - "border-indigo-500 bg-indigo-950/50" - } else { - "border-gray-800 bg-gray-900/50 hover:border-gray-600" - } - div( - list{ - Attrs.class_( - `p-3 rounded border ${activeClass} transition-colors flex items-center justify-between`, - ), - Attrs.ariaLabel(`Arrangement: ${arr.name}`), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-xs font-medium text-gray-300")}, list{text(arr.name)}), - div( - list{Attrs.class_("text-xs text-gray-600 mt-0.5")}, - list{ - text( - `${Int.toString(Array.length(arr.positions))} panels` ++ if arr.builtIn { - " (built-in)" - } else { - "" - }, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - if !isActive { - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-gray-400 rounded hover:bg-gray-700", - ), - Attrs.ariaLabel(`Load ${arr.name} arrangement`), - Events.onClick(Workspace(LoadArrangement(arr.id))), - }, - list{text("Load")}, - ) - } else { - div( - list{Attrs.class_("px-2 py-0.5 text-xs bg-indigo-800 text-indigo-300 rounded")}, - list{text("Active")}, - ) - }, - if !arr.builtIn { - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-red-900/50 text-red-400 rounded hover:bg-red-800/50", - ), - Attrs.ariaLabel(`Delete ${arr.name} arrangement`), - Events.onClick(Workspace(DeleteArrangement(arr.id))), - }, - list{text("Del")}, - ) - } else { - Tea.Html.noNode - }, - }, - ), - }, - ) -} - -/// Render a panel group card with lock/visibility/z-order controls. -let renderGroupCard = (group: panelGroup): Tea_Vdom.t => { - div( - list{ - Attrs.class_( - "p-3 rounded border border-gray-800 bg-gray-900/50 flex items-center justify-between", - ), - Attrs.ariaLabel(`Group: ${group.name}`), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div(list{Attrs.class_("text-xs font-medium text-gray-300")}, list{text(group.name)}), - div( - list{Attrs.class_("text-xs text-gray-600 mt-0.5")}, - list{text(`${Int.toString(Array.length(group.panelIds))} panels`)}, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - button( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded ${if group.locked { - "bg-amber-900/50 text-amber-400" - } else { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - }}`, - ), - Attrs.ariaLabel( - if group.locked { - "Unlock group" - } else { - "Lock group" - }, - ), - Events.onClick(Workspace(ToggleGroupLock(group.id))), - }, - list{ - text( - if group.locked { - "Locked" - } else { - "Lock" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - `px-2 py-0.5 text-xs rounded ${if group.visible { - "bg-gray-800 text-gray-400 hover:bg-gray-700" - } else { - "bg-gray-800 text-gray-600" - }}`, - ), - Attrs.ariaLabel( - if group.visible { - "Hide group" - } else { - "Show group" - }, - ), - Events.onClick(Workspace(ToggleGroupVisibility(group.id))), - }, - list{ - text( - if group.visible { - "Visible" - } else { - "Hidden" - }, - ), - }, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-red-900/50 text-red-400 rounded hover:bg-red-800/50", - ), - Attrs.ariaLabel(`Disband ${group.name} group`), - Events.onClick(Workspace(DisbandGroup(group.id))), - }, - list{text("Disband")}, - ), - }, - ), - }, - ) -} - -/// Render a metadata viewer link. -let renderMetadataLink = (item: repoMetadataItem, label: string, description: string): Tea_Vdom.t< - msg, -> => { - button( - list{ - Attrs.class_( - "w-full text-left p-3 rounded border border-gray-800 bg-gray-900/50 hover:border-gray-600 transition-colors", - ), - Attrs.title(description), - Attrs.ariaLabel(`View ${label}`), - Events.onClick(Workspace(ViewMetadata(item))), - }, - list{ - div(list{Attrs.class_("text-xs font-medium text-gray-400")}, list{text(label)}), - div(list{Attrs.class_("text-xs text-gray-600 mt-0.5")}, list{text(description)}), - }, - ) -} - -/// Full Workspace panel view. -let view = (workspace: workspaceState, keybindings: keybindingsState): Tea_Vdom.t => { - div( - list{ - Attrs.class_("fixed inset-0 bg-gray-950/95 z-40 overflow-auto"), - Attrs.role("dialog"), - Attrs.ariaLabel("Workspace configurator"), - }, - list{ - // Header - div( - list{ - Attrs.class_( - "sticky top-0 bg-gray-950 border-b border-gray-800 p-4 flex items-center justify-between z-10", - ), - }, - list{ - div( - list{Attrs.class_("flex items-center gap-4")}, - list{ - div(list{Attrs.class_("text-lg font-light text-gray-300")}, list{text("Workspace")}), - renderModeBadge(workspace.mode), - renderProtectionBadge(workspace.protection), - renderExecutionBadge(workspace.executionMode), - }, - ), - div( - list{Attrs.class_("flex items-center gap-2")}, - list{ - button( - list{ - Attrs.class_( - "px-3 py-1 bg-indigo-800 text-indigo-200 rounded hover:bg-indigo-700 transition-colors text-sm", - ), - Attrs.ariaLabel("Export workspace configuration to ENSAID_CONFIG.a2ml"), - Events.onClick(Workspace(ExportWorkspaceConfig)), - KeyboardNav.onActivate(Workspace(ExportWorkspaceConfig)), - }, - list{text("Export Config")}, - ), - button( - list{ - Attrs.class_( - "px-3 py-1 bg-gray-800 text-gray-400 rounded hover:bg-gray-700 transition-colors text-sm", - ), - Events.onClick(PanelSwitcher(ClosePanels)), - KeyboardNav.onActivate(PanelSwitcher(ClosePanels)), - }, - list{text("Close")}, - ), - }, - ), - }, - ), - // Body: two-column layout - div( - list{Attrs.class_("p-6 grid grid-cols-2 gap-8 max-w-6xl mx-auto")}, - list{ - // Left column: Arrangements & Groups - div( - list{Attrs.class_("space-y-6")}, - list{ - renderConfigSection( - "Arrangements", - "Named layout presets — save and restore panel positions, sizes, and groups", - { - let cards = - workspace.arrangements - ->Array.map(arr => { - let isActive = workspace.activeArrangementId == Some(arr.id) - renderArrangementCard(arr, isActive) - }) - ->List.fromArray - List.concat( - cards, - list{ - button( - list{ - Attrs.class_( - "w-full p-3 rounded border border-dashed border-gray-700 bg-gray-900/30 text-gray-500 hover:border-indigo-600 hover:text-indigo-400 transition-colors text-xs", - ), - Attrs.ariaLabel("Save the current panel layout as a new arrangement"), - Events.onClick( - Workspace( - SaveArrangement( - "custom-" ++ Float.toString(Date.now()), - "Custom Layout", - ), - ), - ), - }, - list{text("+ Save Current Layout")}, - ), - }, - ) - }, - ), - renderConfigSection( - "Panel Groups", - "Group panels to move, resize, show, and hide them together", - if Array.length(workspace.groups) > 0 { - let groupCards = workspace.groups->Array.map(renderGroupCard)->List.fromArray - groupCards - } else { - list{ - div( - list{Attrs.class_("p-3 text-xs text-gray-600 italic")}, - list{ - text("No groups defined. Groups let you move and resize panels together."), - }, - ), - } - }, - ), - }, - ), - // Right column: Sessions, Protection, Modes, Metadata - div( - list{Attrs.class_("space-y-6")}, - list{ - renderConfigSection( - "Sessions", - "Save, load, fork, and manage working sessions", - { - let sessionCards = if Array.length(workspace.sessions) > 0 { - workspace.sessions - ->Array.map(session => { - let isActive = workspace.activeSessionId == Some(session.id) - let activeClass = if isActive { - "border-indigo-500 bg-indigo-950/50" - } else { - "border-gray-800 bg-gray-900/50 hover:border-gray-600" - } - div( - list{ - Attrs.class_( - `p-3 rounded border ${activeClass} transition-colors flex items-center justify-between`, - ), - Attrs.ariaLabel(`Session: ${session.name}`), - }, - list{ - div( - list{Attrs.class_("flex-1")}, - list{ - div( - list{Attrs.class_("text-xs font-medium text-gray-300")}, - list{text(session.name)}, - ), - div( - list{Attrs.class_("text-xs text-gray-600 mt-0.5")}, - list{ - text( - `${Int.toString( - Array.length(session.checkpoints), - )} checkpoints`, - ), - }, - ), - }, - ), - div( - list{Attrs.class_("flex items-center gap-1")}, - list{ - if !isActive { - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-gray-800 text-gray-400 rounded hover:bg-gray-700", - ), - Events.onClick(Workspace(SwitchSession(session.id))), - }, - list{text("Switch")}, - ) - } else { - div( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-indigo-800 text-indigo-300 rounded", - ), - }, - list{text("Active")}, - ) - }, - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-teal-900/50 text-teal-400 rounded hover:bg-teal-800/50", - ), - Events.onClick( - Workspace( - ForkSession( - "fork-" ++ Float.toString(Date.now()), - session.name ++ " (fork)", - ), - ), - ), - }, - list{text("Fork")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-0.5 text-xs bg-red-900/50 text-red-400 rounded hover:bg-red-800/50", - ), - Events.onClick(Workspace(DeleteSession(session.id))), - }, - list{text("Del")}, - ), - }, - ), - }, - ) - }) - ->List.fromArray - } else { - list{ - div( - list{Attrs.class_("p-3 text-xs text-gray-600 italic")}, - list{ - text( - "No sessions saved. Sessions capture your workspace state for later recall.", - ), - }, - ), - } - } - List.concat( - sessionCards, - list{ - button( - list{ - Attrs.class_( - "w-full p-3 rounded border border-dashed border-gray-700 bg-gray-900/30 text-gray-500 hover:border-teal-600 hover:text-teal-400 transition-colors text-xs", - ), - Attrs.ariaLabel("Create a new session from current state"), - Events.onClick( - Workspace( - CreateSession( - "session-" ++ Float.toString(Date.now()), - "New Session", - ), - ), - ), - }, - list{text("+ New Session")}, - ), - }, - ) - }, - ), - renderConfigSection( - "Workspace Modes", - "Switch between Rhodium (full RSR), Everything, Code (dev-only), or Bespoke", - list{ - renderOption( - "Rhodium Mode", - "Full RSR standard — SCM files, contractiles, AI manifests, governance panels", - workspace.mode == RhodiumMode, - Workspace(SetWorkspaceMode(RhodiumMode)), - ), - renderOption( - "Everything Mode", - "All panels, all tools, all metadata visible", - workspace.mode == EverythingMode, - Workspace(SetWorkspaceMode(EverythingMode)), - ), - renderOption( - "Code Mode", - "Pure dev — hides RSR governance, shows code-focused panels only", - workspace.mode == CodeMode, - Workspace(SetWorkspaceMode(CodeMode)), - ), - renderOption( - "Bespoke Mode", - "Per-repo customisation loaded from PANELS.a2ml", - workspace.mode == BespokeMode, - Workspace(SetWorkspaceMode(BespokeMode)), - ), - }, - ), - renderConfigSection( - "Session Protection", - "Control what mutations are allowed in this session", - list{ - renderOption( - "Open", - "Normal operation, no restrictions", - workspace.protection == Open, - Workspace(SetProtection(Open)), - ), - renderOption( - "Read-Only", - "Browse everything, edit nothing", - workspace.protection == ReadOnly, - Workspace(SetProtection(ReadOnly)), - ), - renderOption( - "Sandboxed", - "All changes reset when the session ends", - workspace.protection == Sandboxed, - Workspace(SetProtection(Sandboxed)), - ), - renderOption( - "Language-Locked", - "Specific file types are immutable (e.g., .idr files)", - switch workspace.protection { - | LanguageLocked(_) => true - | _ => false - }, - Workspace(SetProtection(LanguageLocked([".idr", ".lean"]))), - ), - renderOption( - "Transpilation-Guarded", - "Saves require equivalence proof before committing", - workspace.protection == TranspilationGuarded, - Workspace(SetProtection(TranspilationGuarded)), - ), - renderOption( - "Production-Gated", - "Changes staged, require sign-off before taking effect", - workspace.protection == ProductionGated, - Workspace(SetProtection(ProductionGated)), - ), - }, - ), - renderConfigSection( - "Execution Mode", - "Control how changes are applied", - list{ - renderOption( - "Live", - "Real execution against real data", - workspace.executionMode == Live, - Workspace(SetExecutionMode(Live)), - ), - renderOption( - "Dry Run", - "Preview changes without applying — shows diffs, no mutations", - workspace.executionMode == DryRun, - Workspace(SetExecutionMode(DryRun)), - ), - renderOption( - "Simulation", - "Run scenarios with mock data in a simulated environment", - workspace.executionMode == Simulation, - Workspace(SetExecutionMode(Simulation)), - ), - renderOption( - "Emulation", - "Full emulation of the target environment locally", - workspace.executionMode == Emulation, - Workspace(SetExecutionMode(Emulation)), - ), - }, - ), - renderConfigSection( - "Keybindings", - "Remap keyboard shortcuts — click to rebind, Escape to cancel", - list{ - div( - list{Attrs.class_("p-3 rounded border border-gray-800 bg-gray-900/50")}, - list{ - div( - list{Attrs.class_("text-xs text-gray-400")}, - list{ - text( - `${Int.toString( - Array.length(keybindings.bindings), - )} bindings configured`, - ), - }, - ), - div( - list{Attrs.class_("mt-2 max-h-40 overflow-auto space-y-1")}, - keybindings.bindings - ->Array.map(binding => { - let modStr = - binding.chord.modifiers - ->Array.map(m => - switch m { - | Ctrl => "Ctrl" - | Shift => "Shift" - | Alt => "Alt" - | Meta => "Meta" - } - ) - ->Array.join("+") - let chordStr = if modStr == "" { - binding.chord.key - } else { - modStr ++ "+" ++ binding.chord.key - } - let actionStr = switch binding.action { - | ActionUndo => "Undo" - | ActionRedo => "Redo" - | ActionSave => "Save" - | ActionPrint => "Print" - | ActionResetPanel => "Reset Panel" - | ActionResetAll => "Reset All" - | ActionTogglePaneL => "Toggle Panel-L" - | ActionTogglePaneN => "Toggle Panel-N" - | ActionTogglePaneW => "Toggle Panel-W" - | ActionToggleVab => "Toggle VAB" - | ActionTogglePanelBar => "Toggle Panel Bar" - | ActionFullscreen => "Fullscreen" - | ActionCloseOverlay => "Close Overlay" - | ActionToggleCapture => "Toggle Capture" - | ActionToggleWorkspace => "Toggle Workspace" - | ActionToggleSecurity => "Toggle Security" - | ActionCycleWorkspaceMode => "Cycle Mode" - | ActionToggleDryRun => "Toggle Dry Run" - } - div( - list{Attrs.class_("flex items-center justify-between text-xs")}, - list{ - span(list{Attrs.class_("text-gray-500")}, list{text(actionStr)}), - span( - list{ - Attrs.class_( - "text-gray-400 font-mono bg-gray-800 px-1.5 py-0.5 rounded", - ), - }, - list{text(chordStr)}, - ), - }, - ) - }) - ->List.fromArray, - ), - }, - ), - }, - ), - renderConfigSection( - "Repo Metadata", - "Quick access to SCM files, contractiles, AI manifests, directory structure", - list{ - renderMetadataLink( - MetaStateSCM, - "STATE.scm", - "Current project state — tasks, progress, blockers", - ), - renderMetadataLink( - MetaMetaSCM, - "META.scm", - "Architecture decisions, governance, principles", - ), - renderMetadataLink( - MetaEcosystemSCM, - "ECOSYSTEM.scm", - "Project ecosystem position, dependencies", - ), - renderMetadataLink( - MetaContractiles, - "Contractiles", - "Elastic state contracts — orbital stability, vexation ceiling", - ), - renderMetadataLink( - MetaAIManifest, - "AI Manifest", - "0-AI-MANIFEST.a2ml — entry point for AI agents", - ), - renderMetadataLink( - MetaTrustfile, - "Trustfile", - "Security policy from Trustfile.a2ml", - ), - renderMetadataLink( - MetaDirectoryTree, - "Directory Tree", - "Repository file structure overview", - ), - }, - ), - }, - ), - }, - ), - // Metadata viewer overlay - switch workspace.viewingMetadata { - | Some(_item) => - div( - list{Attrs.class_("fixed inset-0 bg-black/60 z-50 flex items-center justify-center")}, - list{ - div( - list{ - Attrs.class_( - "bg-gray-950 border border-gray-700 rounded-lg w-[600px] max-h-[80vh] flex flex-col", - ), - }, - list{ - div( - list{ - Attrs.class_("p-4 border-b border-gray-800 flex items-center justify-between"), - }, - list{ - div( - list{Attrs.class_("text-sm font-medium text-gray-300")}, - list{text("Metadata Viewer")}, - ), - button( - list{ - Attrs.class_( - "px-2 py-1 text-xs bg-gray-800 text-gray-400 rounded hover:bg-gray-700", - ), - Events.onClick(Workspace(CloseMetadata)), - KeyboardNav.onActivate(Workspace(CloseMetadata)), - }, - list{text("Close")}, - ), - }, - ), - div( - list{Attrs.class_("flex-1 overflow-auto p-4")}, - list{ - switch workspace.metadataContent { - | Some(content) => - pre( - list{Attrs.class_("text-xs text-gray-400 font-mono whitespace-pre-wrap")}, - list{text(content)}, - ) - | None => - div( - list{Attrs.class_("text-xs text-gray-600 italic")}, - list{text("Loading...")}, - ) - }, - }, - ), - }, - ), - }, - ) - | None => Tea.Html.noNode - }, - }, - ) -} diff --git a/src/core/A2mlEngine.affine b/src/core/A2mlEngine.affine new file mode 100644 index 00000000..79e032f2 --- /dev/null +++ b/src/core/A2mlEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module A2mlEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/A2mlEngine.res b/src/core/A2mlEngine.res deleted file mode 100644 index a7433972..00000000 --- a/src/core/A2mlEngine.res +++ /dev/null @@ -1,616 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL A2ML Engine — pure functions for parsing, validating, and querying A2ML -/// manifest files (both S-expression style like 0-AI-MANIFEST.a2ml and sectioned -/// key-value style like clade .a2ml files and Trustfile.a2ml). -/// -/// A2ML files come in two main flavours within PanLL: -/// 1. S-expression manifests: `(manifest (identity ...) (canonical-locations ...) ...)` -/// 2. Sectioned key-value: `[section-name]\nkey = value` (clade files, Trustfile) -/// -/// This engine provides a unified representation via `a2mlSection` trees, with -/// parsing heuristics that detect the format and extract sections accordingly. -/// -/// All functions are pure — no side effects, no Gossamer invocations, no I/O. - -// ============================================================================ -// Types -// ============================================================================ - -/// A single section or key-value node in an A2ML document. -/// Sections can nest: a `[clade-metadata]` section contains key-value children, -/// and an S-expression `(canonical-locations ...)` section contains location entries. -type rec a2mlSection = { - key: string, - value: string, - children: array, -} - -/// A parsed A2ML manifest with its file path, extracted sections, and parse status. -type a2mlManifest = { - path: string, - sections: array, - isValid: bool, - errors: array, -} - -/// Result of validating a manifest against the A2ML schema requirements. -type a2mlValidationResult = { - valid: bool, - errors: array, - warnings: array, - sectionCount: int, -} - -// ============================================================================ -// Internal Helpers -// ============================================================================ - -/// Trim whitespace from both ends of a string. -let trim = (s: string): string => { - s->String.trim -} - -/// Check if a line starts a new section in key-value format: `[section-name]` -let isSectionHeader = (line: string): bool => { - let trimmed = trim(line) - String.startsWith(trimmed, "[") && - String.endsWith(trimmed, "]") && - !String.startsWith(trimmed, "[[") -} - -/// Extract the section name from a `[section-name]` header line. -let extractSectionName = (line: string): string => { - let trimmed = trim(line) - let withoutBrackets = String.sliceToEnd(trimmed, ~start=1) - String.slice(withoutBrackets, ~start=0, ~end=String.length(withoutBrackets) - 1) -} - -/// Check if a line is a `---` section divider (Trustfile-style). -let isSectionDivider = (line: string): bool => { - trim(line) == "---" -} - -/// Check if a line is a `### [SECTION]` header (Trustfile-style). -let isTrustfileSectionHeader = (line: string): bool => { - let trimmed = trim(line) - String.startsWith(trimmed, "### [") && String.endsWith(trimmed, "]") -} - -/// Extract section name from `### [SECTION_NAME]` header. -let extractTrustfileSectionName = (line: string): string => { - let trimmed = trim(line) - let afterHash = String.sliceToEnd(trimmed, ~start=5) - String.slice(afterHash, ~start=0, ~end=String.length(afterHash) - 1) -} - -/// Check if a line is an S-expression opener like `(manifest`, `(identity`, etc. -let isSexprSection = (line: string): bool => { - let trimmed = trim(line) - String.startsWith(trimmed, "(") && !String.startsWith(trimmed, "(;") -} - -/// Extract the S-expression section name from a line like `(manifest` or ` (identity`. -let extractSexprName = (line: string): string => { - let trimmed = trim(line) - let afterParen = String.sliceToEnd(trimmed, ~start=1) - // Take up to the first space or closing paren - let parts = String.split(afterParen, " ") - let first = parts->Array.get(0)->Option.getOr("") - - // Remove trailing paren if present - if String.endsWith(first, ")") { - String.slice(first, ~start=0, ~end=String.length(first) - 1) - } else { - first - } -} - -/// Parse a `key = value` or `key = "value"` line into a section child. -let parseKeyValue = (line: string): option => { - let trimmed = trim(line) - - // Skip comments and empty lines - if trimmed == "" || String.startsWith(trimmed, "#") || String.startsWith(trimmed, ";") { - None - } else { - let eqIdx = String.indexOf(trimmed, " = ") - if eqIdx >= 0 { - let key = String.slice(trimmed, ~start=0, ~end=eqIdx)->trim - let rawValue = String.sliceToEnd(trimmed, ~start=eqIdx + 3)->trim - // Strip surrounding quotes if present - let value = if String.startsWith(rawValue, "\"") && String.endsWith(rawValue, "\"") { - String.slice(rawValue, ~start=1, ~end=String.length(rawValue) - 1) - } else { - rawValue - } - Some({key, value, children: []}) - } else { - // Try simple `key: value` YAML-style (used in Trustfile A2ML) - let colonIdx = String.indexOf(trimmed, ": ") - if colonIdx >= 0 { - let key = String.slice(trimmed, ~start=0, ~end=colonIdx)->trim - let rawValue = String.sliceToEnd(trimmed, ~start=colonIdx + 2)->trim - let value = if String.startsWith(rawValue, "\"") && String.endsWith(rawValue, "\"") { - String.slice(rawValue, ~start=1, ~end=String.length(rawValue) - 1) - } else { - rawValue - } - Some({key, value, children: []}) - } else { - None - } - } - } -} - -/// Detect the A2ML format flavour from file content. -type a2mlFormat = - | SExprFormat - | SectionedKeyValueFormat - | TrustfileFormat - -let detectFormat = (content: string): a2mlFormat => { - let trimmed = trim(content) - if String.startsWith(trimmed, ";") || String.startsWith(trimmed, "(") { - SExprFormat - } else if String.includes(trimmed, "### [") && String.includes(trimmed, "---") { - TrustfileFormat - } else { - SectionedKeyValueFormat - } -} - -// ============================================================================ -// Parsers -// ============================================================================ - -/// Parse S-expression format A2ML (like 0-AI-MANIFEST.a2ml). -/// Extracts top-level sections and their string content as children. -let parseSexprContent = (content: string, path: string): a2mlManifest => { - let lines = String.split(content, "\n") - let sections: array = [] - let errors: array = [] - let currentSection: ref> = ref(None) - let currentChildren: ref> = ref([]) - - lines->Array.forEach(line => { - let trimmed = trim(line) - if trimmed == "" || String.startsWith(trimmed, ";") || String.startsWith(trimmed, ";;") { - // Skip comments and blanks - () - } else if isSexprSection(trimmed) { - // Flush previous section - switch currentSection.contents { - | Some(name) => - let _ = sections->Array.push({ - key: name, - value: "", - children: currentChildren.contents, - }) - | None => () - } - currentSection := Some(extractSexprName(trimmed)) - currentChildren := [] - } else { - // Try to extract key-value pairs from S-expression body - // Lines like `(name "PanLL")` or `(state ".machine_readable/STATE.scm")` - let cleaned = - trimmed - ->String.replaceAll("(", "") - ->String.replaceAll(")", "") - ->trim - let parts = String.split(cleaned, " ") - if Array.length(parts) >= 2 { - let key = parts->Array.get(0)->Option.getOr("") - let rawVal = - parts - ->Array.sliceToEnd(~start=1) - ->Array.join(" ") - let value = if String.startsWith(rawVal, "\"") && String.endsWith(rawVal, "\"") { - String.slice(rawVal, ~start=1, ~end=String.length(rawVal) - 1) - } else { - rawVal - } - let _ = currentChildren.contents->Array.push({key, value, children: []}) - } - } - }) - - // Flush final section - switch currentSection.contents { - | Some(name) => - let _ = sections->Array.push({ - key: name, - value: "", - children: currentChildren.contents, - }) - | None => () - } - - { - path, - sections, - isValid: Array.length(errors) == 0 && Array.length(sections) > 0, - errors, - } -} - -/// Parse sectioned key-value format A2ML (like clade .a2ml files). -let parseSectionedContent = (content: string, path: string): a2mlManifest => { - let lines = String.split(content, "\n") - let sections: array = [] - let errors: array = [] - let currentSection: ref> = ref(None) - let currentChildren: ref> = ref([]) - - lines->Array.forEach(line => { - let trimmed = trim(line) - if trimmed == "" || String.startsWith(trimmed, "#") { - // Skip comments and blanks - () - } else if isSectionHeader(trimmed) { - // Flush previous section - switch currentSection.contents { - | Some(name) => - let _ = sections->Array.push({ - key: name, - value: "", - children: currentChildren.contents, - }) - | None => () - } - currentSection := Some(extractSectionName(trimmed)) - currentChildren := [] - } else { - switch parseKeyValue(trimmed) { - | Some(kv) => - let _ = currentChildren.contents->Array.push(kv) - | None => () - } - } - }) - - // Flush final section - switch currentSection.contents { - | Some(name) => - let _ = sections->Array.push({ - key: name, - value: "", - children: currentChildren.contents, - }) - | None => () - } - - { - path, - sections, - isValid: Array.length(errors) == 0 && Array.length(sections) > 0, - errors, - } -} - -/// Parse Trustfile-format A2ML (`---` dividers with `### [SECTION]` headers). -let parseTrustfileContent = (content: string, path: string): a2mlManifest => { - let lines = String.split(content, "\n") - let sections: array = [] - let errors: array = [] - let currentSection: ref> = ref(None) - let currentChildren: ref> = ref([]) - - lines->Array.forEach(line => { - let trimmed = trim(line) - if isSectionDivider(trimmed) { - // Flush previous section on divider - switch currentSection.contents { - | Some(name) => - let _ = sections->Array.push({ - key: name, - value: "", - children: currentChildren.contents, - }) - currentSection := None - currentChildren := [] - | None => () - } - } else if isTrustfileSectionHeader(trimmed) { - currentSection := Some(extractTrustfileSectionName(trimmed)) - currentChildren := [] - } else if trimmed == "" || String.startsWith(trimmed, "#") { - () - } else { - switch parseKeyValue(trimmed) { - | Some(kv) => - let _ = currentChildren.contents->Array.push(kv) - | None => () - } - } - }) - - // Flush final section - switch currentSection.contents { - | Some(name) => - let _ = sections->Array.push({ - key: name, - value: "", - children: currentChildren.contents, - }) - | None => () - } - - { - path, - sections, - isValid: Array.length(errors) == 0 && Array.length(sections) > 0, - errors, - } -} - -// ============================================================================ -// Public API -// ============================================================================ - -/// Parse A2ML content from any supported format. Auto-detects the flavour -/// (S-expression, sectioned key-value, or Trustfile) and delegates to the -/// appropriate parser. -let parseA2mlContent = (content: string, ~path: string=""): a2mlManifest => { - if String.length(trim(content)) == 0 { - { - path, - sections: [], - isValid: false, - errors: ["Empty content"], - } - } else { - switch detectFormat(content) { - | SExprFormat => parseSexprContent(content, path) - | SectionedKeyValueFormat => parseSectionedContent(content, path) - | TrustfileFormat => parseTrustfileContent(content, path) - } - } -} - -/// Validate a parsed manifest against A2ML schema requirements. -/// -/// For full manifests (0-AI-MANIFEST.a2ml), checks for required sections: -/// - identity or clade-metadata (identification) -/// - canonical-locations (file mapping) -/// - critical-invariants (rules) -/// -/// For clade files, checks for: -/// - clade-metadata (identification) -/// - clade-traits (capability declaration) -/// -/// For Trustfile A2ML, checks for: -/// - META section -/// - TRUSTFILE section -let validateManifest = (manifest: a2mlManifest): a2mlValidationResult => { - let errors: array = [] - let warnings: array = [] - let sectionNames = manifest.sections->Array.map(s => s.key) - - // Check if this is a full manifest, clade file, or trustfile - let hasIdentity = sectionNames->Array.some(n => n == "identity") - let hasCladeMetadata = sectionNames->Array.some(n => n == "clade-metadata") - let hasMeta = sectionNames->Array.some(n => n == "META") - let hasTrustfile = sectionNames->Array.some(n => n == "TRUSTFILE") - - if hasIdentity { - // Full manifest validation - if !(sectionNames->Array.some(n => n == "canonical-locations")) { - let _ = errors->Array.push("Missing required section: canonical-locations") - } - if !(sectionNames->Array.some(n => n == "critical-invariants")) { - let _ = warnings->Array.push("Missing recommended section: critical-invariants") - } - if !(sectionNames->Array.some(n => n == "purpose")) { - let _ = warnings->Array.push("Missing recommended section: purpose") - } - if !(sectionNames->Array.some(n => n == "lifecycle")) { - let _ = warnings->Array.push("Missing recommended section: lifecycle") - } - } else if hasCladeMetadata { - // Clade file validation - if !(sectionNames->Array.some(n => n == "clade-traits")) { - let _ = errors->Array.push("Clade file missing required section: clade-traits") - } - // Check clade-metadata has required keys - let metaSection = manifest.sections->Array.find(s => s.key == "clade-metadata") - switch metaSection { - | Some(section) => - let childKeys = section.children->Array.map(c => c.key) - if !(childKeys->Array.some(k => k == "id")) { - let _ = errors->Array.push("clade-metadata missing required key: id") - } - if !(childKeys->Array.some(k => k == "name")) { - let _ = errors->Array.push("clade-metadata missing required key: name") - } - if !(childKeys->Array.some(k => k == "kind")) { - let _ = warnings->Array.push("clade-metadata missing recommended key: kind") - } - | None => () - } - } else if hasMeta && hasTrustfile { - // Trustfile validation - if !(sectionNames->Array.some(n => n == "THREAT_MODEL")) { - let _ = warnings->Array.push("Trustfile missing recommended section: THREAT_MODEL") - } - if !(sectionNames->Array.some(n => n == "FORMAL_VERIFICATION")) { - let _ = warnings->Array.push("Trustfile missing recommended section: FORMAL_VERIFICATION") - } - } else if hasMeta { - // Partial trustfile — META without TRUSTFILE - let _ = warnings->Array.push("Has META section but no TRUSTFILE section — partial trustfile?") - } else { - // Unknown format — warn but don't error (might be a valid custom A2ML) - let _ = warnings->Array.push("No recognised root section (identity, clade-metadata, or META)") - } - - // Check for empty sections (applies to all formats) - manifest.sections->Array.forEach(section => { - if Array.length(section.children) == 0 && section.value == "" { - let _ = warnings->Array.push(`Section "${section.key}" is empty`) - } - }) - - // Check for empty values in children - manifest.sections->Array.forEach(section => { - section.children->Array.forEach(child => { - if child.value == "" && Array.length(child.children) == 0 { - let _ = - warnings->Array.push(`Key "${child.key}" in section "${section.key}" has empty value`) - } - }) - }) - - { - valid: Array.length(errors) == 0, - errors: Array.concat(manifest.errors, errors), - warnings, - sectionCount: Array.length(manifest.sections), - } -} - -/// Extract canonical file location mappings from a manifest. -/// Returns an array of (logical-name, file-path) tuples. -/// -/// Works with both S-expression manifests (where canonical-locations has -/// children like `(state ".machine_readable/STATE.scm")`) and sectioned -/// key-value files (where `[canonical-locations]` has `key = "path"` entries). -let extractCanonicalLocations = (manifest: a2mlManifest): array<(string, string)> => { - let locSection = manifest.sections->Array.find(s => s.key == "canonical-locations") - switch locSection { - | Some(section) => section.children->Array.map(child => (child.key, child.value)) - | None => [] - } -} - -/// Extract lifecycle hooks (on-enter and on-exit steps) from a manifest. -/// Returns an array of step description strings. -let extractLifecycleHooks = (manifest: a2mlManifest): array => { - let lifecycleSection = manifest.sections->Array.find(s => s.key == "lifecycle") - switch lifecycleSection { - | Some(section) => - section.children->Array.map(child => { - if child.value != "" { - `${child.key}: ${child.value}` - } else { - child.key - } - }) - | None => [] - } -} - -/// Find a section by key name in the manifest. -let findSection = (manifest: a2mlManifest, sectionKey: string): option => { - manifest.sections->Array.find(s => s.key == sectionKey) -} - -/// Get a value from a section by key name. Returns None if section or key not found. -let getValue = (manifest: a2mlManifest, sectionKey: string, key: string): option => { - switch findSection(manifest, sectionKey) { - | Some(section) => - switch section.children->Array.find(c => c.key == key) { - | Some(child) => Some(child.value) - | None => None - } - | None => None - } -} - -/// Extract test coverage policy from A2ML clade traits. -/// Returns (required-coverage-percent, required-test-types, notes) or defaults -/// if the clade does not specify a test policy. -/// -/// Looks for keys in `[clade-traits]` or `[clade-integrations]`: -/// - `test-coverage` or `coverage` → minimum coverage percentage -/// - `test-types` → pipe-separated list (e.g. "unit|integration|property") -/// - `test-notes` or `testing` → free-form test policy notes -let extractTestCoveragePolicy = (manifest: a2mlManifest): (int, array, string) => { - // Try clade-traits first, then clade-integrations - let traitSection = switch findSection(manifest, "clade-traits") { - | Some(s) => Some(s) - | None => findSection(manifest, "clade-integrations") - } - switch traitSection { - | Some(section) => - let coverageStr = switch section.children->Array.find(c => - c.key == "test-coverage" || c.key == "coverage" - ) { - | Some(child) => child.value - | None => "0" - } - let coverage = Int.fromString(coverageStr)->Option.getOr(0) - - let testTypes = switch section.children->Array.find(c => c.key == "test-types") { - | Some(child) => - child.value->String.split("|")->Array.map(s => String.trim(s))->Array.filter(s => s != "") - | None => [] - } - - let notes = switch section.children->Array.find(c => - c.key == "test-notes" || c.key == "testing" - ) { - | Some(child) => child.value - | None => "" - } - - (coverage, testTypes, notes) - | None => (0, [], "") - } -} - -/// Generate an A2ML test coverage section from a clade's test requirements. -/// This is used when creating new A2ML files for panels that inherit clade traits. -let generateTestCoverageSection = ( - coverage: int, - testTypes: array, - notes: string, -): string => { - let typesStr = testTypes->Array.join(" | ") - let notesLine = if notes != "" { - `test-notes = "${notes}"\n` - } else { - "" - } - `[test-policy] -test-coverage = ${Int.toString(coverage)} -test-types = "${typesStr}" -${notesLine}` -} - -/// Generate a human-readable summary of a parsed manifest. -let summariseManifest = (manifest: a2mlManifest): string => { - let sectionCount = Array.length(manifest.sections) - let sectionNames = manifest.sections->Array.map(s => s.key)->Array.join(", ") - let totalKeys = manifest.sections->Array.reduce(0, (acc, s) => acc + Array.length(s.children)) - - let statusLine = if manifest.isValid { - "Status: Valid" - } else { - let errorList = manifest.errors->Array.join("; ") - `Status: Invalid (${errorList})` - } - - let nameLine = switch getValue(manifest, "identity", "name") { - | Some(name) => `Name: ${name}` - | None => - switch getValue(manifest, "clade-metadata", "name") { - | Some(name) => `Clade: ${name}` - | None => "Name: (unknown)" - } - } - - let pathLine = if manifest.path != "" { - `Path: ${manifest.path}` - } else { - "Path: (in-memory)" - } - - `${nameLine} -${pathLine} -${statusLine} -Sections: ${Int.toString(sectionCount)} (${sectionNames}) -Total keys: ${Int.toString(totalKeys)}` -} diff --git a/src/core/AccessibilityEngine.affine b/src/core/AccessibilityEngine.affine new file mode 100644 index 00000000..fc90c5a4 --- /dev/null +++ b/src/core/AccessibilityEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AccessibilityEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AccessibilityEngine.res b/src/core/AccessibilityEngine.res deleted file mode 100644 index 36872549..00000000 --- a/src/core/AccessibilityEngine.res +++ /dev/null @@ -1,496 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AccessibilityEngine — Pure computation + side-effectful persistence -/// for accessibility preferences. -/// -/// Pure functions map accessibility state (theme, palette, animations, font -/// size, focus style) to CSS classes applied at the root element level. -/// -/// Side-effectful functions handle: -/// - localStorage persistence (save/load all preferences) -/// - OS preference detection (prefers-color-scheme, prefers-reduced-motion) -/// - prefers-color-scheme change subscription for System theme mode -/// -/// DESIGN NOTE: Each preference dimension maps to exactly one CSS class on the -/// root element. Theme mode adds either "theme-light" or nothing (dark is default). -/// The "theme-light" class inverts background/text colours via CSS custom properties. - -// ============================================================================ -// LocalStorage key -// ============================================================================ - -/// The single localStorage key under which all accessibility preferences are -/// stored as a JSON object. Using one key keeps reads/writes atomic. -let storageKey = "panll-accessibility" - -// ============================================================================ -// Theme helpers (pure) -// ============================================================================ - -/// Human-readable label for a theme mode. -let themeLabel = (mode: AccessibilityModel.themeMode): string => { - switch mode { - | ThemeDark => "Dark" - | ThemeLight => "Light" - | ThemeSystem => "System" - } -} - -/// CSS class for the resolved theme. Dark returns "" (default), Light returns -/// "theme-light" which triggers CSS custom property overrides. -let themeClass = (state: AccessibilityModel.accessibilityState): string => { - let effective = switch state.theme { - | ThemeSystem => state.resolvedTheme - | other => other - } - switch effective { - | ThemeLight => "theme-light" - | _ => "" - } -} - -// ============================================================================ -// Font size helpers (pure) -// ============================================================================ - -/// Map a font size preset to a pixel value for the root element. -/// Since Tailwind uses rem units, changing the root font-size scales -/// everything proportionally. -let fontSizePx = (preset: AccessibilityModel.fontSizePreset): int => { - switch preset { - | FontSmall => 14 - | FontMedium => 16 - | FontLarge => 18 - | FontExtraLarge => 20 - } -} - -/// Raw JS function to set font size on element. -let setRootFontSize: string => unit = %raw(` - function(px) { - try { document.documentElement.style.fontSize = px + "px"; } catch(e) {} - } -`) - -/// Apply the font size to element via direct DOM manipulation. -/// This scales all rem-based Tailwind sizes proportionally. -let applyFontSize = (preset: AccessibilityModel.fontSizePreset): unit => { - setRootFontSize(Int.toString(fontSizePx(preset))) -} - -/// TEA command that applies font size to the DOM. -let applyFontSizeCmd = (preset: AccessibilityModel.fontSizePreset): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(_callbacks => { - applyFontSize(preset) - }) -} - -/// Map a font size preset to its CSS class name (kept for rootClasses but -/// no longer the primary mechanism — applyFontSize handles actual scaling). -let fontSizeClass = (_preset: AccessibilityModel.fontSizePreset): string => { - // No longer used for root classes — font size is applied directly to . - "" -} - -/// Human-readable label for a font size preset. -let fontSizeLabel = (preset: AccessibilityModel.fontSizePreset): string => { - switch preset { - | FontSmall => "Small (14px)" - | FontMedium => "Medium (16px)" - | FontLarge => "Large (18px)" - | FontExtraLarge => "Extra Large (20px)" - } -} - -// ============================================================================ -// Animation helpers (pure) -// ============================================================================ - -/// Map an animation preference to its corresponding CSS class name. -let animationClass = (pref: AccessibilityModel.animationPreference): string => { - switch pref { - | AnimationsOn => "" - | AnimationsReduced => "animations-reduced" - | AnimationsOff => "animations-off" - } -} - -/// Human-readable label for an animation preference. -let animationLabel = (pref: AccessibilityModel.animationPreference): string => { - switch pref { - | AnimationsOn => "Animations On" - | AnimationsReduced => "Animations Reduced" - | AnimationsOff => "Animations Off" - } -} - -// ============================================================================ -// Focus style helpers (pure) -// ============================================================================ - -/// Map a focus indicator style to its corresponding CSS class name. -let focusStyleClass = (style: AccessibilityModel.focusIndicatorStyle): string => { - switch style { - | FocusDefault => "" - | FocusHighContrast => "focus-high-contrast" - | FocusThick => "focus-thick" - | FocusDotted => "focus-dotted" - } -} - -/// Human-readable label for a focus indicator style. -let focusStyleLabel = (style: AccessibilityModel.focusIndicatorStyle): string => { - switch style { - | FocusDefault => "Default (2px indigo)" - | FocusHighContrast => "High Contrast (3px black)" - | FocusThick => "Thick (4px indigo)" - | FocusDotted => "Dotted (3px dotted)" - } -} - -// ============================================================================ -// Root classes (pure) -// ============================================================================ - -/// Combine all accessibility CSS classes into a single space-separated string -/// for the root element. -/// -/// Called on every render to produce the class attribute for the app root. -/// Filters out empty strings so there are no double-spaces in the output. -/// -/// Example output: "theme-light text-lg animations-reduced focus-high-contrast" -/// Example output (all defaults): "text-base" -let rootClasses = (state: AccessibilityModel.accessibilityState): string => { - let classes = [ - themeClass(state), - fontSizeClass(state.fontSize), - animationClass(state.animations), - focusStyleClass(state.focusStyle), - ] - classes - ->Array.filter(c => c !== "") - ->Array.join(" ") -} - -// ============================================================================ -// OS preference detection (side-effectful) -// ============================================================================ - -/// Detect the OS colour scheme preference via matchMedia. -/// Returns ThemeDark or ThemeLight. -let detectOsColorScheme = (): AccessibilityModel.themeMode => { - try { - let _mql = %raw(`window.matchMedia("(prefers-color-scheme: light)")`) - let matches: bool = %raw(`_mql.matches`) - if matches { - AccessibilityModel.ThemeLight - } else { - ThemeDark - } - } catch { - | _ => ThemeDark - } -} - -/// Detect the OS reduced-motion preference. -/// Returns true if the OS prefers reduced motion. -let detectOsReducedMotion = (): bool => { - try { - let _mql = %raw(`window.matchMedia("(prefers-reduced-motion: reduce)")`) - let matches: bool = %raw(`_mql.matches`) - matches - } catch { - | _ => false - } -} - -// ============================================================================ -// localStorage persistence (side-effectful) -// ============================================================================ - -/// Serialise a theme mode to a JSON-safe string. -let themeToString = (mode: AccessibilityModel.themeMode): string => { - switch mode { - | ThemeDark => "dark" - | ThemeLight => "light" - | ThemeSystem => "system" - } -} - -/// Parse a theme mode from a string. Returns None for unknown values. -let themeFromString = (s: string): option => { - switch s { - | "dark" => Some(ThemeDark) - | "light" => Some(ThemeLight) - | "system" => Some(ThemeSystem) - | _ => None - } -} - -/// Serialise a palette to a string. -let paletteToString = (p: ProvenanceModel.accessibilityPalette): string => { - switch p { - | StandardPalette => "standard" - | DeuteranopiaPalette => "deuteranopia" - | ProtanopiaPalette => "protanopia" - | HighContrastPalette => "high-contrast" - } -} - -/// Parse a palette from a string. -let paletteFromString = (s: string): option => { - switch s { - | "standard" => Some(StandardPalette) - | "deuteranopia" => Some(DeuteranopiaPalette) - | "protanopia" => Some(ProtanopiaPalette) - | "high-contrast" => Some(HighContrastPalette) - | _ => None - } -} - -/// Serialise an animation preference to a string. -let animationToString = (a: AccessibilityModel.animationPreference): string => { - switch a { - | AnimationsOn => "on" - | AnimationsReduced => "reduced" - | AnimationsOff => "off" - } -} - -/// Parse an animation preference from a string. -let animationFromString = (s: string): option => { - switch s { - | "on" => Some(AnimationsOn) - | "reduced" => Some(AnimationsReduced) - | "off" => Some(AnimationsOff) - | _ => None - } -} - -/// Serialise a font size preset to a string. -let fontSizeToString = (f: AccessibilityModel.fontSizePreset): string => { - switch f { - | FontSmall => "small" - | FontMedium => "medium" - | FontLarge => "large" - | FontExtraLarge => "extra-large" - } -} - -/// Parse a font size preset from a string. -let fontSizeFromString = (s: string): option => { - switch s { - | "small" => Some(FontSmall) - | "medium" => Some(FontMedium) - | "large" => Some(FontLarge) - | "extra-large" => Some(FontExtraLarge) - | _ => None - } -} - -/// Serialise a focus style to a string. -let focusStyleToString = (f: AccessibilityModel.focusIndicatorStyle): string => { - switch f { - | FocusDefault => "default" - | FocusHighContrast => "high-contrast" - | FocusThick => "thick" - | FocusDotted => "dotted" - } -} - -/// Parse a focus style from a string. -let focusStyleFromString = (s: string): option => { - switch s { - | "default" => Some(FocusDefault) - | "high-contrast" => Some(FocusHighContrast) - | "thick" => Some(FocusThick) - | "dotted" => Some(FocusDotted) - | _ => None - } -} - -/// Save the current accessibility state to localStorage as JSON. -/// Silently fails if localStorage is unavailable (e.g. private browsing). -let saveToLocalStorage = (state: AccessibilityModel.accessibilityState): unit => { - try { - let json = Dict.make() - Dict.set(json, "theme", JSON.Encode.string(themeToString(state.theme))) - Dict.set(json, "palette", JSON.Encode.string(paletteToString(state.palette))) - Dict.set(json, "animations", JSON.Encode.string(animationToString(state.animations))) - Dict.set(json, "fontSize", JSON.Encode.string(fontSizeToString(state.fontSize))) - Dict.set(json, "focusStyle", JSON.Encode.string(focusStyleToString(state.focusStyle))) - let _jsonStr = JSON.stringify(JSON.Encode.object(json)) - %raw(`localStorage.setItem(storageKey, _jsonStr)`) - } catch { - | _ => () - } -} - -/// Helper to extract a string from a parsed JSON object dict. -let getJsonString = (obj: Dict.t, key: string): option => { - switch Dict.get(obj, key) { - | Some(v) => - switch JSON.Classify.classify(v) { - | String(s) => Some(s) - | _ => None - } - | None => None - } -} - -/// Tea_Json decoder for accessibility preferences from localStorage. -/// Parses string fields and maps them to variant types via existing parsers. -let accessibilityDecoder: Tea_Json.decoder = json => { - open Decoders - open Tea_Json - let inner = map5( - (themeStr, paletteStr, animStr, fontStr, focusStr) => ( - themeStr, - paletteStr, - animStr, - fontStr, - focusStr, - ), - optionalFieldDecoder("theme", string), - optionalFieldDecoder("palette", string), - optionalFieldDecoder("animations", string), - optionalFieldDecoder("fontSize", string), - optionalFieldDecoder("focusStyle", string), - ) - switch inner(json) { - | Ok((themeStr, paletteStr, animStr, fontStr, focusStr)) => { - let theme = switch themeStr { - | Some(s) => themeFromString(s) - | None => None - } - let palette = switch paletteStr { - | Some(s) => paletteFromString(s) - | None => None - } - let animations = switch animStr { - | Some(s) => animationFromString(s) - | None => None - } - let fontSize = switch fontStr { - | Some(s) => fontSizeFromString(s) - | None => None - } - let focusStyle = switch focusStr { - | Some(s) => focusStyleFromString(s) - | None => None - } - let osScheme = detectOsColorScheme() - let resolvedTheme = switch theme { - | Some(ThemeSystem) => osScheme - | Some(t) => t - | None => ThemeDark - } - Ok( - ( - { - palette: switch palette { - | Some(p) => p - | None => StandardPalette - }, - theme: switch theme { - | Some(t) => t - | None => ThemeDark - }, - animations: switch animations { - | Some(a) => a - | None => AnimationsOn - }, - fontSize: switch fontSize { - | Some(f) => f - | None => FontMedium - }, - focusStyle: switch focusStyle { - | Some(f) => f - | None => FocusDefault - }, - toolbarExpanded: false, - resolvedTheme, - }: AccessibilityModel.accessibilityState - ), - ) - } - | Error(e) => Error(e) - } -} - -/// Load accessibility preferences from localStorage. -/// Returns None if nothing is stored or parsing fails. -let loadFromLocalStorage = (): option => { - try { - let raw: Nullable.t = %raw(`localStorage.getItem(storageKey)`) - switch Nullable.toOption(raw) { - | None => None - | Some(jsonStr) => Decoders.decodeOption(accessibilityDecoder, jsonStr) - } - } catch { - | _ => None - } -} - -// ============================================================================ -// Default state (reads OS prefs + localStorage) -// ============================================================================ - -/// Build the initial accessibility state by layering: -/// 1. Hardcoded defaults -/// 2. OS preferences (reduced-motion, colour-scheme) -/// 3. localStorage overrides (user's previous choices) -/// -/// This is called once at startup. -let defaultState: AccessibilityModel.accessibilityState = { - let osScheme = detectOsColorScheme() - let osReduced = detectOsReducedMotion() - let base: AccessibilityModel.accessibilityState = { - palette: StandardPalette, - theme: ThemeDark, - animations: osReduced ? AnimationsReduced : AnimationsOn, - fontSize: FontMedium, - focusStyle: FocusDefault, - toolbarExpanded: false, - resolvedTheme: osScheme, - } - switch loadFromLocalStorage() { - | Some(saved) => saved - | None => base - } -} - -// ============================================================================ -// TEA command for saving (wraps side effect in a Cmd) -// ============================================================================ - -/// Create a TEA command that persists the current accessibility state to -/// localStorage. Fire-and-forget — no result message needed. -let saveCmd = (state: AccessibilityModel.accessibilityState): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(_callbacks => { - saveToLocalStorage(state) - }) -} - -// ============================================================================ -// TEA command for listening to OS colour scheme changes -// ============================================================================ - -/// Create a TEA command that registers a matchMedia listener for -/// prefers-color-scheme changes. When the OS scheme changes and the user -/// is in System theme mode, dispatches a message to update resolvedTheme. -/// -/// The tagger receives a bool: true = OS prefers light, false = OS prefers dark. -let listenColorSchemeChange = (_tagger: bool => 'msg): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(_callbacks => { - try { - let _: unit = %raw(` - window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", (e) => { - callbacks.enqueue(tagger(e.matches)) - }) - `) - } catch { - | _ => () - } - }) -} diff --git a/src/core/AerieEngine.affine b/src/core/AerieEngine.affine new file mode 100644 index 00000000..418b4cc7 --- /dev/null +++ b/src/core/AerieEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AerieEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AerieEngine.res b/src/core/AerieEngine.res deleted file mode 100644 index 0132ed68..00000000 --- a/src/core/AerieEngine.res +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Aerie Engine — pure computation for network diagnostics. - -open AerieModel - -/// Human-readable label for an Aerie category tab. -let categoryLabel = (cat: aerieCategory): string => - switch cat { - | AerieDashboard => "Dashboard" - | AerieSpeedTests => "Speed Tests" - | AerieBgp => "BGP Analysis" - | AerieProbes => "Probes" - } - -/// Classify latency quality. -let latencyQuality = (rttMs: float): string => - if rttMs < 20.0 { - "Excellent" - } else if rttMs < 50.0 { - "Good" - } else if rttMs < 100.0 { - "Fair" - } else { - "Poor" - } - -/// CSS color for latency quality. -let latencyColor = (rttMs: float): string => - if rttMs < 20.0 { - "text-green-400" - } else if rttMs < 50.0 { - "text-emerald-400" - } else if rttMs < 100.0 { - "text-amber-400" - } else { - "text-red-400" - } - -/// Average latency across all results. -let avgLatency = (results: array): float => { - if Array.length(results) > 0 { - results->Array.map(r => r.rttMs)->Array.reduce(0.0, (a, b) => a +. b) /. - Int.toFloat(Array.length(results)) - } else { - 0.0 - } -} - -/// Average jitter across all results. -let avgJitter = (results: array): float => { - if Array.length(results) > 0 { - results->Array.map(r => r.jitterMs)->Array.reduce(0.0, (a, b) => a +. b) /. - Int.toFloat(Array.length(results)) - } else { - 0.0 - } -} - -/// Average packet loss across all results. -let avgPacketLoss = (results: array): float => { - if Array.length(results) > 0 { - results->Array.map(r => r.packetLoss)->Array.reduce(0.0, (a, b) => a +. b) /. - Int.toFloat(Array.length(results)) - } else { - 0.0 - } -} - -/// Classify jitter quality. -let jitterQuality = (jitterMs: float): string => - if jitterMs < 5.0 { - "Excellent" - } else if jitterMs < 20.0 { - "Good" - } else if jitterMs < 50.0 { - "Fair" - } else { - "Poor" - } - -/// MTU mismatch severity label. -let mtuStatus = (result: option): string => - switch result { - | None => "Not tested" - | Some(r) => - if r.mismatch { - "Mismatch: " ++ Int.toString(r.interfaceMtu) ++ " > " ++ Int.toString(r.pathMtu) - } else { - "OK: " ++ Int.toString(r.pathMtu) ++ " bytes" - } - } - -/// CSS color for MTU status. -let mtuColor = (result: option): string => - switch result { - | None => "text-gray-500" - | Some(r) => - if r.mismatch { - "text-red-400" - } else { - "text-green-400" - } - } - -/// Count interfaces that are up. -let interfacesUp = (ifaces: array): int => - ifaces->Array.filter(i => i.isUp)->Array.length - -/// Extended latency quality (adds "Very poor" for mobile/satellite). -let latencyQualityExtended = (rttMs: float): string => - if rttMs < 20.0 { - "Excellent" - } else if rttMs < 50.0 { - "Good" - } else if rttMs < 100.0 { - "Fair" - } else if rttMs < 300.0 { - "Poor" - } else { - "Very poor" - } - -/// Default Aerie panel state — disconnected, no data loaded. -let defaultState: aerieState = { - loaded: false, - loading: false, - error: None, - probes: [], - latencyResults: [], - speedTests: [], - bgpRoutes: [], - activeCategory: AerieDashboard, - bgpAnomalyCount: 0, - mtuResult: None, - interfaces: [], - bojRouting: false, -} diff --git a/src/core/AgentCoordinationEngine.affine b/src/core/AgentCoordinationEngine.affine new file mode 100644 index 00000000..9a0cfbf6 --- /dev/null +++ b/src/core/AgentCoordinationEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentCoordinationEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AgentCoordinationEngine.res b/src/core/AgentCoordinationEngine.res deleted file mode 100644 index 4a08e1df..00000000 --- a/src/core/AgentCoordinationEngine.res +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent Coordination Engine — pure helpers for the coordination -/// view panel. -/// -/// All functions are pure (no side effects). Provides coordination strategy -/// descriptions, memory type classification, topology layout helpers, -/// and node state colour mappings. - -open AgentCoordinationModel - -// ============================================================================ -// Coordination Strategy Helpers -// ============================================================================ - -/// Whether a coordination strategy involves multiple agents. -let isMultiAgent = (strategy: coordination): bool => { - switch strategy { - | Solo => false - | Pipeline => true - | Broadcast => true - | Consensus => true - | Hierarchy => true - | Swarm => true - } -} - -/// Human-readable description of a coordination strategy. -let strategyDescription = (strategy: coordination): string => { - switch strategy { - | Solo => "Single agent operating independently. No inter-agent communication." - | Pipeline => "Sequential chain — each agent passes output to the next." - | Broadcast => "One coordinator sends instructions to all agents simultaneously." - | Consensus => "All agents must agree before any action is taken." - | Hierarchy => "Tree-shaped delegation with a root coordinator and sub-agents." - | Swarm => "Decentralised self-organisation. Agents discover and coordinate autonomously." - } -} - -/// Human-readable display name for a coordination strategy. -let strategyDisplayName = (strategy: coordination): string => { - switch strategy { - | Solo => "Solo" - | Pipeline => "Pipeline" - | Broadcast => "Broadcast" - | Consensus => "Consensus" - | Hierarchy => "Hierarchy" - | Swarm => "Swarm" - } -} - -/// Lucide icon name for a coordination strategy. -let strategyIcon = (strategy: coordination): string => { - switch strategy { - | Solo => "user" - | Pipeline => "arrow-right" - | Broadcast => "radio" - | Consensus => "users" - | Hierarchy => "git-branch" - | Swarm => "hexagon" - } -} - -// ============================================================================ -// Memory Type Helpers -// ============================================================================ - -/// Whether a memory type persists beyond a single session. -let memoryIsPersistent = (mem: memoryType): bool => { - switch mem { - | Ephemeral => false - | Session => false - | Persistent => true - | Shared => true - | Immutable => true - } -} - -/// Human-readable label for a memory type. -let memoryLabel = (mem: memoryType): string => { - switch mem { - | Ephemeral => "Ephemeral" - | Session => "Session" - | Persistent => "Persistent" - | Shared => "Shared" - | Immutable => "Immutable" - } -} - -/// Short icon-like character for a memory type indicator. -let memoryIndicator = (mem: memoryType): string => { - switch mem { - | Ephemeral => "E" - | Session => "S" - | Persistent => "P" - | Shared => "H" - | Immutable => "I" - } -} - -/// Tailwind colour class for a memory type indicator badge. -let memoryColor = (mem: memoryType): string => { - switch mem { - | Ephemeral => "bg-gray-600 text-gray-200" - | Session => "bg-blue-600 text-white" - | Persistent => "bg-emerald-600 text-white" - | Shared => "bg-purple-600 text-white" - | Immutable => "bg-amber-600 text-white" - } -} - -// ============================================================================ -// Node State Colours -// ============================================================================ - -/// Tailwind text colour class for an agent node state. -let nodeStateColor = (state: agentNodeState): string => { - switch state { - | Active => "text-emerald-400" - | Idle => "text-gray-400" - | Disconnected => "text-red-400" - | NodeError => "text-red-600" - } -} - -/// Tailwind border colour class for an agent node state. -let nodeBorderColor = (state: agentNodeState): string => { - switch state { - | Active => "border-emerald-500" - | Idle => "border-gray-500" - | Disconnected => "border-red-500" - | NodeError => "border-red-600" - } -} - -/// Human-readable label for a node state. -let nodeStateLabel = (state: agentNodeState): string => { - switch state { - | Active => "Active" - | Idle => "Idle" - | Disconnected => "Disconnected" - | NodeError => "Error" - } -} - -// ============================================================================ -// Topology Layout -// ============================================================================ - -/// Suggested layout style for a coordination strategy. -/// Returns a CSS-compatible layout hint. -let topologyLayout = (strategy: coordination): string => { - switch strategy { - | Solo => "single" - | Pipeline => "horizontal" - | Broadcast => "star" - | Consensus => "ring" - | Hierarchy => "tree" - | Swarm => "mesh" - } -} - -/// Tailwind border colour for a strategy card. -let strategyBorderColor = (strategy: coordination, isSelected: bool): string => { - if isSelected { - "border-emerald-500 ring-2 ring-emerald-500/30" - } else { - switch strategy { - | Solo => "border-gray-500/40" - | Pipeline => "border-blue-500/40" - | Broadcast => "border-cyan-500/40" - | Consensus => "border-purple-500/40" - | Hierarchy => "border-amber-500/40" - | Swarm => "border-red-500/40" - } - } -} - -// ============================================================================ -// Initial State -// ============================================================================ - -/// Default initial state for the Agent Coordination View. -let init: agentCoordinationState = { - nodes: [], - edges: [], - selectedStrategy: None, - loading: false, -} diff --git a/src/core/AgentOodaEngine.affine b/src/core/AgentOodaEngine.affine new file mode 100644 index 00000000..42d7fffd --- /dev/null +++ b/src/core/AgentOodaEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentOodaEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AgentOodaEngine.res b/src/core/AgentOodaEngine.res deleted file mode 100644 index b50764a0..00000000 --- a/src/core/AgentOodaEngine.res +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent OODA Engine — pure helpers for the OODA session monitor panel. -/// -/// All functions are pure (no side effects). Provides state colour mappings, -/// transition logic, session health indicators, and display helpers. - -open AgentOodaModel - -// ============================================================================ -// State Colour Mappings -// ============================================================================ - -/// Tailwind background colour class for an agent state. -let stateColor = (state: agentState): string => { - switch state { - | Observing => "bg-blue-600 text-white" - | Orienting => "bg-cyan-600 text-white" - | Deciding => "bg-amber-500 text-white" - | Acting => "bg-emerald-600 text-white" - | Halted => "bg-red-600 text-white" - } -} - -/// Tailwind text colour class for an agent state. -let stateTextColor = (state: agentState): string => { - switch state { - | Observing => "text-blue-400" - | Orienting => "text-cyan-400" - | Deciding => "text-amber-400" - | Acting => "text-emerald-400" - | Halted => "text-red-400" - } -} - -/// Tailwind border colour class for an agent state. -let stateBorderColor = (state: agentState): string => { - switch state { - | Observing => "border-blue-500" - | Orienting => "border-cyan-500" - | Deciding => "border-amber-500" - | Acting => "border-emerald-500" - | Halted => "border-red-500" - } -} - -// ============================================================================ -// State Icons and Labels -// ============================================================================ - -/// Lucide icon name for an agent state. -let stateIcon = (state: agentState): string => { - switch state { - | Observing => "eye" - | Orienting => "compass" - | Deciding => "brain" - | Acting => "play" - | Halted => "octagon" - } -} - -/// Human-readable label for an agent state. -let stateLabel = (state: agentState): string => { - switch state { - | Observing => "Observing" - | Orienting => "Orienting" - | Deciding => "Deciding" - | Acting => "Acting" - | Halted => "Halted" - } -} - -// ============================================================================ -// Transition Logic -// ============================================================================ - -/// Whether a transition from the current state to a target state is valid. -/// OODA loops follow: Observing -> Orienting -> Deciding -> Acting -> Observing. -/// Any state can transition to Halted. -let canTransition = (current: agentState, target: agentState): bool => { - switch (current, target) { - | (_, Halted) => true - | (Observing, Orienting) => true - | (Orienting, Deciding) => true - | (Deciding, Acting) => true - | (Acting, Observing) => true - | _ => false - } -} - -/// The next state in the OODA loop (does not include Halted). -let nextState = (current: agentState): agentState => { - switch current { - | Observing => Orienting - | Orienting => Deciding - | Deciding => Acting - | Acting => Observing - | Halted => Halted - } -} - -// ============================================================================ -// Session Health -// ============================================================================ - -/// Health classification for a session based on loop count and state. -type sessionHealthLevel = - /// Healthy — session is progressing normally. - | Healthy - /// Slow — session has a low loop rate. - | Slow - /// Stuck — session has not progressed. - | Stuck - /// Dead — session is halted. - | Dead - -/// Assess the health of a session based on its state and loop count. -let sessionHealth = (session: oodaSession): sessionHealthLevel => { - if session.wasHalted || session.state == Halted { - Dead - } else if session.loopCount == 0 { - Stuck - } else if session.loopCount < 3 { - Slow - } else { - Healthy - } -} - -/// Tailwind text colour for a health level. -let healthColor = (health: sessionHealthLevel): string => { - switch health { - | Healthy => "text-emerald-400" - | Slow => "text-amber-400" - | Stuck => "text-red-400" - | Dead => "text-gray-500" - } -} - -/// Calculate loop rate (loops per second) from detail data. -let loopRate = (detail: sessionDetail): float => { - if detail.totalElapsedMs > 0.0 { - Int.toFloat(detail.session.loopCount) /. (detail.totalElapsedMs /. 1000.0) - } else { - 0.0 - } -} - -// ============================================================================ -// Initial State -// ============================================================================ - -/// Default initial state for the OODA Session Monitor. -let init: agentOodaState = { - sessions: [], - selectedSessionId: None, - selectedDetail: None, - loading: false, -} diff --git a/src/core/AgentSafetyEngine.affine b/src/core/AgentSafetyEngine.affine new file mode 100644 index 00000000..c6e68061 --- /dev/null +++ b/src/core/AgentSafetyEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgentSafetyEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AgentSafetyEngine.res b/src/core/AgentSafetyEngine.res deleted file mode 100644 index 6f9433d4..00000000 --- a/src/core/AgentSafetyEngine.res +++ /dev/null @@ -1,161 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Agent Safety Engine — pure helpers for the safety gate panel. -/// -/// All functions are pure (no side effects). Provides tool call classification, -/// safety check logic, colour mappings, and filtering. - -open AgentSafetyModel - -// ============================================================================ -// Tool Call Classification -// ============================================================================ - -/// Whether a tool call type has side effects (modifies external state). -let hasSideEffects = (tc: toolCall): bool => { - switch tc { - | FileRead => false - | FileWrite => true - | NetworkRequest => true - | ShellExec => true - | DatabaseOp => true - | ExternalApi => true - } -} - -/// Whether a tool call type requires a safety check before execution. -let requiresSafetyCheck = (tc: toolCall): bool => { - hasSideEffects(tc) -} - -/// Whether a safety check outcome allows execution to proceed. -let allowsExecution = (check: safetyCheck): bool => { - switch check { - | AutoApproved => true - | HumanApproved => true - | PendingReview => false - | HumanDenied => false - | Escalated => false - | PolicyBlocked => false - } -} - -/// Whether a safety check outcome needs human intervention. -let needsHuman = (check: safetyCheck): bool => { - switch check { - | PendingReview => true - | Escalated => true - | AutoApproved => false - | HumanApproved => false - | HumanDenied => false - | PolicyBlocked => false - } -} - -// ============================================================================ -// Colour Mappings -// ============================================================================ - -/// Tailwind background colour class for a safety event based on its outcome. -let eventColor = (outcome: safetyCheck): string => { - switch outcome { - | AutoApproved => "bg-emerald-900/20 border-emerald-500/40" - | HumanApproved => "bg-emerald-900/20 border-emerald-500/40" - | PendingReview => "bg-amber-900/20 border-amber-500/40" - | HumanDenied => "bg-red-900/20 border-red-500/40" - | Escalated => "bg-orange-900/20 border-orange-500/40" - | PolicyBlocked => "bg-red-900/20 border-red-500/40" - } -} - -/// Tailwind text colour class for a safety check outcome. -let outcomeTextColor = (outcome: safetyCheck): string => { - switch outcome { - | AutoApproved => "text-emerald-400" - | HumanApproved => "text-emerald-400" - | PendingReview => "text-amber-400" - | HumanDenied => "text-red-400" - | Escalated => "text-orange-400" - | PolicyBlocked => "text-red-400" - } -} - -// ============================================================================ -// Filtering -// ============================================================================ - -/// Filter events to only those pending human review. -let filterPending = (events: array): array => { - events->Array.filter(e => e.outcome == PendingReview) -} - -/// Filter events by agent identifier. -let filterByAgent = (events: array, agentId: string): array => { - events->Array.filter(e => e.agentId == agentId) -} - -// ============================================================================ -// Display Labels -// ============================================================================ - -/// Human-readable label for a tool call type. -let toolCallLabel = (tc: toolCall): string => { - switch tc { - | FileRead => "File Read" - | FileWrite => "File Write" - | NetworkRequest => "Network Request" - | ShellExec => "Shell Exec" - | DatabaseOp => "Database Op" - | ExternalApi => "External API" - } -} - -/// Human-readable label for a safety check outcome. -let outcomeLabel = (check: safetyCheck): string => { - switch check { - | AutoApproved => "Auto-Approved" - | PendingReview => "Pending Review" - | HumanApproved => "Approved" - | HumanDenied => "Denied" - | Escalated => "Escalated" - | PolicyBlocked => "Policy Blocked" - } -} - -// ============================================================================ -// Statistics Computation -// ============================================================================ - -/// Compute aggregate safety statistics from pending and history arrays. -let computeStats = (pending: array, history: array): safetyStats => { - let all = Array.concat(pending, history) - let totalEvents = Array.length(all) - let autoApproved = all->Array.filter(e => e.outcome == AutoApproved)->Array.length - let humanApproved = all->Array.filter(e => e.outcome == HumanApproved)->Array.length - let denied = all->Array.filter(e => e.outcome == HumanDenied)->Array.length - let escalated = all->Array.filter(e => e.outcome == Escalated)->Array.length - let policyBlocked = all->Array.filter(e => e.outcome == PolicyBlocked)->Array.length - let pendingCount = Array.length(pending) - {totalEvents, autoApproved, humanApproved, denied, escalated, policyBlocked, pendingCount} -} - -// ============================================================================ -// Initial State -// ============================================================================ - -/// Default initial state for the Agent Safety Gate. -let init: agentSafetyState = { - pendingEvents: [], - historyEvents: [], - stats: { - totalEvents: 0, - autoApproved: 0, - humanApproved: 0, - denied: 0, - escalated: 0, - policyBlocked: 0, - pendingCount: 0, - }, - loading: false, -} diff --git a/src/core/AgenticBridgeEngine.affine b/src/core/AgenticBridgeEngine.affine new file mode 100644 index 00000000..50a0f103 --- /dev/null +++ b/src/core/AgenticBridgeEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AgenticBridgeEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AgenticBridgeEngine.res b/src/core/AgenticBridgeEngine.res deleted file mode 100644 index 26009f2a..00000000 --- a/src/core/AgenticBridgeEngine.res +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Agentic Bridge Engine — pure computation and helpers for the -/// Agentic Bridge panel. Provides default state, tab metadata, agent -/// status counting, finding aggregation, and OODA phase formatting. - -open AgenticBridgeModel - -/// Default state for the Agentic Bridge panel. -/// Starts on the Agents tab with empty agent and config lists. -let defaultState: agenticBridgeState = { - activeTab: Agents, - agents: [], - agentConfigs: [], - running: false, - selectedAgent: None, - error: None, -} - -/// Human-readable label for each tab in the Agentic Bridge panel. -let tabLabel = (tab: agenticBridgeTab): string => - switch tab { - | Agents => "Agents" - | Config => "Config" - | Execution => "Execution" - | Results => "Results" - } - -/// All tabs in display order. -let allTabs: array = [Agents, Config, Execution, Results] - -/// Count agents matching a given operational status. -let countAgentsByStatus = (agents: array, status: agentStatus): int => - agents->Array.filter(a => a.status === status)->Array.length - -/// Count the total number of findings across all agents. -let countFindings = (agents: array): int => - agents->Array.reduce(0, (acc, agent) => acc + Array.length(agent.findings)) - -/// Count findings by severity across all agents. -let countFindingsBySeverity = (agents: array, severity: findingSeverity): int => - agents->Array.reduce(0, (acc, agent) => - acc + agent.findings->Array.filter(f => f.severity === severity)->Array.length - ) - -/// Human-readable label for an agent operational status. -let agentStatusLabel = (status: agentStatus): string => - switch status { - | AgentIdle => "Idle" - | AgentRunning => "Running" - | AgentPaused => "Paused" - | AgentCompleted => "Completed" - | AgentFailed => "Failed" - } - -/// Human-readable label for a finding severity. -let findingSeverityLabel = (severity: findingSeverity): string => - switch severity { - | FindingCritical => "Critical" - | FindingMajor => "Major" - | FindingMinor => "Minor" - | FindingObservation => "Observation" - } - -/// Format an OODA phase as a human-readable string with description. -let formatOodaPhase = (phase: agenticOodaPhase): string => - switch phase { - | Observe => "Observe — gathering game state information" - | Orient => "Orient — analysing observations against patterns" - | Decide => "Decide — selecting next action" - | Act => "Act — executing chosen action" - } - -/// Short label for an OODA phase (single word). -let oodaPhaseLabel = (phase: agenticOodaPhase): string => - switch phase { - | Observe => "Observe" - | Orient => "Orient" - | Decide => "Decide" - | Act => "Act" - } - -/// Count total actions performed across all agents. -let countTotalActions = (agents: array): int => - agents->Array.reduce(0, (acc, agent) => acc + Array.length(agent.actions)) diff --git a/src/core/AiEngine.affine b/src/core/AiEngine.affine new file mode 100644 index 00000000..4bb3fb2f --- /dev/null +++ b/src/core/AiEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AiEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AiEngine.res b/src/core/AiEngine.res deleted file mode 100644 index 62e3aeb9..00000000 --- a/src/core/AiEngine.res +++ /dev/null @@ -1,463 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AI Engine — pure computation for the multi-provider AI panel. -/// -/// All functions are pure (no side effects, no API calls). Provides: -/// - Default state initialisation -/// - Provider label, colour, and icon helpers -/// - Message formatting and token display -/// - Precedence sorting and provider selection -/// - JSON parsing for Gossamer command responses - -open AiModel - -/// Human-readable label for a provider ID. -let providerLabel = (id: aiProviderId): string => { - switch id { - | Anthropic => "Anthropic" - | Google => "Google" - | Mistral => "Mistral" - | OpenAI => "OpenAI" - | Local => "Local" - } -} - -/// Short label for provider (used in message attribution). -let providerShortLabel = (id: aiProviderId): string => { - switch id { - | Anthropic => "Claude" - | Google => "Gemini" - | Mistral => "Mistral" - | OpenAI => "GPT" - | Local => "Ollama" - } -} - -/// Tailwind CSS colour class for a provider (conversation bubble borders). -let providerColour = (id: aiProviderId): string => { - switch id { - | Anthropic => "border-orange-500" - | Google => "border-blue-500" - | Mistral => "border-yellow-500" - | OpenAI => "border-green-500" - | Local => "border-purple-500" - } -} - -/// Background accent colour for provider badges. -let providerBgColour = (id: aiProviderId): string => { - switch id { - | Anthropic => "bg-orange-500/20 text-orange-300" - | Google => "bg-blue-500/20 text-blue-300" - | Mistral => "bg-yellow-500/20 text-yellow-300" - | OpenAI => "bg-green-500/20 text-green-300" - | Local => "bg-purple-500/20 text-purple-300" - } -} - -/// Icon identifier for a provider (for the provider selector). -let providerIcon = (id: aiProviderId): string => { - switch id { - | Anthropic => "brain" - | Google => "sparkles" - | Mistral => "wind" - | OpenAI => "cpu" - | Local => "server" - } -} - -/// Human-readable label for a provider status. -let statusLabel = (status: aiProviderStatus): string => { - switch status { - | Ready => "Ready" - | Checking => "Checking..." - | QuotaExhausted => "Quota Exhausted" - | AiProviderError(msg) => `Error: ${msg}` - | Disabled => "Disabled" - | NoKey => "No API Key" - } -} - -/// CSS class for status indicator dot. -let statusDotClass = (status: aiProviderStatus): string => { - switch status { - | Ready => "bg-green-400" - | Checking => "bg-yellow-400 animate-pulse" - | QuotaExhausted => "bg-red-400" - | AiProviderError(_) => "bg-red-400" - | Disabled => "bg-gray-600" - | NoKey => "bg-gray-500" - } -} - -/// Human-readable label for a category tab. -let categoryLabel = (cat: aiCategory): string => { - switch cat { - | Conversation => "Conversation" - | SystemPrompt => "System Prompt" - | Providers => "Providers" - | Context => "Context" - } -} - -/// All category tabs in display order. -let allCategories: array = [Conversation, SystemPrompt, Providers, Context] - -/// Human-readable label for a message role. -let roleLabel = (role: aiRole): string => { - switch role { - | User => "You" - | Assistant => "AI" - | System => "System" - } -} - -/// Format a token count for display (e.g., "1.2k", "45"). -let formatTokens = (count: int): string => { - if count >= 1000 { - let k = Int.toFloat(count) /. 1000.0 - Float.toFixed(k, ~digits=1) ++ "k" - } else { - Int.toString(count) - } -} - -/// Sort providers by priority (lowest number first = highest priority). -let sortByPriority = (providers: array): array => { - let sorted = Array.copy(providers) - sorted->Array.sort((a, b) => Int.compare(a.priority, b.priority)) - sorted -} - -/// Select the best available provider: highest priority, enabled, non-exhausted. -let selectProvider = (providers: array): option => { - let sorted = sortByPriority(providers) - sorted->Array.find(p => p.enabled && !p.quotaExhausted) -} - -/// Get the status for a provider from the status array. -let getProviderStatus = ( - statuses: array<(aiProviderId, aiProviderStatus)>, - id: aiProviderId, -): aiProviderStatus => { - switch statuses->Array.find(((pid, _)) => pid === id) { - | Some((_, status)) => status - | None => NoKey - } -} - -/// Serialise a provider ID to a string for the Gossamer command bridge. -let providerIdToString = (id: aiProviderId): string => { - switch id { - | Anthropic => "anthropic" - | Google => "google" - | Mistral => "mistral" - | OpenAI => "openai" - | Local => "local" - } -} - -/// Parse a provider ID from a string. -let providerIdFromString = (s: string): option => { - switch String.toLowerCase(s) { - | "anthropic" => Some(Anthropic) - | "google" => Some(Google) - | "mistral" => Some(Mistral) - | "openai" => Some(OpenAI) - | "local" => Some(Local) - | _ => None - } -} - -/// Default provider configurations. Matches the Rust `AiProvidersFile::defaults()`. -let defaultProviders: array = [ - { - id: Anthropic, - apiKey: None, - envVar: "ANTHROPIC_API_KEY", - enabled: true, - priority: 1, - selectedModel: "claude-opus-4-6", - quotaExhausted: false, - }, - { - id: Google, - apiKey: None, - envVar: "GOOGLE_AI_KEY", - enabled: true, - priority: 2, - selectedModel: "gemini-2.5-pro", - quotaExhausted: false, - }, - { - id: Mistral, - apiKey: None, - envVar: "MISTRAL_API_KEY", - enabled: false, - priority: 3, - selectedModel: "mistral-large-latest", - quotaExhausted: false, - }, - { - id: OpenAI, - apiKey: None, - envVar: "OPENAI_API_KEY", - enabled: false, - priority: 4, - selectedModel: "gpt-4o", - quotaExhausted: false, - }, - { - id: Local, - apiKey: None, - envVar: "", - enabled: false, - priority: 5, - selectedModel: "llama3", - quotaExhausted: false, - }, -] - -/// Default initial state for the AI panel. -let defaultState: aiState = { - providers: defaultProviders, - providerStatuses: [ - (Anthropic, NoKey), - (Google, NoKey), - (Mistral, Disabled), - (OpenAI, Disabled), - (Local, Disabled), - ], - messages: [], - inputText: "", - systemPrompt: "You are an AI assistant embedded in PanLL, a neurosymbolic development environment. You have access to the loaded repository's context, constraints, and panel state.", - autoContext: "", - loading: false, - activeCategory: Conversation, - broadcastMode: false, - error: None, - totalInputTokens: 0, - totalOutputTokens: 0, - streaming: { - active: false, - currentText: "", - pendingToolCalls: [], - completedToolResults: [], - }, -} - -/// Available models for each provider. -let providerModels = (id: aiProviderId): array => { - switch id { - | Anthropic => ["claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5-20251001"] - | Google => ["gemini-2.5-pro", "gemini-2.5-flash"] - | Mistral => ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest"] - | OpenAI => ["gpt-4o", "o3", "gpt-4o-mini"] - | Local => ["llama3", "mistral", "codellama", "phi3"] - } -} - -/// Tea_Json decoder for a message response. -/// Returns Error("quota_exhausted") if the quota_exhausted field is true. -let messageResponseDecoder: Tea_Json.decoder = json => { - open Decoders - open Tea_Json - let inner = map6( - (providerStr, content, model, inputTokens, outputTokens, quotaExhausted) => ( - providerStr, - content, - model, - inputTokens, - outputTokens, - quotaExhausted, - ), - stringField("provider"), - stringField("content"), - stringField("model"), - intField("input_tokens"), - intField("output_tokens"), - boolField("quota_exhausted"), - ) - switch inner(json) { - | Ok((_, _, _, _, _, true)) => Error(Failure("quota_exhausted", json)) - | Ok((providerStr, content, model, inputTokens, outputTokens, _)) => - Ok( - ( - { - role: Assistant, - content, - provider: providerIdFromString(providerStr), - model: Some(model), - inputTokens, - outputTokens, - timestamp: Date.now(), - }: aiMessage - ), - ) - | Error(e) => Error(e) - } -} - -/// Parse a SendMessageResponse from the Gossamer backend JSON. -let parseMessageResponse = (jsonStr: string): result => - Decoders.decode(messageResponseDecoder, jsonStr) - -/// Tea_Json decoder for a single AI provider config. -/// Validates the provider ID, skipping unknown providers. -let providerConfigDecoder: Tea_Json.decoder = json => { - open Decoders - open Tea_Json - let inner = map5( - (idStr, envVar, enabled, priority, model) => (idStr, envVar, enabled, priority, model), - stringField("id"), - stringField("env_var"), - boolField("enabled"), - intField("priority"), - stringField("model"), - ) - switch inner(json) { - | Ok((idStr, envVar, enabled, priority, model)) => - switch providerIdFromString(idStr) { - | Some(id) => - Ok( - ( - { - id, - apiKey: None, - envVar, - enabled, - priority, - selectedModel: model, - quotaExhausted: false, - }: aiProviderConfig - ), - ) - | None => Error(Failure(`Unknown provider id: ${idStr}`, json)) - } - | Error(e) => Error(e) - } -} - -/// Tea_Json decoder for the provider state response envelope. -let providerStateDecoder: Tea_Json.decoder> = Tea_Json.field( - "providers", - Decoders.lenientArray(providerConfigDecoder), -) - -/// Parse provider config state from the Gossamer backend JSON. -let parseProviderState = (jsonStr: string): result, string> => - Decoders.decode(providerStateDecoder, jsonStr) - -// --------------------------------------------------------------------------- -// Streaming helpers — parse stream chunks and manage streaming state -// --------------------------------------------------------------------------- - -/// Parse a stream chunk JSON from a Gossamer event payload. -/// Returns `(chunkType, optionalData)` where chunkType is one of: -/// "TextDelta", "ToolUseStart", "ToolUseDelta", "ToolUseEnd", "Complete", "Error". -let parseStreamChunk = (json: string): result<(string, option), string> => { - try { - let parsed = JSON.parseExn(json) - switch JSON.Classify.classify(parsed) { - | Object(obj) => { - let chunkType = switch Dict.get(obj, "type") { - | Some(v) => - switch JSON.Classify.classify(v) { - | String(s) => s - | _ => "unknown" - } - | None => "unknown" - } - let data = Dict.get(obj, "data") - Ok((chunkType, data)) - } - | _ => Error("Stream chunk is not an object: " ++ json) - } - } catch { - | _ => Error("Failed to parse stream chunk: " ++ json) - } -} - -/// Create default streaming state (inactive, no accumulated data). -let defaultStreamingState = (): streamingState => { - active: false, - currentText: "", - pendingToolCalls: [], - completedToolResults: [], -} - -/// Append a text delta to the streaming state's accumulated text. -let appendTextDelta = (state: streamingState, text: string): streamingState => { - ...state, - currentText: state.currentText ++ text, -} - -/// Start a new tool call (ToolUseStart received). -let startToolCall = (state: streamingState, id: string, name: string): streamingState => { - let newCall: toolCallState = { - id, - name, - accumulatedInput: "", - status: Accumulating, - } - { - ...state, - pendingToolCalls: Array.concat(state.pendingToolCalls, [newCall]), - } -} - -/// Append JSON input to the most recent pending tool call (ToolUseDelta). -let appendToolInput = (state: streamingState, partialJson: string): streamingState => { - let len = Array.length(state.pendingToolCalls) - if len === 0 { - state - } else { - let calls = Array.copy(state.pendingToolCalls) - switch calls[len - 1] { - | Some(last) => - last.accumulatedInput = last.accumulatedInput ++ partialJson - {...state, pendingToolCalls: calls} - | None => state - } - } -} - -/// Info needed to dispatch a tool call to BoJ. -type toolCallDispatchInfo = { - /// The tool_use_id from Claude. - callId: string, - /// The tool name (maps to a BoJ cartridge tool). - callName: string, - /// The accumulated JSON input for the tool. - callInput: string, -} - -/// Mark the most recent pending tool call as ready for execution (ToolUseEnd). -/// Returns the tool call info for dispatch, if any. -let finalizeToolCall = (state: streamingState): (streamingState, option) => { - let len = Array.length(state.pendingToolCalls) - if len === 0 { - (state, None) - } else { - let calls = Array.copy(state.pendingToolCalls) - switch calls[len - 1] { - | Some(last) => - last.status = Executing - ( - {...state, pendingToolCalls: calls}, - Some({callId: last.id, callName: last.name, callInput: last.accumulatedInput}), - ) - | None => (state, None) - } - } -} - -/// Check if all pending tool calls have completed (Completed or Failed). -let allToolCallsComplete = (state: streamingState): bool => { - Array.every(state.pendingToolCalls, tc => - switch tc.status { - | Completed(_) | Failed(_) => true - | Accumulating | Executing => false - } - ) -} diff --git a/src/core/AmbientOpsEngine.affine b/src/core/AmbientOpsEngine.affine new file mode 100644 index 00000000..e7a61dca --- /dev/null +++ b/src/core/AmbientOpsEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AmbientOpsEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/AmbientOpsEngine.res b/src/core/AmbientOpsEngine.res deleted file mode 100644 index 12b0f1a4..00000000 --- a/src/core/AmbientOpsEngine.res +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL AmbientOps Engine — pure helpers for hospital-model sysadmin panel. - -open AmbientOpsModel - -/// Default initial state. -let defaultState: ambientOpsState = { - activeTab: TabDashboard, - findings: [], - scanning: false, - error: None, - clinicianAvailable: false, - networkRepairAvailable: false, - hardwareCrashTeamAvailable: false, -} - -/// Tab label for display. -let tabLabel = (tab: ambientOpsTab): string => { - switch tab { - | TabDashboard => "Dashboard" - | TabClinician => "Clinician" - | TabNetwork => "Network" - | TabHardware => "Hardware" - | TabEmergency => "Emergency" - } -} - -/// All tabs for rendering. -let allTabs: array = [ - TabDashboard, - TabClinician, - TabNetwork, - TabHardware, - TabEmergency, -] - -/// Department label for display. -let departmentLabel = (d: department): string => { - switch d { - | Clinician => "Clinician" - | NetworkAmbulance => "Network Ambulance" - | HardwareCrashTeam => "Hardware Crash Team" - | EmergencyRoom => "Emergency Room" - | AmbientObservatory => "Observatory" - } -} - -/// Severity label for display. -let severityLabel = (s: diagnosticSeverity): string => { - switch s { - | Info => "Info" - | Warning => "Warning" - | Error => "Error" - | Critical => "Critical" - } -} - -/// Count findings by severity. -let countBySeverity = (findings: array, target: diagnosticSeverity): int => { - findings->Array.filter(f => f.severity == target)->Array.length -} - -/// Count findings by department. -let countByDepartment = (findings: array, target: department): int => { - findings->Array.filter(f => f.department == target)->Array.length -} - -/// Filter findings for a specific department. -let findingsForDepartment = (findings: array, target: department): array< - diagnosticFinding, -> => { - findings->Array.filter(f => f.department == target) -} diff --git a/src/core/AntiCrash.affine b/src/core/AntiCrash.affine new file mode 100644 index 00000000..1b49fb96 --- /dev/null +++ b/src/core/AntiCrash.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module AntiCrash; + +// TODO: Complete semantic implementation diff --git a/src/core/AntiCrash.res b/src/core/AntiCrash.res deleted file mode 100644 index 8f35477c..00000000 --- a/src/core/AntiCrash.res +++ /dev/null @@ -1,221 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// AntiCrash Module - The Logical Circuit Breaker -/// -/// Implements the Transduction Controller that intercepts all neural -/// tokens and validates them against symbolic constraints before -/// allowing them to reach the Task Barycentre. -/// -/// No inference passes without symbolic validation. - -open Model - -/// Validation result -type validationResult = - | Valid - | Invalid(string) - | RequiresReview(string) - -/// Initial Anti-Crash state -let init = (): antiCrashState => { - enabled: true, - strictMode: true, - violations: [], - halted: false, - pendingReview: None, -} - -/// Check if a token violates type constraints -let checkTypeConstraints = (token: neuralToken, constraints: array): option< - violationType, -> => { - let activeConstraints = Array.filter(constraints, c => c.active) - - let violation = Array.find(activeConstraints, c => { - let expr = c.expression - - // Check forbidden patterns: !contains("pattern") - if String.includes(expr, "!contains(") { - let pattern = - expr - ->String.replaceAll("!contains(\"", "") - ->String.replaceAll("\")", "") - ->String.trim - - if String.includes(token.content, pattern) { - true - } else { - false - } - } // Check type declarations: type FooBar - else if String.startsWith(expr, "type ") { - let parts = String.split(expr, " ") - if Array.length(parts) >= 2 { - switch parts[1] { - | Some(typeName) => { - let reserved = ["undefined", "null", "NaN", "eval", "function"] - Array.includes(reserved, typeName) - } - | None => false - } - } else { - false - } - } else { - // Fallback: check for dangerous keywords - - String.includes(token.content, "undefined") || - String.includes(token.content, "null") || - String.includes(token.content, "NaN") - } - }) - - switch violation { - | Some(c) => Some(TypeMismatch(c.expression, "inferred type")) - | None => None - } -} - -/// Check for security violations -let checkSecurityConstraints = (token: neuralToken): option => { - // panic-attack:allow dynamic-code-detection-string — these are detection - // patterns for security scanning, not actual code execution. Each string - // is matched against user input to detect dangerous patterns. - let dangerousPatterns = ["eval(", "exec(", "rm -rf", "DROP TABLE", "DELETE FROM", " -// -// Or install via deno.json / import map and bundle it. - -/// Check whether DOMPurify is available in the global scope. -/// Returns true if window.DOMPurify exists and has a sanitize method. -let isAvailable: unit => bool = () => { - %raw(`typeof globalThis.DOMPurify !== 'undefined' && typeof globalThis.DOMPurify.sanitize === 'function'`) -} - -/// Configuration for DOMPurify.sanitize(). -/// Maps to the DOMPurify config object. -type config = { - /// Allowlisted HTML tags. If empty, uses DOMPurify defaults. - allowedTags?: array, - /// Allowlisted HTML attributes. If empty, uses DOMPurify defaults. - allowedAttr?: array, - /// Forbid specific tags (blocklist on top of defaults). - forbidTags?: array, - /// Forbid specific attributes. - forbidAttr?: array, - /// Return a DOM node instead of string (we always want string). - returnDom?: bool, - /// Allow custom elements. - customElementHandling?: bool, -} - -/// Default configuration that blocks the most dangerous elements. -/// Removes: script, iframe, object, embed, form, base, meta (with http-equiv), -/// svg (event handlers), math, link (stylesheet injection), template. -let defaultConfig: config = { - forbidTags: [ - "script", - "iframe", - "object", - "embed", - "form", - "base", - "meta", - "link", - "template", - "math", - "svg", - ], - forbidAttr: [ - "onerror", - "onload", - "onclick", - "onmouseover", - "onfocus", - "onblur", - "onsubmit", - "onchange", - "oninput", - "onkeydown", - "onkeyup", - "onkeypress", - "formaction", - "xlink:href", - "action", - ], -} - -/// Sanitise HTML using DOMPurify with default high-assurance config. -/// Returns Some(sanitised) if DOMPurify is available, None otherwise. -let sanitize = (_html: string): option => { - if isAvailable() { - let _forbidTags = defaultConfig.forbidTags - let _forbidAttr = defaultConfig.forbidAttr - let result: string = %raw(` - globalThis.DOMPurify.sanitize(_html, { - FORBID_TAGS: _forbidTags || [], - FORBID_ATTR: _forbidAttr || [], - ALLOW_ARIA_ATTR: true, - ALLOW_DATA_ATTR: false, - RETURN_DOM: false, - RETURN_DOM_FRAGMENT: false, - WHOLE_DOCUMENT: false - }) - `) - Some(result) - } else { - None - } -} - -/// Sanitise HTML using a custom DOMPurify configuration. -/// Returns Some(sanitised) if DOMPurify is available, None otherwise. -let sanitizeWithConfig = (_html: string, cfg: config): option => { - if isAvailable() { - let _forbidTags = switch cfg.forbidTags { - | Some(tags) => tags - | None => [] - } - let _forbidAttr = switch cfg.forbidAttr { - | Some(attrs) => attrs - | None => [] - } - let _allowedTags = switch cfg.allowedTags { - | Some(tags) => tags - | None => [] - } - let _allowedAttr = switch cfg.allowedAttr { - | Some(attrs) => attrs - | None => [] - } - let _hasAllowedTags = Option.isSome(cfg.allowedTags) - let _hasAllowedAttr = Option.isSome(cfg.allowedAttr) - let result: string = %raw(` - (function() { - var config = { - FORBID_TAGS: _forbidTags, - FORBID_ATTR: _forbidAttr, - ALLOW_ARIA_ATTR: true, - ALLOW_DATA_ATTR: false, - RETURN_DOM: false - }; - if (_hasAllowedTags) config.ALLOWED_TAGS = _allowedTags; - if (_hasAllowedAttr) config.ALLOWED_ATTR = _allowedAttr; - return globalThis.DOMPurify.sanitize(_html, config); - })() - `) - Some(result) - } else { - None - } -} - -/// Get DOMPurify version string, if available. -let version = (): option => { - if isAvailable() { - let v: string = %raw(`globalThis.DOMPurify.version || "unknown"`) - Some(v) - } else { - None - } -} diff --git a/src/core/DatabaseBridgeEngine.affine b/src/core/DatabaseBridgeEngine.affine new file mode 100644 index 00000000..35f0e250 --- /dev/null +++ b/src/core/DatabaseBridgeEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DatabaseBridgeEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/DatabaseBridgeEngine.res b/src/core/DatabaseBridgeEngine.res deleted file mode 100644 index 83ece5be..00000000 --- a/src/core/DatabaseBridgeEngine.res +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Database Bridge Engine — pure computation and helpers for the -/// Database Bridge panel. Provides default state, tab metadata, schema and -/// query counting, and proof obligation status formatting. - -open DatabaseBridgeModel - -/// Default state for the Database Bridge panel. -/// Starts on the Schema tab with empty schema, query, and obligation lists. -let defaultState: databaseBridgeState = { - activeTab: Schema, - schemas: [], - queries: [], - proofObligations: [], - gameStateSnapshot: None, - connected: false, - error: None, -} - -/// Human-readable label for each tab in the Database Bridge panel. -let tabLabel = (tab: databaseBridgeTab): string => - switch tab { - | Schema => "Schema" - | Queries => "Queries" - | GameState => "Game State" - | ProofObligations => "Proof Obligations" - } - -/// All tabs in display order. -let allTabs: array = [Schema, Queries, GameState, ProofObligations] - -/// Count the total number of registered schemas. -let countSchemas = (state: databaseBridgeState): int => Array.length(state.schemas) - -/// Count the total number of columns across all schemas. -let countTotalColumns = (schemas: array): int => - schemas->Array.reduce(0, (acc, s) => acc + Array.length(s.columns)) - -/// Count query history entries. -let countQueries = (state: databaseBridgeState): int => Array.length(state.queries) - -/// Count queries by execution status. -let countQueriesByStatus = (queries: array, status: queryStatus): int => - queries->Array.filter(q => q.status === status)->Array.length - -/// Format a proof obligation status as a human-readable string. -let formatObligationStatus = (status: proofObligationStatus): string => - switch status { - | ObligationProven => "Proven" - | ObligationUnproven => "Unproven" - | ObligationViolated => "Violated" - | ObligationTimeout => "Timeout" - } - -/// Count proof obligations by verification status. -let countObligationsByStatus = ( - obligations: array, - status: proofObligationStatus, -): int => obligations->Array.filter(o => o.status === status)->Array.length - -/// Compute the percentage of proven obligations (0.0 to 100.0). -/// Returns 100.0 when there are no obligations. -let proofObligationPercent = (obligations: array): float => { - let total = Array.length(obligations) - if total === 0 { - 100.0 - } else { - let proven = obligations->Array.filter(o => o.status === ObligationProven)->Array.length - Int.toFloat(proven) /. Int.toFloat(total) *. 100.0 - } -} - -/// Summarise a game state snapshot as a human-readable string. -/// Returns "No snapshot" when no snapshot is available. -let formatSnapshotSummary = (snapshot: option): string => - switch snapshot { - | None => "No snapshot" - | Some(s) => - `${Int.toString(s.tableCount)} tables, ${Int.toString(s.totalRows)} rows, ${Int.toString( - s.sizeBytes, - )} bytes` - } diff --git a/src/core/DatabasesEngine.affine b/src/core/DatabasesEngine.affine new file mode 100644 index 00000000..3740015c --- /dev/null +++ b/src/core/DatabasesEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DatabasesEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/DatabasesEngine.res b/src/core/DatabasesEngine.res deleted file mode 100644 index c23eb97c..00000000 --- a/src/core/DatabasesEngine.res +++ /dev/null @@ -1,178 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Databases Engine — pure logic for the Databases panel. -/// -/// Initialises per-module state from DatabaseRegistry, provides default state, -/// and contains utility functions for filtering, sorting, and aggregating -/// database module information. - -open DatabaseModule -open DatabasesModel - -/// Initialise the databases panel state with all registered modules. -let defaultState: databasesState = { - modules: DatabaseRegistry.allModules()->Array.map(initModuleState), - selectedModule: "verisim", - activeCategory: DbDashboard, - queryInput: "", - queryLoading: false, - queryHistory: [], - schemaEntities: [ - { - name: "octads", - kind: "table", - fields: [ - "id", - "graph", - "vector", - "tensor", - "semantic", - "document", - "temporal", - "provenance", - "spatial", - ], - entryCount: 5, - }, - { - name: "entities", - kind: "table", - fields: ["id", "name", "modality", "created_at", "updated_at"], - entryCount: 12, - }, - { - name: "drift_log", - kind: "table", - fields: ["id", "dimension", "score", "timestamp", "resolved"], - entryCount: 24, - }, - { - name: "proof_certificates", - kind: "table", - fields: ["id", "type", "contract", "status", "hash"], - entryCount: 8, - }, - { - name: "federation_peers", - kind: "table", - fields: ["node_id", "address", "state", "last_seen"], - entryCount: 3, - }, - ], - selectedEntity: None, - entityDetail: None, - filterText: "", - loading: false, - error: None, - lastTypeCheck: None, - bojRouting: false, -} - -/// Find a module state by ID. -let findModule = (state: databasesState, id: string): option => { - state.modules->Array.find(m => m.config.id == id) -} - -/// Get the currently selected module state. -let selectedModuleState = (state: databasesState): option => { - findModule(state, state.selectedModule) -} - -/// Update a specific module state by ID. -let updateModule = ( - state: databasesState, - id: string, - updater: moduleState => moduleState, -): databasesState => { - { - ...state, - modules: state.modules->Array.map(m => - if m.config.id == id { - updater(m) - } else { - m - } - ), - } -} - -/// Count connected modules. -let connectedCount = (state: databasesState): int => { - state.modules - ->Array.filter(m => - switch m.connection { - | Connected(_) => true - | _ => false - } - ) - ->Array.length -} - -/// Count total capabilities across all modules. -let totalCapabilities = (state: databasesState): int => { - state.modules->Array.reduce(0, (acc, m) => acc + Array.length(m.config.capabilities)) -} - -/// Filter schema entities by search text. -let filteredEntities = (state: databasesState): array => { - if state.filterText == "" { - state.schemaEntities - } else { - let needle = state.filterText->String.toLowerCase - state.schemaEntities->Array.filter(e => - e.name->String.toLowerCase->String.includes(needle) || - e.kind->String.toLowerCase->String.includes(needle) - ) - } -} - -/// Add a query to the history (most recent first, capped at 100). -let addToHistory = (state: databasesState, entry: queryHistoryEntry): databasesState => { - let history = [entry]->Array.concat(state.queryHistory) - let capped = if Array.length(history) > 100 { - history->Array.slice(~start=0, ~end=100) - } else { - history - } - {...state, queryHistory: capped} -} - -/// Connection status label for display. -let connectionLabel = (status: connectionStatus): string => { - switch status { - | Disconnected => "Disconnected" - | Connecting => "Connecting..." - | Connected(url) => "Connected (" ++ url ++ ")" - | Error(msg) => "Error: " ++ msg - } -} - -/// Connection status CSS colour class. -let connectionColour = (status: connectionStatus): string => { - switch status { - | Disconnected => "bg-gray-600" - | Connecting => "bg-amber-400 animate-pulse" - | Connected(_) => "bg-emerald-400" - | Error(_) => "bg-red-400" - } -} - -/// Module accent colour for UI differentiation. -let moduleAccent = (id: string): string => { - switch id { - | "verisim" => "#34d399" // emerald - | "quandledb" => "#818cf8" // indigo - | "lithoglyph" => "#fb923c" // orange - | _ => "#9ca3af" // gray - } -} - -/// Module icon label. -let moduleIcon = (id: string): string => { - switch id { - | "verisim" => "VDB" - | "quandledb" => "QDB" - | "lithoglyph" => "LG" - | _ => "DB" - } -} diff --git a/src/core/DebugLogger.affine b/src/core/DebugLogger.affine new file mode 100644 index 00000000..6ebbf2e5 --- /dev/null +++ b/src/core/DebugLogger.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DebugLogger; + +// TODO: Complete semantic implementation diff --git a/src/core/DebugLogger.res b/src/core/DebugLogger.res deleted file mode 100644 index 574366c0..00000000 --- a/src/core/DebugLogger.res +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// PanLL Debug Logger — structured logging routed through Observatory. -/// -/// Replaces ad-hoc console.log with structured, observable debug output. -/// All log entries become OTLP spans visible in Observatory panel. - -/// Log level for structured debug output. -type logLevel = - | Debug - | Info - | Warn - | Error - -/// A structured log entry. -type logEntry = { - level: logLevel, - source: string, - message: string, - timestamp: float, - metadata: array<(string, string)>, -} - -/// Format log level as string. -let levelLabel = (level: logLevel): string => - switch level { - | Debug => "DEBUG" - | Info => "INFO" - | Warn => "WARN" - | Error => "ERROR" - } - -/// Format log level as OTLP severity number. -let levelToOtelSeverity = (level: logLevel): int => - switch level { - | Debug => 5 - | Info => 9 - | Warn => 13 - | Error => 17 - } - -/// Create a log entry. -let makeEntry = ( - level: logLevel, - source: string, - message: string, - ~metadata: array<(string, string)>=[], -): logEntry => { - level, - source, - message, - timestamp: Date.now(), - metadata, -} - -/// Format entry as OTLP log record (for ObservabilityEngine export). -let toOtelLogRecord = (entry: logEntry): Dict.t => { - let dict = Dict.make() - Dict.set(dict, "timeUnixNano", JSON.Encode.float(entry.timestamp *. 1000000.0)) - Dict.set(dict, "severityNumber", JSON.Encode.int(levelToOtelSeverity(entry.level))) - Dict.set(dict, "severityText", JSON.Encode.string(levelLabel(entry.level))) - Dict.set(dict, "body", JSON.Encode.string(entry.message)) - dict -} - -/// Ring buffer for recent log entries (last 200). -let maxEntries = 200 - -/// Add entry to ring buffer. -let addEntry = (entries: array, entry: logEntry): array => { - let next = Array.concat(entries, [entry]) - if Array.length(next) > maxEntries { - next->Array.sliceToEnd(~start=Array.length(next) - maxEntries) - } else { - next - } -} - -/// Filter entries by level. -let filterByLevel = (entries: array, minLevel: logLevel): array => { - let minSev = levelToOtelSeverity(minLevel) - entries->Array.filter(e => levelToOtelSeverity(e.level) >= minSev) -} - -/// Filter entries by source. -let filterBySource = (entries: array, source: string): array => - entries->Array.filter(e => e.source == source) diff --git a/src/core/DebuggingWorkbenchEngine.affine b/src/core/DebuggingWorkbenchEngine.affine new file mode 100644 index 00000000..48487e29 --- /dev/null +++ b/src/core/DebuggingWorkbenchEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DebuggingWorkbenchEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/DebuggingWorkbenchEngine.res b/src/core/DebuggingWorkbenchEngine.res deleted file mode 100644 index 5a3d5a3b..00000000 --- a/src/core/DebuggingWorkbenchEngine.res +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Debugging Workbench Engine — pure computation and helpers for the -/// Debugging Workbench panel. Provides default state, time-travel navigation, -/// snapshot management, watch expression helpers, and console formatting. - -open DebuggingWorkbenchModel - -/// Default initial state for the Debugging Workbench panel. -/// Starts on the Time Travel tab with empty snapshot and watch lists. -let defaultState: debuggingWorkbenchState = { - activeTab: TabTimeTravel, - timeTravel: { - currentIndex: 0, - snapshots: [], - isTimeTravelling: false, - }, - watches: [], - consoleLog: [], - consoleEntries: [], - selectedSnapshot: None, - error: None, -} - -/// Human-readable label for each tab in the Debugging Workbench panel. -let tabLabel = (tab: debuggingWorkbenchTab): string => - switch tab { - | TabTimeTravel => "Time Travel" - | TabStateInspector => "State Inspector" - | TabWatchExpressions => "Watch Expressions" - | TabConsole => "Console" - } - -/// All tabs in display order. -let allTabs: array = [ - TabTimeTravel, - TabStateInspector, - TabWatchExpressions, - TabConsole, -] - -/// Number of captured snapshots. -let snapshotCount = (tt: timeTravelState): int => Array.length(tt.snapshots) - -/// Whether the time-travel slider can move backward. -let canGoBack = (tt: timeTravelState): bool => tt.currentIndex > 0 - -/// Whether the time-travel slider can move forward. -let canGoForward = (tt: timeTravelState): bool => tt.currentIndex < Array.length(tt.snapshots) - 1 - -/// Get the current snapshot (at the current index). -let currentSnapshot = (tt: timeTravelState): option => { - let count = Array.length(tt.snapshots) - if count == 0 || tt.currentIndex >= count { - None - } else { - Some(tt.snapshots->Array.getUnsafe(tt.currentIndex)) - } -} - -/// Progress through the snapshot timeline as a percentage (0.0 to 100.0). -let timelineProgress = (tt: timeTravelState): float => { - let count = Array.length(tt.snapshots) - if count <= 1 { - 100.0 - } else { - Float.fromInt(tt.currentIndex) /. Float.fromInt(count - 1) *. 100.0 - } -} - -/// Duration between first and last snapshot in seconds. -let timelineDuration = (tt: timeTravelState): float => { - let count = Array.length(tt.snapshots) - if count < 2 { - 0.0 - } else { - let first = tt.snapshots->Array.getUnsafe(0) - let last = tt.snapshots->Array.getUnsafe(count - 1) - (last.timestamp -. first.timestamp) /. 1000.0 - } -} - -/// Number of watch expressions defined. -let watchCount = (watches: array): int => Array.length(watches) - -/// Console log entry count (legacy string-based). -let consoleLineCount = (log: array): int => Array.length(log) - -/// Count console entries by level. -let countConsoleByLevel = (entries: array, level: string): int => - entries->Array.filter(e => e.level == level)->Array.length - -/// CSS colour class for console entry level. -let consoleLevelColor = (level: string): string => - switch level { - | "error" => "text-red-400" - | "warn" => "text-yellow-400" - | "info" => "text-blue-400" - | _ => "text-gray-300" - } - -/// Format a timestamp as a relative time label (e.g., "2.3s ago"). -let formatRelativeTime = (timestamp: float, now: float): string => { - let diffMs = now -. timestamp - let seconds = diffMs /. 1000.0 - if seconds < 60.0 { - Float.toFixed(seconds, ~digits=1) ++ "s ago" - } else { - let minutes = seconds /. 60.0 - Float.toFixed(minutes, ~digits=1) ++ "m ago" - } -} diff --git a/src/core/Decoders.affine b/src/core/Decoders.affine new file mode 100644 index 00000000..5d504995 --- /dev/null +++ b/src/core/Decoders.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module Decoders; + +// TODO: Complete semantic implementation diff --git a/src/core/Decoders.res b/src/core/Decoders.res deleted file mode 100644 index 4133d21c..00000000 --- a/src/core/Decoders.res +++ /dev/null @@ -1,471 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) -// -// Decoders.res — Extended Tea_Json decoder combinators and domain-specific -// decoders for PanLL Gossamer command responses. -// -// Provides map6–map13 for records with more than 5 fields, plus convenience -// wrappers used across Update.res and engine modules. - -open Tea_Json - -// ── Extended map combinators ─────────────────────────────────────────── -// -// Tea_Json provides map–map5. These extend coverage for larger records -// by composing map5 + mapN internally. - -/// Combine six decoders with a function. -let map6 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f) => 'g, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, -): decoder<'g> => { - json => { - switch (d1(json), d2(json), d3(json), d4(json), d5(json), d6(json)) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv)) => Ok(f(a, b, c, d, e, fv)) - | (Error(e), _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _) => Error(e) - | (_, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine seven decoders with a function. -let map7 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g) => 'h, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, -): decoder<'h> => { - json => { - switch (d1(json), d2(json), d3(json), d4(json), d5(json), d6(json), d7(json)) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv), Ok(g)) => Ok(f(a, b, c, d, e, fv, g)) - | (Error(e), _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine eight decoders with a function. -let map8 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h) => 'i, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, - d8: decoder<'h>, -): decoder<'i> => { - json => { - switch (d1(json), d2(json), d3(json), d4(json), d5(json), d6(json), d7(json), d8(json)) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv), Ok(g), Ok(h)) => Ok(f(a, b, c, d, e, fv, g, h)) - | (Error(e), _, _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine nine decoders with a function. -let map9 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i) => 'j, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, - d8: decoder<'h>, - d9: decoder<'i>, -): decoder<'j> => { - json => { - switch ( - d1(json), - d2(json), - d3(json), - d4(json), - d5(json), - d6(json), - d7(json), - d8(json), - d9(json), - ) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv), Ok(g), Ok(h), Ok(i)) => - Ok(f(a, b, c, d, e, fv, g, h, i)) - | (Error(e), _, _, _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine ten decoders with a function. -let map10 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j) => 'k, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, - d8: decoder<'h>, - d9: decoder<'i>, - d10: decoder<'j>, -): decoder<'k> => { - json => { - switch ( - d1(json), - d2(json), - d3(json), - d4(json), - d5(json), - d6(json), - d7(json), - d8(json), - d9(json), - d10(json), - ) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv), Ok(g), Ok(h), Ok(i), Ok(j)) => - Ok(f(a, b, c, d, e, fv, g, h, i, j)) - | (Error(e), _, _, _, _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _, _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _, _, _, _) => Error(e) - | (_, _, _, _, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, _, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine eleven decoders with a function. -let map11 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k) => 'l, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, - d8: decoder<'h>, - d9: decoder<'i>, - d10: decoder<'j>, - d11: decoder<'k>, -): decoder<'l> => { - json => { - switch ( - d1(json), - d2(json), - d3(json), - d4(json), - d5(json), - d6(json), - d7(json), - d8(json), - d9(json), - d10(json), - d11(json), - ) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv), Ok(g), Ok(h), Ok(i), Ok(j), Ok(k)) => - Ok(f(a, b, c, d, e, fv, g, h, i, j, k)) - | (Error(e), _, _, _, _, _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _, _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _, _, _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _, _, _, _, _) => Error(e) - | (_, _, _, _, _, Error(e), _, _, _, _, _) => Error(e) - | (_, _, _, _, _, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, _, _, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, _, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine twelve decoders with a function. -let map12 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, 'l) => 'm, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, - d8: decoder<'h>, - d9: decoder<'i>, - d10: decoder<'j>, - d11: decoder<'k>, - d12: decoder<'l>, -): decoder<'m> => { - json => { - switch ( - d1(json), - d2(json), - d3(json), - d4(json), - d5(json), - d6(json), - d7(json), - d8(json), - d9(json), - d10(json), - d11(json), - d12(json), - ) { - | (Ok(a), Ok(b), Ok(c), Ok(d), Ok(e), Ok(fv), Ok(g), Ok(h), Ok(i), Ok(j), Ok(k), Ok(l)) => - Ok(f(a, b, c, d, e, fv, g, h, i, j, k, l)) - | (Error(e), _, _, _, _, _, _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _, _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _, _, _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _, _, _, _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _, _, _, _, _, _) => Error(e) - | (_, _, _, _, _, Error(e), _, _, _, _, _, _) => Error(e) - | (_, _, _, _, _, _, Error(e), _, _, _, _, _) => Error(e) - | (_, _, _, _, _, _, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, _, _, _, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, _, _, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, _, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -/// Combine thirteen decoders with a function. -let map13 = ( - f: ('a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, 'k, 'l, 'm) => 'n, - d1: decoder<'a>, - d2: decoder<'b>, - d3: decoder<'c>, - d4: decoder<'d>, - d5: decoder<'e>, - d6: decoder<'f>, - d7: decoder<'g>, - d8: decoder<'h>, - d9: decoder<'i>, - d10: decoder<'j>, - d11: decoder<'k>, - d12: decoder<'l>, - d13: decoder<'m>, -): decoder<'n> => { - json => { - switch ( - d1(json), - d2(json), - d3(json), - d4(json), - d5(json), - d6(json), - d7(json), - d8(json), - d9(json), - d10(json), - d11(json), - d12(json), - d13(json), - ) { - | ( - Ok(a), - Ok(b), - Ok(c), - Ok(d), - Ok(e), - Ok(fv), - Ok(g), - Ok(h), - Ok(i), - Ok(j), - Ok(k), - Ok(l), - Ok(m), - ) => - Ok(f(a, b, c, d, e, fv, g, h, i, j, k, l, m)) - | (Error(e), _, _, _, _, _, _, _, _, _, _, _, _) => Error(e) - | (_, Error(e), _, _, _, _, _, _, _, _, _, _, _) => Error(e) - | (_, _, Error(e), _, _, _, _, _, _, _, _, _, _) => Error(e) - | (_, _, _, Error(e), _, _, _, _, _, _, _, _, _) => Error(e) - | (_, _, _, _, Error(e), _, _, _, _, _, _, _, _) => Error(e) - | (_, _, _, _, _, Error(e), _, _, _, _, _, _, _) => Error(e) - | (_, _, _, _, _, _, Error(e), _, _, _, _, _, _) => Error(e) - | (_, _, _, _, _, _, _, Error(e), _, _, _, _, _) => Error(e) - | (_, _, _, _, _, _, _, _, Error(e), _, _, _, _) => Error(e) - | (_, _, _, _, _, _, _, _, _, Error(e), _, _, _) => Error(e) - | (_, _, _, _, _, _, _, _, _, _, Error(e), _, _) => Error(e) - | (_, _, _, _, _, _, _, _, _, _, _, Error(e), _) => Error(e) - | (_, _, _, _, _, _, _, _, _, _, _, _, Error(e)) => Error(e) - } - } -} - -// ── Convenience wrappers ──────────────────────────────────────────────── - -/// Decode a JSON string, returning Ok(value) or Error(formatted message). -/// Wraps Tea_Json.decodeString with human-readable error output. -let decode = (decoder: decoder<'a>, jsonString: string): result<'a, string> => { - switch decodeString(decoder, jsonString) { - | Ok(v) => Ok(v) - | Error(e) => Error(errorToString(e)) - } -} - -/// Decode a JSON string, returning Some(value) or None on failure. -/// Use when you only need the happy path and failures map to defaults. -let decodeOption = (decoder: decoder<'a>, jsonString: string): option<'a> => { - switch decodeString(decoder, jsonString) { - | Ok(v) => Some(v) - | Error(_) => None - } -} - -/// Decode a JSON string, returning the value or a default on failure. -let decodeWithDefault = (decoder: decoder<'a>, default: 'a, jsonString: string): 'a => { - switch decodeString(decoder, jsonString) { - | Ok(v) => v - | Error(_) => default - } -} - -/// Field with default value — returns default if field is missing or wrong type. -let fieldWithDefault = (name: string, decoder: decoder<'a>, default: 'a): decoder<'a> => { - json => { - switch field(name, decoder)(json) { - | Ok(v) => Ok(v) - | Error(_) => Ok(default) - } - } -} - -/// Decode a string field, defaulting to "" if missing. -let stringField = (name: string): decoder => fieldWithDefault(name, string, "") - -/// Decode a float field, defaulting to 0.0 if missing. -let floatField = (name: string): decoder => fieldWithDefault(name, float, 0.0) - -/// Decode an int field, defaulting to 0 if missing. -let intField = (name: string): decoder => fieldWithDefault(name, int, 0) - -/// Decode a bool field, defaulting to false if missing. -let boolField = (name: string): decoder => fieldWithDefault(name, bool, false) - -/// Decode an array field, defaulting to empty array if missing. -let arrayField = (name: string, itemDecoder: decoder<'a>): decoder> => - fieldWithDefault(name, array(itemDecoder), []) - -/// Decode an array, silently skipping elements that fail to decode. -/// Use instead of Tea_Json.array when malformed elements should be -/// dropped rather than failing the entire decode. -let lenientArray = (decoder: decoder<'a>): decoder> => { - json => { - switch json { - | Array(arr) => - Ok( - Array.filterMap(arr, item => { - switch decoder(item) { - | Ok(v) => Some(v) - | Error(_) => None - } - }), - ) - | _ => Error(Failure("Expected an array", json)) - } - } -} - -/// Decode a string array field, defaulting to empty array. -/// Common pattern for proversUsed, goals, proofScript, tacticsApplied, etc. -let stringArrayField = (name: string): decoder> => - fieldWithDefault(name, lenientArray(string), []) - -/// Map a decoded value through a transformation function. -/// Useful for variant mapping (e.g., int → trustLevel variant). -let mapValue = (decoder: decoder<'a>, f: 'a => 'b): decoder<'b> => map(decoder, f) - -/// Decode the "result" wrapper common in Cloudflare-style API responses. -/// Handles both `[...]` and `{ "result": [...] }` shapes. -let resultArrayOrDirect = (itemDecoder: decoder<'a>): decoder> => { - oneOf([lenientArray(itemDecoder), field("result", lenientArray(itemDecoder))]) -} - -/// Decode an optional field — returns Some(value) if present and decodable, -/// None otherwise. Useful for fields like "priority" or "comment" that may -/// be absent or null. -let optionalFieldDecoder = (name: string, decoder: decoder<'a>): decoder> => { - json => { - switch field(name, decoder)(json) { - | Ok(v) => Ok(Some(v)) - | Error(_) => Ok(None) - } - } -} - -/// Decode a string-keyed dict from a JSON object, extracting values as floats. -/// Used for heatmaps and score objects. -let floatDict: decoder> = json => { - switch json { - | Object(d) => { - let pairs = [] - Dict.forEachWithKey(d, (val, key) => { - switch float(val) { - | Ok(n) => Array.push(pairs, (key, n))->ignore - | Error(_) => () - } - }) - Ok(pairs) - } - | _ => Error(Failure("Expected an object", json)) - } -} - -/// Decode a string-keyed dict from a JSON object, extracting values as ints. -/// Used for query pattern and proof type counts. -let intDict: decoder> = json => { - switch json { - | Object(d) => { - let pairs = [] - Dict.forEachWithKey(d, (val, key) => { - switch int(val) { - | Ok(n) => Array.push(pairs, (key, n))->ignore - | Error(_) => () - } - }) - Ok(pairs) - } - | _ => Error(Failure("Expected an object", json)) - } -} diff --git a/src/core/DeviceNetworkDesignerEngine.affine b/src/core/DeviceNetworkDesignerEngine.affine new file mode 100644 index 00000000..e88d92c1 --- /dev/null +++ b/src/core/DeviceNetworkDesignerEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DeviceNetworkDesignerEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/DeviceNetworkDesignerEngine.res b/src/core/DeviceNetworkDesignerEngine.res deleted file mode 100644 index f4497ec9..00000000 --- a/src/core/DeviceNetworkDesignerEngine.res +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Device Network Designer Engine — pure computation and helpers for -/// wiring devices, configuring security levels, and validating topology. -/// -/// Provides default state, tab labels, and utility functions for counting -/// devices by type, counting connections, and summarising validation results. - -open DeviceNetworkDesignerModel - -/// Default state for the Device Network Designer panel. -let defaultState: deviceNetworkDesignerState = { - activeTab: Designer, - devices: [], - connections: [], - validation: None, - selectedDevice: None, - selectedConnection: None, - wiringMode: false, - error: None, -} - -/// Human-readable label for a device network designer category tab. -let tabLabel = (cat: deviceNetworkDesignerCategory): string => - switch cat { - | Designer => "Designer" - | Devices => "Devices" - | Wiring => "Wiring" - | Validation => "Validation" - } - -/// All category tabs in display order. -let allTabs: array = [Designer, Devices, Wiring, Validation] - -/// Count devices matching a given device type string. -let countDevicesByType = (devices: array, deviceType: string): int => - devices->Array.filter(d => d.deviceType === deviceType)->Array.length - -/// Count total wire connections. -let countConnections = (connections: array): int => connections->Array.length - -/// Produce a human-readable validation summary string. -/// Returns "No validation run" if no result is available. -let validationSummary = (validation: option): string => - switch validation { - | None => "No validation run" - | Some(v) => { - let status = v.valid ? "PASS" : "FAIL" - let errorCount = v.errors->Array.length - let warningCount = v.warnings->Array.length - `${status}: ${Int.toString(v.deviceCount)} devices, ${Int.toString( - v.connectionCount, - )} connections, ${Int.toString(errorCount)} errors, ${Int.toString(warningCount)} warnings` - } - } diff --git a/src/core/DlcWorkshopEngine.affine b/src/core/DlcWorkshopEngine.affine new file mode 100644 index 00000000..8e1385f9 --- /dev/null +++ b/src/core/DlcWorkshopEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module DlcWorkshopEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/DlcWorkshopEngine.res b/src/core/DlcWorkshopEngine.res deleted file mode 100644 index 9cadc24b..00000000 --- a/src/core/DlcWorkshopEngine.res +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL DLC Workshop Engine — pure computation and helpers for the -/// IDApTIK DLC puzzle pack creation and testing panel. - -open DlcWorkshopModel - -/// Human-readable labels for category tabs. -let categoryLabel = (cat: dlcWorkshopCategory): string => - switch cat { - | WorkshopPuzzles => "Puzzles" - | WorkshopComposer => "Composer" - | WorkshopTesting => "Testing" - | WorkshopAssets => "Assets" - | WorkshopPackaging => "Packaging" - } - -/// Human-readable difficulty labels. -let difficultyLabel = (diff: puzzleDifficulty): string => - switch diff { - | DifficultyTutorial => "Tutorial" - | DifficultyEasy => "Easy" - | DifficultyMedium => "Medium" - | DifficultyHard => "Hard" - | DifficultyExpert => "Expert" - | DifficultyNightmare => "Nightmare" - } - -/// Colour class for each difficulty. -let difficultyColour = (diff: puzzleDifficulty): string => - switch diff { - | DifficultyTutorial => "text-emerald-400" - | DifficultyEasy => "text-cyan-400" - | DifficultyMedium => "text-amber-400" - | DifficultyHard => "text-orange-400" - | DifficultyExpert => "text-red-400" - | DifficultyNightmare => "text-purple-400" - } - -/// Test status label. -let testStatusLabel = (status: testRunStatus): string => - switch status { - | TestNotRun => "Not Run" - | TestRunning => "Running..." - | TestPassed => "Passed" - | TestFailed(reason) => `Failed: ${reason}` - | TestTimeout => "Timeout" - } - -/// Test status colour. -let testStatusColour = (status: testRunStatus): string => - switch status { - | TestNotRun => "text-gray-500" - | TestRunning => "text-amber-400" - | TestPassed => "text-emerald-400" - | TestFailed(_) => "text-red-400" - | TestTimeout => "text-orange-400" - } - -/// All difficulty levels for filter dropdown. -let allDifficulties: array = [ - DifficultyTutorial, - DifficultyEasy, - DifficultyMedium, - DifficultyHard, - DifficultyExpert, - DifficultyNightmare, -] - -/// Count puzzles by difficulty. -let countByDifficulty = (puzzles: array, diff: puzzleDifficulty): int => - puzzles->Array.filter(p => p.difficulty === diff)->Array.length - -/// Count passed tests. -let passedTests = (puzzles: array): int => - puzzles->Array.filter(p => p.testStatus === TestPassed)->Array.length - -/// Filter puzzles by text and difficulty. -let filterPuzzles = ( - puzzles: array, - filterText: string, - filterDifficulty: option, -): array => { - let filtered = switch filterDifficulty { - | Some(diff) => puzzles->Array.filter(p => p.difficulty === diff) - | None => puzzles - } - if filterText === "" { - filtered - } else { - let lower = String.toLowerCase(filterText) - filtered->Array.filter(p => - String.includes(String.toLowerCase(p.name), lower) || - String.includes(String.toLowerCase(p.description), lower) - ) - } -} - -/// Default pack metadata. -let defaultPackMeta: dlcPackMeta = { - packId: "", - name: "Untitled Pack", - version: "0.1.0", - author: "Jonathan D.A. Jewell", - description: "", - puzzleCount: 0, - totalSizeBytes: 0, -} - -/// Default state for the DLC Workshop panel. -let defaultState: dlcWorkshopState = { - activeCategory: WorkshopPuzzles, - puzzles: [], - chains: [], - assets: [], - packMeta: defaultPackMeta, - selectedPuzzleId: None, - selectedChainId: None, - composerInstructions: [], - testResults: [], - filterText: "", - filterDifficulty: None, - showTestOutput: false, - loading: false, - error: None, -} diff --git a/src/core/EchidnaEngine.affine b/src/core/EchidnaEngine.affine new file mode 100644 index 00000000..ef6b5f70 --- /dev/null +++ b/src/core/EchidnaEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EchidnaEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/EchidnaEngine.res b/src/core/EchidnaEngine.res deleted file mode 100644 index 39e86445..00000000 --- a/src/core/EchidnaEngine.res +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ECHIDNA Engine — pure helpers for theorem prover dispatch panel. - -open EchidnaModel - -/// Default enterprise model state. -let defaultEnterpriseModelState: enterpriseModelState = { - elements: [], - constraints: [], - checkResults: [], - checking: false, - activeMetamodel: None, - activeLayer: None, - lastXmiImport: None, -} - -/// Default initial state. -let defaultState: echidnaState = { - connected: false, - endpoint: "http://localhost:9000/api/v1", - version: None, - provers: [], - lastProofResult: None, - proofError: None, - proofLoading: false, - session: None, - tacticSuggestions: [], - selectedProver: None, - proofInput: "", - menuExpanded: false, - activeTab: EchidnaProofTab, - tacticInput: "", - sessionLoading: false, - lastProofObligations: None, - bojRouting: false, - enterpriseModel: defaultEnterpriseModelState, -} - -/// Tab label for display. -let tabLabel = (tab: echidnaTab): string => { - switch tab { - | EchidnaProofTab => "Proof Workbench" - | EchidnaEnterpriseTab => "Enterprise Model" - } -} diff --git a/src/core/EditorBridgeEngine.affine b/src/core/EditorBridgeEngine.affine new file mode 100644 index 00000000..9095d41a --- /dev/null +++ b/src/core/EditorBridgeEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EditorBridgeEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/EditorBridgeEngine.res b/src/core/EditorBridgeEngine.res deleted file mode 100644 index b2bdb2ea..00000000 --- a/src/core/EditorBridgeEngine.res +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Editor Bridge Engine — pure computation and helpers for -/// federating with external code editors. - -open EditorBridgeModel - -/// Human-readable labels for category tabs. -let categoryLabel = (cat: editorBridgeCategory): string => - switch cat { - | BridgeOverview => "Overview" - | BridgeDiagnostics => "Diagnostics" - | BridgeSymbols => "Symbols" - | BridgeActivity => "Activity" - | BridgeSettings => "Settings" - } - -/// Human-readable editor name. -let editorLabel = (editor: editorKind): string => - switch editor { - | EditorVSCodium => "VSCodium" - | EditorVSCode => "VS Code" - | EditorZed => "Zed" - | EditorHelix => "Helix" - | EditorNeovim => "Neovim" - | EditorEmacs => "Emacs" - | EditorKakoune => "Kakoune" - | EditorVisualParadigm => "Visual Paradigm" - | EditorSparxEA => "Sparx EA" - | EditorArchi => "Archi" - | EditorCamundaModeler => "Camunda Modeler" - | EditorMagicDraw => "MagicDraw" - | EditorCustom(name) => name - } - -/// All supported editors. -let allEditors: array = [ - EditorVSCodium, - EditorVSCode, - EditorZed, - EditorHelix, - EditorNeovim, - EditorEmacs, - EditorKakoune, -] - -/// Connection state label. -let connectionLabel = (conn: editorConnection): string => - switch conn { - | EditorDisconnected => "Disconnected" - | EditorConnecting => "Connecting..." - | EditorConnected(version) => `Connected (${version})` - | EditorError(err) => `Error: ${err}` - } - -/// Connection colour. -let connectionColour = (conn: editorConnection): string => - switch conn { - | EditorDisconnected => "text-gray-500" - | EditorConnecting => "text-amber-400" - | EditorConnected(_) => "text-emerald-400" - | EditorError(_) => "text-red-400" - } - -/// Severity colour for diagnostics. -let severityColour = (severity: string): string => - switch severity { - | "error" => "text-red-400" - | "warning" => "text-amber-400" - | "info" => "text-blue-400" - | "hint" => "text-gray-400" - | _ => "text-gray-400" - } - -/// Filter diagnostics by severity flags. -let filterDiagnostics = ( - diagnostics: array, - showErrors: bool, - showWarnings: bool, - showInfo: bool, - filterText: string, -): array => { - let bySeverity = diagnostics->Array.filter(d => - switch d.severity { - | "error" => showErrors - | "warning" => showWarnings - | "info" | "hint" => showInfo - | _ => true - } - ) - if filterText === "" { - bySeverity - } else { - let lower = String.toLowerCase(filterText) - bySeverity->Array.filter(d => - String.includes(String.toLowerCase(d.message), lower) || - String.includes(String.toLowerCase(d.filePath), lower) - ) - } -} - -/// Filter symbols by text. -let filterSymbols = (symbols: array, filterText: string): array< - workspaceSymbol, -> => { - if filterText === "" { - symbols - } else { - let lower = String.toLowerCase(filterText) - symbols->Array.filter(s => - String.includes(String.toLowerCase(s.name), lower) || - String.includes(String.toLowerCase(s.containerName), lower) - ) - } -} - -/// Count diagnostics by severity. -let countBySeverity = (diagnostics: array, severity: string): int => - diagnostics->Array.filter(d => d.severity === severity)->Array.length - -/// Default state for the Editor Bridge panel. -let defaultState: editorBridgeState = { - activeCategory: BridgeOverview, - editorKind: EditorVSCodium, - connection: EditorDisconnected, - openFiles: [], - diagnostics: [], - symbols: [], - activity: [], - selectedFilePath: None, - diagnosticFilter: "", - symbolFilter: "", - showWarnings: true, - showErrors: true, - showInfo: false, - autoSync: true, - lspPort: 6008, - loading: false, - error: None, - bojRouting: false, -} diff --git a/src/core/EnsaidConfigEngine.affine b/src/core/EnsaidConfigEngine.affine new file mode 100644 index 00000000..99d93c54 --- /dev/null +++ b/src/core/EnsaidConfigEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EnsaidConfigEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/EnsaidConfigEngine.res b/src/core/EnsaidConfigEngine.res deleted file mode 100644 index 22cc04aa..00000000 --- a/src/core/EnsaidConfigEngine.res +++ /dev/null @@ -1,503 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL ENSAID_CONFIG Engine — generates well-annotated .machine_readable/ENSAID_CONFIG.a2ml -/// files from the combined state of Minter, Provisioner, Workspace, and Automation Router. -/// -/// The generated file is human-readable and heavily annotated so users can tweak -/// it manually without needing to re-open PanLL. Every section includes comments -/// explaining what each field does, what values are valid, and what the defaults are. -/// -/// This is a pure module — no side effects. It builds a string; the Cmd module writes it. - -open ProvisionerModel -open WorkspaceModel -open AutomationRouterModel - -// ============================================================================ -// Helpers -// ============================================================================ - -/// Render an isolation tier to its ENSAID_CONFIG string value. -let isolationToString = (iso: panelIsolation): string => { - switch iso { - | Native => "native" - | StandardPod => "container" - | HardenedPod => "vm" - } -} - -/// Render a workspace mode to its ENSAID_CONFIG string value. -let workspaceModeToString = (mode: workspaceMode): string => { - switch mode { - | RhodiumMode => "rhodium" - | EverythingMode => "everything" - | CodeMode => "code" - | BespokeMode => "bespoke" - } -} - -/// Render session protection to its ENSAID_CONFIG string value. -let protectionToString = (prot: sessionProtection): string => { - switch prot { - | Open => "open" - | ReadOnly => "readonly" - | Sandboxed => "sandboxed" - | LanguageLocked(_) => "language-locked" - | TranspilationGuarded => "transpilation-guarded" - | ProductionGated => "production-gated" - } -} - -/// Render execution mode to its ENSAID_CONFIG string value. -let executionModeToString = (mode: executionMode): string => { - switch mode { - | Live => "live" - | DryRun => "dry-run" - | Simulation => "simulation" - | Emulation => "emulation" - } -} - -/// Render an approval mode to its ENSAID_CONFIG string value. -let approvalToString = (mode: approvalMode): string => { - switch mode { - | AutoFire => "auto-fire" - | RequireApproval => "require-approval" - | ApproveOnce => "approve-once" - | DryRunFirst => "dry-run-first" - } -} - -/// Render a trigger event to a TOML inline table. -let triggerToToml = (trigger: triggerEvent): string => { - switch trigger { - | FileChanged(pattern) => `{ event = "file-changed", pattern = "${pattern}" }` - | PanelMessage(panel, msg) => - `{ event = "panel-message", panel = "${panel}", message = "${msg}" }` - | Timer(seconds) => `{ event = "timer", seconds = ${Int.toString(seconds)} }` - | Manual => `{ event = "manual" }` - | PanelStateChange(panel, field) => - `{ event = "panel-state-change", panel = "${panel}", field = "${field}" }` - } -} - -/// Extract language-lock extensions if applicable. -let languageLockExtensions = (prot: sessionProtection): option> => { - switch prot { - | LanguageLocked(exts) => Some(exts) - | _ => None - } -} - -// ============================================================================ -// Section Generators -// ============================================================================ - -/// Generate the file header with SPDX and explanatory comments. -let generateHeader = (repoName: string): string => { - `# SPDX-License-Identifier: MPL-2.0 -# -# ENSAID_CONFIG.a2ml — eNSAID Environment Configuration for ${repoName} -# -# This file configures PanLL (and any eNSAID-compatible tool) for this repository. -# It was generated by PanLL but is designed to be human-editable. Every section -# includes comments explaining what each field does and what values are valid. -# -# Canonical location: .machine_readable/ENSAID_CONFIG.a2ml -# Spec: https://github.com/hyperpolymath/standards/tree/main/ensaid-config -# -# To regenerate this file from PanLL, use any of: -# - Provisioner panel → Export Config -# - Workspace panel → Export Config -# - Automation Router panel → Settings → Save to Repo -# - Minter panel → after minting a panel, choose "Update ENSAID_CONFIG" -` -} - -/// Generate the [ensaid] section. -let generateEnsaidSection = (): string => { - ` -# ───────────────────────────────────────────────────────────────── -# [ensaid] — Core eNSAID identity -# -# version: Spec version this file conforms to (semver). -# tool: Which eNSAID tool generated this file. Informational only — -# other tools MUST NOT reject configs with a different tool value. -# ───────────────────────────────────────────────────────────────── -[ensaid] -version = "1.0.0" -tool = "panll" -` -} - -/// Generate the [workspace] section from Workspace state. -let generateWorkspaceSection = (ws: workspaceState): string => { - let modeStr = workspaceModeToString(ws.mode) - let protStr = protectionToString(ws.protection) - let execStr = executionModeToString(ws.executionMode) - - let langLock = switch languageLockExtensions(ws.protection) { - | Some(exts) => { - let extList = exts->Array.map(e => `"${e}"`)->Array.join(", ") - ` -# Only editable when protection = "language-locked". -# Files with these extensions are immutable; everything else is open. -[workspace.language-lock] -extensions = [${extList}] -` - } - | None => "" - } - - ` -# ───────────────────────────────────────────────────────────────── -# [workspace] — Workspace mode, protection, and execution policy -# -# mode: Controls which panels and metadata are visible. -# "rhodium" — Full RSR standard compliance view (all governance visible) -# "everything" — Every panel and tool enabled -# "code" — Pure development experience (hides governance panels) -# "bespoke" — Per-repo custom panel set (uses [panels] section below) -# -# protection: What operations are permitted in this workspace. -# "open" — No restrictions, full read-write -# "readonly" — Browse everything, edit nothing -# "sandboxed" — Changes reset when the session ends -# "language-locked" — Only files matching language-lock extensions editable -# "transpilation-guarded" — Saves require equivalence proof -# "production-gated" — Changes staged, require explicit sign-off -# -# execution: How commands and builds run. -# "live" — Real execution against real data -# "dry-run" — Preview changes without applying -# "simulation" — Run against simulated environment with mock data -# "emulation" — Full emulation of target environment locally -# ───────────────────────────────────────────────────────────────── -[workspace] -mode = "${modeStr}" -protection = "${protStr}" -execution = "${execStr}" -` ++ - langLock -} - -/// Generate the [preferences] section. -let generatePreferencesSection = (humidity: string, arrangementId: option): string => { - let arrStr = switch arrangementId { - | Some(id) => id - | None => "default-3-panel" - } - - ` -# ───────────────────────────────────────────────────────────────── -# [preferences] — Display and behaviour preferences -# -# humidity: Intensity of the Orbital Drift Aura (ambient background). -# "high" — Strong colour wash (immersive) -# "medium" — Subtle tint (balanced) -# "low" — Barely visible (minimal distraction) -# -# default-arrangement: Which panel layout loads by default. -# Built-in options: "default-3-panel", "ai-focus", "debug-layout", "teaching-mode" -# Or any custom arrangement ID you've saved. -# -# auto-connect: Whether panels auto-connect to their backends on load. -# true — Panels connect immediately (faster startup) -# false — Panels wait for manual connection (more control) -# ───────────────────────────────────────────────────────────────── -[preferences] -humidity = "${humidity}" -default-arrangement = "${arrStr}" -auto-connect = true -` -} - -/// Generate the [panels] section from Provisioner panel configs. -let generatePanelsSection = (configs: array): string => { - let enabledPanels = configs->Array.filter(c => c.enabled) - let disabledPanels = configs->Array.filter(c => !c.enabled) - - let enabledEntries = - enabledPanels - ->Array.map(c => { - let isoStr = isolationToString(c.isolation) - let endpointLine = if c.endpoint !== "" { - `\nendpoint = "${c.endpoint}"` - } else { - "" - } - let autoLine = if !c.autoConnect { - "\nauto-connect = false" - } else { - "\nauto-connect = true" - } - let envLines = if c.envVars->Array.length > 0 { - let pairs = c.envVars->Array.map(((k, v)) => `${k} = "${v}"`)->Array.join(", ") - `\nenv = { ${pairs} }` - } else { - "" - } - ` -[[panels.enabled]] -id = "${c.panelName}" -isolation = "${isoStr}"${autoLine}${endpointLine}${envLines} -` - }) - ->Array.join("") - - let disabledSection = if disabledPanels->Array.length > 0 { - let ids = disabledPanels->Array.map(c => `"${c.panelName}"`)->Array.join(", ") - ` -# Panels hidden for this repo context. Users can still enable them -# manually through the PanLL UI if the tool supports it. -[panels.disabled] -ids = [${ids}] -` - } else { - ` -# No panels explicitly disabled. Uncomment to hide panels: -# [panels.disabled] -# ids = ["cloudguard", "aerie"] -` - } - - ` -# ───────────────────────────────────────────────────────────────── -# [panels] — Panel visibility, enablement, and configuration -# -# Each [[panels.enabled]] entry declares a panel that should be active. -# If this section is empty or absent, ALL panels are available (tool default). -# -# Fields per panel: -# id: Panel identifier (matches PanLL's panel registry) -# isolation: "native" (in-process), "container" (Podman), "vm" (Stapeln hardened) -# auto-connect: Whether this panel connects to its backend on load -# endpoint: Backend URL (only for panels with external services) -# env: Environment variables scoped to this panel's processes -# ───────────────────────────────────────────────────────────────── -[panels] -version = "1.0.0" -` ++ - enabledEntries ++ - disabledSection -} - -/// Generate the [workflows] section from Automation Router rules. -let generateWorkflowsSection = (rules: array): string => { - let ruleEntries = if rules->Array.length > 0 { - rules - ->Array.map(rule => { - let triggerStr = triggerToToml(rule.trigger) - let approvalStr = approvalToString(rule.approval) - let enabledStr = if !rule.enabled { - "\nenabled = false" - } else { - "" - } - let condLines = - rule.conditions - ->Array.map(cond => { - `condition = { panel = "${cond.panelId}", field = "${cond.field}", operator = "${cond.operator}", value = "${cond.value}" }` - }) - ->Array.join("\n") - let condSection = if condLines !== "" { - "\n" ++ condLines - } else { - "" - } - let actionLines = - rule.actions - ->Array.map(act => { - let argsStr = if act.args->Array.length > 0 { - let pairs = act.args->Array.map(((k, v)) => `${k} = "${v}"`)->Array.join(", ") - `, args = { ${pairs} }` - } else { - "" - } - `action = { panel = "${act.panelId}", message = "${act.message}"${argsStr} }` - }) - ->Array.join("\n") - - ` -# ${rule.description} -[[workflows.rule]] -name = "${rule.name}" -trigger = ${triggerStr}${condSection} -${actionLines} -approval = "${approvalStr}"${enabledStr} -` - }) - ->Array.join("") - } else { - ` -# No automation rules defined yet. Here's an example to get started: -# -# [[workflows.rule]] -# name = "build-on-save" -# trigger = { event = "file-changed", pattern = "src/**/*.res" } -# condition = { panel = "build-dashboard", field = "watchMode", equals = true } -# action = { panel = "build-dashboard", message = "TriggerBuild", args = { target = "game" } } -# approval = "auto-fire" -` - } - - ` -# ───────────────────────────────────────────────────────────────── -# [workflows] — Automation Router event-driven cross-panel rules -# -# Rules fire when a trigger event occurs. Each rule has: -# name: Human-readable identifier (must be unique) -# trigger: What event activates this rule -# condition: (optional) Additional check before the action fires -# action: What to do — sends a message to a panel -# approval: How the action is gated: -# "auto-fire" — Runs immediately, no confirmation -# "require-approval" — Queues in Pending tab, waits for user approval -# "approve-once" — Asks once, then auto-fires for identical triggers -# "dry-run-first" — Shows what would happen, then asks to confirm -# enabled: (optional) Set to false to disable without deleting -# -# Trigger types: -# file-changed: A file matching 'pattern' (glob) was saved -# panel-message: A panel emitted a specific message -# timer: Fires every N seconds -# manual: Only fires when explicitly triggered by user -# panel-state-change: A panel's state field changed value -# ───────────────────────────────────────────────────────────────── -[workflows] -version = "1.0.0" -` ++ - ruleEntries -} - -/// Generate the [clades] section (placeholder — clade overrides are per-repo). -let generateCladesSection = (): string => { - ` -# ───────────────────────────────────────────────────────────────── -# [clades] — Panel clade trait and capability overrides -# -# Clades are the taxonomic categories of panels (ai, bridge, builder, -# database, directive, loader, meta, network, scanner, terminal, viewer). -# Override traits and capabilities here for this repo context only — -# the global clade registry is not modified. -# -# Fields per override: -# id: Clade identifier to customise -# traits: Key-value pairs to set or override (booleans/strings) -# capabilities-add: Capabilities to add for this repo -# capabilities-remove: Capabilities to remove for this repo -# ───────────────────────────────────────────────────────────────── -[clades] -version = "1.0.0" - -# Example: give the build dashboard a work-items trait for this repo -# [[clades.override]] -# id = "build-dashboard" -# traits = { has-work-items = true } -# capabilities-add = ["CustomCheck"] -` -} - -/// Generate the [portfolios] section from Provisioner portfolios. -let generatePortfoliosSection = (portfolios: array): string => { - let customPortfolios = portfolios->Array.filter(p => !p.builtIn) - - let portfolioEntries = if customPortfolios->Array.length > 0 { - customPortfolios - ->Array.map(p => { - let panelList = p.panels->Array.map(id => `"${id}"`)->Array.join(", ") - let isoStr = isolationToString(p.defaultIsolation) - ` -# ${p.description} -[[portfolios.custom]] -id = "${p.id}" -name = "${p.name}" -description = "${p.description}" -panels = [${panelList}] -default-isolation = "${isoStr}" -` - }) - ->Array.join("") - } else { - ` -# No custom portfolios defined yet. Here's an example: -# -# [[portfolios.custom]] -# id = "my-project-dev" -# name = "My Project Development" -# description = "Panels tailored to this project's workflow" -# panels = ["valence-shell", "editor-bridge", "build-dashboard"] -# default-isolation = "native" -` - } - - ` -# ───────────────────────────────────────────────────────────────── -# [portfolios] — Custom panel bundles for this repo's workflow -# -# A portfolio is a named collection of panels that form a workspace -# configuration. Define portfolios here to give collaborators a -# one-click setup for common workflows. -# -# Fields: -# id: Unique identifier (kebab-case) -# name: Human-readable name -# description: What this portfolio is for -# panels: Ordered list of panel IDs to include -# default-isolation: Default isolation tier for panels in this bundle -# ("native", "container", "vm") -# Panels with explicit isolation in [panels] override this. -# ───────────────────────────────────────────────────────────────── -[portfolios] -version = "1.0.0" -` ++ - portfolioEntries -} - -// ============================================================================ -// Main Generator -// ============================================================================ - -/// Generate a complete, well-annotated ENSAID_CONFIG.a2ml from PanLL state. -/// -/// This is the primary entry point. Each panel calls this with whatever state -/// it has access to. Missing state uses sensible defaults. -let generate = ( - ~repoName: string, - ~workspace: option=?, - ~humidity: string="medium", - ~panelConfigs: array=[], - ~portfolios: array=[], - ~automationRules: array=[], - (), -): string => { - let ws = switch workspace { - | Some(w) => w - | None => { - mode: RhodiumMode, - protection: Open, - executionMode: Live, - groups: [], - arrangements: [], - activeArrangementId: None, - sessions: [], - activeSessionId: None, - polyTools: [], - configuratorOpen: false, - configuratorTab: TabArrangements, - viewingMetadata: None, - metadataContent: None, - } - } - - let arrangementId = ws.activeArrangementId - - generateHeader(repoName) ++ - generateEnsaidSection() ++ - generateWorkspaceSection(ws) ++ - generatePreferencesSection(humidity, arrangementId) ++ - generatePanelsSection(panelConfigs) ++ - generateWorkflowsSection(automationRules) ++ - generateCladesSection() ++ - generatePortfoliosSection(portfolios) -} diff --git a/src/core/ErrorBoundary.affine b/src/core/ErrorBoundary.affine new file mode 100644 index 00000000..29ad0553 --- /dev/null +++ b/src/core/ErrorBoundary.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ErrorBoundary; + +// TODO: Complete semantic implementation diff --git a/src/core/ErrorBoundary.res b/src/core/ErrorBoundary.res deleted file mode 100644 index 71d917b8..00000000 --- a/src/core/ErrorBoundary.res +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) - -/// ErrorBoundary — Standardized error handling for Gossamer command dispatch. -/// -/// Wraps RuntimeBridge.invoke with structured error capture and TEA message -/// dispatch. When a command fails, the error is captured as a result type -/// and routed through the normal TEA update cycle rather than crashing. -/// -/// New command modules (ServiceCmd, SettingsCmd, IdentityCmd) use this -/// pattern. Existing commands can adopt incrementally. -/// -/// Part of Connected Workbench v0.2.0. - -/// Extract a human-readable error message from a JavaScript exception. -/// -/// Handles the common case where the caught value is an Error object -/// with a `.message` property, as well as plain string rejections. -let extractErrorMessage: exn => string = %raw(` - function(err) { - if (err && typeof err === 'object' && typeof err.message === 'string') { - return err.message; - } - if (typeof err === 'string') { - return err; - } - return 'Unknown error'; - } -`) - -/// Invoke a Gossamer command with error boundary protection. -/// -/// On success: calls tagger with `Ok(result)`. -/// On failure: calls tagger with `Error(contextMessage: detailMessage)`. -/// Never throws — all exceptions are caught and routed through the tagger. -/// -/// @param cmd The Gossamer command name (e.g. "verisimdb_health") -/// @param args The command payload (any JSON-serializable value) -/// @param context Human-readable context for error messages (e.g. "VeriSimDB health check") -/// @param tagger TEA message constructor for the result -let invokeWithBoundary = ( - cmd: string, - args: 'a, - context: string, - tagger: result<'b, string> => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - RuntimeBridge.invoke(cmd, args) - ->Promise.then(result => { - callbacks.enqueue(tagger(Ok(result))) - Promise.resolve() - }) - ->Promise.catch(err => { - let detail = extractErrorMessage(err) - callbacks.enqueue(tagger(Error(context ++ ": " ++ detail))) - Promise.resolve() - }) - ->ignore - }) -} - -/// Fire-and-forget variant — logs errors to Console.warn, no TEA message. -/// -/// Use for operations where failure is acceptable and does not need -/// to be reflected in the UI (e.g. background telemetry, cache warming). -/// -/// @param cmd The Gossamer command name -/// @param args The command payload -/// @param context Human-readable context for warning messages -let invokeFireAndForget = (cmd: string, args: 'a, context: string): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(_callbacks => { - RuntimeBridge.invoke(cmd, args) - ->Promise.catch(err => { - let detail = extractErrorMessage(err) - Console.warn(context ++ ": " ++ detail) - Promise.resolve() - }) - ->ignore - }) -} - -/// Invoke with a timeout boundary. -/// -/// Wraps a Gossamer command with a client-side timeout. If the command -/// does not respond within `timeoutMs` milliseconds, the tagger receives -/// an Error with a timeout message. The backend request may still complete -/// (this is a client-side optimistic abort, not a cancellation). -/// -/// @param cmd The Gossamer command name -/// @param args The command payload -/// @param context Human-readable context for error messages -/// @param timeoutMs Maximum wait time in milliseconds -/// @param tagger TEA message constructor for the result -let invokeWithTimeout = ( - cmd: string, - args: 'a, - context: string, - timeoutMs: int, - tagger: result<'b, string> => 'msg, -): Tea_Cmd.t<'msg> => { - Tea_Cmd.call(callbacks => { - let resolved = ref(false) - - // Start the actual invoke - RuntimeBridge.invoke(cmd, args) - ->Promise.then(result => { - if !resolved.contents { - resolved := true - callbacks.enqueue(tagger(Ok(result))) - } - Promise.resolve() - }) - ->Promise.catch(err => { - if !resolved.contents { - resolved := true - let detail = extractErrorMessage(err) - callbacks.enqueue(tagger(Error(context ++ ": " ++ detail))) - } - Promise.resolve() - }) - ->ignore - - // Timeout guard - let _ = setTimeout(() => { - if !resolved.contents { - resolved := true - callbacks.enqueue(tagger(Error(context ++ ": timed out after " ++ Int.toString(timeoutMs) ++ "ms"))) - } - }, timeoutMs) - }) -} diff --git a/src/core/EvangeliserEngine.affine b/src/core/EvangeliserEngine.affine new file mode 100644 index 00000000..22edb361 --- /dev/null +++ b/src/core/EvangeliserEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EvangeliserEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/EvangeliserEngine.res b/src/core/EvangeliserEngine.res deleted file mode 100644 index 03ee5d91..00000000 --- a/src/core/EvangeliserEngine.res +++ /dev/null @@ -1,1455 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Evangeliser Engine — pure logic for JS→ReScript pattern detection. -/// -/// Contains the pattern library (52 patterns across 20 categories), the -/// Makaton-inspired glyph registry, the regex-based scanner, and narrative -/// generation. All functions are pure — no side effects, no commands. -/// -/// Panel mapping: -/// Panel-L: constraintSummary, filterPatterns, categoryLabel -/// Panel-N: scanCode (regex matching), narrativeFor -/// Panel-W: formatMatch, coverageStats, difficultyLabel - -open EvangeliserModel - -// ============================================================================ -// Category and Difficulty Helpers -// ============================================================================ - -/// Human-readable label for a pattern category. -let categoryLabel = (cat: evangeliserCategory): string => { - switch cat { - | NullSafety => "Null Safety" - | Async => "Async" - | ErrorHandling => "Error Handling" - | ArrayOperations => "Array Operations" - | Conditionals => "Conditionals" - | Destructuring => "Destructuring" - | Defaults => "Defaults" - | Functional => "Functional" - | Templates => "Templates" - | ArrowFunctions => "Arrow Functions" - | Variants => "Variants" - | Modules => "Modules" - | TypeSafety => "Type Safety" - | Immutability => "Immutability" - | PatternMatching => "Pattern Matching" - | PipeOperator => "Pipe Operator" - | OopToFp => "OOP to FP" - | ClassesToRecords => "Classes to Records" - | InheritanceToComposition => "Inheritance to Composition" - | StateMachines => "State Machines" - | DataModeling => "Data Modeling" - } -} - -/// Short code for a category (for compact UI). -let categoryCode = (cat: evangeliserCategory): string => { - switch cat { - | NullSafety => "null" - | Async => "async" - | ErrorHandling => "err" - | ArrayOperations => "arr" - | Conditionals => "cond" - | Destructuring => "dest" - | Defaults => "def" - | Functional => "fn" - | Templates => "tmpl" - | ArrowFunctions => "arrow" - | Variants => "var" - | Modules => "mod" - | TypeSafety => "type" - | Immutability => "imm" - | PatternMatching => "match" - | PipeOperator => "pipe" - | OopToFp => "fp" - | ClassesToRecords => "rec" - | InheritanceToComposition => "comp" - | StateMachines => "fsm" - | DataModeling => "data" - } -} - -/// Tailwind colour class for a category. -let categoryColour = (cat: evangeliserCategory): string => { - switch cat { - | NullSafety | TypeSafety => "text-red-400" - | Async | ErrorHandling => "text-amber-400" - | ArrayOperations | Functional => "text-emerald-400" - | Conditionals | PatternMatching => "text-cyan-400" - | Destructuring | Defaults => "text-violet-400" - | Templates | ArrowFunctions => "text-blue-400" - | Variants | DataModeling => "text-pink-400" - | Modules | Immutability => "text-teal-400" - | PipeOperator => "text-indigo-400" - | OopToFp | ClassesToRecords | InheritanceToComposition => "text-orange-400" - | StateMachines => "text-lime-400" - } -} - -/// Human-readable label for difficulty. -let difficultyLabel = (diff: evangeliserDifficulty): string => { - switch diff { - | Beginner => "Beginner" - | Intermediate => "Intermediate" - | Advanced => "Advanced" - } -} - -/// Tailwind colour class for difficulty badge. -let difficultyColour = (diff: evangeliserDifficulty): string => { - switch diff { - | Beginner => "text-emerald-400 bg-emerald-900/40" - | Intermediate => "text-amber-400 bg-amber-900/40" - | Advanced => "text-red-400 bg-red-900/40" - } -} - -/// Human-readable label for view layer. -let viewLayerLabel = (vl: evangeliserViewLayer): string => { - switch vl { - | ViewRaw => "Raw" - | ViewFolded => "Folded" - | ViewGlyphed => "Glyphed" - | ViewWysiwyg => "WYSIWYG" - } -} - -// ============================================================================ -// All categories as an array (for iteration in UI) -// ============================================================================ - -let allCategories: array = [ - NullSafety, - Async, - ErrorHandling, - ArrayOperations, - Conditionals, - Destructuring, - Defaults, - Functional, - Templates, - ArrowFunctions, - Variants, - Modules, - TypeSafety, - Immutability, - PatternMatching, - PipeOperator, - OopToFp, - ClassesToRecords, - InheritanceToComposition, - StateMachines, - DataModeling, -] - -// ============================================================================ -// Glyph Registry — 21 Makaton-inspired glyphs -// ============================================================================ - -let builtinGlyphs: array = [ - { - symbol: "\xf0\x9f\x94\x84", - name: "Transform", - meaning: "Data transformation or mapping", - semanticCategory: Transformation, - }, - { - symbol: "\xf0\x9f\x8e\xaf", - name: "Target", - meaning: "Precise type targeting", - semanticCategory: Safety, - }, - { - symbol: "\xf0\x9f\x9b\xa1\xef\xb8\x8f", - name: "Shield", - meaning: "Protection from null/undefined", - semanticCategory: Safety, - }, - { - symbol: "\xe2\x9e\xa1\xef\xb8\x8f", - name: "Flow", - meaning: "Pipe operator — data flows left to right", - semanticCategory: Flow, - }, - { - symbol: "\xf0\x9f\x94\x80", - name: "Branch", - meaning: "Pattern matching — exhaustive branching", - semanticCategory: Flow, - }, - { - symbol: "\xf0\x9f\x93\xa6", - name: "Package", - meaning: "Module encapsulation", - semanticCategory: Structure, - }, - { - symbol: "\xe2\x9c\xa8", - name: "Sparkle", - meaning: "Type inference — types appear automatically", - semanticCategory: Safety, - }, - { - symbol: "\xf0\x9f\x94\x92", - name: "Lock", - meaning: "Immutability — data cannot change", - semanticCategory: State, - }, - { - symbol: "\xf0\x9f\x8c\xb1", - name: "Seed", - meaning: "Small code grows into safe patterns", - semanticCategory: Transformation, - }, - { - symbol: "\xf0\x9f\x94\x8d", - name: "Search", - meaning: "Pattern detection in JS code", - semanticCategory: Transformation, - }, - { - symbol: "\xf0\x9f\x8f\x97\xef\xb8\x8f", - name: "Build", - meaning: "Constructing types and records", - semanticCategory: Structure, - }, - { - symbol: "\xe2\x9a\xa1", - name: "Lightning", - meaning: "Fast compilation — instant feedback", - semanticCategory: Transformation, - }, - { - symbol: "\xf0\x9f\xa7\xa9", - name: "Puzzle", - meaning: "Composable pieces that fit together", - semanticCategory: Structure, - }, - { - symbol: "\xf0\x9f\x8e\xad", - name: "Masks", - meaning: "Variants — different faces of a type", - semanticCategory: Data, - }, - { - symbol: "\xf0\x9f\x93\x90", - name: "Notebook", - meaning: "Record types — structured data", - semanticCategory: Data, - }, - { - symbol: "\xf0\x9f\x8c\x8a", - name: "Wave", - meaning: "Async operations with promises", - semanticCategory: Flow, - }, - { - symbol: "\xf0\x9f\x94\xa7", - name: "Wrench", - meaning: "Utility function or helper", - semanticCategory: Transformation, - }, - { - symbol: "\xf0\x9f\x8c\xb3", - name: "Tree", - meaning: "Recursive data structures", - semanticCategory: Data, - }, - { - symbol: "\xf0\x9f\x92\xa1", - name: "Lightbulb", - meaning: "Insight — the 'aha' moment", - semanticCategory: Transformation, - }, - { - symbol: "\xf0\x9f\x8e\xb5", - name: "Note", - meaning: "Harmony — code that reads naturally", - semanticCategory: Flow, - }, - { - symbol: "\xf0\x9f\x97\x9d\xef\xb8\x8f", - name: "Compress", - meaning: "Concise expression — less boilerplate", - semanticCategory: Structure, - }, -] - -/// Look up a glyph by name. -let findGlyph = (name: string): option => { - builtinGlyphs->Array.find(g => g.name === name) -} - -/// Get glyph symbols for an array of glyph names. -let glyphSymbols = (names: array): string => { - names - ->Array.filterMap(n => findGlyph(n)->Option.map(g => g.symbol)) - ->Array.join(" ") -} - -// ============================================================================ -// Pattern Library — 52 patterns across 20 categories -// ============================================================================ - -let builtinPatterns: array = [ - // --- NullSafety (3) --- - { - id: "null-check-option", - name: "Null Check to Option", - category: NullSafety, - difficulty: Beginner, - jsPattern: "!==?\\s*(null|undefined)", - confidence: 0.85, - jsExample: "if (user !== null && user !== undefined) { ... }", - rescriptExample: "switch user { | Some(u) => ... | None => ... }", - narrative: { - celebrate: "You're being careful about null checks!", - minimize: "But the compiler can't verify you caught every case.", - better: "ReScript's Option type makes null impossible — the compiler checks exhaustively.", - safety: "Option eliminates null reference exceptions at compile time.", - }, - glyphs: ["Shield", "Target"], - tags: ["null", "undefined", "option", "safety"], - relatedPatterns: ["optional-chain-option", "nullish-coalesce-default"], - learningObjectives: ["Understand Option as a replacement for null checks"], - }, - { - id: "optional-chain-option", - name: "Optional Chaining to Option.map", - category: NullSafety, - difficulty: Intermediate, - jsPattern: "\\?\\.\\w+", - confidence: 0.80, - jsExample: "const name = user?.profile?.name", - rescriptExample: "let name = user->Option.flatMap(u => u.profile)->Option.map(p => p.name)", - narrative: { - celebrate: "Optional chaining is a great JS feature!", - minimize: "But it silently produces undefined deep in chains.", - better: "Option.flatMap makes every step explicit and type-checked.", - safety: "Each step in the chain has a known type — no hidden undefined.", - }, - glyphs: ["Shield", "Flow"], - tags: ["optional-chaining", "option", "flatmap"], - relatedPatterns: ["null-check-option"], - learningObjectives: ["Chain Option operations with flatMap and map"], - }, - { - id: "nullish-coalesce-default", - name: "Nullish Coalescing to Default", - category: NullSafety, - difficulty: Beginner, - jsPattern: "\\?\\?", - confidence: 0.90, - jsExample: "const port = config.port ?? 3000", - rescriptExample: "let port = config.port->Option.getOr(3000)", - narrative: { - celebrate: "Using ?? for defaults is clean!", - minimize: "But ?? only handles null/undefined, not other falsy values.", - better: "Option.getOr is explicit about what 'missing' means.", - safety: "The default value must match the Option's inner type.", - }, - glyphs: ["Shield", "Sparkle"], - tags: ["nullish", "coalescing", "default"], - relatedPatterns: ["null-check-option"], - learningObjectives: ["Use Option.getOr for safe defaults"], - }, - // --- Async (3) --- - { - id: "promise-then-pipe", - name: "Promise.then to Pipe", - category: Async, - difficulty: Intermediate, - jsPattern: "\\.then\\s*\\(", - confidence: 0.75, - jsExample: "fetch(url).then(r => r.json()).then(data => process(data))", - rescriptExample: "Fetch.fetch(url)->Promise.then(r => r->Response.json)->Promise.then(data => process(data))", - narrative: { - celebrate: "Promise chains show clear async flow!", - minimize: "Error handling in long chains can be tricky.", - better: "ReScript preserves the chain with type-safe pipe syntax.", - safety: "Every promise step has a known resolved type.", - }, - glyphs: ["Wave", "Flow"], - tags: ["promise", "then", "async", "fetch"], - relatedPatterns: ["async-await-promise"], - learningObjectives: ["Use Promise.then with pipe operator"], - }, - { - id: "async-await-promise", - name: "Async/Await to Promise", - category: Async, - difficulty: Intermediate, - jsPattern: "async\\s+function|async\\s*\\(", - confidence: 0.70, - jsExample: "async function getData() { const res = await fetch(url); return await res.json(); }", - rescriptExample: "let getData = () => Fetch.fetch(url)->Promise.then(res => res->Response.json)", - narrative: { - celebrate: "Async/await makes async code readable!", - minimize: "But forgotten awaits silently return promises instead of values.", - better: "ReScript's promise chains are explicit — no hidden async.", - safety: "The type system tracks what's a Promise and what's resolved.", - }, - glyphs: ["Wave", "Lightning"], - tags: ["async", "await", "promise"], - relatedPatterns: ["promise-then-pipe"], - learningObjectives: ["Express async operations as typed Promise chains"], - }, - { - id: "try-catch-async", - name: "Try/Catch Async to Result", - category: Async, - difficulty: Advanced, - jsPattern: "try\\s*\\{[^}]*await", - confidence: 0.65, - jsExample: "try { const data = await fetch(url); } catch (e) { handleError(e); }", - rescriptExample: "Fetch.fetch(url)->Promise.then(data => Ok(data))->Promise.catch(_ => Error(NetworkError))", - narrative: { - celebrate: "Wrapping async in try/catch shows error awareness!", - minimize: "But catch(e) gives you 'unknown' — no type information.", - better: "Promise.catch with Result gives typed error handling.", - safety: "Error variants are exhaustively checked by the compiler.", - }, - glyphs: ["Shield", "Wave"], - tags: ["try", "catch", "async", "result"], - relatedPatterns: ["async-await-promise", "try-catch-result"], - learningObjectives: ["Combine Promise with Result for typed error handling"], - }, - // --- ErrorHandling (3) --- - { - id: "try-catch-result", - name: "Try/Catch to Result", - category: ErrorHandling, - difficulty: Beginner, - jsPattern: "try\\s*\\{", - confidence: 0.80, - jsExample: "try { JSON.parse(input) } catch (e) { return null }", - rescriptExample: "switch JSON.parseExn(input) { | data => Ok(data) | exception _ => Error(ParseError) }", - narrative: { - celebrate: "Handling errors explicitly is great practice!", - minimize: "But catch gives you 'any' — you don't know what went wrong.", - better: "Result with typed error variants tells you exactly what failed.", - safety: "Every error path must be handled — the compiler won't let you forget.", - }, - glyphs: ["Shield", "Branch"], - tags: ["try", "catch", "result", "error"], - relatedPatterns: ["error-code-variant"], - learningObjectives: ["Replace try/catch with Result type"], - }, - { - id: "error-code-variant", - name: "Error Codes to Variants", - category: ErrorHandling, - difficulty: Intermediate, - jsPattern: "error\\.code\\s*===", - confidence: 0.75, - jsExample: "if (error.code === 'NOT_FOUND') { ... } else if (error.code === 'FORBIDDEN') { ... }", - rescriptExample: "switch error { | NotFound => ... | Forbidden => ... | ServerError(code) => ... }", - narrative: { - celebrate: "Checking error codes shows thorough handling!", - minimize: "But string codes can be misspelled without any warning.", - better: "Variant types make every error case a checked constructor.", - safety: "Adding a new error variant forces you to handle it everywhere.", - }, - glyphs: ["Branch", "Target"], - tags: ["error", "code", "variant"], - relatedPatterns: ["try-catch-result"], - learningObjectives: ["Model errors as variant types"], - }, - { - id: "throw-panic", - name: "Throw to Panic/Result", - category: ErrorHandling, - difficulty: Beginner, - jsPattern: "throw\\s+new\\s+Error", - confidence: 0.85, - jsExample: "if (!valid) throw new Error('Invalid input')", - rescriptExample: "if !valid { Error(InvalidInput) } else { Ok(data) }", - narrative: { - celebrate: "Throwing errors signals problems clearly!", - minimize: "But throws are invisible in the type signature.", - better: "Returning Result makes errors part of the function's contract.", - safety: "Callers must handle the error — it's in the return type.", - }, - glyphs: ["Shield", "Target"], - tags: ["throw", "error", "result"], - relatedPatterns: ["try-catch-result"], - learningObjectives: ["Replace throw with Result return types"], - }, - // --- ArrayOperations (3) --- - { - id: "array-map-pipe", - name: "Array.map to Pipe", - category: ArrayOperations, - difficulty: Beginner, - jsPattern: "\\.map\\s*\\(", - confidence: 0.70, - jsExample: "users.map(u => u.name).filter(n => n.length > 0)", - rescriptExample: "users->Array.map(u => u.name)->Array.filter(n => String.length(n) > 0)", - narrative: { - celebrate: "Chaining map and filter is idiomatic functional style!", - minimize: "Just note: JS array methods can return unexpected types.", - better: "ReScript's pipe operator makes the data flow crystal clear.", - safety: "Every step is type-checked — no accidental type coercion.", - }, - glyphs: ["Transform", "Flow"], - tags: ["array", "map", "pipe"], - relatedPatterns: ["array-reduce-fold", "array-find-option"], - learningObjectives: ["Use pipe operator with Array functions"], - }, - { - id: "array-reduce-fold", - name: "Array.reduce to Array.reduce", - category: ArrayOperations, - difficulty: Intermediate, - jsPattern: "\\.reduce\\s*\\(", - confidence: 0.70, - jsExample: "nums.reduce((sum, n) => sum + n, 0)", - rescriptExample: "nums->Array.reduce(0, (sum, n) => sum + n)", - narrative: { - celebrate: "Reduce is the Swiss army knife of array operations!", - minimize: "The accumulator type is often implicitly 'any'.", - better: "ReScript's reduce has explicit initial value and typed accumulator.", - safety: "The accumulator type is inferred and checked at every step.", - }, - glyphs: ["Transform", "Sparkle"], - tags: ["array", "reduce", "fold"], - relatedPatterns: ["array-map-pipe"], - learningObjectives: ["Use Array.reduce with explicit types"], - }, - { - id: "array-find-option", - name: "Array.find to Option", - category: ArrayOperations, - difficulty: Beginner, - jsPattern: "\\.find\\s*\\(", - confidence: 0.75, - jsExample: "const admin = users.find(u => u.role === 'admin')", - rescriptExample: "let admin = users->Array.find(u => u.role === Admin)", - narrative: { - celebrate: "Using find is cleaner than manual loops!", - minimize: "But find returns undefined when nothing matches.", - better: "ReScript's Array.find returns Option — you must handle the None case.", - safety: "No more 'cannot read property of undefined' from unfound elements.", - }, - glyphs: ["Search", "Shield"], - tags: ["array", "find", "option"], - relatedPatterns: ["array-map-pipe"], - learningObjectives: ["Understand Array.find returns Option"], - }, - // --- Conditionals (3) --- - { - id: "ternary-switch", - name: "Ternary to Switch", - category: Conditionals, - difficulty: Beginner, - jsPattern: "\\?[^?].*:", - confidence: 0.60, - jsExample: "const label = status === 'active' ? 'Active' : status === 'inactive' ? 'Inactive' : 'Unknown'", - rescriptExample: "let label = switch status { | Active => \"Active\" | Inactive => \"Inactive\" | Unknown => \"Unknown\" }", - narrative: { - celebrate: "Ternaries are concise for simple conditions!", - minimize: "Nested ternaries become hard to read quickly.", - better: "Switch expressions are flat and exhaustive.", - safety: "Add a new status? The compiler tells you every switch that needs updating.", - }, - glyphs: ["Branch", "Sparkle"], - tags: ["ternary", "switch", "conditional"], - relatedPatterns: ["if-else-switch"], - learningObjectives: ["Replace nested ternaries with switch expressions"], - }, - { - id: "if-else-switch", - name: "If/Else Chain to Switch", - category: Conditionals, - difficulty: Beginner, - jsPattern: "if\\s*\\(.*\\)\\s*\\{[^}]*\\}\\s*else\\s*if", - confidence: 0.75, - jsExample: "if (type === 'a') { ... } else if (type === 'b') { ... } else { ... }", - rescriptExample: "switch type { | A => ... | B => ... }", - narrative: { - celebrate: "If/else chains handle multiple cases!", - minimize: "But there's no guarantee you covered every case.", - better: "Switch with variants is exhaustive — miss a case and the compiler tells you.", - safety: "Exhaustive pattern matching eliminates 'else' as a catch-all.", - }, - glyphs: ["Branch", "Target"], - tags: ["if", "else", "switch", "exhaustive"], - relatedPatterns: ["ternary-switch"], - learningObjectives: ["Use exhaustive switch expressions"], - }, - { - id: "typeof-variant", - name: "Typeof Check to Variant", - category: Conditionals, - difficulty: Intermediate, - jsPattern: "typeof\\s+\\w+\\s*===", - confidence: 0.70, - jsExample: "if (typeof value === 'string') { ... } else if (typeof value === 'number') { ... }", - rescriptExample: "switch value { | String(s) => ... | Number(n) => ... }", - narrative: { - celebrate: "Runtime type checks show defensive coding!", - minimize: "But typeof only catches a few types and misses objects.", - better: "Variant types carry their tag at compile time — no runtime checks needed.", - safety: "The type is known before runtime — no typeof surprises.", - }, - glyphs: ["Target", "Branch"], - tags: ["typeof", "variant", "type-check"], - relatedPatterns: ["if-else-switch"], - learningObjectives: ["Replace typeof checks with variant types"], - }, - // --- Destructuring (2) --- - { - id: "object-destructure-record", - name: "Object Destructuring to Record", - category: Destructuring, - difficulty: Beginner, - jsPattern: "const\\s*\\{[^}]+\\}\\s*=", - confidence: 0.80, - jsExample: "const { name, age, email } = user", - rescriptExample: "let { name, age, email } = user", - narrative: { - celebrate: "Destructuring is clean and readable!", - minimize: "But accessing a non-existent field silently gives undefined.", - better: "ReScript records have fixed fields — destructuring only works on known fields.", - safety: "Misspell a field name and the compiler catches it immediately.", - }, - glyphs: ["Notebook", "Sparkle"], - tags: ["destructuring", "record", "object"], - relatedPatterns: ["spread-record-update"], - learningObjectives: ["Use record destructuring with type safety"], - }, - { - id: "spread-record-update", - name: "Spread to Record Update", - category: Destructuring, - difficulty: Intermediate, - jsPattern: "\\.\\.\\.\\w+", - confidence: 0.65, - jsExample: "const updated = { ...user, name: 'New Name' }", - rescriptExample: "let updated = { ...user, name: \"New Name\" }", - narrative: { - celebrate: "Spread for immutable updates is great practice!", - minimize: "But you can accidentally spread in extra or wrong fields.", - better: "ReScript's record update syntax only allows declared fields.", - safety: "The spread expression must match the record type exactly.", - }, - glyphs: ["Notebook", "Lock"], - tags: ["spread", "record", "update", "immutable"], - relatedPatterns: ["object-destructure-record"], - learningObjectives: ["Use immutable record updates with spread"], - }, - // --- Defaults (2) --- - { - id: "default-params", - name: "Default Parameters to Option", - category: Defaults, - difficulty: Beginner, - jsPattern: "function\\s+\\w+\\s*\\([^)]*=", - confidence: 0.75, - jsExample: "function greet(name = 'World') { return `Hello ${name}!` }", - rescriptExample: "let greet = (~name=\"World\") => `Hello ${name}!`", - narrative: { - celebrate: "Default parameters reduce boilerplate!", - minimize: "But defaults are invisible at the call site.", - better: "ReScript's labeled arguments with defaults are self-documenting.", - safety: "The type of the default must match the parameter type.", - }, - glyphs: ["Sparkle", "Wrench"], - tags: ["default", "parameter", "labeled"], - relatedPatterns: ["nullish-coalesce-default"], - learningObjectives: ["Use labeled arguments with defaults"], - }, - { - id: "or-default", - name: "|| Default to Option.getOr", - category: Defaults, - difficulty: Beginner, - jsPattern: "\\|\\|\\s*['\"`\\d]", - confidence: 0.70, - jsExample: "const name = input || 'default'", - rescriptExample: "let name = input->Option.getOr(\"default\")", - narrative: { - celebrate: "Using || for defaults is a common pattern!", - minimize: "But || treats 0, '', and false as falsy too.", - better: "Option.getOr only triggers on None — not on empty strings or zero.", - safety: "The fallback type must match the Option's inner type.", - }, - glyphs: ["Shield", "Wrench"], - tags: ["or", "default", "falsy", "option"], - relatedPatterns: ["nullish-coalesce-default"], - learningObjectives: ["Distinguish between falsy and None"], - }, - // --- Functional (3) --- - { - id: "callback-fn", - name: "Callback to First-Class Function", - category: Functional, - difficulty: Beginner, - jsPattern: "function\\s*\\(\\w+\\s*,\\s*callback\\)", - confidence: 0.65, - jsExample: "function processData(data, callback) { callback(transform(data)) }", - rescriptExample: "let processData = (data, callback) => callback(transform(data))", - narrative: { - celebrate: "Using callbacks shows functional thinking!", - minimize: "But callback types are often 'any' in JS.", - better: "ReScript functions are fully typed — the callback's signature is explicit.", - safety: "The callback's parameter and return types are checked at the call site.", - }, - glyphs: ["Transform", "Target"], - tags: ["callback", "function", "higher-order"], - relatedPatterns: ["compose-pipe"], - learningObjectives: ["Understand typed higher-order functions"], - }, - { - id: "compose-pipe", - name: "Function Composition to Pipe", - category: Functional, - difficulty: Intermediate, - jsPattern: "compose\\(|pipe\\(", - confidence: 0.70, - jsExample: "const result = compose(toUpper, trim, validate)(input)", - rescriptExample: "let result = input->validate->String.trim->String.toUpperCase", - narrative: { - celebrate: "Function composition is elegant!", - minimize: "But compose/pipe utilities add runtime overhead and type complexity.", - better: "ReScript's -> operator is zero-cost function composition.", - safety: "Each step's output type must match the next step's input type.", - }, - glyphs: ["Flow", "Lightning"], - tags: ["compose", "pipe", "function"], - relatedPatterns: ["callback-fn"], - learningObjectives: ["Use pipe operator for zero-cost composition"], - }, - { - id: "iife-block", - name: "IIFE to Block Expression", - category: Functional, - difficulty: Intermediate, - jsPattern: "\\(\\s*\\(\\s*\\)\\s*=>\\s*\\{", - confidence: 0.70, - jsExample: "const result = (() => { const x = compute(); return x * 2; })()", - rescriptExample: "let result = { let x = compute(); x * 2 }", - narrative: { - celebrate: "IIFEs create scoped expressions!", - minimize: "But the syntax is verbose and easy to get wrong.", - better: "ReScript blocks are expressions — no IIFE needed.", - safety: "The block's return type is inferred from the last expression.", - }, - glyphs: ["Compress", "Sparkle"], - tags: ["iife", "block", "expression"], - relatedPatterns: ["compose-pipe"], - learningObjectives: ["Use block expressions instead of IIFEs"], - }, - // --- Templates (2) --- - { - id: "template-literal", - name: "Template Literal to String Interpolation", - category: Templates, - difficulty: Beginner, - jsPattern: "`[^`]*\\$\\{", - confidence: 0.85, - jsExample: "const msg = `Hello ${name}, you have ${count} items`", - rescriptExample: "let msg = `Hello ${name}, you have ${Int.toString(count)} items`", - narrative: { - celebrate: "Template literals are readable and powerful!", - minimize: "JS implicitly converts anything to string inside ${}.", - better: "ReScript requires explicit toString — no surprise coercions.", - safety: "Only strings can be interpolated — Int.toString makes intent clear.", - }, - glyphs: ["Sparkle", "Target"], - tags: ["template", "string", "interpolation"], - relatedPatterns: [], - learningObjectives: ["Use explicit type conversion in string interpolation"], - }, - { - id: "string-concat-interp", - name: "String Concatenation to Interpolation", - category: Templates, - difficulty: Beginner, - jsPattern: "\\+\\s*['\"]|['\"]\\s*\\+", - confidence: 0.60, - jsExample: "const url = baseUrl + '/api/' + version + '/users'", - rescriptExample: "let url = `${baseUrl}/api/${version}/users`", - narrative: { - celebrate: "Building strings dynamically is useful!", - minimize: "Concatenation with + can accidentally coerce non-strings.", - better: "String interpolation is cleaner and type-safe.", - safety: "Each interpolated value must be a string type.", - }, - glyphs: ["Sparkle", "Flow"], - tags: ["string", "concatenation", "interpolation"], - relatedPatterns: ["template-literal"], - learningObjectives: ["Prefer interpolation over concatenation"], - }, - // --- ArrowFunctions (2) --- - { - id: "arrow-fn-let", - name: "Arrow Function to Let Binding", - category: ArrowFunctions, - difficulty: Beginner, - jsPattern: "const\\s+\\w+\\s*=\\s*\\([^)]*\\)\\s*=>", - confidence: 0.85, - jsExample: "const add = (a, b) => a + b", - rescriptExample: "let add = (a, b) => a + b", - narrative: { - celebrate: "Arrow functions are clean and concise!", - minimize: "But parameter types are implicit in JS.", - better: "ReScript infers types from usage — add works for int or float, not both.", - safety: "The type system prevents accidentally adding a string to a number.", - }, - glyphs: ["Lightning", "Sparkle"], - tags: ["arrow", "function", "let"], - relatedPatterns: ["callback-fn"], - learningObjectives: ["Understand let bindings with type inference"], - }, - { - id: "arrow-implicit-return", - name: "Implicit Return", - category: ArrowFunctions, - difficulty: Beginner, - jsPattern: "=>\\s*[^{]", - confidence: 0.55, - jsExample: "const double = x => x * 2", - rescriptExample: "let double = x => x * 2", - narrative: { - celebrate: "Implicit return is beautifully concise!", - minimize: "In JS, forgetting braces changes the return value.", - better: "In ReScript, every expression returns — no braces confusion.", - safety: "The return type is always inferred and checked.", - }, - glyphs: ["Lightning", "Compress"], - tags: ["arrow", "return", "expression"], - relatedPatterns: ["arrow-fn-let"], - learningObjectives: ["Understand expression-based return values"], - }, - // --- Variants (3) --- - { - id: "string-enum-variant", - name: "String Enum to Variant", - category: Variants, - difficulty: Beginner, - jsPattern: "type\\s+\\w+\\s*=\\s*['\"]\\w+['\"]\\s*\\|", - confidence: 0.80, - jsExample: "type Status = 'active' | 'inactive' | 'pending'", - rescriptExample: "type status = Active | Inactive | Pending", - narrative: { - celebrate: "String union types model states well!", - minimize: "But string comparisons can have typos at runtime.", - better: "Variants are constructors — misspelling one is a compile error.", - safety: "Pattern matching on variants is exhaustive — every case handled.", - }, - glyphs: ["Masks", "Target"], - tags: ["enum", "variant", "union"], - relatedPatterns: ["tagged-union-variant"], - learningObjectives: ["Model states with variant types"], - }, - { - id: "tagged-union-variant", - name: "Tagged Union to Variant", - category: Variants, - difficulty: Intermediate, - jsPattern: "type.*=.*\\{\\s*kind:\\s*['\"]", - confidence: 0.75, - jsExample: "type Shape = { kind: 'circle', radius: number } | { kind: 'rect', w: number, h: number }", - rescriptExample: "type shape = Circle({ radius: float }) | Rect({ w: float, h: float })", - narrative: { - celebrate: "Tagged unions are a powerful JS pattern!", - minimize: "But the tag field is a stringly-typed convention.", - better: "ReScript variants with payloads are first-class tagged unions.", - safety: "Each constructor's payload is typed — no accessing .radius on a Rect.", - }, - glyphs: ["Masks", "Branch"], - tags: ["tagged", "union", "variant", "payload"], - relatedPatterns: ["string-enum-variant"], - learningObjectives: ["Use variant constructors with typed payloads"], - }, - { - id: "discriminated-switch", - name: "Discriminated Switch to Pattern Match", - category: Variants, - difficulty: Intermediate, - jsPattern: "switch\\s*\\(\\w+\\.kind\\)", - confidence: 0.80, - jsExample: "switch (shape.kind) { case 'circle': ... case 'rect': ... }", - rescriptExample: "switch shape { | Circle({radius}) => ... | Rect({w, h}) => ... }", - narrative: { - celebrate: "Switching on discriminant fields is idiomatic!", - minimize: "But JS switch has fall-through and no exhaustiveness check.", - better: "ReScript switch is an expression, has no fall-through, and is exhaustive.", - safety: "Payload fields are destructured and typed in each branch.", - }, - glyphs: ["Branch", "Sparkle"], - tags: ["switch", "discriminated", "pattern-match"], - relatedPatterns: ["tagged-union-variant"], - learningObjectives: ["Destructure variant payloads in switch"], - }, - // --- Modules (2) --- - { - id: "namespace-module", - name: "Namespace to Module", - category: Modules, - difficulty: Intermediate, - jsPattern: "export\\s+(const|function|class)\\s+", - confidence: 0.55, - jsExample: "export function validateEmail(email) { ... }", - rescriptExample: "// In Validation.res\nlet validateEmail = (email: string): result => ...", - narrative: { - celebrate: "Named exports organise code well!", - minimize: "But JS modules need explicit import/export ceremony.", - better: "ReScript files are modules automatically — no export keyword needed.", - safety: "Every function's type is inferred from its implementation.", - }, - glyphs: ["Package", "Compress"], - tags: ["module", "export", "namespace"], - relatedPatterns: [], - learningObjectives: ["Understand file-as-module convention"], - }, - { - id: "barrel-module", - name: "Barrel Export to Module Open", - category: Modules, - difficulty: Advanced, - jsPattern: "export\\s*\\{[^}]+\\}\\s*from", - confidence: 0.60, - jsExample: "export { validateEmail, validatePhone } from './validation'", - rescriptExample: "// In Utils.res\ninclude Validation // Re-exports all of Validation", - narrative: { - celebrate: "Barrel files simplify imports!", - minimize: "But barrel files can cause tree-shaking issues.", - better: "ReScript's include re-exports a module cleanly — dead code is eliminated.", - safety: "Include brings all types and values into scope — no partial re-exports.", - }, - glyphs: ["Package", "Flow"], - tags: ["barrel", "export", "include"], - relatedPatterns: ["namespace-module"], - learningObjectives: ["Use include for module re-exports"], - }, - // --- TypeSafety (2) --- - { - id: "any-generic", - name: "Any Type to Generic", - category: TypeSafety, - difficulty: Intermediate, - jsPattern: ":\\s*any", - confidence: 0.90, - jsExample: "function identity(x: any): any { return x }", - rescriptExample: "let identity = (x: 'a): 'a => x", - narrative: { - celebrate: "At least you're using type annotations!", - minimize: "But 'any' defeats the purpose of types entirely.", - better: "ReScript generics ('a) preserve type information through the function.", - safety: "identity(42) returns int, identity(\"hi\") returns string — no cast needed.", - }, - glyphs: ["Target", "Sparkle"], - tags: ["any", "generic", "type-parameter"], - relatedPatterns: [], - learningObjectives: ["Replace 'any' with type parameters"], - }, - { - id: "type-assertion-pattern", - name: "Type Assertion to Pattern Match", - category: TypeSafety, - difficulty: Advanced, - jsPattern: "as\\s+\\w+", - confidence: 0.65, - jsExample: "const el = document.getElementById('app') as HTMLDivElement", - rescriptExample: "switch document->Document.getElementById(\"app\") { | Some(el) => ... | None => ... }", - narrative: { - celebrate: "Type assertions show you know the expected type!", - minimize: "But 'as' lies to the compiler — it trusts you blindly.", - better: "ReScript's nullable DOM APIs return Option — you handle both cases.", - safety: "No type assertion can crash at runtime — every path is checked.", - }, - glyphs: ["Shield", "Branch"], - tags: ["assertion", "cast", "option"], - relatedPatterns: ["null-check-option"], - learningObjectives: ["Replace type assertions with Option handling"], - }, - // --- Immutability (2) --- - { - id: "let-const-let", - name: "Const/Let to Let", - category: Immutability, - difficulty: Beginner, - jsPattern: "(const|let)\\s+\\w+\\s*=", - confidence: 0.50, - jsExample: "let count = 0; count = count + 1;", - rescriptExample: "let count = ref(0); count := count.contents + 1", - narrative: { - celebrate: "Using let for mutable state is honest!", - minimize: "But JS let allows mutation by default — bugs hide easily.", - better: "ReScript's let is immutable. Mutation requires an explicit ref().", - safety: "Immutable by default means fewer accidental state changes.", - }, - glyphs: ["Lock", "Sparkle"], - tags: ["const", "let", "immutable", "ref"], - relatedPatterns: ["spread-record-update"], - learningObjectives: ["Understand immutable-by-default bindings"], - }, - { - id: "object-freeze-record", - name: "Object.freeze to Record", - category: Immutability, - difficulty: Intermediate, - jsPattern: "Object\\.freeze\\(", - confidence: 0.85, - jsExample: "const config = Object.freeze({ port: 3000, host: 'localhost' })", - rescriptExample: "let config = { port: 3000, host: \"localhost\" } // Already immutable!", - narrative: { - celebrate: "Freezing objects prevents accidental mutation!", - minimize: "But freeze is shallow — nested objects are still mutable.", - better: "ReScript records are deeply immutable by default — no freeze needed.", - safety: "Attempting to mutate a record field is a compile error.", - }, - glyphs: ["Lock", "Compress"], - tags: ["freeze", "immutable", "record"], - relatedPatterns: ["let-const-let"], - learningObjectives: ["Understand records are immutable by default"], - }, - // --- PatternMatching (3) --- - { - id: "switch-match", - name: "Switch Statement to Pattern Match", - category: PatternMatching, - difficulty: Beginner, - jsPattern: "switch\\s*\\(", - confidence: 0.65, - jsExample: "switch (action.type) { case 'INCREMENT': ... case 'DECREMENT': ... }", - rescriptExample: "switch action { | Increment => ... | Decrement => ... }", - narrative: { - celebrate: "Switch statements handle multiple cases!", - minimize: "But JS switch has fall-through and no exhaustiveness.", - better: "ReScript switch is exhaustive, has no fall-through, and is an expression.", - safety: "Add a new action type and the compiler flags every unhandled switch.", - }, - glyphs: ["Branch", "Target"], - tags: ["switch", "pattern-match", "exhaustive"], - relatedPatterns: ["nested-match"], - learningObjectives: ["Use exhaustive pattern matching"], - }, - { - id: "nested-match", - name: "Nested If to Nested Match", - category: PatternMatching, - difficulty: Intermediate, - jsPattern: "if\\s*\\(.*\\)\\s*\\{[^}]*if\\s*\\(", - confidence: 0.60, - jsExample: "if (user) { if (user.role === 'admin') { if (user.active) { ... } } }", - rescriptExample: "switch (user, user.role, user.active) { | (Some(u), Admin, true) => ... | _ => ... }", - narrative: { - celebrate: "Nested checks are thorough!", - minimize: "But deeply nested ifs are hard to follow.", - better: "Tuple pattern matching flattens nested conditions into one switch.", - safety: "All combinations are checked — no forgotten edge cases.", - }, - glyphs: ["Branch", "Compress"], - tags: ["nested", "pattern-match", "tuple"], - relatedPatterns: ["switch-match"], - learningObjectives: ["Flatten nested conditions with tuple matching"], - }, - { - id: "guard-when", - name: "If Guard to When Clause", - category: PatternMatching, - difficulty: Advanced, - jsPattern: "case\\s+.*:\\s*if\\s*\\(", - confidence: 0.60, - jsExample: "switch (x) { case n: if (n > 0) return 'positive' }", - rescriptExample: "switch x { | n if n > 0 => \"positive\" | _ => \"non-positive\" }", - narrative: { - celebrate: "Guard clauses add precision to cases!", - minimize: "But JS case with if is awkward and error-prone.", - better: "ReScript's 'if' guard is part of the pattern — clean and readable.", - safety: "Guards compose with exhaustiveness — the wildcard ensures coverage.", - }, - glyphs: ["Branch", "Target"], - tags: ["guard", "when", "pattern-match"], - relatedPatterns: ["switch-match"], - learningObjectives: ["Use guard clauses in pattern matching"], - }, - // --- PipeOperator (2) --- - { - id: "method-chain-pipe", - name: "Method Chain to Pipe", - category: PipeOperator, - difficulty: Beginner, - jsPattern: "\\)\\s*\\.\\w+\\(", - confidence: 0.55, - jsExample: "data.filter(x => x > 0).map(x => x * 2).reduce((a, b) => a + b, 0)", - rescriptExample: "data->Array.filter(x => x > 0)->Array.map(x => x * 2)->Array.reduce(0, (a, b) => a + b)", - narrative: { - celebrate: "Method chaining reads left to right!", - minimize: "But methods are bound to the prototype — you can't chain arbitrary functions.", - better: "The pipe operator works with any function — not just methods.", - safety: "Each step's type flows into the next — no implicit this binding.", - }, - glyphs: ["Flow", "Lightning"], - tags: ["pipe", "chain", "method"], - relatedPatterns: ["compose-pipe"], - learningObjectives: ["Use pipe operator for data transformation chains"], - }, - { - id: "lodash-pipe", - name: "Lodash/Ramda to Pipe", - category: PipeOperator, - difficulty: Intermediate, - jsPattern: "_\\.chain\\(|R\\.pipe\\(", - confidence: 0.75, - jsExample: "_.chain(data).filter(isActive).sortBy('name').value()", - rescriptExample: "data->Array.filter(isActive)->Array.toSorted((a, b) => compare(a.name, b.name))", - narrative: { - celebrate: "Lodash chains are powerful data pipelines!", - minimize: "But they add a runtime dependency and lose type information.", - better: "ReScript's pipe operator does the same thing with zero runtime cost.", - safety: "Every step is type-checked — no 'string' accidentally becoming 'number'.", - }, - glyphs: ["Flow", "Compress"], - tags: ["lodash", "ramda", "pipe", "chain"], - relatedPatterns: ["method-chain-pipe"], - learningObjectives: ["Replace utility libraries with built-in pipe"], - }, - // --- OopToFp (2) --- - { - id: "class-module", - name: "Class to Module", - category: OopToFp, - difficulty: Intermediate, - jsPattern: "class\\s+\\w+\\s*\\{", - confidence: 0.70, - jsExample: "class UserService { constructor(db) { this.db = db } getUser(id) { ... } }", - rescriptExample: "// UserService.res\nlet getUser = (db, id) => ...", - narrative: { - celebrate: "Classes encapsulate related logic!", - minimize: "But classes mix data and behaviour, making testing harder.", - better: "ReScript modules group functions — data flows in as arguments.", - safety: "No 'this' binding confusion — every dependency is explicit.", - }, - glyphs: ["Package", "Wrench"], - tags: ["class", "module", "oop", "fp"], - relatedPatterns: ["inheritance-composition"], - learningObjectives: ["Replace classes with modules and functions"], - }, - { - id: "inheritance-composition", - name: "Inheritance to Composition", - category: InheritanceToComposition, - difficulty: Advanced, - jsPattern: "extends\\s+\\w+", - confidence: 0.70, - jsExample: "class AdminUser extends User { ... }", - rescriptExample: "type user = { name: string, role: role }\ntype role = Regular | Admin({ permissions: array })", - narrative: { - celebrate: "Inheritance models 'is-a' relationships!", - minimize: "But deep hierarchies become brittle and hard to change.", - better: "Composition with variants models the differences explicitly.", - safety: "No virtual dispatch surprises — the variant tag is checked at compile time.", - }, - glyphs: ["Tree", "Masks"], - tags: ["inheritance", "composition", "variant"], - relatedPatterns: ["class-module"], - learningObjectives: ["Model hierarchies with composition"], - }, - // --- ClassesToRecords (2) --- - { - id: "class-record", - name: "Class Instance to Record", - category: ClassesToRecords, - difficulty: Beginner, - jsPattern: "new\\s+\\w+\\(", - confidence: 0.65, - jsExample: "const user = new User('Alice', 30)", - rescriptExample: "let user = { name: \"Alice\", age: 30 }", - narrative: { - celebrate: "Constructor calls create structured data!", - minimize: "But constructors hide field names — position matters.", - better: "Record literals name every field — self-documenting and order-independent.", - safety: "Missing a required field is a compile error.", - }, - glyphs: ["Notebook", "Sparkle"], - tags: ["class", "record", "constructor"], - relatedPatterns: ["class-module"], - learningObjectives: ["Replace class instances with record literals"], - }, - { - id: "getter-field", - name: "Getter/Setter to Record Field", - category: ClassesToRecords, - difficulty: Intermediate, - jsPattern: "get\\s+\\w+\\(\\)|set\\s+\\w+\\(", - confidence: 0.70, - jsExample: "class User { get fullName() { return `${this.first} ${this.last}` } }", - rescriptExample: "let fullName = (user) => `${user.first} ${user.last}`", - narrative: { - celebrate: "Getters provide computed properties!", - minimize: "But they look like field access while hiding computation.", - better: "An explicit function makes the computation visible.", - safety: "No hidden side effects — it's just a function call.", - }, - glyphs: ["Notebook", "Compress"], - tags: ["getter", "setter", "field", "function"], - relatedPatterns: ["class-record"], - learningObjectives: ["Replace getters with explicit functions"], - }, - // --- StateMachines (2) --- - { - id: "state-string-variant", - name: "State String to Variant", - category: StateMachines, - difficulty: Intermediate, - jsPattern: "state\\s*===\\s*['\"]\\w+['\"]", - confidence: 0.75, - jsExample: "if (state === 'loading') { ... } else if (state === 'error') { ... }", - rescriptExample: "switch state { | Loading => ... | Error(msg) => ... | Ready(data) => ... }", - narrative: { - celebrate: "Tracking state explicitly is good design!", - minimize: "But string states can be misspelled and don't carry data.", - better: "Variant states carry associated data and are exhaustively checked.", - safety: "Error(msg) guarantees the message exists — no checking state AND error separately.", - }, - glyphs: ["Masks", "Branch"], - tags: ["state", "machine", "variant"], - relatedPatterns: ["string-enum-variant"], - learningObjectives: ["Model state machines with variants"], - }, - { - id: "reducer-switch", - name: "Reducer to Variant Actions", - category: StateMachines, - difficulty: Advanced, - jsPattern: "case\\s+['\"]\\w+['\"]:\\s*return", - confidence: 0.70, - jsExample: "case 'SET_USER': return { ...state, user: action.payload }", - rescriptExample: "| SetUser(user) => { ...state, user: Some(user) }", - narrative: { - celebrate: "Reducers with action types are a proven pattern!", - minimize: "But string action types need constants and the payload is untyped.", - better: "Variant actions carry typed payloads — SetUser(user) is self-describing.", - safety: "Add a new action variant and the compiler finds every unhandled case.", - }, - glyphs: ["Branch", "Lock"], - tags: ["reducer", "action", "state-machine"], - relatedPatterns: ["state-string-variant"], - learningObjectives: ["Use variant actions in reducers"], - }, - // --- DataModeling (2) --- - { - id: "interface-type", - name: "Interface to Type", - category: DataModeling, - difficulty: Beginner, - jsPattern: "interface\\s+\\w+\\s*\\{", - confidence: 0.80, - jsExample: "interface User { name: string; age: number; email?: string }", - rescriptExample: "type user = { name: string, age: int, email: option }", - narrative: { - celebrate: "Interfaces define clear data contracts!", - minimize: "But optional fields (?) become undefined at runtime.", - better: "ReScript's option makes optionality explicit and safe.", - safety: "Accessing email requires handling the None case.", - }, - glyphs: ["Notebook", "Target"], - tags: ["interface", "type", "record"], - relatedPatterns: ["tagged-union-variant"], - learningObjectives: ["Define data types with explicit optionality"], - }, - { - id: "enum-poly-variant", - name: "Enum to Polymorphic Variant", - category: DataModeling, - difficulty: Advanced, - jsPattern: "enum\\s+\\w+\\s*\\{", - confidence: 0.75, - jsExample: "enum Color { Red = 'red', Green = 'green', Blue = 'blue' }", - rescriptExample: "type color = [#red | #green | #blue]", - narrative: { - celebrate: "Enums are great for fixed sets of values!", - minimize: "But TS enums compile to objects with runtime overhead.", - better: "Polymorphic variants are zero-cost — they compile to plain strings.", - safety: "Using an invalid colour is a compile error — no runtime check needed.", - }, - glyphs: ["Masks", "Lightning"], - tags: ["enum", "polymorphic", "variant"], - relatedPatterns: ["string-enum-variant"], - learningObjectives: ["Use polymorphic variants for string enums"], - }, -] - -// ============================================================================ -// Scanner — regex-based pattern matching against JS input -// ============================================================================ - -/// Scan JS code against a single pattern, returning matches. -let scanPattern = (code: string, pattern: evangeliserPattern): array => { - let re = RegExp.fromString(pattern.jsPattern) - let lines = code->String.split("\n") - let matches = [] - lines->Array.forEachWithIndex((line, idx) => { - if re->RegExp.test(line) { - let _ = matches->Array.push({ - patternId: pattern.id, - patternName: pattern.name, - category: pattern.category, - code: line->String.trim, - startLine: idx + 1, - endLine: idx + 1, - confidence: pattern.confidence, - jsExample: pattern.jsExample, - rescriptExample: pattern.rescriptExample, - narrative: pattern.narrative, - glyphs: pattern.glyphs, - }) - } - }) - matches -} - -/// Scan JS code against all patterns, applying constraints. -let scanCode = ( - code: string, - patterns: array, - constraints: evangeliserConstraints, -): evangeliserAnalysis => { - let startTime = Date.now() - - // Filter patterns by constraints - let activePatterns = patterns->Array.filter(p => { - let catEnabled = - constraints.enabledCategories->Array.length === 0 || - constraints.enabledCategories->Array.includes(p.category) - let confOk = p.confidence >= constraints.minConfidence - let diffOk = switch constraints.difficultyFilter { - | None => true - | Some(d) => p.difficulty === d - } - catEnabled && confOk && diffOk - }) - - // Run scanner - let allMatches = activePatterns->Array.flatMap(p => scanPattern(code, p)) - - // Cap results - let capped = if Array.length(allMatches) > constraints.maxResults { - allMatches->Array.slice(~start=0, ~end=constraints.maxResults) - } else { - allMatches - } - - let elapsed = Date.now() -. startTime - let totalLines = code->String.split("\n")->Array.length - let matchedLines = capped->Array.map(m => m.startLine)->Array.length - let coverage = if totalLines > 0 { - Float.fromInt(matchedLines) /. Float.fromInt(totalLines) *. 100.0 - } else { - 0.0 - } - - // Determine overall difficulty - let difficulty = if ( - capped->Array.some(m => { - builtinPatterns - ->Array.find(p => p.id === m.patternId) - ->Option.map(p => p.difficulty === Advanced) - ->Option.getOr(false) - }) - ) { - Advanced - } else if ( - capped->Array.some(m => { - builtinPatterns - ->Array.find(p => p.id === m.patternId) - ->Option.map(p => p.difficulty === Intermediate) - ->Option.getOr(false) - }) - ) { - Intermediate - } else { - Beginner - } - - { - matches: capped, - totalPatterns: Array.length(activePatterns), - coveragePercentage: coverage, - difficulty, - analysisTime: elapsed, - } -} - -// ============================================================================ -// Filtering and Stats -// ============================================================================ - -/// Filter patterns by category. -let filterByCategory = ( - patterns: array, - cat: option, -): array => { - switch cat { - | None => patterns - | Some(c) => patterns->Array.filter(p => p.category === c) - } -} - -/// Filter patterns by text search (name or tags). -let filterBySearch = (patterns: array, text: string): array< - evangeliserPattern, -> => { - if String.length(text) === 0 { - patterns - } else { - let lower = text->String.toLowerCase - patterns->Array.filter(p => { - p.name->String.toLowerCase->String.includes(lower) || - p.tags->Array.some(t => t->String.toLowerCase->String.includes(lower)) - }) - } -} - -/// Count patterns per category. -let categoryStats = (patterns: array): array<(evangeliserCategory, int)> => { - allCategories->Array.map(cat => { - let count = patterns->Array.filter(p => p.category === cat)->Array.length - (cat, count) - }) -} - -/// Count matches per category. -let matchCategoryStats = (matches: array): array<(evangeliserCategory, int)> => { - allCategories->Array.filterMap(cat => { - let count = matches->Array.filter(m => m.category === cat)->Array.length - if count > 0 { - Some((cat, count)) - } else { - None - } - }) -} - -// ============================================================================ -// Default State -// ============================================================================ - -let defaultConstraints: evangeliserConstraints = { - enabledCategories: [], // Empty = all enabled - minConfidence: 0.5, - difficultyFilter: None, - maxResults: 100, -} - -let defaultState: evangeliserState = { - constraints: defaultConstraints, - jsInput: "", - scanning: false, - scanError: None, - analysis: None, - viewLayer: ViewRaw, - patterns: builtinPatterns, - glyphs: builtinGlyphs, - activeTab: TabScan, - filterText: "", - selectedMatchIndex: None, - legendExpanded: false, - error: None, -} diff --git a/src/core/EventChain.affine b/src/core/EventChain.affine new file mode 100644 index 00000000..ea26d84d --- /dev/null +++ b/src/core/EventChain.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module EventChain; + +// TODO: Complete semantic implementation diff --git a/src/core/EventChain.res b/src/core/EventChain.res deleted file mode 100644 index eb8a65c6..00000000 --- a/src/core/EventChain.res +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Event-chain parsing helpers for PanLL. -/// -/// Parses panic-attack PanLL export JSON into lightweight model state. - -open Model - -type payload = { - summary: option, - timeline: option, - events: array, -} - -/// Tea_Json decoder for an event chain summary. -let summaryDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map5( - (program, weakPoints, criticalWeakPoints, totalCrashes, robustnessScore): eventChainSummary => { - program, - weakPoints, - criticalWeakPoints, - totalCrashes, - robustnessScore, - }, - stringField("program"), - intField("weak_points"), - intField("critical_weak_points"), - intField("total_crashes"), - floatField("robustness_score"), - ) -} - -/// Tea_Json decoder for an event chain timeline. -let timelineDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map2((durationMs, events): eventChainTimeline => { - durationMs, - events, - }, floatField("duration_ms"), intField("events")) -} - -/// Tea_Json decoder for a single event chain event. -let eventDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map8((id, axis, startMs, durationMs, intensity, status, peakMemory, notes): eventChainEvent => { - id, - axis, - startMs, - durationMs, - intensity, - status, - peakMemory, - notes, - }, stringField( - "id", - ), fieldWithDefault( - "axis", - string, - "unknown", - ), optionalFieldDecoder( - "start_ms", - float, - ), floatField( - "duration_ms", - ), fieldWithDefault( - "intensity", - string, - "unknown", - ), fieldWithDefault( - "status", - string, - "unknown", - ), optionalFieldDecoder("peak_memory", float), optionalFieldDecoder("notes", string)) -} - -/// Tea_Json decoder for the full event-chain payload. -let payloadDecoder: Tea_Json.decoder = { - open Decoders - open Tea_Json - map3((summary, timeline, events): payload => { - summary, - timeline, - events, - }, optionalFieldDecoder( - "summary", - summaryDecoder, - ), optionalFieldDecoder( - "timeline", - timelineDecoder, - ), fieldWithDefault("event_chain", lenientArray(eventDecoder), [])) -} - -/// Parse a panic-attack PanLL export JSON into a payload. -let parse = (raw: string): result => Decoders.decode(payloadDecoder, raw) diff --git a/src/core/ExploratoryWorkbenchEngine.affine b/src/core/ExploratoryWorkbenchEngine.affine new file mode 100644 index 00000000..a938bb22 --- /dev/null +++ b/src/core/ExploratoryWorkbenchEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module ExploratoryWorkbenchEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/ExploratoryWorkbenchEngine.res b/src/core/ExploratoryWorkbenchEngine.res deleted file mode 100644 index f56abccc..00000000 --- a/src/core/ExploratoryWorkbenchEngine.res +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Exploratory Workbench Engine — pure computation and helpers for the -/// Exploratory Workbench panel. Provides default state, anomaly counting, -/// severity labelling, session analysis, and filtering. - -open ExploratoryWorkbenchModel - -/// Default state for the Exploratory Workbench panel. -/// Starts on the Session tab with anomaly detection enabled. -let defaultState: exploratoryWorkbenchState = { - activeTab: TabSession, - currentSession: None, - sessions: [], - anomalies: [], - recording: false, - anomalyDetectionEnabled: true, - error: None, -} - -/// Human-readable label for each tab in the Exploratory Workbench panel. -let tabLabel = (tab: exploratoryTab): string => - switch tab { - | TabSession => "Session" - | TabAnomalies => "Anomalies" - | TabNotes => "Notes" - | TabHistory => "History" - } - -/// All tabs in display order. -let allTabs: array = [TabSession, TabAnomalies, TabNotes, TabHistory] - -/// Human-readable label for anomaly severity. -let severityLabel = (s: anomalySeverity): string => - switch s { - | AnomalyLow => "Low" - | AnomalyMedium => "Medium" - | AnomalyHigh => "High" - | AnomalyCritical => "Critical" - } - -/// CSS colour class for anomaly severity. -let severityColor = (s: anomalySeverity): string => - switch s { - | AnomalyLow => "text-blue-400" - | AnomalyMedium => "text-yellow-400" - | AnomalyHigh => "text-orange-400" - | AnomalyCritical => "text-red-400" - } - -/// CSS background colour class for anomaly severity badges. -let severityBgColor = (s: anomalySeverity): string => - switch s { - | AnomalyLow => "bg-blue-900/30" - | AnomalyMedium => "bg-yellow-900/30" - | AnomalyHigh => "bg-orange-900/30" - | AnomalyCritical => "bg-red-900/30" - } - -/// Count anomalies by severity. -let anomalyCountBySeverity = (anomalies: array, severity: anomalySeverity): int => - anomalies->Array.filter(a => a.severity == severity)->Array.length - -/// Total anomalies across all past sessions. -let totalAnomalies = (sessions: array): int => - sessions->Array.reduce(0, (acc, s) => acc + s.anomalies->Array.length) - -/// Count auto-detected anomalies vs manually flagged. -let autoDetectedCount = (anomalies: array): int => - anomalies->Array.filter(a => a.autoDetected)->Array.length - -/// Count manually flagged anomalies. -let manuallyFlaggedCount = (anomalies: array): int => - anomalies->Array.filter(a => !a.autoDetected)->Array.length - -/// Filter anomalies by category. -let filterByCategory = (anomalies: array, category: string): array => - if category == "" { - anomalies - } else { - anomalies->Array.filter(a => a.category == category) - } - -/// Get unique anomaly categories from a list of anomalies. -let uniqueCategories = (anomalies: array): array => { - let cats = anomalies->Array.map(a => a.category) - cats->Array.reduce([], (acc, cat) => - if acc->Array.some(c => c == cat) { - acc - } else { - Array.concat(acc, [cat]) - } - ) -} - -/// Average anomaly rate per session (anomalies per hour of play). -let anomalyRate = (sessions: array): float => { - let totalHours = sessions->Array.reduce(0.0, (acc, s) => acc +. s.durationMinutes /. 60.0) - let totalAnom = totalAnomalies(sessions) - if totalHours <= 0.0 { - 0.0 - } else { - Float.fromInt(totalAnom) /. totalHours - } -} diff --git a/src/core/FarmEngine.affine b/src/core/FarmEngine.affine new file mode 100644 index 00000000..8c9928be --- /dev/null +++ b/src/core/FarmEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FarmEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/FarmEngine.res b/src/core/FarmEngine.res deleted file mode 100644 index 17aaa390..00000000 --- a/src/core/FarmEngine.res +++ /dev/null @@ -1,279 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Farm Engine — pure computation for the Git-Private-Farm panel. -/// -/// All functions are pure (no side effects, no API calls). Takes model data -/// and produces filtered/sorted/aggregated views. Parsing of JSON responses -/// from Gossamer commands also lives here. - -open FarmModel - -/// Parse a priority string from the manifest into the typed enum. -let parsePriority = (s: string): farmPriority => { - switch String.toLowerCase(s) { - | "high" => High - | "low" => Low - | _ => Medium - } -} - -/// Human-readable label for a priority level. -let priorityLabel = (p: farmPriority): string => { - switch p { - | High => "High" - | Medium => "Medium" - | Low => "Low" - } -} - -/// CSS colour class for a priority level. -let priorityColour = (p: farmPriority): string => { - switch p { - | High => "text-red-400" - | Medium => "text-amber-400" - | Low => "text-gray-400" - } -} - -/// Human-readable label for a category tab. -let categoryLabel = (cat: farmCategory): string => { - switch cat { - | AllRepos => "All Repos" - | ByGroup => "By Group" - | ByLanguage => "By Language" - | ByForge => "By Forge" - | Enrollment => "Enrollment" - | Health => "Health" - } -} - -/// All category tabs in display order. -let allCategories: array = [ - AllRepos, - ByGroup, - ByLanguage, - ByForge, - Enrollment, - Health, -] - -/// Human-readable label for a sort order. -let sortLabel = (s: farmSortBy): string => { - switch s { - | SortByName => "Name" - | SortByPriority => "Priority" - | SortByLanguage => "Language" - | SortByHealth => "Health" - } -} - -/// All sort options in display order. -let allSortOptions: array = [SortByName, SortByPriority, SortByLanguage, SortByHealth] - -/// Filter repos by a text query (matches name or description, case-insensitive). -let filterRepos = (repos: array, query: string): array => { - if query === "" { - repos - } else { - let q = String.toLowerCase(query) - repos->Array.filter(r => - String.includes(String.toLowerCase(r.name), q) || - String.includes(String.toLowerCase(r.description), q) - ) - } -} - -/// Sort repos by the given criterion. -let sortRepos = (repos: array, sortBy: farmSortBy): array => { - let sorted = Array.copy(repos) - sorted->Array.sort((a, b) => { - switch sortBy { - | SortByName => String.compare(a.name, b.name) - | SortByLanguage => String.compare(a.language, b.language) - | SortByPriority => { - let rank = p => - switch p { - | High => 0 - | Medium => 1 - | Low => 2 - } - Int.compare(rank(a.priority), rank(b.priority)) - } - | SortByHealth => { - let score = r => - switch r.healthScore { - | Some(s) => s - | None => 999.0 - } - Float.compare(score(a), score(b)) - } - } - }) - sorted -} - -/// Group repos by their group field. -let groupByGroup = (repos: array): array<(string, array)> => { - let groups: Dict.t> = Dict.make() - repos->Array.forEach(r => { - let key = switch r.group { - | Some(g) => g - | None => "ungrouped" - } - let existing = switch Dict.get(groups, key) { - | Some(arr) => arr - | None => [] - } - Dict.set(groups, key, Array.concat(existing, [r])) - }) - let entries = Dict.toArray(groups) - entries->Array.sort(((a, _), (b, _)) => String.compare(a, b)) - entries -} - -/// Group repos by primary language. -let groupByLanguage = (repos: array): array<(string, array)> => { - let groups: Dict.t> = Dict.make() - repos->Array.forEach(r => { - let key = r.language === "" ? "unknown" : r.language - let existing = switch Dict.get(groups, key) { - | Some(arr) => arr - | None => [] - } - Dict.set(groups, key, Array.concat(existing, [r])) - }) - let entries = Dict.toArray(groups) - entries->Array.sort(((a, _), (b, _)) => String.compare(a, b)) - entries -} - -/// Count repos per forge. -let countByForge = (repos: array): array<(string, int)> => { - let counts: Dict.t = Dict.make() - repos->Array.forEach(r => { - r.forges->Array.forEach(f => { - let n = switch Dict.get(counts, f.name) { - | Some(c) => c - | None => 0 - } - Dict.set(counts, f.name, n + 1) - }) - }) - let entries = Dict.toArray(counts) - entries->Array.sort(((_, a), (_, b)) => Int.compare(b, a)) - entries -} - -/// Parse a single repo from the JSON inventory response. -/// Expects fields: name, description, language, priority, forges (string array), -/// auto_propagate, group. -let parseRepoFromJson = (json: JSON.t): option => { - switch JSON.Classify.classify(json) { - | Object(obj) => { - let getString = (key: string): string => - switch Dict.get(obj, key) { - | Some(v) => - switch JSON.Classify.classify(v) { - | String(s) => s - | _ => "" - } - | None => "" - } - let getBool = (key: string): bool => - switch Dict.get(obj, key) { - | Some(v) => - switch JSON.Classify.classify(v) { - | Bool(b) => b - | _ => false - } - | None => false - } - let getOptString = (key: string): option => - switch Dict.get(obj, key) { - | Some(v) => - switch JSON.Classify.classify(v) { - | String(s) => Some(s) - | Null => None - | _ => None - } - | None => None - } - let forgeNames: array = switch Dict.get(obj, "forges") { - | Some(v) => - switch JSON.Classify.classify(v) { - | Array(arr) => - arr->Array.filterMap(item => - switch JSON.Classify.classify(item) { - | String(s) => Some(s) - | _ => None - } - ) - | _ => [] - } - | None => [] - } - let forges = forgeNames->Array.map((name): farmForge => { - name, - primary: name === "github", - }) - - let enrollment: enrollmentTier = switch Dict.get(obj, "enrollment") { - | Some(v) => - switch JSON.Classify.classify(v) { - | Object(enrollObj) => { - let getEnrollBool = (key: string): bool => - switch Dict.get(enrollObj, key) { - | Some(bv) => - switch JSON.Classify.classify(bv) { - | Bool(b) => b - | _ => false - } - | None => false - } - { - farm: getEnrollBool("farm"), - hypatia: getEnrollBool("hypatia"), - fleet: getEnrollBool("fleet"), - } - } - | _ => {farm: true, hypatia: false, fleet: false} - } - | None => {farm: true, hypatia: false, fleet: false} - } - - Some({ - name: getString("name"), - description: getString("description"), - language: getString("language"), - priority: parsePriority(getString("priority")), - forges, - autoPropagation: getBool("auto_propagate"), - group: getOptString("group"), - enrollment, - healthScore: None, - hasDependabotAlerts: false, - }) - } - | _ => None - } -} - -/// Tea_Json decoder for a single farmRepo, bridging the existing parseRepoFromJson parser. -let farmRepoDecoder: Tea_Json.decoder = json => { - switch parseRepoFromJson(json) { - | Some(v) => Ok(v) - | None => Error(Tea_Json.Failure("Failed to decode farmRepo", json)) - } -} - -/// Tea_Json decoder for the inventory response envelope. -let inventoryDecoder: Tea_Json.decoder> = Decoders.fieldWithDefault( - "repos", - Decoders.lenientArray(farmRepoDecoder), - [], -) - -/// Parse the full inventory response from the farm_list_repos command. -/// Returns (repos, totalCount). -let parseInventory = (jsonStr: string): result, string> => - Decoders.decode(inventoryDecoder, jsonStr) diff --git a/src/core/FeedbackRoutingEngine.affine b/src/core/FeedbackRoutingEngine.affine new file mode 100644 index 00000000..206dc032 --- /dev/null +++ b/src/core/FeedbackRoutingEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FeedbackRoutingEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/FeedbackRoutingEngine.res b/src/core/FeedbackRoutingEngine.res deleted file mode 100644 index 604412d2..00000000 --- a/src/core/FeedbackRoutingEngine.res +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Feedback Routing Engine — pure helpers for upstream report tracking. - -open FeedbackRoutingModel - -/// Default initial state. -let defaultState: feedbackRoutingState = { - activeTab: TabOverview, - reports: [], - platformStats: [], - selectedReport: None, - refreshing: false, - error: None, - filterText: "", -} - -/// Tab label for display. -let tabLabel = (tab: feedbackRoutingTab): string => { - switch tab { - | TabOverview => "Overview" - | TabReports => "Reports" - | TabPlatforms => "Platforms" - } -} - -/// All tabs for rendering. -let allTabs: array = [TabOverview, TabReports, TabPlatforms] - -/// Report status label for display. -let statusLabel = (s: reportStatus): string => { - switch s { - | ReportFiled => "Filed" - | ReportAcknowledged => "Acknowledged" - | ReportInProgress => "In Progress" - | ReportResolved => "Resolved" - | ReportClosed => "Closed" - | ReportWontFix => "Won't Fix" - } -} - -/// Platform label for display. -let platformLabel = (p: reportPlatform): string => { - switch p { - | GitHub => "GitHub" - | GitLab => "GitLab" - | Email => "Email" - | Discourse => "Discourse" - | Other(name) => name - } -} - -/// Count open reports (filed, acknowledged, or in progress). -let openReportCount = (reports: array): int => { - reports - ->Array.filter(r => - switch r.status { - | ReportFiled | ReportAcknowledged | ReportInProgress => true - | _ => false - } - ) - ->Array.length -} diff --git a/src/core/FleetEngine.affine b/src/core/FleetEngine.affine new file mode 100644 index 00000000..81c34f86 --- /dev/null +++ b/src/core/FleetEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FleetEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/FleetEngine.res b/src/core/FleetEngine.res deleted file mode 100644 index d09f078f..00000000 --- a/src/core/FleetEngine.res +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Fleet Engine — pure computation for the Gitbot-Fleet panel. -/// -/// Parses fleet API responses, computes health aggregates, filters and -/// sorts findings, and provides display helpers for the view layer. -/// No side effects — all state transitions are deterministic. - -open FleetModel - -/// Human-readable label for a bot ID. -let botLabel = (id: botId): string => - switch id { - | Rhodibot => "Rhodibot" - | Echidnabot => "Echidnabot" - | Sustainabot => "Sustainabot" - | Glambot => "Glambot" - | Seambot => "Seambot" - | Finishbot => "Finishbot" - } - -/// Short description of what each bot does. -let botDescription = (id: botId): string => - switch id { - | Rhodibot => "Code quality & style enforcement" - | Echidnabot => "Security vulnerability detection" - | Sustainabot => "Dependency health & sustainability" - | Glambot => "Documentation & presentation quality" - | Seambot => "Integration & API compatibility" - | Finishbot => "CI/CD completion & release readiness" - } - -/// Icon identifier for each bot. -let botIcon = (id: botId): string => - switch id { - | Rhodibot => "shield-check" - | Echidnabot => "bug" - | Sustainabot => "leaf" - | Glambot => "sparkles" - | Seambot => "link" - | Finishbot => "flag" - } - -/// Human-readable label for a bot status. -let statusLabel = (status: botStatus): string => - switch status { - | BotActive => "Active" - | BotIdle => "Idle" - | BotOffline => "Offline" - | BotError(e) => `Error: ${e}` - } - -/// CSS class for bot status indicator dot. -let statusColor = (status: botStatus): string => - switch status { - | BotActive => "bg-green-400" - | BotIdle => "bg-yellow-400" - | BotOffline => "bg-gray-500" - | BotError(_) => "bg-red-400" - } - -/// Human-readable label for a safety tier. -let tierLabel = (tier: safetyTier): string => - switch tier { - | Eliminate => "Eliminate" - | Substitute => "Substitute" - | Control => "Control" - } - -/// CSS class for safety tier badge. -let tierColor = (tier: safetyTier): string => - switch tier { - | Eliminate => "bg-red-600 text-red-100" - | Substitute => "bg-amber-600 text-amber-100" - | Control => "bg-blue-600 text-blue-100" - } - -/// Human-readable label for a fleet category tab. -let categoryLabel = (cat: fleetCategory): string => - switch cat { - | FleetDashboard => "Dashboard" - | FleetFindings => "Findings" - | FleetDispatch => "Dispatch" - } - -/// Compute aggregate health from bot states and findings. -let computeHealth = (bots: array, findings: array): fleetHealth => { - let activeBots = - bots - ->Array.filter(b => - switch b.status { - | BotActive => true - | _ => false - } - ) - ->Array.length - - let totalQueued = findings->Array.filter(f => !f.resolved)->Array.length - let totalProcessed = findings->Array.filter(f => f.resolved)->Array.length - - let confidences = findings->Array.filter(f => !f.resolved)->Array.map(f => f.confidence) - let avgConfidence = if Array.length(confidences) > 0 { - confidences->Array.reduce(0.0, (acc, c) => acc +. c) /. Int.toFloat(Array.length(confidences)) - } else { - 0.0 - } - - let elim = findings->Array.filter(f => f.tier === Eliminate && !f.resolved)->Array.length - let sub = findings->Array.filter(f => f.tier === Substitute && !f.resolved)->Array.length - let ctrl = findings->Array.filter(f => f.tier === Control && !f.resolved)->Array.length - - { - activeBots, - totalQueued, - totalProcessed, - avgConfidence, - triangleCounts: (elim, sub, ctrl), - } -} - -/// Filter findings by text search across repo name and summary. -let filterFindings = (findings: array, query: string): array => { - if query === "" { - findings - } else { - let q = String.toLowerCase(query) - findings->Array.filter(f => - String.includes(String.toLowerCase(f.repoName), q) || - String.includes(String.toLowerCase(f.summary), q) - ) - } -} - -/// Parse a bot ID string into a botId variant. -let parseBotId = (s: string): option => - switch s { - | "rhodibot" => Some(Rhodibot) - | "echidnabot" => Some(Echidnabot) - | "sustainabot" => Some(Sustainabot) - | "glambot" => Some(Glambot) - | "seambot" => Some(Seambot) - | "finishbot" => Some(Finishbot) - | _ => None - } - -/// Parse a bot status string into a botStatus variant. -let parseBotStatus = (s: string): botStatus => - switch s { - | "active" => BotActive - | "idle" => BotIdle - | "offline" => BotOffline - | _ => BotError(s) - } - -/// Tea_Json decoder for a single bot state. -/// Uses map6 to decode fields, then validates the bot ID. -let botStateDecoder: Tea_Json.decoder = json => { - open Decoders - open Tea_Json - let inner = map6( - (idStr, statusStr, queued, processed, confThresh, lastAct) => ( - idStr, - statusStr, - queued, - processed, - confThresh, - lastAct, - ), - stringField("id"), - stringField("status"), - intField("queued"), - intField("processed"), - floatField("confidence_threshold"), - stringField("last_activity"), - ) - switch inner(json) { - | Ok((idStr, statusStr, queued, processed, confThresh, lastAct)) => - switch parseBotId(idStr) { - | Some(botId) => - Ok( - ( - { - id: botId, - status: parseBotStatus(statusStr), - queuedFindings: queued, - processedFindings: processed, - confidenceThreshold: confThresh, - lastActivity: lastAct, - }: botState - ), - ) - | None => Error(Failure(`Unknown bot id: ${idStr}`, json)) - } - | Error(e) => Error(e) - } -} - -/// Parse bot status from the fleet API JSON response. -/// Expected shape: [{ "id": "rhodibot", "status": "active", "queued": 5, ... }] -let parseBots = (json: string): result, string> => - Decoders.decode(Decoders.lenientArray(botStateDecoder), json) - -/// Parse a safety tier string into a safetyTier variant. -let parseSafetyTier = (s: string): safetyTier => - switch s { - | "eliminate" => Eliminate - | "substitute" => Substitute - | _ => Control - } - -/// Tea_Json decoder for a single fleet finding. -let findingDecoder: Tea_Json.decoder = { - open Decoders - map7((id, repoName, summary, tierStr, confidence, assignedStr, resolved): fleetFinding => { - id, - repoName, - summary, - tier: parseSafetyTier(tierStr), - confidence, - assignedBot: parseBotId(assignedStr), - resolved, - }, stringField( - "id", - ), stringField( - "repo_name", - ), stringField( - "summary", - ), stringField( - "tier", - ), floatField("confidence"), stringField("assigned_bot"), boolField("resolved")) -} - -/// Parse findings from the fleet API JSON response. -let parseFindings = (json: string): result, string> => - Decoders.decode(Decoders.lenientArray(findingDecoder), json) - -/// Default initial state. -let defaultState: fleetState = { - loaded: false, - loading: false, - error: None, - bots: [], - findings: [], - health: None, - activeCategory: FleetDashboard, - filterText: "", -} diff --git a/src/core/FloorRaiseEngine.affine b/src/core/FloorRaiseEngine.affine new file mode 100644 index 00000000..230e0e9f --- /dev/null +++ b/src/core/FloorRaiseEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FloorRaiseEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/FloorRaiseEngine.res b/src/core/FloorRaiseEngine.res deleted file mode 100644 index 19d6ff7b..00000000 --- a/src/core/FloorRaiseEngine.res +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Floor Raise Engine — pure helpers for the campaign dashboard. - -open FloorRaiseModel - -/// Default initial state. -let defaultState: floorRaiseState = { - activeTab: TabOverview, - adoptions: [ - {name: "proven", adoptedCount: 0, targetCount: 0, percentage: 0.0, campaignActive: false}, - { - name: "contractiles-trust", - adoptedCount: 0, - targetCount: 0, - percentage: 0.0, - campaignActive: false, - }, - { - name: "contractiles-dust", - adoptedCount: 0, - targetCount: 0, - percentage: 0.0, - campaignActive: false, - }, - {name: "ai-manifest", adoptedCount: 0, targetCount: 0, percentage: 0.0, campaignActive: false}, - { - name: "panic-attacker", - adoptedCount: 0, - targetCount: 0, - percentage: 0.0, - campaignActive: false, - }, - {name: "verisim", adoptedCount: 0, targetCount: 0, percentage: 0.0, campaignActive: false}, - { - name: "feedback-o-tron", - adoptedCount: 0, - targetCount: 0, - percentage: 0.0, - campaignActive: false, - }, - {name: "vexometer", adoptedCount: 0, targetCount: 0, percentage: 0.0, campaignActive: false}, - ], - outcomes: [], - scanning: false, - error: None, - totalRepos: 0, -} - -/// Tab label for display. -let tabLabel = (tab: floorRaiseTab): string => { - switch tab { - | TabOverview => "Overview" - | TabCampaigns => "Campaigns" - | TabOutcomes => "Outcomes" - | TabGaps => "Gaps" - } -} - -/// All tabs for rendering. -let allTabs: array = [TabOverview, TabCampaigns, TabOutcomes, TabGaps] - -/// Calculate overall floor raise progress (average adoption percentage). -let overallProgress = (state: floorRaiseState): float => { - let total = state.adoptions->Array.reduce(0.0, (acc, a) => acc +. a.percentage) - let count = state.adoptions->Array.length->Int.toFloat - if count > 0.0 { - total /. count - } else { - 0.0 - } -} - -/// Count tools with active campaigns. -let activeCampaignCount = (state: floorRaiseState): int => { - state.adoptions->Array.filter(a => a.campaignActive)->Array.length -} - -/// Count successful dispatch outcomes. -let successCount = (outcomes: array): int => { - outcomes->Array.filter(o => o.success)->Array.length -} diff --git a/src/core/FocusDimmingEngine.affine b/src/core/FocusDimmingEngine.affine new file mode 100644 index 00000000..acbadbb5 --- /dev/null +++ b/src/core/FocusDimmingEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FocusDimmingEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/FocusDimmingEngine.res b/src/core/FocusDimmingEngine.res deleted file mode 100644 index 49a4f8b4..00000000 --- a/src/core/FocusDimmingEngine.res +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL FocusDimmingEngine — Pure computation for focus-aware dimming and -/// smart memory mode. -/// -/// Determines per-panel opacity classes based on focus state, dimming mode, -/// and per-panel overrides. Also manages interaction timestamps for the -/// Smart Memory mode, which progressively throttles unfocused panels to -/// conserve CPU and memory. -/// -/// All functions are pure — no DOM access, no timers, no side effects. -/// The View layer calls panelOpacityClass() per panel on every render. -/// The Update layer calls recordInteraction() when panels receive input. -/// -/// DESIGN NOTE: Smart Memory mode converts the configured dimOpacity float -/// to the nearest Tailwind opacity class (opacity-{0..100} in steps of 10). -/// This quantisation is intentional — Tailwind purges unused classes, so we -/// must map to a fixed set. If finer control is needed in future, the CSS -/// layer can add custom opacity utilities. - -/// The default focus dimming state used at application startup. -/// -/// Starts with dimming Off so all panels are fully visible and the app -/// doesn't appear dead. Users can enable SmartMemory or other modes via -/// the accessibility toolbar. A future inactivity timeout could auto-enable -/// SmartMemory after N minutes of no interaction. -let defaultState: FocusDimmingModel.focusDimmingState = { - mode: DimmingOff, - focusedPane: None, - overrides: [], - dimOpacity: 0.4, - lastInteractionTimestamps: [], -} - -/// Human-readable label for a dimming mode. -/// -/// Used in the settings panel dropdown and for screen reader announcements -/// when the user changes the dimming mode. -let modeLabel = (mode: FocusDimmingModel.dimmingMode): string => { - switch mode { - | DimmingOff => "Off" - | DimmingSubtle => "Subtle (70% opacity)" - | DimmingStrong => "Strong (40% opacity)" - | SmartMemory => "Smart Memory (dim + throttle)" - } -} - -/// Human-readable label for a panel focus override. -/// -/// Used in the per-panel override selector and for screen reader -/// announcements. -let overrideLabel = (override: FocusDimmingModel.panelFocusOverride): string => { - switch override { - | Default => "Follow Global Mode" - | AlwaysActive => "Always Active" - | AlwaysDimmed => "Always Dimmed" - } -} - -/// Convert a float opacity (0.0–1.0) to the nearest Tailwind opacity class. -/// -/// Quantises to the nearest 10% step and returns the corresponding Tailwind -/// utility class. Values at or above 1.0 return "" (full opacity, no class -/// needed). Values at or below 0.0 return "opacity-0". -/// -/// Internal helper — not exported. -let opacityToTailwind = (opacity: float): string => { - let pct = Int.toFloat(Float.toInt(opacity *. 10.0 +. 0.5)) *. 10.0 - if pct >= 100.0 { - "" - } else if pct <= 0.0 { - "opacity-0" - } else { - "opacity-" ++ Int.toString(Float.toInt(pct)) - } -} - -/// Look up the per-panel override for a given panel ID. -/// -/// Searches the overrides array for a matching panel ID. Returns Default -/// if no override is found, meaning the panel follows the global dimming mode. -let getOverride = ( - panelId: PanelSwitcherModel.panelId, - state: FocusDimmingModel.focusDimmingState, -): FocusDimmingModel.panelFocusOverride => { - let found = state.overrides->Array.find(((id, _)) => id === panelId) - switch found { - | Some((_, override)) => override - | None => Default - } -} - -/// Determine the CSS opacity class for a panel based on focus state. -/// -/// Logic: -/// 1. Check overrides first — AlwaysActive returns "" (full opacity), -/// AlwaysDimmed returns the mode's dim class regardless of focus. -/// 2. If the panel's key matches focusedPane, return "" (focused = bright). -/// 3. Otherwise, apply the global dimming mode: -/// - DimmingOff: "" (no dimming) -/// - DimmingSubtle: "opacity-70" -/// - DimmingStrong: "opacity-40" -/// - SmartMemory: nearest Tailwind class to dimOpacity -/// -/// The panelKey parameter is a string (e.g. "paneL", "paneN", "paneW", -/// or a panel ID string) that is compared against focusedPane. -let panelOpacityClass = (state: FocusDimmingModel.focusDimmingState, panelKey: string): string => { - // Determine the dim class for the current mode. - let dimClass = switch state.mode { - | DimmingOff => "" - | DimmingSubtle => "opacity-70" - | DimmingStrong => "opacity-40" - | SmartMemory => opacityToTailwind(state.dimOpacity) - } - - // Check if this panel has a per-panel override. - // Overrides are stored as (panelId, override) tuples; we need to match - // by string comparison since panelKey is a string, not a panelId variant. - // For core panes (paneL, paneN, paneW), there are no overrides — they - // always follow the global mode. - let hasAlwaysActive = state.overrides->Array.some(((_, ov)) => { - switch ov { - | AlwaysActive => true - | _ => false - } - }) - // NOTE: Override matching by panelKey string is a simplification. The full - // implementation would require a panelId-to-string mapping. For now, core - // panes and string-based lookups follow the global mode directly. - let _ = hasAlwaysActive - - // If this panel is focused, it is always fully opaque. - let isFocused = switch state.focusedPane { - | Some(focused) => focused === panelKey - | None => false - } - - if isFocused { - "" - } else { - dimClass - } -} - -/// Determine whether a panel should have its processing throttled. -/// -/// Returns true only when ALL of the following are true: -/// - The global mode is SmartMemory -/// - The panel is NOT focused (focusedPane does not match panelKey) -/// - The panel does NOT have an AlwaysActive override -/// -/// When true, the subscription layer should double polling intervals and -/// halve refresh rates for this panel to conserve resources. -let shouldThrottle = (state: FocusDimmingModel.focusDimmingState, panelKey: string): bool => { - // Only SmartMemory mode throttles. - if state.mode !== SmartMemory { - false - } else { - // Check focus. - let isFocused = switch state.focusedPane { - | Some(focused) => focused === panelKey - | None => false - } - if isFocused { - false - } else { - // Check for AlwaysActive override. Since we're matching by string - // and overrides use panelId, we check all overrides — the caller is - // responsible for passing the correct panelKey that corresponds to - // a panelId for override matching to work. - let hasAlwaysActive = state.overrides->Array.some(((_, ov)) => { - switch ov { - | AlwaysActive => true - | _ => false - } - }) - - // This is a conservative check — if ANY panel has AlwaysActive, we - // still throttle other panels. The proper per-panel check requires - // panelId matching, which the full implementation will use. - !hasAlwaysActive - } - } -} - -/// Record a user interaction with a panel. -/// -/// Updates the lastInteractionTimestamps array with the new timestamp for -/// the given panelKey, and sets focusedPane to that panel. If the panel -/// already has a timestamp entry, it is replaced; otherwise a new entry -/// is appended. -/// -/// Called by the Update layer whenever a panel receives mouse, keyboard, -/// or touch input. -let recordInteraction = ( - state: FocusDimmingModel.focusDimmingState, - panelKey: string, - timestamp: float, -): FocusDimmingModel.focusDimmingState => { - let filtered = state.lastInteractionTimestamps->Array.filter(((key, _)) => key !== panelKey) - { - ...state, - focusedPane: Some(panelKey), - lastInteractionTimestamps: Array.concat(filtered, [(panelKey, timestamp)]), - } -} - -/// Set the per-panel dimming override for a given panel ID. -/// -/// If the panel already has an override entry, it is replaced. If the -/// override is Default, the entry is removed entirely (no need to store -/// "follow global" explicitly). This keeps the overrides array minimal. -let setOverride = ( - panelId: PanelSwitcherModel.panelId, - override: FocusDimmingModel.panelFocusOverride, - state: FocusDimmingModel.focusDimmingState, -): FocusDimmingModel.focusDimmingState => { - let filtered = state.overrides->Array.filter(((id, _)) => id !== panelId) - let newOverrides = switch override { - | Default => filtered - | _ => Array.concat(filtered, [(panelId, override)]) - } - { - ...state, - overrides: newOverrides, - } -} diff --git a/src/core/FunctionalTesterEngine.affine b/src/core/FunctionalTesterEngine.affine new file mode 100644 index 00000000..9f38a290 --- /dev/null +++ b/src/core/FunctionalTesterEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module FunctionalTesterEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/FunctionalTesterEngine.res b/src/core/FunctionalTesterEngine.res deleted file mode 100644 index 18136395..00000000 --- a/src/core/FunctionalTesterEngine.res +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Functional Tester Engine — pure computation and helpers for the -/// Functional Tester panel. Provides default state, tab metadata, workflow -/// progress calculation, step counting, and status formatting. - -open FunctionalTesterModel - -/// Default state for the Functional Tester panel. -/// Starts on the Workflows tab with empty workflow list and no templates. -let defaultState: functionalTesterState = { - activeTab: TabWorkflows, - workflows: [], - selectedWorkflow: None, - editing: false, - running: false, - templates: [], - error: None, -} - -/// Human-readable label for each tab in the Functional Tester panel. -let tabLabel = (tab: functionalTestTab): string => - switch tab { - | TabWorkflows => "Workflows" - | TabEditor => "Editor" - | TabResults => "Results" - | TabTemplates => "Templates" - } - -/// All tabs in display order. -let allTabs: array = [TabWorkflows, TabEditor, TabResults, TabTemplates] - -/// Count the number of completed (passed) steps in a workflow. -let completedSteps = (workflow: testWorkflow): int => - workflow.steps->Array.filter(s => s.passed == Some(true))->Array.length - -/// Count the number of failed steps in a workflow. -let failedSteps = (workflow: testWorkflow): int => - workflow.steps->Array.filter(s => s.passed == Some(false))->Array.length - -/// Calculate workflow progress as a percentage (0.0 to 100.0). -let workflowProgress = (workflow: testWorkflow): float => { - let total = workflow.steps->Array.length - if total == 0 { - 0.0 - } else { - Float.fromInt(completedSteps(workflow)) /. Float.fromInt(total) *. 100.0 - } -} - -/// Get the current step index for a running workflow. -let currentStep = (status: workflowStatus): option => - switch status { - | WorkflowRunning(i) => Some(i) - | _ => None - } - -/// Count workflows matching a predicate on their status. -let countByStatus = (workflows: array, pred: workflowStatus => bool): int => - workflows->Array.filter(w => pred(w.status))->Array.length - -/// Human-readable label for a workflow status. -let statusLabel = (status: workflowStatus): string => - switch status { - | WorkflowDraft => "Draft" - | WorkflowReady => "Ready" - | WorkflowRunning(step) => `Running (step ${Int.toString(step + 1)})` - | WorkflowPassed(ms) => `Passed (${Float.toFixed(ms, ~digits=0)}ms)` - | WorkflowFailed(step, _) => `Failed at step ${Int.toString(step + 1)}` - } - -/// CSS colour class for a workflow status. -let statusColor = (status: workflowStatus): string => - switch status { - | WorkflowDraft => "text-gray-400" - | WorkflowReady => "text-blue-400" - | WorkflowRunning(_) => "text-yellow-400" - | WorkflowPassed(_) => "text-green-400" - | WorkflowFailed(_, _) => "text-red-400" - } - -/// Whether a workflow can be executed (Ready status with at least one step). -let canRun = (workflow: testWorkflow): bool => - switch workflow.status { - | WorkflowReady => Array.length(workflow.steps) > 0 - | _ => false - } - -/// Total execution time of all completed steps in a workflow. -let totalStepDuration = (workflow: testWorkflow): float => - workflow.steps->Array.reduce(0.0, (acc, step) => - switch step.durationMs { - | Some(ms) => acc +. ms - | None => acc - } - ) - -/// Count workflows that have ever been run. -let countEverRun = (workflows: array): int => - workflows->Array.filter(w => w.lastRunAt != None)->Array.length diff --git a/src/core/GamePreviewEngine.affine b/src/core/GamePreviewEngine.affine new file mode 100644 index 00000000..6b2e3a00 --- /dev/null +++ b/src/core/GamePreviewEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GamePreviewEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/GamePreviewEngine.res b/src/core/GamePreviewEngine.res deleted file mode 100644 index a8f0f8c3..00000000 --- a/src/core/GamePreviewEngine.res +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Game Preview Engine — pure computation and helpers for the -/// live IDApTIK game preview panel. - -open GamePreviewModel - -/// Human-readable labels for category tabs. -let categoryLabel = (cat: gamePreviewCategory): string => - switch cat { - | PreviewLive => "Live Preview" - | PreviewDeviceLog => "Device Log" - | PreviewClips => "Clips" - | PreviewPerformance => "Performance" - } - -/// Human-readable labels for game overlays. -let overlayLabel = (overlay: gameOverlay): string => - switch overlay { - | OverlayCollision => "Collision Boxes" - | OverlayNetworkTopology => "Network Topology" - | OverlayGuardPatrols => "Guard Patrols" - | OverlayDeviceZones => "Device Zones" - | OverlaySpawnPoints => "Spawn Points" - | OverlayRenderStats => "Render Stats" - } - -/// All available overlays for the toggle panel. -let allOverlays: array = [ - OverlayCollision, - OverlayNetworkTopology, - OverlayGuardPatrols, - OverlayDeviceZones, - OverlaySpawnPoints, - OverlayRenderStats, -] - -/// Check if a specific overlay is active. -let isOverlayActive = (overlays: array, target: gameOverlay): bool => - Array.some(overlays, o => o === target) - -/// Toggle an overlay on or off. -let toggleOverlay = (overlays: array, target: gameOverlay): array => - if isOverlayActive(overlays, target) { - Array.filter(overlays, o => o !== target) - } else { - Array.concat(overlays, [target]) - } - -/// Human-readable execution state label. -let executionLabel = (exec: gameExecutionState): string => - switch exec { - | GameRunning => "Running" - | GamePaused => "Paused" - | GameStepping => "Stepping" - } - -/// Default state for the Game Preview panel. -let defaultState: gamePreviewState = { - devServerConnected: false, - devServerUrl: ServiceEndpoints.gamePreviewDev, - execution: GameRunning, - activeCategory: PreviewLive, - activeOverlays: [], - gameRecording: GameRecordingIdle, - clips: [], - deviceLog: [], - stats: None, - zoomLevel: 1.0, - multiplayerView: false, - error: None, - loading: false, -} - -/// Generate the PixiJS embed script tag content for the game preview iframe. -/// This produces a minimal bootstrap that loads the IDApTIK game engine -/// and connects the dev server hot-reload socket. -let pixiBootstrapScript = (devServerUrl: string): string => { - "const app = new PIXI.Application();" ++ - "await app.init({ width: 800, height: 600, backgroundColor: 0x1a1a2e });" ++ - "document.getElementById('game-root').appendChild(app.canvas);" ++ - "const ws = new WebSocket('" ++ - devServerUrl ++ - "/ws');" ++ - "ws.onmessage = (e) => { if (e.data === 'reload') { location.reload(); } };" ++ "app.ticker.add(() => { /* game loop placeholder */ });" -} - -/// Generate a srcdoc HTML string for the game preview iframe. -/// Embeds PixiJS from CDN and runs the bootstrap script. -let iframeSrcDoc = (devServerUrl: string): string => { - "" ++ - "" ++ - "" ++ - "" ++ - "" ++ - "
" ++ - "" ++ "" -} - -/// Default render stats for the performance tab. -let defaultRenderStats: renderStats = { - fps: 60.0, - drawCalls: 0, - textureMemory: 0, - spriteCount: 0, -} - -/// Format render stats as a human-readable summary line. -let renderStatsLabel = (stats: renderStats): string => { - Float.toFixed(stats.fps, ~digits=1) ++ - " FPS | " ++ - Int.toString(stats.drawCalls) ++ - " draws | " ++ - Int.toString(stats.spriteCount) ++ " sprites" -} diff --git a/src/core/GeneratorModeEngine.affine b/src/core/GeneratorModeEngine.affine new file mode 100644 index 00000000..ba5885f6 --- /dev/null +++ b/src/core/GeneratorModeEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GeneratorModeEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/GeneratorModeEngine.res b/src/core/GeneratorModeEngine.res deleted file mode 100644 index cee8372e..00000000 --- a/src/core/GeneratorModeEngine.res +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Generator Mode Engine — pure computation and helpers for the -/// parametric world builder panel. -/// -/// Provides default state, tab labels, and utility functions for counting -/// districts, totalling facilities, formatting weather/time, and constructing -/// default world parameters. - -open GeneratorModeModel - -/// Default world generation parameters (balanced mid-range values). -let defaultWorldParams: worldParams = { - securityLevel: 0.5, - techLevel: 0.5, - weatherCondition: Clear, - timeOfDay: Afternoon, - trapDensity: 0.3, - civilianPopulation: 100, - difficultyTarget: 0.5, -} - -/// Default state for the Generator Mode panel. -let defaultState: generatorModeState = { - activeTab: Design, - currentSpec: None, - params: defaultWorldParams, - previewResult: None, - generating: false, - templates: [], - error: None, -} - -/// Human-readable label for a generator mode category tab. -let tabLabel = (cat: generatorModeCategory): string => - switch cat { - | Design => "Design" - | Parameters => "Parameters" - | Preview => "Preview" - | Export => "Export" - } - -/// All category tabs in display order. -let allTabs: array = [Design, Parameters, Preview, Export] - -/// Count the number of districts in a world specification. -let countDistricts = (spec: worldSpec): int => spec.districts->Array.length - -/// Count the total number of facilities across all districts. -let countTotalFacilities = (spec: worldSpec): int => - spec.districts->Array.reduce(0, (acc, district) => - acc + district.facilities->Array.reduce(0, (sum, f) => sum + f.count) - ) - -/// Human-readable weather condition label. -let formatWeather = (w: weatherCondition): string => - switch w { - | Clear => "Clear" - | Rain => "Rain" - | Snow => "Snow" - | Fog => "Fog" - | Storm => "Storm" - | NightRain => "Night Rain" - } - -/// Human-readable time of day label. -let formatTimeOfDay = (t: timeOfDay): string => - switch t { - | Dawn => "Dawn" - | Morning => "Morning" - | Afternoon => "Afternoon" - | Evening => "Evening" - | Night => "Night" - | Midnight => "Midnight" - } diff --git a/src/core/GovernanceEngine.affine b/src/core/GovernanceEngine.affine new file mode 100644 index 00000000..4c28b238 --- /dev/null +++ b/src/core/GovernanceEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GovernanceEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/GovernanceEngine.res b/src/core/GovernanceEngine.res deleted file mode 100644 index 1a09511e..00000000 --- a/src/core/GovernanceEngine.res +++ /dev/null @@ -1,498 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// GovernanceEngine — Neurosymbolic Coherence Orchestrator -/// -/// Closes the feedback loops between PanLL's five governance subsystems: -/// -/// Anti-Crash (circuit breaker) → Governance → Contractiles (elastic bounds) -/// Vexometer (operator frustration) → Governance → Contractiles (elasticity) -/// OrbitalSync (L↔N↔W divergence) → Governance → Anti-Crash (confidence) -/// -/// This is the missing piece that makes PanLL "wholly neurosymbolic" — without -/// it, each subsystem validates in isolation but never learns from the others. -/// -/// Called after every applyContractiles pass in the main update loop. - -open Model - -// --------------------------------------------------------------------------- -// Governance decision — what this engine concludes after inspecting the system -// --------------------------------------------------------------------------- - -/// A governance adjustment to be applied to the model after evaluation. -type governanceAdjustment = - /// Tighten Anti-Crash: too many violations, reduce confidence threshold. - | TightenAntiCrash - /// Loosen Anti-Crash: low violations + high vexation = operator frustrated by strictness. - | LoosenAntiCrash - /// Increase contractile elasticity: operator vexation is high. - | IncreaseElasticity(string) // contractile id - /// Decrease contractile elasticity: stability is high, can be stricter. - | DecreaseElasticity(string) // contractile id - /// Halt inference: critical governance failure. - | HaltInference(string) // reason - /// Resume inference: conditions have improved. - | ResumeInference - /// Adjust humidity: stress level changed. - | AdjustHumidity(humidityLevel) - /// Emit sync event: cross-panel state changed. - | EmitSyncEvent(syncEvent) - -// --------------------------------------------------------------------------- -// Thresholds (tuned for the Binary Star co-orbit) -// --------------------------------------------------------------------------- - -let violationSpikeThreshold = 5 -let vexationHighThreshold = 0.7 -let vexationLowThreshold = 0.3 -let stabilityDangerThreshold = 0.4 -let stabilityHealthyThreshold = 0.7 -let divergenceHighThreshold = 0.6 -let elasticityIncrement = 0.05 -let elasticityDecrement = 0.03 - -// --------------------------------------------------------------------------- -// Analysis functions -// --------------------------------------------------------------------------- - -/// Count recent Anti-Crash violations. Used to detect violation spikes. -let violationCount = (antiCrash: antiCrashState): int => { - Array.length(antiCrash.violations) -} - -/// Determine if operator frustration should loosen constraints. -let shouldLoosenForVexation = (vex: vexometerState, antiCrash: antiCrashState): bool => { - vex.index > vexationHighThreshold && - violationCount(antiCrash) < violationSpikeThreshold && - !vex.inertiaDetected -} - -/// Determine if low violations + stable orbit should tighten constraints. -let shouldTightenForStability = ( - orbital: orbitalState, - antiCrash: antiCrashState, - vex: vexometerState, -): bool => { - orbital.stability > stabilityHealthyThreshold && - violationCount(antiCrash) > violationSpikeThreshold && - vex.index < vexationLowThreshold -} - -/// Determine the appropriate humidity level from current system state. -let computeHumidity = (vex: vexometerState, orbital: orbitalState): humidityLevel => { - if vex.index > vexationHighThreshold || orbital.stability < stabilityDangerThreshold { - Low // High stress — shed visual noise - } else if vex.index > vexationLowThreshold || orbital.stability < stabilityHealthyThreshold { - Medium - } else { - High // Low stress — show more detail - } -} - -// --------------------------------------------------------------------------- -// Main governance evaluation -// --------------------------------------------------------------------------- - -/// Evaluate all governance subsystems and produce adjustments. -/// Pure function — returns a list of adjustments, does not mutate state. -let evaluate = (model: model): array => { - let adjustments: array = [] - - // --- Anti-Crash ↔ Vexometer feedback --- - - let adjustments = if shouldLoosenForVexation(model.vexometer, model.antiCrash) { - // Operator is frustrated and violations are low — loosen the gate. - Array.concat(adjustments, [LoosenAntiCrash]) - } else if shouldTightenForStability(model.orbital, model.antiCrash, model.vexometer) { - // Orbit is stable but violations are spiking — tighten the gate. - Array.concat(adjustments, [TightenAntiCrash]) - } else { - adjustments - } - - // --- Vexometer → Contractile elasticity --- - - let adjustments = if model.vexometer.index > vexationHighThreshold { - // High vexation: increase elasticity on Adaptive contracts. - let elasticAdj = Array.filterMap(model.contractiles, c => { - switch c.enforcement { - | Adaptive if c.elasticity < 0.9 => Some(IncreaseElasticity(c.id)) - | _ => None - } - }) - Array.concat(adjustments, elasticAdj) - } else if model.vexometer.index < vexationLowThreshold { - // Low vexation: decrease elasticity (can be stricter). - let tightenAdj = Array.filterMap(model.contractiles, c => { - switch c.enforcement { - | Adaptive if c.elasticity > 0.1 => Some(DecreaseElasticity(c.id)) - | _ => None - } - }) - Array.concat(adjustments, tightenAdj) - } else { - adjustments - } - - // --- OrbitalSync divergence → governance --- - - let adjustments = if model.orbital.divergenceLevel > divergenceHighThreshold { - // L↔N divergence is high — emit a sync event and consider halting. - let syncAdj = [EmitSyncEvent(CrossPaneLink("governance:divergence-high", "antiCrash"))] - let haltAdj = if model.orbital.stability < stabilityDangerThreshold { - [ - HaltInference( - "Orbital stability critically low — L↔N divergence exceeds safe threshold", - ), - ] - } else { - [] - } - Array.concat(adjustments, Array.concat(syncAdj, haltAdj)) - } else { - adjustments - } - - // --- Inertia detection → inference resumption --- - - let adjustments = if model.vexometer.inertiaDetected && !model.paneN.inferenceActive { - // System has been idle too long — suggest resumption if conditions are met. - if ( - model.orbital.stability > stabilityHealthyThreshold && violationCount(model.antiCrash) === 0 - ) { - Array.concat(adjustments, [ResumeInference]) - } else { - adjustments - } - } else { - adjustments - } - - // --- S5: Hypatia confidence → Anti-Crash strictness --- - // When Hypatia neural networks have low average confidence, tighten - // the Anti-Crash gate. When confidence is high, allow looser validation. - - let adjustments = if Array.length(model.hypatia.networks) > 0 { - let activeNets = model.hypatia.networks->Array.filter(n => - switch n.status { - | NetActive => true - | _ => false - } - ) - let avgConf = if Array.length(activeNets) > 0 { - activeNets->Array.reduce(0.0, (acc, n) => acc +. n.confidence) /. - Int.toFloat(Array.length(activeNets)) - } else { - 0.0 - } - if avgConf < 0.5 && !model.antiCrash.strictMode { - // Low neural confidence → tighten validation. - Array.concat(adjustments, [TightenAntiCrash]) - } else if avgConf > 0.8 && model.antiCrash.strictMode && violationCount(model.antiCrash) < 2 { - // High neural confidence + few violations → can loosen. - Array.concat(adjustments, [LoosenAntiCrash]) - } else { - adjustments - } - } else { - adjustments - } - - // --- Humidity adjustment --- - - let targetHumidity = computeHumidity(model.vexometer, model.orbital) - let adjustments = if targetHumidity !== model.humidity { - Array.concat(adjustments, [AdjustHumidity(targetHumidity)]) - } else { - adjustments - } - - adjustments -} - -// --------------------------------------------------------------------------- -// Apply adjustments to model -// --------------------------------------------------------------------------- - -/// Apply a single governance adjustment to the model. Pure. -let applyAdjustment = (model: model, adj: governanceAdjustment): model => { - switch adj { - | TightenAntiCrash => { - ...model, - antiCrash: {...model.antiCrash, strictMode: true}, - } - - | LoosenAntiCrash => { - ...model, - antiCrash: {...model.antiCrash, strictMode: false}, - } - - | IncreaseElasticity(contractId) => { - let newContractiles = Array.map(model.contractiles, c => { - if c.id === contractId { - {...c, elasticity: Math.min(1.0, c.elasticity +. elasticityIncrement)} - } else { - c - } - }) - {...model, contractiles: newContractiles} - } - - | DecreaseElasticity(contractId) => { - let newContractiles = Array.map(model.contractiles, c => { - if c.id === contractId { - {...c, elasticity: Math.max(0.0, c.elasticity -. elasticityDecrement)} - } else { - c - } - }) - {...model, contractiles: newContractiles} - } - - | HaltInference(reason) => { - let violation = BoundaryViolation(reason) - { - ...model, - paneN: {...model.paneN, inferenceActive: false}, - antiCrash: { - ...model.antiCrash, - halted: true, - violations: Array.concat(model.antiCrash.violations, [violation]), - }, - } - } - - | ResumeInference => { - ...model, - paneN: {...model.paneN, inferenceActive: true}, - antiCrash: {...model.antiCrash, halted: false}, - } - - | AdjustHumidity(level) => { - ...model, - humidity: level, - } - - | EmitSyncEvent(event) => { - let newSync = { - ...model.syncState, - pendingSync: Array.concat(model.syncState.pendingSync, [event]), - } - {...model, syncState: newSync} - } - } -} - -// --------------------------------------------------------------------------- -// Nesy-MCP governance queries — deferred decisions that need async validation -// --------------------------------------------------------------------------- - -/// A governance query represents a decision that cannot be made purely — -/// it requires consultation with the BoJ nesy-mcp cartridge for real-time -/// neural validation before the adjustment can be applied. -type governanceQuery = - /// Ask nesy-mcp for a confidence score on a borderline governance decision. - | NesyConfidenceQuery(string) - /// Request nesy-mcp to validate a governance adjustment before applying it. - | NesyValidateAdjustment(governanceAdjustment) - /// Probe nesy-mcp for overall stability metrics from the neural subsystem. - | NesyStabilityProbe - -// --------------------------------------------------------------------------- -// Nesy-aware governance evaluation -// --------------------------------------------------------------------------- - -/// Evaluate governance subsystems, returning both immediate adjustments and -/// deferred queries that require nesy-mcp validation. -/// -/// The "uncertain zone" for vexation is [0.3, 0.7] and for orbital stability -/// is [0.4, 0.7]. When both fall in these ranges simultaneously, the engine -/// cannot confidently decide — it emits a NesyConfidenceQuery instead. -/// -/// Any HaltInference decision is always preceded by a NesyValidateAdjustment -/// query so the neural subsystem can double-check the halt rationale. -/// -/// Pure function — returns tuples, does not mutate state. -let evaluateWithCmd = (model: model): (array, array) => { - let adjustments: array = [] - let queries: array = [] - - // --- Uncertain-zone detection --- - // When vexation AND orbital stability are both in their borderline ranges, - // defer the Anti-Crash decision to nesy-mcp instead of guessing. - - let vexInUncertainZone = - model.vexometer.index >= vexationLowThreshold && model.vexometer.index <= vexationHighThreshold - - let stabilityInBorderline = - model.orbital.stability >= stabilityDangerThreshold && - model.orbital.stability <= stabilityHealthyThreshold - - // --- Anti-Crash ↔ Vexometer feedback (with nesy deferral) --- - - let (adjustments, queries) = if vexInUncertainZone && stabilityInBorderline { - // Both metrics are borderline — defer to nesy-mcp. - let q = NesyConfidenceQuery( - `vexation=${Float.toString(model.vexometer.index)},` ++ - `stability=${Float.toString(model.orbital.stability)},` ++ - `violations=${Int.toString(violationCount(model.antiCrash))}`, - ) - (adjustments, Array.concat(queries, [q])) - } else if shouldLoosenForVexation(model.vexometer, model.antiCrash) { - (Array.concat(adjustments, [LoosenAntiCrash]), queries) - } else if shouldTightenForStability(model.orbital, model.antiCrash, model.vexometer) { - (Array.concat(adjustments, [TightenAntiCrash]), queries) - } else { - (adjustments, queries) - } - - // --- Vexometer → Contractile elasticity --- - - let adjustments = if model.vexometer.index > vexationHighThreshold { - let elasticAdj = Array.filterMap(model.contractiles, c => { - switch c.enforcement { - | Adaptive if c.elasticity < 0.9 => Some(IncreaseElasticity(c.id)) - | _ => None - } - }) - Array.concat(adjustments, elasticAdj) - } else if model.vexometer.index < vexationLowThreshold { - let tightenAdj = Array.filterMap(model.contractiles, c => { - switch c.enforcement { - | Adaptive if c.elasticity > 0.1 => Some(DecreaseElasticity(c.id)) - | _ => None - } - }) - Array.concat(adjustments, tightenAdj) - } else { - adjustments - } - - // --- OrbitalSync divergence → governance (with nesy validation on halt) --- - - let (adjustments, queries) = if model.orbital.divergenceLevel > divergenceHighThreshold { - let syncAdj = [EmitSyncEvent(CrossPaneLink("governance:divergence-high", "antiCrash"))] - let (haltAdj, haltQueries) = if model.orbital.stability < stabilityDangerThreshold { - let haltReason = "Orbital stability critically low — L↔N divergence exceeds safe threshold" - // Always validate a HaltInference through nesy-mcp first. - let halt = HaltInference(haltReason) - ([halt], [NesyValidateAdjustment(halt)]) - } else { - ([], []) - } - (Array.concat(adjustments, Array.concat(syncAdj, haltAdj)), Array.concat(queries, haltQueries)) - } else { - (adjustments, queries) - } - - // --- Inertia detection → inference resumption --- - - let adjustments = if model.vexometer.inertiaDetected && !model.paneN.inferenceActive { - if ( - model.orbital.stability > stabilityHealthyThreshold && violationCount(model.antiCrash) === 0 - ) { - Array.concat(adjustments, [ResumeInference]) - } else { - adjustments - } - } else { - adjustments - } - - // --- S5: Hypatia confidence → Anti-Crash strictness --- - - let adjustments = if Array.length(model.hypatia.networks) > 0 { - let activeNets = model.hypatia.networks->Array.filter(n => - switch n.status { - | NetActive => true - | _ => false - } - ) - let avgConf = if Array.length(activeNets) > 0 { - activeNets->Array.reduce(0.0, (acc, n) => acc +. n.confidence) /. - Int.toFloat(Array.length(activeNets)) - } else { - 0.0 - } - if avgConf < 0.5 && !model.antiCrash.strictMode { - Array.concat(adjustments, [TightenAntiCrash]) - } else if avgConf > 0.8 && model.antiCrash.strictMode && violationCount(model.antiCrash) < 2 { - Array.concat(adjustments, [LoosenAntiCrash]) - } else { - adjustments - } - } else { - adjustments - } - - // --- Humidity adjustment --- - - let targetHumidity = computeHumidity(model.vexometer, model.orbital) - let adjustments = if targetHumidity !== model.humidity { - Array.concat(adjustments, [AdjustHumidity(targetHumidity)]) - } else { - adjustments - } - - // --- Stability probe: emit when any query was generated --- - - let queries = if Array.length(queries) > 0 { - Array.concat(queries, [NesyStabilityProbe]) - } else { - queries - } - - (adjustments, queries) -} - -/// Apply all governance adjustments to the model. Pure fold. -let applyAll = (model: model, adjustments: array): model => { - Array.reduce(adjustments, model, applyAdjustment) -} - -/// Full governance pass: evaluate + apply. Called from the main update loop. -let govern = (model: model): model => { - let adjustments = evaluate(model) - applyAll(model, adjustments) -} - -/// Nesy-aware governance pass: evaluate, apply immediate adjustments, and -/// dispatch async nesy-mcp queries through GovernanceCmd. Returns updated -/// model plus any Tea commands for nesy queries. -let governWithCmd = (model: model, nesyTagger: result => 'msg): ( - model, - Tea_Cmd.t<'msg>, -) => { - let (adjustments, queries) = evaluateWithCmd(model) - let newModel = applyAll(model, adjustments) - - // Convert governance queries into Tea commands via GovernanceCmd. - let cmds = queries->Array.map(query => { - switch query { - | NesyConfidenceQuery(q) => GovernanceCmd.queryNesyConfidence(q, nesyTagger) - | NesyValidateAdjustment(_adj) => { - // Serialize the adjustment type as a string descriptor for nesy-mcp. - let adjStr = switch _adj { - | TightenAntiCrash => "TightenAntiCrash" - | LoosenAntiCrash => "LoosenAntiCrash" - | HaltInference(reason) => "HaltInference:" ++ reason - | ResumeInference => "ResumeInference" - | IncreaseElasticity(id) => "IncreaseElasticity:" ++ id - | DecreaseElasticity(id) => "DecreaseElasticity:" ++ id - | AdjustHumidity(_) => "AdjustHumidity" - | EmitSyncEvent(_) => "EmitSyncEvent" - } - GovernanceCmd.validateAdjustment(adjStr, nesyTagger) - } - | NesyStabilityProbe => GovernanceCmd.probeStability(nesyTagger) - } - }) - - let cmd = if Array.length(cmds) > 0 { - Tea_Cmd.batch(cmds->List.fromArray) - } else { - Tea_Cmd.none - } - - (newModel, cmd) -} diff --git a/src/core/GuardAiTunerEngine.affine b/src/core/GuardAiTunerEngine.affine new file mode 100644 index 00000000..f625ff2a --- /dev/null +++ b/src/core/GuardAiTunerEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module GuardAiTunerEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/GuardAiTunerEngine.res b/src/core/GuardAiTunerEngine.res deleted file mode 100644 index f6fd1004..00000000 --- a/src/core/GuardAiTunerEngine.res +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Guard AI Tuner Engine — pure computation and helpers for tuning -/// guard patrol behaviour, alert thresholds, and spawn rates. -/// -/// Provides default state, tab labels, and utility functions for averaging -/// spawn rates and alert thresholds, counting patrol points, and formatting -/// patrol pattern names. - -open GuardAiTunerModel - -/// Default state for the Guard AI Tuner panel. -let defaultState: guardAiTunerState = { - activeTab: Profiles, - guards: [], - routes: [], - presets: [], - selectedGuard: None, - editing: false, - error: None, -} - -/// Human-readable label for a guard AI tuner category tab. -let tabLabel = (cat: guardAiTunerCategory): string => - switch cat { - | Profiles => "Profiles" - | PatrolEditor => "Patrol Editor" - | Thresholds => "Thresholds" - | Presets => "Presets" - } - -/// All category tabs in display order. -let allTabs: array = [Profiles, PatrolEditor, Thresholds, Presets] - -/// Average spawn rate across all guard profiles. Returns 0.0 if no guards. -let avgSpawnRate = (guards: array): float => { - let len = guards->Array.length - if len === 0 { - 0.0 - } else { - guards->Array.reduce(0.0, (acc, g) => acc +. g.spawnRate) /. Int.toFloat(len) - } -} - -/// Average alert threshold across all guard profiles. Returns 0.0 if no guards. -let avgAlertThreshold = (guards: array): float => { - let len = guards->Array.length - if len === 0 { - 0.0 - } else { - guards->Array.reduce(0.0, (acc, g) => acc +. g.alertThreshold) /. Int.toFloat(len) - } -} - -/// Count total patrol points across all routes. -let countPatrolPoints = (routes: array): int => - routes->Array.reduce(0, (acc, r) => acc + r.points->Array.length) - -/// Human-readable label for a patrol pattern string. -let formatPatrolPattern = (pattern: string): string => - switch pattern { - | "loop" => "Loop" - | "pingpong" => "Ping-Pong" - | "random" => "Random" - | "stationary" => "Stationary" - | other => other - } diff --git a/src/core/HelpContent.affine b/src/core/HelpContent.affine new file mode 100644 index 00000000..44d0a6a2 --- /dev/null +++ b/src/core/HelpContent.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module HelpContent; + +// TODO: Complete semantic implementation diff --git a/src/core/HelpContent.res b/src/core/HelpContent.res deleted file mode 100644 index 53407a79..00000000 --- a/src/core/HelpContent.res +++ /dev/null @@ -1,495 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// HelpContent — Static help content data for the PanLL help system. -/// -/// This module provides all the help entries and glossary terms that -/// populate the help panel. Content is organised by category and -/// optionally linked to specific panels via panelId references. -/// -/// The module is pure data — no logic, no side effects. Functions -/// return fresh arrays of content records on each call, ensuring -/// immutability and preventing accidental mutation of shared state. -/// -/// Content covers five categories: -/// - GettingStarted: orientation and first steps -/// - PanelGuide: one entry per major panel/tool -/// - Shortcuts: keyboard shortcut reference -/// - FAQ: common questions and troubleshooting -/// - Architecture: system design and neurosymbolic concepts -/// -/// The glossary provides accessible definitions for all neurosymbolic -/// terminology used throughout PanLL, making the interface learnable -/// even for users unfamiliar with formal methods or neural-symbolic AI. - -/// Returns all help entries for the PanLL help system. -/// -/// Each entry has a unique string ID, a human-readable title, a body -/// of explanatory text, a category for filtering, an optional panelId -/// for contextual scoping, and an array of keyword strings for search. -/// -/// Entries are returned in a logical reading order within each category: -/// GettingStarted first, then PanelGuide, Shortcuts, FAQ, Architecture. -/// -/// @returns The complete array of help entries -let allEntries = (): array => { - [ - // ========================================================================= - // Getting Started - // ========================================================================= - - /// Welcome entry — the first thing a new user should read. - { - id: "welcome", - title: "Welcome to PanLL", - body: "PanLL (formally eNSAID: Environment for NeSy-Agentic Integrated Development) is a neurosymbolic mission control interface. It combines symbolic reasoning (formal proofs, type systems, constraints) with neural computation (language models, learned heuristics) in a unified four-panel workspace (Panel-A ambient substrate, Panel-L logic/symbolic, Panel-N neural, Panel-W world/barycentre). PanLL is designed for accessibility-first interaction, putting the operator in control of both symbolic and neural subsystems at all times.", - category: GettingStarted, - panelId: None, - keywords: [ - "welcome", - "introduction", - "ensaid", - "neurosymbolic", - "mission control", - "getting started", - ], - }, - /// Explains the fundamental four-panel layout. - { - id: "four-panel-layout", - title: "Understanding the Four-Panel Layout", - body: "PanLL organises work into four panels. Panel-A (Ambient) provides persistent context and ergonomic substrate around the workspace. Panel-L (Logic) displays constraints, formal structure, and symbolic state — it is where you define what must be true. Panel-N (Neural) hosts agent reasoning, OODA loops, and decision-making processes — it is where thinking happens. Panel-W (World/Barycentre) shows results, output, and verification status — it is where you see outcomes. The L + N orbiting W relationship is the Binary Star core; Panel-A surrounds as ambient substrate. Position is design intuition rather than architectural — the relationship is by flow and viewing perspective.", - category: GettingStarted, - panelId: None, - keywords: ["panels", "layout", "four-panel", "three-panel", "panel-a", "panel-l", "panel-n", "panel-w", "ambient", "binary star", "workspace"], - }, - /// How to use the panel switcher to navigate between tools. - { - id: "panel-switcher", - title: "Navigating with the Panel Switcher", - body: "The panel switcher is the primary navigation mechanism in PanLL. It appears as a bar or sidebar listing all available panels grouped by clade (taxonomic function). Click or use keyboard shortcuts to switch the active panel. Each panel represents a different tool or view — from theorem proving to database simulation to security scanning. The switcher also shows panel status indicators so you can see at a glance which panels need attention.", - category: GettingStarted, - panelId: None, - keywords: ["navigation", "switcher", "panel switcher", "clade", "tabs"], - }, - /// The Dark Start splash screen and how to transition to standard mode. - { - id: "dark-start", - title: "Dark Start and Standard Mode", - body: "When PanLL launches, it shows the Dark Start screen — a brief animated splash displaying the Binary Star architecture. This is not just decorative: it runs startup diagnostics and establishes connections to backend services (ECHIDNA, TypeLL, VeriSimDB). Once initialisation completes, PanLL transitions to Standard Mode where all panels become interactive. If Dark Start stalls, check your backend service connections.", - category: GettingStarted, - panelId: None, - keywords: ["dark start", "splash", "startup", "boot", "initialisation", "standard mode"], - }, - // ========================================================================= - // Panel Guides - // ========================================================================= - - /// Guide for the ECHIDNA theorem prover panel. - { - id: "panel-echidna", - title: "ECHIDNA — Theorem Prover", - body: "The ECHIDNA panel connects to the ECHIDNA theorem prover backend for formal verification. Use it to submit proof obligations, inspect proof states, and track verification progress. ECHIDNA handles dependent types, refinement types, and constructive proofs. The panel shows proof trees, goal states, and tactic suggestions. Proof results feed into the provenance system, raising the trust level of verified code to 'Verified'.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelDatabases), - keywords: [ - "echidna", - "theorem prover", - "formal verification", - "proofs", - "dependent types", - "tactics", - ], - }, - /// Guide for the TypeLL verification kernel panel. - { - id: "panel-typell", - title: "TypeLL — Verification Kernel", - body: "TypeLL is PanLL's type checking and inference engine. It operates as a verification kernel that checks code against type-level specifications. The TypeLL panel shows type errors, inferred types, and refinement constraints in real time. It integrates with ECHIDNA for proofs that go beyond what the type system alone can verify. Use TypeLL to ensure your code satisfies its contracts before committing.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelTypeLL), - keywords: [ - "typell", - "type checking", - "type inference", - "verification", - "kernel", - "contracts", - ], - }, - /// Guide for the VeriSimDB simulation database panel. - { - id: "panel-verisim", - title: "VeriSimDB — Simulation Database", - body: "VeriSimDB is a verification-aware simulation database. It stores simulation runs, test results, and verification outcomes with full provenance tracking. The VeriSimDB panel lets you query past simulations, compare runs, and inspect how changes affected verification status over time. It serves as the persistent memory of your verification workflow, ensuring no proof or test result is ever lost.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelDatabases), - keywords: ["verisim", "database", "simulation", "test results", "provenance", "history"], - }, - /// Guide for the Farm panel (repo management). - { - id: "panel-farm", - title: "Farm — Repository Management", - body: "The Farm panel manages your repository fleet. It shows the status of all repos registered in the git-private-farm manifest, including mirror sync status (GitHub, GitLab, Bitbucket), CI/CD pipeline results, and RSR compliance scores. Use Farm to bulk-manage repos, trigger mirror syncs, and identify repos that need attention. The Farm integrates with gitbot-fleet for automated maintenance.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelFarm), - keywords: ["farm", "repositories", "git", "mirrors", "gitlab", "bitbucket", "rsr"], - }, - /// Guide for the Fleet panel (bot orchestration). - { - id: "panel-fleet", - title: "Fleet — Bot Orchestration", - body: "The Fleet panel provides visibility into the gitbot-fleet — the collection of automated bots that maintain your repositories. Bots include rhodibot (RSR compliance), echidnabot (formal verification flagging), sustainabot (dependency updates), glambot (documentation), seambot (integration testing), and finishbot (completion tracking). The Fleet panel shows bot activity, queued tasks, and confidence thresholds for automated fixes.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelFleet), - keywords: ["fleet", "bots", "gitbot", "rhodibot", "echidnabot", "sustainabot", "automation"], - }, - /// Guide for the Hypatia security scanning panel. - { - id: "panel-hypatia", - title: "Hypatia — Security Intelligence", - body: "Hypatia is the neurosymbolic security scanning system. The Hypatia panel shows scan results, vulnerability findings, and security posture across your repositories. Hypatia goes beyond traditional SAST/DAST by combining symbolic analysis (formal reasoning about code properties) with neural pattern recognition (learned vulnerability signatures). Findings are ranked by severity and include remediation suggestions.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelHypatia), - keywords: [ - "hypatia", - "security", - "scanning", - "vulnerabilities", - "sast", - "neurosymbolic security", - ], - }, - /// Guide for the Aerie network analysis panel. - { - id: "panel-aerie", - title: "Aerie — Network Analysis", - body: "The Aerie panel provides network analysis and simulation capabilities. It visualises network topologies, analyses BGP routing, and monitors IPv6 connectivity. Aerie integrates with the proven cryptographic library for protocol verification. Use Aerie to simulate network changes before deploying them, verify routing policies, and monitor the health of distributed systems.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelAerie), - keywords: ["aerie", "network", "bgp", "ipv6", "routing", "topology", "simulation"], - }, - /// Guide for the Playgrounds panel (experimental code execution). - { - id: "panel-playgrounds", - title: "Playgrounds — Experimental Execution", - body: "The Playgrounds panel provides sandboxed environments for experimental code execution. Run ReScript, Rust, Gleam, or Elixir snippets in isolated containers with full type checking and verification. Playground results can be saved to VeriSimDB for later reference. Use Playgrounds to prototype ideas, test hypotheses, and explore APIs without affecting your main codebase.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelPlaygrounds), - keywords: ["playgrounds", "sandbox", "experimental", "repl", "execution", "prototype"], - }, - /// Guide for the Security panel (trust and provenance). - { - id: "panel-security", - title: "Security — Trust and Provenance", - body: "The Security panel displays the provenance and trust surface of your codebase. It shows who wrote each piece of code, how it was reviewed, and what trust level it carries (Verified, Human Reviewed, AI Assisted, Unreviewed AI, Unknown). The Hostile UX system applies deliberate friction to unreviewed AI code, demanding explicit human review before it can be deployed. The Security panel also shows contractile enforcement status.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelSecurity), - keywords: ["security", "provenance", "trust", "hostile ux", "code review", "contractiles"], - }, - /// Guide for the Workspace panel (project overview). - { - id: "panel-workspace", - title: "Workspace — Project Overview", - body: "The Workspace panel gives you a high-level view of the current project state. It shows task progress, milestone tracking, blocker status, and the overall completion dashboard. The workspace integrates data from all other panels — verification status from ECHIDNA, test results from VeriSimDB, security findings from Hypatia — into a unified project health summary. The Task Barycentre indicator shows where the current work sits on the symbolic-neural spectrum.", - category: PanelGuide, - panelId: Some(PanelSwitcherModel.PanelWorkspace), - keywords: [ - "workspace", - "project", - "overview", - "tasks", - "milestones", - "dashboard", - "barycentre", - ], - }, - // ========================================================================= - // Shortcuts - // ========================================================================= - - /// Comprehensive keyboard shortcut reference. - { - id: "keyboard-shortcuts", - title: "Keyboard Shortcuts Reference", - body: "PanLL supports extensive keyboard navigation. Global shortcuts: Ctrl+/ (toggle help), Ctrl+K (command palette), Escape (close active overlay). Panel navigation: Ctrl+1 through Ctrl+9 (switch to panel by position), Ctrl+[ and Ctrl+] (previous/next panel). Within panels: Tab (cycle focus), Enter (activate), Arrow keys (navigate lists). The panel switcher responds to type-ahead filtering — just start typing a panel name. All shortcuts are customisable in Settings.", - category: Shortcuts, - panelId: None, - keywords: [ - "keyboard", - "shortcuts", - "hotkeys", - "keybindings", - "accessibility", - "navigation", - "ctrl", - ], - }, - // ========================================================================= - // FAQ - // ========================================================================= - - /// Troubleshooting ECHIDNA connection issues. - { - id: "faq-echidna-connection", - title: "Why can't I connect to ECHIDNA?", - body: "ECHIDNA runs as a separate backend service. If the connection fails: (1) Check that the ECHIDNA service is running — it should be accessible on its configured port. (2) Verify the connection URL in PanLL settings matches your ECHIDNA deployment. (3) Check your network/firewall rules. (4) Look at the Dark Start diagnostic output for connection errors. (5) If using a containerised ECHIDNA, ensure the container is healthy with `podman ps`. The ECHIDNA panel status indicator will show red when disconnected.", - category: Faq, - panelId: Some(PanelSwitcherModel.PanelDatabases), - keywords: ["echidna", "connection", "error", "troubleshooting", "backend", "service"], - }, - /// Explains the provenance bar and trust levels. - { - id: "faq-provenance-bar", - title: "What does the Provenance bar show?", - body: "The Provenance bar is a horizontal stacked bar chart showing the trust composition of your codebase. Each segment represents a trust level: green for Verified (formally proven), blue for Human Reviewed, yellow for AI Assisted (human-reviewed AI code), orange for Unreviewed AI (needs review), and grey for Unknown. The bar updates in real time as code is verified, reviewed, or added. Click any segment to drill down into the specific files at that trust level. The goal is to minimise orange and grey segments.", - category: Faq, - panelId: Some(PanelSwitcherModel.PanelSecurity), - keywords: [ - "provenance", - "bar", - "trust level", - "verified", - "human reviewed", - "ai assisted", - "unreviewed", - ], - }, - /// How to change the PanLL colour palette. - { - id: "faq-colour-palettes", - title: "How do I switch colour palettes?", - body: "PanLL supports multiple colour palettes for accessibility and preference. Open Settings (gear icon or Ctrl+,) and navigate to the Appearance section. Available palettes include the default neurosymbolic theme (dark with drift aura), a high-contrast mode for maximum readability, and several colour-blind-safe palettes (deuteranopia, protanopia, tritanopia). Each palette adjusts the drift aura colours, panel borders, and syntax highlighting to remain distinguishable. Changes apply immediately — no restart needed.", - category: Faq, - panelId: None, - keywords: [ - "colour", - "color", - "palette", - "theme", - "accessibility", - "appearance", - "settings", - "contrast", - ], - }, - // ========================================================================= - // Architecture - // ========================================================================= - - /// The Binary Star architectural model. - { - id: "arch-binary-star", - title: "Binary Star Architecture", - body: "PanLL's core architecture models the relationship between symbolic and neural computation as a binary star system. Neither subsystem is primary — they co-orbit a shared barycentre, each influencing the other through gravitational coupling. The symbolic star provides formal guarantees (proofs, types, contracts). The neural star provides learned heuristics (pattern recognition, natural language understanding, probabilistic reasoning). Orbital stability measures how well these subsystems stay in sync. When they drift apart, the Drift Aura shifts colour to alert the operator.", - category: Architecture, - panelId: None, - keywords: [ - "binary star", - "architecture", - "symbolic", - "neural", - "co-orbital", - "barycentre", - "drift", - ], - }, - /// How the TEA update loop drives the PanLL UI. - { - id: "arch-tea-loop", - title: "TEA Update Loop", - body: "PanLL's UI is driven by The Elm Architecture (TEA): a unidirectional data flow where the entire application state is a single immutable value. Every user action produces a message (Msg), which is processed by an update function to produce a new state and optional side-effect commands (Cmd). The view function renders the state to virtual DOM, and the runtime diffs and patches the real DOM. This architecture guarantees predictable state transitions, makes time-travel debugging possible, and eliminates an entire class of UI bugs related to shared mutable state.", - category: Architecture, - panelId: None, - keywords: [ - "tea", - "elm architecture", - "update loop", - "unidirectional", - "state", - "msg", - "cmd", - "view", - ], - }, - /// How symbolic and neural systems integrate in PanLL. - { - id: "arch-neurosymbolic", - title: "Neurosymbolic Integration", - body: "PanLL integrates symbolic and neural computation at multiple levels. At the data level, VeriSimDB stores both formal proofs and neural predictions with unified provenance. At the reasoning level, ECHIDNA proofs can constrain neural outputs, and neural heuristics can guide proof search. At the UI level, the three-panel layout makes the symbolic-neural interaction visible and controllable. The Task Barycentre shows where each task sits on the spectrum. Contractiles enforce boundaries — for example, requiring that neural suggestions pass type checking before being presented to the operator.", - category: Architecture, - panelId: None, - keywords: [ - "neurosymbolic", - "integration", - "symbolic", - "neural", - "barycentre", - "contractiles", - "proofs", - ], - }, - ] -} - -/// Returns the complete glossary of neurosymbolic terms used in PanLL. -/// -/// Each glossary term has a human-readable name and an accessible -/// definition written for users who may not have a background in -/// formal methods or neural-symbolic AI. Definitions explain both -/// what the term means and how it manifests in the PanLL interface. -/// -/// Terms are returned in a logical grouping order: core architectural -/// concepts first, then UI concepts, then specific subsystem names. -/// -/// @returns The complete array of glossary terms -let allGlossaryTerms = (): array => { - [ - /// The centre of gravity of a task on the symbolic-neural spectrum. - { - term: "Task Barycentre", - definition: "The centre of gravity of a task on the symbolic-neural spectrum. A task with high symbolic mass (many proofs, strict types) has its barycentre closer to the symbolic side. A task relying heavily on neural heuristics (ML inference, pattern matching) shifts toward the neural side. PanLL displays the barycentre as a position indicator, helping operators understand the nature of their current work and allocate attention accordingly.", - relatedTerms: [], - extendedDescription: None, - }, - /// The weight of formal structure in a computation. - { - term: "Symbolic Mass", - definition: "The weight or density of formal structure in a computation — proofs, type constraints, contractiles, invariants, and specifications. High symbolic mass means the task is heavily constrained by formal reasoning. In PanLL, symbolic mass contributes to the Task Barycentre calculation and influences how much formal verification overhead a task carries. Increasing symbolic mass generally increases trust but also increases the cost of change.", - relatedTerms: [], - extendedDescription: None, - }, - /// The flow of statistical and learned computation. - { - term: "Neural Stream", - definition: "The flow of statistical, learned, and probabilistic computation through the system. Neural streams carry predictions, generated text, pattern matches, and heuristic suggestions. In PanLL, neural streams are always visible and auditable — they flow through Panel-N where the operator can inspect, redirect, or halt them. Neural stream output that has not been formally verified carries a lower trust level.", - relatedTerms: [], - extendedDescription: None, - }, - /// PanLL's co-orbital architecture model. - { - term: "Binary Star", - definition: "PanLL's co-orbital architecture model where the symbolic subsystem and neural subsystem orbit each other like a gravitationally bound binary star. Neither is primary — they exert mutual influence. The symbolic star provides rigour and guarantees; the neural star provides flexibility and learning. The system's health depends on orbital stability: if the two drift too far apart, reasoning becomes unreliable. The Drift Aura visualises this relationship.", - relatedTerms: [], - extendedDescription: None, - }, - /// The agent reasoning cycle. - { - term: "OODA Loop", - definition: "Observe-Orient-Decide-Act: a decision cycle used for agent reasoning in PanLL. Agents observe the current state (panel data, verification results), orient by analysing context (symbolic constraints, neural predictions), decide on an action (suggest a fix, request a proof), and act (execute the decision). PanLL makes OODA loops visible in Panel-N so operators can inspect agent reasoning at each stage and intervene if needed.", - relatedTerms: [], - extendedDescription: None, - }, - /// An elastic constraint with enforcement levels. - { - term: "Contractile", - definition: "An elastic constraint that can be enforced at different levels: Strict (hard failure on violation), Adaptive (warning with escalation), or Warn (informational only). Contractiles define boundaries for code quality, security, and correctness. For example, a contractile might require that all public functions have type annotations (Strict) or that neural suggestions are reviewed within 24 hours (Adaptive). Contractiles are defined in Trustfile.a2ml and enforced by the security system.", - relatedTerms: [], - extendedDescription: None, - }, - /// A measure of operator frustration. - { - term: "Vexation Index", - definition: "A measure of operator frustration and friction in the interface. PanLL tracks interaction patterns (rapid undo sequences, repeated failed commands, dismissed suggestions) to estimate vexation. High vexation triggers adaptive UI responses: simplifying the interface, offering guided help, or reducing notification frequency. The vexation index is part of PanLL's accessibility-first design philosophy — the tool should reduce cognitive load, not add to it.", - relatedTerms: [], - extendedDescription: None, - }, - /// How well the symbolic and neural subsystems stay in sync. - { - term: "Orbital Stability", - definition: "A measure of how well the symbolic and neural subsystems remain in synchronisation. High stability means proofs align with neural predictions, types match inferred behaviour, and formal specifications agree with learned models. Low stability indicates drift — the two subsystems are producing contradictory results. PanLL computes orbital stability continuously and displays it via the Drift Aura. Operators should investigate when stability drops below threshold.", - relatedTerms: [], - extendedDescription: None, - }, - /// The visual indicator of orbital stability. - { - term: "Drift Aura", - definition: "A visual indicator of orbital stability rendered as a subtle background colour shift across the PanLL interface. When symbolic and neural subsystems are well-synchronised, the aura is calm and neutral. As drift increases, the aura shifts colour — typically toward warmer tones — alerting the operator that the Binary Star system needs attention. The aura's opacity is affected by the Humidity parameter. It is designed to be noticeable without being distracting.", - relatedTerms: [], - extendedDescription: None, - }, - /// Deliberate friction for unreviewed AI code. - { - term: "Hostile UX", - definition: "A deliberate design pattern where PanLL applies friction to unreviewed AI-generated code. Rather than letting AI output flow smoothly into the codebase, Hostile UX demands explicit human review by making unreviewed code visually distinct (orange provenance highlighting), requiring additional confirmation steps, and blocking automated deployment. This is not a bug — it is a safety feature that ensures human oversight of AI contributions.", - relatedTerms: [], - extendedDescription: None, - }, - /// The code trust surface. - { - term: "Provenance", - definition: "The trust surface of code showing who wrote it, how it was generated, and how much review it has received. PanLL tracks provenance at the function level, recording whether code was hand-written by a verified human, generated by AI with human review, generated by AI without review, or of unknown origin. Provenance data feeds into the Security panel's trust bar and the Hostile UX system. Higher provenance means higher deployment confidence.", - relatedTerms: [], - extendedDescription: None, - }, - /// Classification of code authorship. - { - term: "Trust Level", - definition: "A classification of code authorship and review status. Five levels exist: Verified (formally proven correct), Human Reviewed (read and approved by a human), AI Assisted (AI-generated but human-reviewed), Unreviewed AI (AI-generated, not yet reviewed — triggers Hostile UX), and Unknown (no provenance data available). Trust levels are displayed in the provenance bar and determine what automated actions are permitted on the code.", - relatedTerms: [], - extendedDescription: None, - }, - /// The cognitive governance system. - { - term: "Anti-Crash", - definition: "A cognitive governance system built into PanLL that halts operations when safety violations are detected. Anti-Crash monitors for conditions like unchecked believe_me in proofs, deployment of unreviewed AI code, contractile violations above threshold, and orbital stability collapse. When triggered, Anti-Crash pauses the pipeline and requires explicit operator acknowledgement before proceeding. It is the last line of defence against automated systems running off the rails.", - relatedTerms: [], - extendedDescription: None, - }, - /// PanLL's initial splash screen. - { - term: "Dark Start", - definition: "PanLL's initial splash screen displayed during startup. Dark Start shows an animated visualisation of the Binary Star architecture while running backend diagnostics — checking connections to ECHIDNA, TypeLL, VeriSimDB, and other services. It provides visual feedback on initialisation progress. Once all systems are online, Dark Start fades into Standard Mode. A stalled Dark Start usually indicates a backend connectivity issue.", - relatedTerms: [], - extendedDescription: None, - }, - /// Environmental parameter affecting drift aura. - { - term: "Humidity", - definition: "An environmental parameter that affects the opacity and visual prominence of the Drift Aura. Higher humidity makes the aura more visible; lower humidity makes it more subtle. Operators can adjust humidity in settings to match their preference — some prefer a clearly visible aura for constant awareness, while others prefer a faint one that only becomes noticeable during significant drift events. Humidity has no effect on the underlying orbital stability calculation.", - relatedTerms: [], - extendedDescription: None, - }, - /// Taxonomic grouping of panels by function. - { - term: "Clade", - definition: "A taxonomic grouping of panels by function. PanLL organises its panels into clades: AI (neural reasoning panels), Bridge (integration and communication panels), Builder (development and construction panels), Verifier (formal verification panels), Monitor (observability panels), and others. Clades appear in the panel switcher as visual groupings, making it easier to find the right panel. The term is borrowed from biology, where a clade is a group sharing a common ancestor.", - relatedTerms: [], - extendedDescription: None, - }, - /// The theorem prover backend. - { - term: "ECHIDNA", - definition: "The theorem prover backend used by PanLL for formal verification. ECHIDNA handles dependent types, refinement types, and constructive proofs. It runs as a separate service that PanLL connects to via its protocol. ECHIDNA proof results are stored in VeriSimDB and reflected in the provenance system. The name references the echidna — a creature that, like formal proofs, is spiny, resilient, and hard to argue with.", - relatedTerms: [], - extendedDescription: None, - }, - /// The verification kernel. - { - term: "TypeLL", - definition: "PanLL's verification kernel for type checking and type inference. TypeLL operates as a lightweight, fast type checker that runs continuously as code is edited, providing immediate feedback on type errors and inferred types. For properties that go beyond what the type system can express, TypeLL delegates to ECHIDNA for full theorem proving. TypeLL is the first line of formal verification — fast and always-on.", - relatedTerms: [], - extendedDescription: None, - }, - /// The verification-aware simulation database. - { - term: "VeriSimDB", - definition: "A verification-aware simulation database that stores test results, proof outcomes, simulation runs, and their provenance metadata. VeriSimDB is not just a data store — it understands the verification status of its contents and can answer queries like 'show me all functions whose proofs were invalidated by the last commit'. It serves as the persistent memory layer for the entire PanLL verification pipeline.", - relatedTerms: [], - extendedDescription: None, - }, - /// The combination of neural and symbolic reasoning. - { - term: "Neurosymbolic", - definition: "An approach to artificial intelligence that combines neural networks (statistical learning from data) with symbolic reasoning (logical inference over structured representations). PanLL is a neurosymbolic system because it uses both: neural models for language understanding, code generation, and pattern recognition alongside symbolic systems for theorem proving, type checking, and formal verification. The Binary Star architecture makes this combination explicit and controllable.", - relatedTerms: [], - extendedDescription: None, - }, - /// PanLL's full formal name. - { - term: "eNSAID", - definition: "Environment for NeSy-Agentic Integrated Development — the full formal name for PanLL. 'NeSy' stands for Neural-Symbolic, reflecting the neurosymbolic architecture. 'Agentic' refers to the autonomous agent reasoning capabilities (OODA loops, gitbot-fleet). 'Integrated Development' indicates that PanLL is a complete development environment, not just a viewer or monitor. In practice, everyone just calls it PanLL.", - relatedTerms: [], - extendedDescription: None, - }, - ] -} diff --git a/src/core/HelpEngine.affine b/src/core/HelpEngine.affine new file mode 100644 index 00000000..2839afba --- /dev/null +++ b/src/core/HelpEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module HelpEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/HelpEngine.res b/src/core/HelpEngine.res deleted file mode 100644 index a77497ab..00000000 --- a/src/core/HelpEngine.res +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// HelpEngine — Pure functional engine for the PanLL help system. -/// -/// Contains all logic for searching, filtering, and navigating help content. -/// Operates on data from HelpModel and HelpContent, keeping all state -/// transformations pure and side-effect free. -/// -/// The help system supports: -/// - Full-text search across titles, bodies, and keywords -/// - Category-based filtering (GettingStarted, Glossary, PanelGuide, Shortcuts, Faq, Architecture) -/// - Panel-scoped contextual help -/// - A glossary of neurosymbolic terminology -/// - An 8-step onboarding walkthrough for new users -/// -/// All functions are deterministic — given the same inputs, they always -/// produce the same outputs. No side effects, no mutation. - -/// Returns the standard 8-step onboarding walkthrough for new PanLL users. -/// -/// The walkthrough introduces users to the core concepts of the PanLL -/// interface in a logical progression: from the overall layout, through -/// the three-panel model, the Binary Star architecture, navigation, -/// keyboard shortcuts, the glossary, contextual help, and finally -/// encouragement to explore freely. -let defaultOnboardingSteps = (): array => { - [ - { - id: "welcome", - title: "Welcome to PanLL", - description: "PanLL (eNSAID) is your neurosymbolic mission control. This walkthrough will introduce the key concepts you need to get started.", - targetSelector: None, - completed: false, - }, - { - id: "three-panels", - title: "The Three-Panel Layout", - description: "PanLL uses three panels: Panel-L (left) for constraints and formal structure, Panel-N (centre) for agent reasoning, and Panel-W (right) for results and output. Together they form the neurosymbolic workspace.", - targetSelector: Some(".panll-panels"), - completed: false, - }, - { - id: "binary-star", - title: "Binary Star Architecture", - description: "Symbolic and neural subsystems orbit each other like a binary star system. The Drift Aura — a subtle background colour shift — shows you how stable that orbit is in real time.", - targetSelector: Some(".drift-aura"), - completed: false, - }, - { - id: "panel-switcher", - title: "Panel Switcher", - description: "Use the panel switcher on the right edge to navigate between different tools: ECHIDNA (theorem proving), TypeLL (type checking), VeriSimDB (simulation database), Hypatia (security scanning), and many more. Panels are grouped by category.", - targetSelector: Some(".panel-switcher"), - completed: false, - }, - { - id: "shortcuts", - title: "Keyboard Shortcuts", - description: "Press F1 or ? to open help at any time. Most panel switches have dedicated shortcuts for fast navigation. All shortcuts are customisable in the Workspace panel.", - targetSelector: None, - completed: false, - }, - { - id: "glossary", - title: "The Glossary", - description: "PanLL uses specialised terminology from neurosymbolic computing. Open the Glossary tab in this help panel to look up any unfamiliar term, from 'Task Barycentre' to 'Hostile UX'.", - targetSelector: None, - completed: false, - }, - { - id: "contextual-help", - title: "Contextual Help", - description: "When you open help from within a specific panel, the help system automatically filters to show content relevant to that panel. You can always clear the filter to see everything.", - targetSelector: None, - completed: false, - }, - { - id: "start-exploring", - title: "Start Exploring", - description: "You are ready to go. Switch between panels, run verifications, inspect provenance, and monitor orbital stability. If you get stuck, this help system is always one shortcut away.", - targetSelector: None, - completed: false, - }, - ] -} - -/// Returns the default initial state for the help system. -/// -/// Starts with empty collections for entries and glossary terms, -/// which are populated from HelpContent when the help panel first opens. -/// The onboarding walkthrough is inactive by default and includes the -/// standard 8-step sequence. -let defaultState: HelpModel.helpState = { - searchQuery: "", - filteredEntries: [], - activeCategory: GettingStarted, - activeEntry: None, - glossary: [], - onboarding: { - active: false, - currentStep: 0, - steps: defaultOnboardingSteps(), - completedOnce: false, - }, - contextPanelId: None, -} - -/// Searches help entries by matching a query string against each entry's -/// title, body text, and keywords. Case-insensitive substring matching. -/// An empty query returns all entries unchanged. -let searchEntries = (query: string, entries: array): array< - HelpModel.helpEntry, -> => { - let q = String.toLowerCase(query) - if q === "" { - entries - } else { - entries->Array.filter(entry => { - let titleMatch = entry.title->String.toLowerCase->String.includes(q) - let bodyMatch = entry.body->String.toLowerCase->String.includes(q) - let keywordMatch = - entry.keywords->Array.some(kw => kw->String.toLowerCase->String.includes(q)) - titleMatch || bodyMatch || keywordMatch - }) - } -} - -/// Filters help entries to only those belonging to the specified category. -let filterByCategory = ( - category: HelpModel.helpCategory, - entries: array, -): array => { - entries->Array.filter(entry => entry.category === category) -} - -/// Filters help entries by their associated panel ID. -/// If panelId is None, all entries are returned unfiltered. -let filterByPanel = ( - panelId: option, - entries: array, -): array => { - switch panelId { - | None => entries - | Some(pid) => - entries->Array.filter(entry => { - switch entry.panelId { - | None => false - | Some(entryPid) => entryPid === pid - } - }) - } -} - -/// Finds a single help entry by its unique string identifier. -let findEntry = (id: string, entries: array): option => { - entries->Array.find(entry => entry.id === id) -} - -/// Finds a glossary term by exact case-insensitive match on the term name. -let findGlossaryTerm = (term: string, glossary: array): option< - HelpModel.glossaryTerm, -> => { - let t = String.toLowerCase(term) - glossary->Array.find(g => g.term->String.toLowerCase === t) -} - -/// Searches the glossary by matching a query against both term names -/// and their definitions. Case-insensitive. Empty query returns all terms. -let searchGlossary = (query: string, glossary: array): array< - HelpModel.glossaryTerm, -> => { - let q = String.toLowerCase(query) - if q === "" { - glossary - } else { - glossary->Array.filter(g => { - let termMatch = g.term->String.toLowerCase->String.includes(q) - let defMatch = g.definition->String.toLowerCase->String.includes(q) - termMatch || defMatch - }) - } -} - -/// Converts a help category variant to a human-readable display label. -let categoryLabel = (cat: HelpModel.helpCategory): string => { - switch cat { - | GettingStarted => "Getting Started" - | Glossary => "Glossary" - | PanelGuide => "Panel Guides" - | Shortcuts => "Shortcuts" - | Faq => "FAQ" - | Architecture => "Architecture" - } -} - -/// Advances the onboarding walkthrough to the next step. -/// If at the last step, marks onboarding as complete. -let nextOnboardingStep = (state: HelpModel.onboardingState): HelpModel.onboardingState => { - let maxStep = Array.length(state.steps) - 1 - if state.currentStep >= maxStep { - {...state, active: false, completedOnce: true} - } else { - {...state, currentStep: state.currentStep + 1} - } -} - -/// Moves the onboarding walkthrough back to the previous step. -/// If at the first step, returns state unchanged. -let prevOnboardingStep = (state: HelpModel.onboardingState): HelpModel.onboardingState => { - if state.currentStep <= 0 { - state - } else { - {...state, currentStep: state.currentStep - 1} - } -} diff --git a/src/core/HypatiaEngine.affine b/src/core/HypatiaEngine.affine new file mode 100644 index 00000000..1cf61dbb --- /dev/null +++ b/src/core/HypatiaEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module HypatiaEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/HypatiaEngine.res b/src/core/HypatiaEngine.res deleted file mode 100644 index 71c802bb..00000000 --- a/src/core/HypatiaEngine.res +++ /dev/null @@ -1,622 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Hypatia Engine — pure computation for the Hypatia panel. -/// -/// Parses Elixir API responses, computes aggregate metrics, filters -/// and sorts scan results, and provides display helpers. - -open HypatiaModel -open FleetModel - -/// Human-readable label for a neural network ID. -let netLabel = (id: neuralNetId): string => - switch id { - | GraphOfTrust => "Graph of Trust" - | MixtureOfExperts => "Mixture of Experts" - | LiquidStateMachine => "Liquid State Machine" - | EchoStateNetwork => "Echo State Network" - | RadialNeuralNetwork => "Radial Neural Network" - } - -/// Description of what each network does. -let netDescription = (id: neuralNetId): string => - switch id { - | GraphOfTrust => "PageRank trust over repos/bots/recipes" - | MixtureOfExperts => "Domain-specific confidence (7 experts)" - | LiquidStateMachine => "Temporal anomaly detection" - | EchoStateNetwork => "Confidence trajectory forecasting" - | RadialNeuralNetwork => "Finding similarity + novelty detection" - } - -/// CSS class for network status indicator. -let netStatusColor = (status: neuralNetStatus): string => - switch status { - | NetActive => "bg-green-400" - | NetTraining => "bg-blue-400 animate-pulse" - | NetOffline => "bg-gray-500" - | NetError(_) => "bg-red-400" - } - -/// Human-readable label for a pipeline stage. -let stageLabel = (stage: pipelineStage): string => - switch stage { - | Ingestion => "Ingestion" - | Analysis => "Analysis" - | Routing => "Routing" - | Dispatch => "Dispatch" - | Complete => "Complete" - } - -/// Category tab label. -let categoryLabel = (cat: hypatiaCategory): string => - switch cat { - | HypatiaDashboard => "Dashboard" - | HypatiaScans => "Scans" - | HypatiaQuarantine => "Quarantine" - | HypatiaNeural => "Neural" - | HypatiaRecipes => "Recipes" - } - -/// Filter scan results by text search. -let filterScans = (scans: array, query: string): array => { - if query === "" { - scans - } else { - let q = String.toLowerCase(query) - scans->Array.filter(s => String.includes(String.toLowerCase(s.repoName), q)) - } -} - -/// Compute average confidence across all active networks. -let avgConfidence = (networks: array): float => { - let active = networks->Array.filter(n => - switch n.status { - | NetActive => true - | _ => false - } - ) - if Array.length(active) > 0 { - active->Array.map(n => n.confidence)->Array.reduce(0.0, (a, b) => a +. b) /. - Int.toFloat(Array.length(active)) - } else { - 0.0 - } -} - -/// Parse a neural network ID string into a neuralNetId variant. -let parseNetId = (s: string): option => - switch s { - | "graph_of_trust" => Some(GraphOfTrust) - | "mixture_of_experts" => Some(MixtureOfExperts) - | "liquid_state_machine" => Some(LiquidStateMachine) - | "echo_state_network" => Some(EchoStateNetwork) - | "radial_neural_network" => Some(RadialNeuralNetwork) - | _ => None - } - -/// Parse a neural network status string into a neuralNetStatus variant. -let parseNetStatus = (s: string): neuralNetStatus => - switch s { - | "active" => NetActive - | "training" => NetTraining - | "offline" => NetOffline - | _ => NetError(s) - } - -/// Tea_Json decoder for a single neural network state. -/// Validates the network ID, skipping unknown networks. -let networkDecoder: Tea_Json.decoder = json => { - open Decoders - open Tea_Json - let inner = map5( - (idStr, statusStr, confidence, inferenceCount, version) => ( - idStr, - statusStr, - confidence, - inferenceCount, - version, - ), - stringField("id"), - stringField("status"), - floatField("confidence"), - intField("inference_count"), - stringField("version"), - ) - switch inner(json) { - | Ok((idStr, statusStr, confidence, inferenceCount, version)) => - switch parseNetId(idStr) { - | Some(netId) => - Ok( - ( - { - id: netId, - status: parseNetStatus(statusStr), - confidence, - inferenceCount, - version, - }: neuralNetState - ), - ) - | None => Error(Failure(`Unknown neural net id: ${idStr}`, json)) - } - | Error(e) => Error(e) - } -} - -/// Parse neural network states from API JSON. -let parseNetworks = (json: string): result, string> => - Decoders.decode(Decoders.lenientArray(networkDecoder), json) - -/// Tea_Json decoder for a single scan result. -let scanResultDecoder: Tea_Json.decoder = { - open Decoders - map6((repoName, riskScore, findingCount, quarantineCount, lastScanned, passed): scanResult => { - repoName, - riskScore, - findingCount, - quarantineCount, - lastScanned, - passed, - }, stringField( - "repo_name", - ), floatField( - "risk_score", - ), intField( - "finding_count", - ), intField("quarantine_count"), stringField("last_scanned"), boolField("passed")) -} - -/// Parse scan results from API JSON. -let parseScans = (json: string): result, string> => - Decoders.decode(Decoders.lenientArray(scanResultDecoder), json) - -/// Safety tier label. -let tierLabel = (tier: safetyTier): string => - switch tier { - | Eliminate => "Eliminate" - | Substitute => "Substitute" - | Control => "Control" - } - -/// Safety tier colour class. -let tierColor = (tier: safetyTier): string => - switch tier { - | Eliminate => "text-red-400" - | Substitute => "text-amber-400" - | Control => "text-blue-400" - } - -/// Safety tier background colour class. -let tierBg = (tier: safetyTier): string => - switch tier { - | Eliminate => "bg-red-900/30 border-red-800" - | Substitute => "bg-amber-900/30 border-amber-800" - | Control => "bg-blue-900/30 border-blue-800" - } - -/// Built-in sample recipe entries (the 34-recipe Hypatia inventory). -let sampleRecipes = (): array => [ - { - id: "hyp-001", - name: "believe_me detector", - description: "Flags Idris2 believe_me — proof-hole that compiles silently", - confidence: 0.99, - tier: Eliminate, - hasFixScript: true, - languages: ["Idris2"], - timesTriggered: 4566, - lastTriggered: "2026-02-22", - }, - { - id: "hyp-002", - name: "sorry detector", - description: "Flags Lean sorry — incomplete proof placeholder", - confidence: 0.99, - tier: Eliminate, - hasFixScript: true, - languages: ["Lean"], - timesTriggered: 0, - lastTriggered: "never", - }, - { - id: "hyp-003", - name: "Admitted detector", - description: "Flags Coq Admitted — unproven theorem accepted", - confidence: 0.99, - tier: Eliminate, - hasFixScript: false, - languages: ["Coq"], - timesTriggered: 22, - lastTriggered: "2026-02-18", - }, - { - id: "hyp-004", - name: "unsafeCoerce detector", - description: "Flags Haskell unsafeCoerce — type-system bypass", - confidence: 0.98, - tier: Eliminate, - hasFixScript: true, - languages: ["Haskell"], - timesTriggered: 3, - lastTriggered: "2026-01-15", - }, - { - id: "hyp-005", - name: "Obj.magic detector", - description: "Flags OCaml Obj.magic — unsafe type coercion", - confidence: 0.98, - tier: Eliminate, - hasFixScript: true, - languages: ["OCaml"], - timesTriggered: 1, - lastTriggered: "2026-02-10", - }, - { - id: "hyp-006", - name: "assert_total guard", - description: "Flags Idris2 assert_total — totality escape hatch", - confidence: 0.97, - tier: Substitute, - hasFixScript: true, - languages: ["Idris2"], - timesTriggered: 0, - lastTriggered: "never", - }, - { - id: "hyp-007", - name: "SPDX header check", - description: "Verifies SPDX-License-Identifier present in all source files", - confidence: 0.99, - tier: Control, - hasFixScript: true, - languages: ["*"], - timesTriggered: 892, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-008", - name: "SHA pin validator", - description: "Ensures GitHub Actions use SHA-pinned versions", - confidence: 0.99, - tier: Eliminate, - hasFixScript: true, - languages: ["YAML"], - timesTriggered: 341, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-009", - name: "permissions: read-all", - description: "Verifies workflow-level read-all permissions", - confidence: 0.98, - tier: Control, - hasFixScript: true, - languages: ["YAML"], - timesTriggered: 257, - lastTriggered: "2026-03-13", - }, - { - id: "hyp-010", - name: "npm/bun blocker", - description: "Blocks npm/bun usage — enforces Deno-first policy", - confidence: 0.99, - tier: Eliminate, - hasFixScript: false, - languages: ["JSON", "YAML"], - timesTriggered: 45, - lastTriggered: "2026-03-10", - }, - { - id: "hyp-011", - name: "TypeScript blocker", - description: "Blocks .ts/.tsx files — enforces ReScript-first policy", - confidence: 0.99, - tier: Eliminate, - hasFixScript: false, - languages: ["TypeScript"], - timesTriggered: 12, - lastTriggered: "2026-03-08", - }, - { - id: "hyp-012", - name: "SCM file location", - description: "Ensures STATE/META/ECOSYSTEM.scm are in .machine_readable/ only", - confidence: 0.99, - tier: Eliminate, - hasFixScript: true, - languages: ["Scheme"], - timesTriggered: 15, - lastTriggered: "2026-02-20", - }, - { - id: "hyp-013", - name: "secret scanner", - description: "Detects API keys, tokens, passwords in source code", - confidence: 0.95, - tier: Eliminate, - hasFixScript: false, - languages: ["*"], - timesTriggered: 7, - lastTriggered: "2026-03-01", - }, - { - id: "hyp-014", - name: "unsafe block auditor", - description: "Audits Rust unsafe blocks for // SAFETY: comments", - confidence: 0.97, - tier: Substitute, - hasFixScript: true, - languages: ["Rust"], - timesTriggered: 28, - lastTriggered: "2026-03-12", - }, - { - id: "hyp-015", - name: "editorconfig check", - description: "Validates .editorconfig present and correct", - confidence: 0.99, - tier: Control, - hasFixScript: true, - languages: ["*"], - timesTriggered: 8, - lastTriggered: "2026-02-14", - }, - { - id: "hyp-016", - name: "CODEQL matrix", - description: "Validates CodeQL language matrix matches repo languages", - confidence: 0.96, - tier: Control, - hasFixScript: true, - languages: ["YAML"], - timesTriggered: 34, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-017", - name: "Trustfile validator", - description: "Validates Trustfile.a2ml structure and security level", - confidence: 0.93, - tier: Control, - hasFixScript: false, - languages: ["A2ML"], - timesTriggered: 4, - lastTriggered: "2026-02-22", - }, - { - id: "hyp-018", - name: "TOPOLOGY.md checker", - description: "Verifies TOPOLOGY.md has architecture diagram + dashboard", - confidence: 0.91, - tier: Control, - hasFixScript: true, - languages: ["Markdown"], - timesTriggered: 261, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-019", - name: "mirror sync check", - description: "Ensures GitLab/Bitbucket mirrors are in sync", - confidence: 0.95, - tier: Control, - hasFixScript: true, - languages: ["YAML"], - timesTriggered: 19, - lastTriggered: "2026-03-13", - }, - { - id: "hyp-020", - name: "Justfile present", - description: "Validates justfile exists with standard recipes", - confidence: 0.97, - tier: Control, - hasFixScript: true, - languages: ["Just"], - timesTriggered: 190, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-021", - name: "AI manifest check", - description: "Verifies 0-AI-MANIFEST.a2ml or AI.a2ml present", - confidence: 0.98, - tier: Control, - hasFixScript: true, - languages: ["A2ML"], - timesTriggered: 173, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-022", - name: "branch protection", - description: "Checks main branch protection rules enabled", - confidence: 0.94, - tier: Substitute, - hasFixScript: true, - languages: ["*"], - timesTriggered: 8, - lastTriggered: "2026-02-14", - }, - { - id: "hyp-023", - name: "scorecard >= 7.0", - description: "Ensures OpenSSF Scorecard passes with score >= 7.0", - confidence: 0.92, - tier: Substitute, - hasFixScript: false, - languages: ["*"], - timesTriggered: 52, - lastTriggered: "2026-03-13", - }, - { - id: "hyp-024", - name: "TruffleHog scan", - description: "Runs TruffleHog for leaked credentials in history", - confidence: 0.95, - tier: Eliminate, - hasFixScript: false, - languages: ["*"], - timesTriggered: 3, - lastTriggered: "2026-02-28", - }, - { - id: "hyp-025", - name: "Guix/Nix policy", - description: "Enforces Guix/Nix packaging policy", - confidence: 0.90, - tier: Control, - hasFixScript: false, - languages: ["Nix", "Guile"], - timesTriggered: 0, - lastTriggered: "never", - }, - { - id: "hyp-026", - name: "workflow linter", - description: "Validates GitHub Actions workflow syntax and best practices", - confidence: 0.97, - tier: Control, - hasFixScript: true, - languages: ["YAML"], - timesTriggered: 67, - lastTriggered: "2026-03-14", - }, - { - id: "hyp-027", - name: "RSR antipattern", - description: "Detects Rhodium Standard Repository antipatterns", - confidence: 0.94, - tier: Substitute, - hasFixScript: true, - languages: ["*"], - timesTriggered: 31, - lastTriggered: "2026-03-12", - }, - { - id: "hyp-028", - name: "SECURITY.md check", - description: "Validates SECURITY.md present with proper disclosure policy", - confidence: 0.98, - tier: Control, - hasFixScript: true, - languages: ["Markdown"], - timesTriggered: 15, - lastTriggered: "2026-02-22", - }, - { - id: "hyp-029", - name: ".well-known check", - description: "Validates .well-known/ directory contents", - confidence: 0.91, - tier: Control, - hasFixScript: true, - languages: ["*"], - timesTriggered: 8, - lastTriggered: "2026-02-14", - }, - { - id: "hyp-030", - name: "author attribution", - description: "Validates author name/email in package manifests", - confidence: 0.97, - tier: Control, - hasFixScript: true, - languages: ["TOML", "JSON"], - timesTriggered: 42, - lastTriggered: "2026-03-10", - }, - { - id: "hyp-031", - name: "duplicate workflow", - description: "Detects duplicate GitHub Actions workflows", - confidence: 0.96, - tier: Substitute, - hasFixScript: true, - languages: ["YAML"], - timesTriggered: 5, - lastTriggered: "2026-02-22", - }, - { - id: "hyp-032", - name: "transmute ban", - description: "Bans std::mem::transmute in Rust except FFI boundaries", - confidence: 0.98, - tier: Eliminate, - hasFixScript: false, - languages: ["Rust"], - timesTriggered: 0, - lastTriggered: "never", - }, - { - id: "hyp-033", - name: "nested .git detector", - description: "Detects rogue .git directories inside monorepo subdirectories", - confidence: 0.99, - tier: Eliminate, - hasFixScript: true, - languages: ["*"], - timesTriggered: 117, - lastTriggered: "2026-02-20", - }, - { - id: "hyp-034", - name: "SLSA provenance", - description: "Validates SLSA build provenance attestation", - confidence: 0.90, - tier: Substitute, - hasFixScript: false, - languages: ["*"], - timesTriggered: 2, - lastTriggered: "2026-03-01", - }, - { - id: "hyp-035", - name: "innerHTML ban (SafeDOM)", - description: "Flags innerHTML/outerHTML usage outside SafeDOMCore — all DOM mounts must use SafeDOM defence-in-depth", - confidence: 0.99, - tier: Eliminate, - hasFixScript: true, - languages: ["ReScript", "JavaScript"], - timesTriggered: 0, - lastTriggered: "never", - }, -] - -/// Filter recipes by text search. -let filterRecipes = (recipes: array, query: string): array => { - if query === "" { - recipes - } else { - let q = String.toLowerCase(query) - recipes->Array.filter(r => - String.includes(String.toLowerCase(r.name), q) || - String.includes(String.toLowerCase(r.description), q) || - r.languages->Array.some(l => String.includes(String.toLowerCase(l), q)) - ) - } -} - -/// Find a recipe by id. -let findRecipe = (recipes: array, id: string): option => - recipes->Array.find(r => r.id === id) - -/// Default initial state. -let defaultState: hypatiaState = { - loaded: false, - loading: false, - error: None, - networks: [], - scans: [], - learningCycle: None, - activeCategory: HypatiaDashboard, - filterText: "", - totalRepos: 0, - quarantinedCount: 0, - triangleCounts: None, - recipes: None, - recipeEntries: sampleRecipes(), - selectedRecipe: None, - recipeFilter: "", - outcomes: None, -} diff --git a/src/core/InterfacesEngine.affine b/src/core/InterfacesEngine.affine new file mode 100644 index 00000000..47826fd7 --- /dev/null +++ b/src/core/InterfacesEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module InterfacesEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/InterfacesEngine.res b/src/core/InterfacesEngine.res deleted file mode 100644 index 5edcd6fe..00000000 --- a/src/core/InterfacesEngine.res +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Interfaces Engine — pure computation for ABI/FFI inventory. - -open InterfacesModel - -let categoryLabel = (cat: interfacesCategory): string => - switch cat { - | IfaceDashboard => "Dashboard" - | IfaceAbi => "ABI (Idris2)" - | IfaceFfi => "FFI (Zig)" - | IfaceBindings => "Bindings" - } - -/// Total ABI exports across all definitions. -let totalAbiExports = (defs: array): int => - defs->Array.reduce(0, (acc, d) => acc + d.exportCount) - -/// Total believe_me count (should always be 0 in proven-servers). -let totalBelieveMe = (defs: array): int => - defs->Array.reduce(0, (acc, d) => acc + d.believeMeCount) - -/// Overall verification rate. -let verificationRate = (defs: array): float => { - if Array.length(defs) > 0 { - let verified = defs->Array.filter(d => d.verified)->Array.length - Int.toFloat(verified) /. Int.toFloat(Array.length(defs)) - } else { - 0.0 - } -} - -/// Average binding coverage across all languages. -let avgCoverage = (bindings: array): float => { - if Array.length(bindings) > 0 { - bindings->Array.map(b => b.coverage)->Array.reduce(0.0, (a, b) => a +. b) /. - Int.toFloat(Array.length(bindings)) - } else { - 0.0 - } -} - -let defaultState: interfacesState = { - loaded: false, - loading: false, - error: None, - abiDefs: [], - ffiImpls: [], - bindings: [], - activeCategory: IfaceDashboard, - totalBelieveMe: 0, -} diff --git a/src/core/K9Engine.affine b/src/core/K9Engine.affine new file mode 100644 index 00000000..251c714b --- /dev/null +++ b/src/core/K9Engine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module K9Engine; + +// TODO: Complete semantic implementation diff --git a/src/core/K9Engine.res b/src/core/K9Engine.res deleted file mode 100644 index c688a78a..00000000 --- a/src/core/K9Engine.res +++ /dev/null @@ -1,555 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL K9 Engine — pure functions for parsing, validating, and querying K9 -/// contractile files (.k9.ncl). K9 files are Nickel configuration components -/// with three security levels: -/// -/// - Kennel: Pure data configuration. No execution, no contracts, no I/O. -/// Identified by `leash = 'Kennel` or absence of contracts/recipes. -/// -/// - Yard: Configuration with Nickel contract validation. Has type contracts -/// and validation blocks but no execution recipes. No I/O. -/// Identified by `leash = 'Yard` or presence of contracts without recipes. -/// -/// - Hunt: Full execution with Just recipes. Has system access, network, -/// filesystem writes, subprocess spawning. Requires signature. -/// Identified by `leash = 'Hunt` or presence of `recipes` blocks. -/// -/// K9 files may also serve as layout presets for PanLL panel arrangements -/// (e.g., `layouts/protocol-design.k9.ncl`), which define pane sizes, -/// promoted actions, and default prover configurations. -/// -/// All functions are pure — no side effects, no Gossamer invocations, no I/O. - -// ============================================================================ -// Types -// ============================================================================ - -/// The three K9 security trust levels, ordered by increasing privilege. -type k9SecurityLevel = - | Kennel - | Yard - | Hunt - -/// A parsed K9 contractile with metadata and validation status. -type k9Contractile = { - path: string, - name: string, - securityLevel: k9SecurityLevel, - isValid: bool, - errors: array, -} - -/// A parsed panel layout configuration extracted from a K9 layout file. -type k9Layout = { - name: string, - panels: array, - discipline: string, -} - -/// Structured metadata extracted from a K9 pedigree block. -type k9Pedigree = { - schemaVersion: string, - componentType: string, - trustLevel: string, - allowNetwork: bool, - allowFilesystemWrite: bool, - allowSubprocess: bool, - author: string, - description: string, -} - -// ============================================================================ -// Internal Helpers -// ============================================================================ - -/// Trim whitespace from both ends. -let trim = (s: string): string => { - s->String.trim -} - -/// Check if content contains the K9 magic header. -let hasK9Magic = (content: string): bool => { - let trimmed = trim(content) - String.startsWith(trimmed, "K9!") -} - -/// Check if content contains Nickel contract annotations (pipes with types). -let hasNickelContracts = (content: string): bool => { - // Yard-level files use Nickel contract syntax: `| Type`, `| std.contract.*` - String.includes(content, "| String") || - String.includes(content, "| Number") || - String.includes(content, "| Bool") || - String.includes(content, "| Array") || - String.includes(content, "| std.contract") || - String.includes(content, "| std.string") || - String.includes(content, "| std.array") -} - -/// Check if content contains execution recipes (Hunt-level). -let hasRecipes = (content: string): bool => { - String.includes(content, "recipes = {") || String.includes(content, "recipes=") -} - -/// Check if content declares a specific leash level. -let declaresLeash = (content: string, level: string): bool => { - String.includes(content, `leash = '${level}`) -} - -/// Extract a string value from a `key = "value"` or `key = 'value` line pattern. -let extractStringValue = (content: string, key: string): option => { - // Look for `key = "value"` pattern - let pattern = `${key} = "` - let idx = String.indexOf(content, pattern) - if idx >= 0 { - let afterKey = String.sliceToEnd(content, ~start=idx + String.length(pattern)) - let endIdx = String.indexOf(afterKey, "\"") - if endIdx >= 0 { - Some(String.slice(afterKey, ~start=0, ~end=endIdx)) - } else { - None - } - } else { - // Try `key = 'EnumValue` pattern (Nickel enum) - let enumPattern = `${key} = '` - let enumIdx = String.indexOf(content, enumPattern) - if enumIdx >= 0 { - let afterKey = String.sliceToEnd(content, ~start=enumIdx + String.length(enumPattern)) - // Take until comma, newline, or whitespace - let chars = String.split(afterKey, "") - let result = ref("") - let done = ref(false) - chars->Array.forEach(c => { - if !done.contents { - if c == "," || c == "\n" || c == " " || c == "}" { - done := true - } else { - result := result.contents ++ c - } - } - }) - if result.contents != "" { - Some(result.contents) - } else { - None - } - } else { - None - } - } -} - -/// Extract a boolean value from a `key = true/false` pattern. -let extractBoolValue = (content: string, key: string): option => { - if String.includes(content, `${key} = true`) { - Some(true) - } else if String.includes(content, `${key} = false`) { - Some(false) - } else { - None - } -} - -/// Extract a numeric value from a `key = 42` pattern. -let extractIntValue = (content: string, key: string): option => { - let pattern = `${key} = ` - let idx = String.indexOf(content, pattern) - if idx >= 0 { - let afterKey = String.sliceToEnd(content, ~start=idx + String.length(pattern)) - let chars = String.split(afterKey, "") - let digits = ref("") - let done = ref(false) - chars->Array.forEach(c => { - if !done.contents { - if c >= "0" && c <= "9" { - digits := digits.contents ++ c - } else { - done := true - } - } - }) - Int.fromString(digits.contents) - } else { - None - } -} - -/// Extract an array of strings from a `key = ["a", "b"]` pattern. -let extractStringArray = (content: string, key: string): array => { - let pattern = `${key} = [` - let idx = String.indexOf(content, pattern) - if idx >= 0 { - let afterKey = String.sliceToEnd(content, ~start=idx + String.length(pattern)) - let endIdx = String.indexOf(afterKey, "]") - if endIdx >= 0 { - let arrayContent = String.slice(afterKey, ~start=0, ~end=endIdx) - arrayContent - ->String.split(",") - ->Array.map(s => { - let trimmed = trim(s) - - // Remove surrounding quotes - if String.startsWith(trimmed, "\"") && String.endsWith(trimmed, "\"") { - String.slice(trimmed, ~start=1, ~end=String.length(trimmed) - 1) - } else { - trimmed - } - }) - ->Array.filter(s => s != "") - } else { - [] - } - } else { - [] - } -} - -// ============================================================================ -// Public API -// ============================================================================ - -/// Detect the security level of a K9 file from its content. -/// -/// Priority: -/// 1. Explicit `leash = 'Hunt/Yard/Kennel` declaration takes precedence. -/// 2. If `recipes` block present → Hunt. -/// 3. If Nickel contracts present → Yard. -/// 4. Otherwise → Kennel (pure data). -let detectSecurityLevel = (content: string): k9SecurityLevel => { - // Check explicit leash declarations first - if declaresLeash(content, "Hunt") { - Hunt - } else if declaresLeash(content, "Yard") { - Yard - } else if declaresLeash(content, "Kennel") { - Kennel - } else if hasRecipes(content) { - // Implicit detection: recipes means Hunt - Hunt - } else if hasNickelContracts(content) { - // Implicit detection: contracts means Yard - Yard - } else { - // Default: pure data - Kennel - } -} - -/// Validate the structural integrity of a K9 contractile. -/// -/// Checks: -/// - Non-empty content -/// - K9 magic header present (if K9 template format) -/// - Pedigree block present -/// - Security level consistency (declared vs detected) -/// - Hunt-level files must declare side_effects -/// - Hunt-level files must have signature_required = true -let validateContractile = (content: string, ~path: string=""): k9Contractile => { - let errors: array = [] - let trimmedContent = trim(content) - - if String.length(trimmedContent) == 0 { - let _ = errors->Array.push("Empty K9 file") - } - - // Check for pedigree block - let hasPedigree = - String.includes(content, "pedigree = {") || String.includes(content, "pedigree=") - if !hasPedigree && !String.includes(content, "LayoutPreset") { - // Layout files import pedigree from pedigree-layout.ncl, so they're exempt - let _ = errors->Array.push("Missing pedigree block") - } - - let detectedLevel = detectSecurityLevel(content) - - // Hunt-level specific checks - if detectedLevel == Hunt { - if !String.includes(content, "side_effects") { - let _ = errors->Array.push("Hunt-level K9 must declare side_effects") - } - if !String.includes(content, "signature_required = true") { - let _ = errors->Array.push("Hunt-level K9 must have signature_required = true") - } - } - - // Extract name from metadata - let name = switch extractStringValue(content, "name") { - | Some(n) => n - | None => - // Try extracting from filename - if path != "" { - let parts = String.split(path, "/") - let filename = parts->Array.get(Array.length(parts) - 1)->Option.getOr("unknown") - String.replace(filename, ".k9.ncl", "") - } else { - "unknown" - } - } - - { - path, - name, - securityLevel: detectedLevel, - isValid: Array.length(errors) == 0, - errors, - } -} - -/// Parse panel layout configuration from a K9 layout file. -/// -/// Layout K9 files define PanLL panel arrangements with pane sizes, -/// promoted actions, and default prover selections. They are always -/// Yard-level (pure config with Nickel contracts for validation). -let parseLayoutPanels = (content: string): k9Layout => { - let name = switch extractStringValue(content, "name") { - | Some(n) => n - | None => "Unknown Layout" - } - - // Extract promoted actions as the panel configuration - let panels = extractStringArray(content, "promoted_actions") - - // Determine discipline from layout name or description - let discipline = if String.includes(content, "protocol") || String.includes(content, "Protocol") { - "protocol-design" - } else if String.includes(content, "logic") || String.includes(content, "Logic") { - "logic-and-proofs" - } else if String.includes(content, "database") || String.includes(content, "Database") { - "database-design" - } else if String.includes(content, "language") || String.includes(content, "Language") { - "language-design" - } else { - "general" - } - - {name, panels, discipline} -} - -/// Human-readable label for a security level. -let securityLevelLabel = (level: k9SecurityLevel): string => { - switch level { - | Kennel => "Kennel (data-only)" - | Yard => "Yard (validated config)" - | Hunt => "Hunt (full execution)" - } -} - -/// Colour code for a security level (for UI rendering). -/// - Kennel: green (safe, no execution) -/// - Yard: amber/yellow (contracts but no I/O) -/// - Hunt: red (full system access) -let securityLevelColour = (level: k9SecurityLevel): string => { - switch level { - | Kennel => "#22c55e" - | Yard => "#f59e0b" - | Hunt => "#ef4444" - } -} - -/// CSS class name for a security level badge. -let securityLevelClass = (level: k9SecurityLevel): string => { - switch level { - | Kennel => "k9-kennel" - | Yard => "k9-yard" - | Hunt => "k9-hunt" - } -} - -/// Extract the pedigree metadata block from K9 content. -let extractPedigree = (content: string): k9Pedigree => { - { - schemaVersion: extractStringValue(content, "schema_version")->Option.getOr("unknown"), - componentType: extractStringValue(content, "component_type")->Option.getOr("unknown"), - trustLevel: extractStringValue(content, "trust_level")->Option.getOr("unknown"), - allowNetwork: extractBoolValue(content, "allow_network")->Option.getOr(false), - allowFilesystemWrite: extractBoolValue(content, "allow_filesystem_write")->Option.getOr(false), - allowSubprocess: extractBoolValue(content, "allow_subprocess")->Option.getOr(false), - author: extractStringValue(content, "author")->Option.getOr("unknown"), - description: extractStringValue(content, "description")->Option.getOr(""), - } -} - -/// Extract pane sizes from a layout K9 file. -/// Returns (pane_l_size, pane_n_size, pane_w_size) or (33, 34, 33) as default. -let extractPaneSizes = (content: string): (int, int, int) => { - let l = extractIntValue(content, "pane_l_size")->Option.getOr(33) - let n = extractIntValue(content, "pane_n_size")->Option.getOr(34) - let w = extractIntValue(content, "pane_w_size")->Option.getOr(33) - (l, n, w) -} - -/// Generate a human-readable summary of a K9 contractile. -let summariseContractile = (contractile: k9Contractile): string => { - let statusLine = if contractile.isValid { - "Status: Valid" - } else { - let errorList = contractile.errors->Array.join("; ") - `Status: Invalid (${errorList})` - } - - `Name: ${contractile.name} -Path: ${contractile.path} -Security: ${securityLevelLabel(contractile.securityLevel)} -${statusLine}` -} - -// ============================================================================ -// A2ML/K9 Integration Functions -// ============================================================================ - -/// Generate a K9 Kennel-level Nickel schema from a panel module's configuration. -/// Kennel schemas are pure data — no contracts, no execution, no I/O. -/// They capture the shape of a module's configurable state as a K9 data file. -/// -/// Takes the module name and a list of (field-name, field-type, default-value) triples -/// describing the module's configurable fields. -let generateKennelSchema = ( - moduleName: string, - fields: array<(string, string, string)>, -): string => { - let fieldLines = - fields - ->Array.map(((name, typ, defaultVal)) => { - ` ${name} = ${defaultVal}, # ${typ}` - }) - ->Array.join("\n") - - `# K9 Kennel Schema — auto-generated from ${moduleName} module config -# Security: Kennel (data-only, no contracts, no execution) -# SPDX-License-Identifier: MPL-2.0 - -K9! - -{ - pedigree = { - schema_version = "1.0", - component_type = "ModuleConfig", - trust_level = "kennel", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - author = "panll-generator", - description = "Auto-generated Kennel schema for ${moduleName} panel module", - }, - - leash = 'Kennel, - - module = "${moduleName}", - - config = { -${fieldLines} - }, -}` -} - -/// Generate a K9 Yard-level Nickel contract from a BoJ cartridge definition. -/// Yard contracts add Nickel type validation to cartridge configuration — -/// they verify port ranges, protocol lists, grade values, and layer status -/// without executing anything. -/// -/// Takes a cartridge name, list of protocol names, and port configuration. -let generateYardContract = ( - cartridgeName: string, - protocols: array, - restPort: int, - grpcPort: int, - graphqlPort: int, - grade: string, -): string => { - let protoArray = protocols->Array.map(p => `"${p}"`)->Array.join(", ") - let portValidation = if restPort > 0 || grpcPort > 0 || graphqlPort > 0 { - ` rest_port | std.contract.from_predicate (fun p => p >= 1024 && p <= 65535) = ${Int.toString( - restPort, - )}, - grpc_port | std.contract.from_predicate (fun p => p >= 1024 && p <= 65535) = ${Int.toString( - grpcPort, - )}, - graphql_port | std.contract.from_predicate (fun p => p >= 1024 && p <= 65535) = ${Int.toString( - graphqlPort, - )},` - } else { - ` rest_port = 0, - grpc_port = 0, - graphql_port = 0,` - } - - `# K9 Yard Contract — auto-generated from BoJ cartridge "${cartridgeName}" -# Security: Yard (validated config with Nickel contracts, no execution) -# SPDX-License-Identifier: MPL-2.0 - -K9! - -{ - pedigree = { - schema_version = "1.0", - component_type = "CartridgeConfig", - trust_level = "yard", - allow_network = false, - allow_filesystem_write = false, - allow_subprocess = false, - author = "panll-generator", - description = "Auto-generated Yard contract for BoJ cartridge ${cartridgeName}", - }, - - leash = 'Yard, - - cartridge = { - name | String = "${cartridgeName}", - grade | std.contract.from_predicate (fun g => std.array.elem g ["A", "B", "C", "D"]) = "${grade}", - protocols | Array String = [${protoArray}], -${portValidation} - }, - - layers = { - abi_ready | Bool = false, - ffi_ready | Bool = false, - adapter_ready | Bool = false, - shared_lib_ready | Bool = false, - }, -}` -} - -/// Extract configurable fields from a BoJ cartridge for Kennel schema generation. -/// Returns (field-name, field-type, default-value) triples. -let cartridgeToKennelFields = (name: string, protocols: array): array<( - string, - string, - string, -)> => { - let protoStr = "[" ++ protocols->Array.map(p => `"${p}"`)->Array.join(", ") ++ "]" - [ - ("name", "String", `"${name}"`), - ("loaded", "Bool", "false"), - ("protocols", "Array String", protoStr), - ("rest_port", "Number", "0"), - ("grpc_port", "Number", "0"), - ("graphql_port", "Number", "0"), - ] -} - -/// Check if a K9 Hunt-level execution is permitted for a given clade permission set. -/// Returns (allowed, reason) where reason explains the decision. -/// -/// Hunt-level K9 files require: -/// 1. The clade must have `hunt` in its isolation requirements -/// 2. The clade must have signing enabled -/// 3. The contractile must have signature_required = true -let checkHuntPermission = ( - cladeIsolation: string, - cladeSigning: bool, - contractileContent: string, -): (bool, string) => { - let huntAllowed = - String.includes(cladeIsolation, "hunt") || String.includes(cladeIsolation, "full") - let hasSig = String.includes(contractileContent, "signature_required = true") - - if !huntAllowed { - (false, "Clade isolation level does not permit Hunt execution") - } else if !cladeSigning { - (false, "Clade does not have signing enabled — Hunt requires signed contractiles") - } else if !hasSig { - (false, "Contractile does not declare signature_required = true") - } else { - (true, "Hunt execution permitted — clade isolation allows it, signing verified") - } -} diff --git a/src/core/KeybindingsEngine.affine b/src/core/KeybindingsEngine.affine new file mode 100644 index 00000000..39d2b057 --- /dev/null +++ b/src/core/KeybindingsEngine.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module KeybindingsEngine; + +// TODO: Complete semantic implementation diff --git a/src/core/KeybindingsEngine.res b/src/core/KeybindingsEngine.res deleted file mode 100644 index 08da682a..00000000 --- a/src/core/KeybindingsEngine.res +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL Keybindings Engine — default bindings, lookup, and conflict detection. -/// -/// Pure functions for managing the keybinding map. No side effects — all keyboard -/// event handling happens in SubscriptionsFixed.res which calls these lookup -/// functions to determine which action a keypress should trigger. - -open KeybindingsModel - -/// Helper: create a chord with standard modifier combinations. -let chord = (modifiers: array, key: string): keyChord => { - modifiers, - key, -} - -/// Helper: create a keybinding with default (non-custom) flag. -let bind = (modifiers: array, key: string, action: keybindingAction): keybinding => { - chord: chord(modifiers, key), - action, - custom: false, -} - -/// Default keybindings — the standard set that ships with PanLL. -/// Users can override any of these via the keybinding editor. -let defaults: array = [ - // Undo/Redo - bind([Ctrl], "z", ActionUndo), - bind([Ctrl, Shift], "Z", ActionRedo), - // Save - bind([Ctrl], "s", ActionSave), - // Print active panel - bind([Ctrl], "p", ActionPrint), - // Reset - bind([Ctrl, Shift], "R", ActionResetPanel), - bind([Ctrl, Shift, Alt], "R", ActionResetAll), - // Pane toggles (preserving existing Ctrl+Shift shortcuts) - bind([Ctrl, Shift], "L", ActionTogglePaneL), - bind([Ctrl, Shift], "N", ActionTogglePaneN), - bind([Ctrl, Shift], "B", ActionTogglePaneW), - bind([Ctrl, Shift], "W", ActionTogglePaneW), - bind([Ctrl, Shift], "V", ActionToggleVab), - // Panel bar - bind([Ctrl], "`", ActionTogglePanelBar), - // Fullscreen active panel - bind([], "F11", ActionFullscreen), - // Close overlay (Escape) - bind([], "Escape", ActionCloseOverlay), - // New panel shortcuts - bind([Ctrl, Shift], "C", ActionToggleCapture), - bind([Ctrl, Shift], "K", ActionToggleWorkspace), - bind([Ctrl, Shift], "S", ActionToggleSecurity), - // Workspace mode cycling - bind([Ctrl, Shift], "M", ActionCycleWorkspaceMode), - // Dry run toggle - bind([Ctrl, Shift], "D", ActionToggleDryRun), -] - -/// Look up which action (if any) a key event should trigger. -/// Returns None if no binding matches the event. -let lookup = ( - bindings: array, - ctrlKey: bool, - shiftKey: bool, - altKey: bool, - metaKey: bool, - key: string, -): option => { - // Build the set of active modifiers from the event. - let activeModifiers = { - let mods = [] - let mods = if ctrlKey { - Array.concat(mods, [Ctrl]) - } else { - mods - } - let mods = if shiftKey { - Array.concat(mods, [Shift]) - } else { - mods - } - let mods = if altKey { - Array.concat(mods, [Alt]) - } else { - mods - } - let mods = if metaKey { - Array.concat(mods, [Meta]) - } else { - mods - } - mods - } - - // Find the first binding whose chord matches. - let match_ = Array.find(bindings, binding => { - let chordMods = binding.chord.modifiers - // Exact modifier set match (same length + all present). - let modsMatch = - Array.length(chordMods) === Array.length(activeModifiers) && - Array.every(chordMods, m => Array.some(activeModifiers, am => am === m)) - // Key match (case-sensitive for shifted keys, case-insensitive for unshifted). - let keyMatch = binding.chord.key === key - modsMatch && keyMatch - }) - - switch match_ { - | Some(b) => Some(b.action) - | None => None - } -} - -/// Detect conflicts: two or more bindings with the same chord. -/// Returns pairs of conflicting actions. -let detectConflicts = (bindings: array): array<( - keybindingAction, - keybindingAction, -)> => { - let conflicts = [] - let len = Array.length(bindings) - let result = ref(conflicts) - for i in 0 to len - 2 { - for j in i + 1 to len - 1 { - switch (bindings[i], bindings[j]) { - | (Some(a), Some(b)) => { - let sameModifiers = - Array.length(a.chord.modifiers) === Array.length(b.chord.modifiers) && - Array.every(a.chord.modifiers, m => Array.some(b.chord.modifiers, bm => bm === m)) - let sameKey = a.chord.key === b.chord.key - if sameModifiers && sameKey { - result := Array.concat(result.contents, [(a.action, b.action)]) - } - } - | _ => () - } - } - } - result.contents -} - -/// Replace or add a keybinding for a given action. If the action already -/// has a binding, it is replaced. Otherwise, a new binding is appended. -let rebind = (bindings: array, action: keybindingAction, newChord: keyChord): array< - keybinding, -> => { - let exists = Array.some(bindings, b => b.action === action) - if exists { - Array.map(bindings, b => - if b.action === action { - {chord: newChord, action, custom: true} - } else { - b - } - ) - } else { - Array.concat(bindings, [{chord: newChord, action, custom: true}]) - } -} - -/// Reset a single action's binding back to its default. -let resetBinding = (bindings: array, action: keybindingAction): array => { - let defaultBinding = Array.find(defaults, b => b.action === action) - switch defaultBinding { - | Some(db) => - Array.map(bindings, b => - if b.action === action { - db - } else { - b - } - ) - | None => bindings - } -} - -/// Reset all bindings to defaults. -let resetAll = (): array => defaults - -/// Initial keybindings state. -let defaultState: keybindingsState = { - bindings: defaults, - recording: false, - recordingAction: None, - conflicts: [], -} diff --git a/src/core/KeyboardNav.affine b/src/core/KeyboardNav.affine new file mode 100644 index 00000000..65ccf1b2 --- /dev/null +++ b/src/core/KeyboardNav.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module KeyboardNav; + +// TODO: Complete semantic implementation diff --git a/src/core/KeyboardNav.res b/src/core/KeyboardNav.res deleted file mode 100644 index 2cb9c9fa..00000000 --- a/src/core/KeyboardNav.res +++ /dev/null @@ -1,147 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// PanLL KeyboardNav — composable keyboard navigation handlers for panels. -/// -/// Provides standard keyboard interaction patterns that components can compose -/// into their event attributes. Follows WAI-ARIA Authoring Practices: -/// - Enter/Space activates buttons -/// - Arrow keys navigate lists and tab bars -/// - Escape closes overlays/modals -/// - Home/End jump to first/last item -/// -/// Usage in components: -/// button(list{ -/// Events.onClick(MyAction), -/// KeyboardNav.onActivate(MyAction), // Enter + Space -/// Attrs.tabIndex(0), -/// }, list{text("Click me")}) -/// -/// All functions are pure — they return event attributes, not side effects. - -open Tea_Vdom -open Tea_Html - -/// Key string constants for readability. -module Key = { - let enter = "Enter" - let space = " " - let escape = "Escape" - let arrowUp = "ArrowUp" - let arrowDown = "ArrowDown" - let arrowLeft = "ArrowLeft" - let arrowRight = "ArrowRight" - let home = "Home" - let end_ = "End" - let tab = "Tab" -} - -/// Dispatch a message when Enter or Space is pressed (button activation pattern). -let onActivate = (msg: 'msg): attribute<'msg> => - Events.onKeyDown(key => - if key === Key.enter || key === Key.space { - Some(msg) - } else { - None - } - ) - -/// Dispatch a message when Escape is pressed (close/dismiss pattern). -let onEscape = (msg: 'msg): attribute<'msg> => - Events.onKeyDown(key => - if key === Key.escape { - Some(msg) - } else { - None - } - ) - -/// Dispatch messages for vertical arrow navigation (list/menu pattern). -let onVerticalNav = (~onUp: 'msg, ~onDown: 'msg): attribute<'msg> => - Events.onKeyDown(key => - if key === Key.arrowUp { - Some(onUp) - } else if key === Key.arrowDown { - Some(onDown) - } else { - None - } - ) - -/// Dispatch messages for horizontal arrow navigation (tab bar pattern). -let onHorizontalNav = (~onLeft: 'msg, ~onRight: 'msg): attribute<'msg> => - Events.onKeyDown(key => - if key === Key.arrowLeft { - Some(onLeft) - } else if key === Key.arrowRight { - Some(onRight) - } else { - None - } - ) - -/// Dispatch messages for Home/End navigation (jump to first/last). -let onHomeEnd = (~onHome: 'msg, ~onEnd: 'msg): attribute<'msg> => - Events.onKeyDown(key => - if key === Key.home { - Some(onHome) - } else if key === Key.end_ { - Some(onEnd) - } else { - None - } - ) - -/// Combined list navigation: ArrowUp/Down + Home/End + Enter to select + Escape to dismiss. -/// Useful for dropdown menus, comboboxes, listboxes. -let onListNav = ( - ~onUp: 'msg, - ~onDown: 'msg, - ~onHome: 'msg, - ~onEnd: 'msg, - ~onSelect: 'msg, - ~onDismiss: 'msg, -): attribute<'msg> => - Events.onKeyDown(key => - if key === Key.arrowUp { - Some(onUp) - } else if key === Key.arrowDown { - Some(onDown) - } else if key === Key.home { - Some(onHome) - } else if key === Key.end_ { - Some(onEnd) - } else if key === Key.enter || key === Key.space { - Some(onSelect) - } else if key === Key.escape { - Some(onDismiss) - } else { - None - } - ) - -/// Helper: make a focusable div container (tabIndex 0). -let focusable: attribute<'msg> = Attrs.tabIndex(0) - -/// Helper: programmatically focusable but not in tab order (tabIndex -1). -let focusableHidden: attribute<'msg> = Attrs.tabIndex(-1) - -/// Helper: screen reader only text (visually hidden but announced). -let srOnly = (label: string): Tea_Vdom.t<'msg> => - span( - list{Attrs.class_("sr-only")}, - list{text(label)}, - ) - -/// Helper: aria-live polite region wrapper (for status updates). -let livePolite = (children: list>): Tea_Vdom.t<'msg> => - div(list{Attrs.ariaLive("polite")}, children) - -/// Helper: aria-live assertive region wrapper (for errors/alerts). -let liveAssertive = (children: list>): Tea_Vdom.t<'msg> => - div(list{Attrs.ariaLive("assertive")}, children) - -/// Bundle: onClick + keyboard activation + tabIndex in one call. -/// Use instead of bare Events.onClick for all interactive elements. -/// This is the primary entry point for keyboard accessibility. -let clickable = (msg: 'msg): list> => - list{Events.onClick(msg), onActivate(msg), Attrs.tabIndex(0)} diff --git a/src/core/KeyboardUtil.affine b/src/core/KeyboardUtil.affine new file mode 100644 index 00000000..7a2b91b3 --- /dev/null +++ b/src/core/KeyboardUtil.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +// Ported via Harvard Engine bulk-processor + +module KeyboardUtil; + +// TODO: Complete semantic implementation diff --git a/src/core/KeyboardUtil.res b/src/core/KeyboardUtil.res deleted file mode 100644 index fc808ff3..00000000 --- a/src/core/KeyboardUtil.res +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -/// Keyboard accessibility utilities for PanLL. -/// -/// Provides onKeyDown handlers for common interaction patterns: -/// Enter/Space for button activation, Escape for dialog dismissal, -/// and arrow keys for list navigation. These ensure WCAG 2.1 Level A -/// compliance (2.1.1 Keyboard) across all interactive components. -/// -/// Each helper returns a `Tea_Vdom.attribute<'msg>` that attaches a -/// `keydown` event listener to the element. The listener checks the -/// pressed key and dispatches the given message only when the key -/// matches the expected activation pattern. Non-matching keys are -/// ignored (the event is not prevented), preserving normal browser -/// keyboard behaviour for Tab, arrow keys, etc. - -/// Trigger a message on Enter or Space key press (button activation pattern). -/// -/// This implements the WAI-ARIA button activation pattern: interactive -/// elements that are not native `