feat(v3-batch3): B3-WP4 — {{equip}}/<RefName> reference-relative resolution

Replace the deleted equipment-base-prefix {{equip}} mechanism (impossible now
that equipment is reference-only) with per-reference resolution:
{{equip}}/<RefName> resolves through the owning equipment's UnsTagReference rows
(by effective name) to the backing raw tag's RawPath. Every unresolved <RefName>
is a clear deploy error + authoring rejection + Monaco diagnostic — preserving
"editor accepts <=> publish accepts".

Commons:
- EquipmentScriptPaths: delete DeriveEquipmentBase; SubstituteEquipmentToken now
  takes the equipment's reference map (effectiveName -> RawPath) and substitutes
  {{equip}}/<RefName> inside path literals (unresolved left intact, never throws);
  add ExtractEquipReferenceNames (path-literal scoped) +
  ExtractEquipReferenceNamesFromText (message templates). Slash syntax replaces
  the v2 dot joint.
- New EquipmentReferenceMap: shared, input-shape-agnostic builder (entity + JSON
  sides) over RawPathResolver — the single authority both compose seams + the
  validator use, so resolved RawPaths agree byte-for-byte.

Compose seams (byte-parity kept):
- AddressSpaceComposer.Compose + DeploymentArtifact.ParseComposition both build
  the per-equipment reference map from UnsTagReferences + Tags + raw topology and
  substitute VirtualTag AND ScriptedAlarm-predicate sources before dependency
  extraction (runtime dep refs = resolved RawPaths).

Deploy gate + authoring + editor:
- DraftValidator.ValidateEquipReferenceResolution: EquipReferenceUnresolved error
  for unresolved {{equip}}/<RefName> in VirtualTag/ScriptedAlarm scripts + alarm
  message-template tokens (naming script, equipment, missing ref).
- UnsTreeService.ValidateEquipTokenAsync (Wave-A M1): reference-relative authoring
  rejection, replacing the stale DeriveEquipmentBase(empty)->reject-all logic.
- ScriptAnalysisService: {{equip}}/ completion offers the equipment's reference
  effective names; DiagnoseAsync flags unresolved refs (OTSCRIPT_EQUIPREF) same as
  the deploy gate. Requests carry optional EquipmentId (threaded MonacoEditor ->
  monaco-init.js -> fetch bodies). IScriptTagCatalog.GetEquipmentRelativeLeavesAsync
  repurposed to GetEquipmentReferenceNamesAsync(equipmentId, filter).

Tests: EquipmentScriptPaths substitution/extraction (slash pinned); composer<->
artifact reference-resolution byte-parity; DraftValidator unresolved-ref;
ScriptAnalysis completion+diagnostic; M1 authoring gate. Build clean (0/0); all
five affected suites green.

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
Joseph Doherty
2026-07-16 06:58:17 -04:00
parent a88dc86173
commit dc0d7653b9
23 changed files with 1142 additions and 426 deletions
@@ -23,6 +23,11 @@
const KIND_MAP = { Method: 0, Field: 3, Property: 9, Event: 10, Class: 5, Module: 8, Variable: 4, Text: 18 };
// The owning-equipment id is stamped onto each editor's model at createEditor time so the globally
// registered csharp providers (which only receive the model) can thread it into the {{equip}}/<RefName>
// completion + diagnostics. Null on the shared ScriptEdit page (equip-ref features inert there).
function equipmentIdOf(model) { return (model && model.__otEquipmentId) || null; }
function registerCSharpProviders() {
monaco.languages.registerCompletionItemProvider("csharp", {
triggerCharacters: [".", "(", "\""],
@@ -31,7 +36,7 @@
const resp = await fetch("/api/script-analysis/completions", {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ codeText: model.getValue(), line: position.lineNumber, column: position.column })
body: JSON.stringify({ codeText: model.getValue(), line: position.lineNumber, column: position.column, equipmentId: equipmentIdOf(model) })
});
if (!resp.ok) return { suggestions: [] };
const data = await resp.json();
@@ -72,7 +77,7 @@
const resp = await fetch("/api/script-analysis/hover", {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ codeText: model.getValue(), line: position.lineNumber, column: position.column })
body: JSON.stringify({ codeText: model.getValue(), line: position.lineNumber, column: position.column, equipmentId: equipmentIdOf(model) })
});
if (!resp.ok) return null;
const data = await resp.json();
@@ -149,7 +154,7 @@
const resp = await fetch("/api/script-analysis/diagnostics", {
method: "POST", credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: model.getValue() })
body: JSON.stringify({ code: model.getValue(), equipmentId: equipmentIdOf(model) })
});
if (!resp.ok) return [];
const data = await resp.json();
@@ -192,6 +197,9 @@
// inside ctx.GetTag("…") / ctx.SetVirtualTag("…") without requiring an explicit Ctrl+Space.
quickSuggestions: { other: true, comments: false, strings: true }
});
// Stamp the owning-equipment id on the model so the global csharp providers can thread it into the
// {{equip}}/<RefName> completion + diagnostics (null on the shared ScriptEdit page).
try { const m0 = editor.getModel(); if (m0) m0.__otEquipmentId = options.equipmentId || null; } catch (x) {}
let diagTimer = null;
const scheduleDiagnostics = function () {
if (diagTimer) clearTimeout(diagTimer);