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
@@ -28,6 +28,7 @@ public sealed class ProbeCommand : FocasCommandBase
{
ConfigureLogging();
ValidateOptions();
ValidateDataType(Address, DataType);
var ct = console.RegisterCancellationHandler();
var probeTag = new FocasTagDefinition(
@@ -19,7 +19,7 @@ public sealed class ReadCommand : FocasCommandBase
/// <summary>Gets the data type to interpret the address as.</summary>
[CommandOption("type", 't', Description =
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
/// <inheritdoc />
@@ -28,6 +28,7 @@ public sealed class ReadCommand : FocasCommandBase
ConfigureLogging();
// Validate numeric option ranges before any driver work.
ValidateOptions();
ValidateDataType(Address, DataType);
var ct = console.RegisterCancellationHandler();
var tagName = SynthesiseTagName(Address, DataType);
@@ -18,7 +18,7 @@ public sealed class SubscribeCommand : FocasCommandBase
/// <summary>Gets the data type of the address.</summary>
[CommandOption("type", 't', Description =
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
/// <summary>Gets the polling interval in milliseconds.</summary>
@@ -33,6 +33,7 @@ public sealed class SubscribeCommand : FocasCommandBase
// --interval-ms) before any driver work so a zero/negative interval surfaces as a
// clean CommandException rather than a tight-spinning poll loop.
ValidateOptions(IntervalMs);
ValidateDataType(Address, DataType);
var ct = console.RegisterCancellationHandler();
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
@@ -20,7 +20,7 @@ public sealed class WriteCommand : FocasCommandBase
/// <summary>Gets the data type of the value to write.</summary>
[CommandOption("type", 't', Description =
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")]
"Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16). String only on FIXED: paths.")]
public FocasDataType DataType { get; init; } = FocasDataType.Int16;
/// <summary>Gets the value to write.</summary>
@@ -37,6 +37,7 @@ public sealed class WriteCommand : FocasCommandBase
// port/timeout surfaces as a clean CommandException rather than an opaque
// downstream exception.
ValidateOptions();
ValidateDataType(Address, DataType);
var ct = console.RegisterCancellationHandler();
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
@@ -89,4 +89,17 @@ public abstract class FocasCommandBase : DriverCommandBase
throw new CliFx.Exceptions.CommandException(
$"--interval-ms must be positive (got {iv}).");
}
/// <summary>
/// Refuses an <c>--address</c> / <c>--type</c> pair the driver cannot serve (String on a PMC / PARAM / MACRO
/// address — the driver skips such a tag, so the command would only ever print BadNodeIdUnknown). Same
/// rule as the driver and the AdminUI editor: <see cref="FocasTagDefinitionFactory.DescribeDataTypeMismatch"/>.
/// </summary>
/// <param name="address">The FOCAS address.</param>
/// <param name="type">The data type.</param>
protected static void ValidateDataType(string address, FocasDataType type)
{
if (FocasTagDefinitionFactory.DescribeDataTypeMismatch(type, address) is { } error)
throw new CliFx.Exceptions.CommandException(error);
}
}