fix(focas): refuse String on PMC/PARAM/MACRO in the factory, editor and CLI (R07:U8)

FocasDataType.String was authorable on any address, but no area read returns text: PMC
fell through to a byte read, PARAM to Int32, MACRO to a double, and the String node
received a boxed number at Good.

One rule, FocasTagDefinitionFactory.DescribeDataTypeMismatch, now backs every path:
- FromTagConfig rejects the pair, so the driver skips + reports it (BadNodeIdUnknown,
  never a Good number) and Inspect names the reason at deploy;
- FocasTagConfigModel.Validate refuses it on the typed fields;
- the AdminUI editor only offers String where it is readable and shows the rule's text
  inline (kept visible for a loaded blob that already carries it);
- the CLI read/subscribe/probe/write commands refuse it at the boundary.
String stays valid on FIXED: paths (Identity/Program name/mode text) and is ignored on
ALARM: tags; an unparseable address keeps its own diagnosis.

Razor change (FocasTagConfigEditor.razor) needs the Wave 4 live check.

Refs #740
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 14:15:35 -04:00
parent 6423ed272f
commit cc140593a0
11 changed files with 214 additions and 5 deletions
@@ -15,8 +15,22 @@
</div>
<div class="col-md-3"><label class="form-label">Data type</label>
<select class="form-select form-select-sm" value="@_m.DataType" @onchange="@(e => Update(() => _m.DataType = ParseEnum(e.Value, FocasDataType.Int32)))">
@foreach (var v in Enum.GetValues<FocasDataType>()) { <option value="@v">@v</option> }
</select></div>
@foreach (var v in Enum.GetValues<FocasDataType>())
{
@* String is only readable from the fixed tree (FocasTagDefinitionFactory.DescribeDataTypeMismatch):
offered there, and kept visible when a loaded blob already carries it so the select never lies. *@
if (v != FocasDataType.String || _m.DataType == FocasDataType.String
|| FocasTagDefinitionFactory.DescribeDataTypeMismatch(v, _m.Address) is null)
{
<option value="@v">@v</option>
}
}
</select>
@if (FocasTagDefinitionFactory.DescribeDataTypeMismatch(_m.DataType, _m.Address) is { } typeError)
{
<div class="form-text text-danger small">@typeError</div>
}
</div>
<div class="col-12">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="focas-writable" disabled checked="false" />
@@ -80,6 +80,10 @@ public sealed class FocasTagConfigModel
// ahead of the parse consult below and is never replaced by it.
if (FocasAddress.TryParse(Address) is null) return DescribeUnparseableAddress(Address);
// The driver's own address/data-type rule (String only on FIXED: paths) — checked on the typed fields so a
// model built in the editor is judged too, not only a loaded blob the parse gate below can see.
if (FocasTagDefinitionFactory.DescribeDataTypeMismatch(DataType, Address) is { } typeError) return typeError;
// Parse-gate backstop. ⚠️ It gates on the PARSE VERDICT, never on Inspect being non-empty: FOCAS's
// inspector emits advisories for blobs the reader ACCEPTS (an unparseable address, an authored
// 'writable':true on a read-only wire), and promoting those to authoring errors would make the modal