5.8 KiB
Script-editor ctx tag-completion correctness + truthfulness — Design
Brainstormed 2026-06-18. Backlog item: stillpending.md §4 "Monaco
ctx.SetVirtualTag(...)write-target completions/hover are best-effort" (AdminUI/ScriptAnalysis/IScriptTagCatalog.cs,ScriptAnalysisService.cs). Off master651018f9.
Problem (verified in code)
The Monaco script editor's tag-path completion + hover (ScriptEdit page and the inline virtual-tag
modal) route through one private gate, ScriptAnalysisService.TryGetTagPathLiteral(token, …). Two
defects:
-
Receiver-blind gate.
TryGetTagPathLiteralaccepts ANY<expr>.GetTag("…")/<expr>.SetVirtualTag("…")— it matches the member-access method name only (method is not ("GetTag" or "SetVirtualTag")) and never inspects the receiver. So a straysomeOtherObject.GetTag("…")wrongly triggers tag-path completion/hover. This makes the editor more permissive than the runtime, whose dependency harvest is syntacticallyctx-anchored (Commons/Types/EquipmentScriptPaths.GetTagRefRegex=ctx\s*\.\s*(?:GetTag|SetVirtualTag)\s*\(, consumed byPhase7Composerto buildEquipmentVirtualTagPlan.DependencyRefs). -
SetVirtualTagadvertises a no-op.IScriptTagCatalog/ScriptTagCatalogemits a virtual tag's leafNameas aSetVirtualTagwrite-target completion, documented as "best-effort … the live resolution of virtual-tag cascade/write targets is unconfirmed." It is now CONFIRMED:ctx.SetVirtualTag(...)is a no-op in production. The active evaluator bindingHost/Engines/RoslynVirtualTagEvaluator.csruns in single-tag mode and drops every cross-tag write (logged: "cross-tag write to {Path} dropped (single-tag adapter)"). The cross-tag cascade engine that DOES honorSetVirtualTag(Core.VirtualTags.VirtualTagEngine,OnScriptSetVirtualTagvalidating the target against registered vtags) is dormant — not wired into the host. So there is no production write-target resolution to be "authoritative" about.
Scope decision
The original audit framed a fix sub-goal as "return authoritative SetVirtualTag write-target
paths." Given finding (2), that goal is moot and explicitly dropped — the cascade feature is
dormant; no authoritative production key exists. This phase ships the genuinely-fixable, fully
live-verifiable core:
- Component 1 — receiver-type guard (correctness)
- Component 2 — truthful
SetVirtualTaghover (honesty)
Both AdminUI-only. No runtime, Commons/wire/proto, or EF change. No bUnit.
Component 1 — receiver-type guard
In TryGetTagPathLiteral, after the member-access (ma) and method-name checks, require the
receiver to be the identifier ctx:
if (ma.Expression is not IdentifierNameSyntax { Identifier.ValueText: "ctx" }) return false;
This deliberately mirrors the runtime regex (syntactic ctx anchor) so the editor accepts EXACTLY
what Phase7Composer harvests — not a semantic "is the receiver a ScriptContext" check, which
could accept a differently-named local that the runtime regex would never harvest. The guard lives
in the single shared gate, so it fixes both Complete and Hover at once. Effect: foo.GetTag("…")
no longer offers tag completions/hover; only ctx.GetTag(…) / ctx.SetVirtualTag(…) do.
Component 2 — truthful SetVirtualTag
TryGetTagPathLiteral surfaces WHICH method matched (a small out-param / enum, e.g.
ScriptTagMethod { GetTag, SetVirtualTag }). The Hover path, when the matched method is
SetVirtualTag, appends a clear note to the hover markdown:
⚠ Cross-tag
SetVirtualTagwrites are currently dropped in single-tag mode (the cascade engine is not wired into the host); this write will not take effect at runtime.
So an author is no longer silently misled into relying on a no-op. The completion list still helps
type a path (the truthfulness signal is the hover). Restricting SetVirtualTag suggestions to
virtual-tag targets only is a possible refinement left to the implementation plan — NOT a hard
requirement — because the dormant-engine write-target key is itself unverified (and the whole write
is dropped), so over-engineering the suggestion set adds risk without runtime payoff.
Error handling
Unchanged. TryGetTagPathLiteral already returns false cleanly on every non-match; the receiver
guard is one more early return false. The method-match out-param is only read when the gate returns
true.
Testing
xUnit + Shouldly in the existing AdminUI test project, exercising ScriptAnalysisService.Complete /
Hover directly (NO bUnit — these are service-level, not Razor):
ctx.GetTag("→ tag-path completions offered.foo.GetTag("/something.SetVirtualTag("→ tag-path completions NOT offered (receiver guard).- Hover on a
ctx.SetVirtualTag("…")literal → markdown carries the single-tag-drop note. - Hover on a
ctx.GetTag("…")literal → unchanged (no spurious note).
Live /run on the docker-dev rig (login-disabled; Chrome-driven against http://localhost:9200
ScriptEdit page + inline vtag modal Monaco): confirm ctx.GetTag(" shows completions, a non-ctx
receiver does not, and the SetVirtualTag hover shows the truthful note.
Deferred / out of scope
- "Authoritative
SetVirtualTagwrite-target key" — moot (cascade engine dormant; production write is a no-op). - UNS browse-path as a non-inserted completion detail (the catalog's own documented discoverability follow-up) — not in this phase.
- Wiring the cross-tag cascade engine into the host (the real fix for making
SetVirtualTagfunctional) — a much larger, separate effort.
Done =
Build clean + dotnet test (AdminUI) green + live /run confirms the three behaviors → merge to
master + push.