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
@@ -169,4 +169,26 @@ public sealed class FocasTagConfigModelTests
[InlineData("""{"address":"R100","dataType":2}""")]
public void Validate_accepts_every_dataType_the_driver_accepts(string json)
=> FocasTagConfigModel.FromJson(json).Validate().ShouldBeNull();
/// <summary>
/// #740 / R07:U8 — String on a PMC / PARAM / MACRO address is refused by the driver (no area read
/// returns text), so the modal refuses it too, for a model built in the editor and for a loaded blob.
/// </summary>
[Theory]
[InlineData("R100")]
[InlineData("PARAM:6711")]
[InlineData("MACRO:500")]
public void Validate_refuses_string_on_an_area_address(string address)
{
new FocasTagConfigModel { Address = address, DataType = FocasDataType.String }
.Validate().ShouldNotBeNull().ShouldContain("String");
FocasTagConfigModel.FromJson($$"""{"address":"{{address}}","dataType":"String"}""")
.Validate().ShouldNotBeNull().ShouldContain("String");
}
/// <summary>Positive control: the fixed tree's string fields keep String.</summary>
[Fact]
public void Validate_accepts_string_on_a_fixed_tree_path()
=> new FocasTagConfigModel { Address = "FIXED:Identity/SeriesNumber", DataType = FocasDataType.String }
.Validate().ShouldBeNull();
}