diff --git a/docs/plans/2026-06-09-monaco-script-editor-design.md b/docs/plans/2026-06-09-monaco-script-editor-design.md new file mode 100644 index 00000000..8d8c6288 --- /dev/null +++ b/docs/plans/2026-06-09-monaco-script-editor-design.md @@ -0,0 +1,285 @@ +# Monaco Script Editor (Roslyn-backed IntelliSense) — Design + +**Date:** 2026-06-09 +**Status:** Approved (brainstorming complete; ready for implementation plan) +**Author:** brainstorming session +**Related:** `docs/Uns.md`, `src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/`, sister project `scadabridge` (`/Users/dohertj2/Desktop/scadabridge/src`) + +## Goal + +Replace OtOpcUa's proof-of-concept script editor (a CDN Monaco instance mounted +over a hidden textarea via an `eval` + `Task.Delay(50)` race) with a proper, +reusable Monaco code editor offering **full Roslyn-backed IntelliSense** — the +same class of experience as the sister project **scadabridge**: completions, +hover, signature help, live diagnostics, document formatting, inlay hints, and +context-aware tag-path completion — but re-seated on OtOpcUa's *actual* script +compile context so what the editor accepts/rejects exactly matches what publish +enforces. + +## Background — current state + +| Aspect | scadabridge (reference) | OtOpcUa (today) | +|---|---|---| +| Monaco delivery | Vendored locally (`wwwroot/lib/monaco/vs/`, served from `_content/`), v0.42 | CDN (jsDelivr `monaco-editor@0.52.2`), injected at runtime | +| Editor component | Reusable `MonacoEditor.razor` (`Value`/`ValueChanged`, theme, toolbar, `DotNetObjectReference`) | None — `eval`-injected loader over a hidden ``, sync-back via dispatched `input` events | +| IntelliSense | Full: completions/hover/signature-help/diagnostics/format/inlay via `/api/script-analysis/*` → `ScriptAnalysisService` (Roslyn) | None — plaintext `csharp` highlighting only | +| Load robustness | Deterministic | Fragile 50 ms delay; silent fallback to textarea | +| Air-gap | Safe (vendored) | Falls back to a plain textarea when CDN unreachable | + +The current code self-documents the gap: comments say richer editing "lands in +Phase D.2." + +### OtOpcUa scripting runtime (the context IntelliSense must reproduce) + +- **Evaluator:** `ScriptEvaluator` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs`) + builds a hand-rolled `CSharpCompilation` (NOT `CSharpScript`). It wraps the + user's source in: + ```csharp + using System; + using System.Linq; + using ZB.MOM.WW.OtOpcUa.Core.Abstractions; + using ZB.MOM.WW.OtOpcUa.Core.Scripting; + namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.Compiled; + public static class CompiledScript + { + public static TResult Run(ScriptGlobals globals) + { + var ctx = globals.ctx; + #line 1 + «user source» // statement body, MUST end with an explicit `return` + } + } + ``` +- **Globals / context:** `ScriptGlobals` exposes `ctx` of type + `VirtualTagContext : ScriptContext`. The in-scope API a script author can call: + - `ctx.GetTag(string path)` → `DataValueSnapshot` (`.Value`, `.StatusCode`, `.SourceTimestampUtc`, `.ServerTimestampUtc`) + - `ctx.SetVirtualTag(string path, object? value)` + - `ctx.Now` → `DateTime` + - `ctx.Logger` → `Serilog.ILogger` + - `ScriptContext.Deadband(double current, double previous, double tolerance)` (static) + + **These are real, referenceable types** (`Scripting.Abstractions`), so Roslyn + analysis can point at the genuine types — no mirror "host" classes needed + (scadabridge had to build mirrors because its runtime globals weren't + directly referenceable). +- **Sandbox:** `ScriptSandbox.Build(typeof(VirtualTagContext))` + (`src/Core/Scripting/ScriptSandbox.cs`) supplies the imports (above) and the + restricted metadata-reference set (pinned OtOpcUa assemblies + a BCL subset; + no `System.IO`/`System.Net`/`Process`/`Reflection`). +- **Hard gates publish enforces:** + - `ForbiddenTypeAnalyzer` — semantic gate against type-forwarding sandbox + escapes (e.g. resolving `HttpClient`). + - `DependencyExtractor.Extract(source)` — AST walk requiring `ctx.GetTag` / + `ctx.SetVirtualTag` paths to be **string literals**; rejects variables, + concatenation, interpolation. Also yields the read/write dependency sets. +- **Binding:** `VirtualTag.ScriptId` → `Script.ScriptId` (logical FK). A `Script` + is **global** — one script may be referenced by multiple virtual tags across + different equipment, and those tags may have different `DataType`s. The script + is edited as a `Script` row (`SourceCode` + SHA-256 `SourceHash`), not per tag. + +## Decisions (from brainstorming) + +1. **Scope:** Full IntelliSense parity with scadabridge (completions, hover, + signature help, diagnostics, formatting, inlay hints) — Roslyn-backed. +2. **Location:** Reusable `MonacoEditor.razor`, wired into **both** the + ScriptEdit page (`/scripts/{id}`) **and** the Edit-Virtual-Tag modal + (inline script-source panel). +3. **Tag-path completion:** Yes, in v1 — global completion of all configured + tag + virtual-tag paths inside `ctx.GetTag("…")` / `ctx.SetVirtualTag("…")` + string literals. +4. **Vendoring:** Monaco vendored locally (air-gap safe), replacing the CDN. +5. **Modal save-model:** Inline script editing in the modal is its **own** + `RowVersion`-guarded Save, separate from the virtual-tag Create/Save, with a + "Used by N virtual tags — edits affect all of them" warning. + +## Architecture + +``` +┌──────────────────────────── AdminUI (Blazor Server) ────────────────────────────┐ +│ │ +│ Components/Shared/MonacoEditor.razor ◄── Value/ValueChanged, theme, toolbar │ +│ │ (DotNetObjectReference both ways) │ +│ ▼ │ +│ wwwroot/js/monaco-init.js ──registers──► completion / hover / signatureHelp / │ +│ │ diagnostics / format / inlayHints │ +│ │ fetch POST /api/script-analysis/* │ +│ ▼ │ +│ ScriptAnalysis/ScriptAnalysisEndpoints.cs (minimal-API group, authz-gated) │ +│ ▼ │ +│ ScriptAnalysis/ScriptAnalysisService.cs ── Roslyn over OtOpcUa's REAL wrapper │ +│ │ ├─ ScriptSandbox.Build(typeof(VirtualTagContext)) (imports + refs) │ +│ │ ├─ CompiledScript.Run(ScriptGlobals) wrapper │ +│ │ ├─ ForbiddenTypeAnalyzer (sandbox-escape squiggles) │ +│ │ ├─ DependencyExtractor (dynamic-path squiggles) │ +│ │ └─ IScriptTagCatalog (tag-path string-literal completions) │ +│ ▼ │ +│ wwwroot/lib/monaco/vs/… (vendored Monaco, one