fix(flow-debugger): final-review remediation — 2000-row trace pull, shared ExceptionHeader parser, surrogate-safe clip, wording, alarm source, register renumber (I1/I2/M1-M8)

This commit is contained in:
Joseph Doherty
2026-09-09 13:30:48 -04:00
parent 0bacc3630e
commit 97f01ae98e
21 changed files with 462 additions and 71 deletions
@@ -0,0 +1,62 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
/// <summary>
/// Flow Debugger (final review M1): the ONE parser for the header line of a captured
/// exception detail. A .NET <c>Exception.ToString()</c> opens with
/// <c>Namespace.Type: message</c>, and a wrapper (an <c>AggregateException</c>, a rethrow)
/// chains its inner exceptions on that same first line with <c>" ---&gt; "</c>.
///
/// <para>
/// Two surfaces render that header — the graph node's subtitle (namespace stripped, so the
/// badge stays narrow) and the step detail panel's exception block (type in full) — and they
/// had drifted: the panel showed the OUTERMOST exception while the node showed the innermost.
/// Both now call this, so they always agree on which exception is being named.
/// </para>
/// </summary>
public static class ExceptionHeader
{
private const string InnerMarker = " ---> ";
/// <summary>
/// Parses the innermost exception's type and message out of <paramref name="detail"/>.
/// Takes the first line, follows the <c>" ---&gt; "</c> chain to its LAST segment (the
/// innermost exception, which is the one a script author has to act on) and splits it on
/// the first <c>": "</c>.
/// </summary>
/// <param name="detail">The captured detail (a full <c>ToString()</c>), or null.</param>
/// <param name="shortenType">
/// When true the namespace is stripped from the head of the type name — only the head,
/// so a generic arity/argument suffix (<c>`1[System.String]</c>) is kept verbatim.
/// </param>
/// <returns>
/// The type and message; the whole line as the type with an EMPTY message when the line
/// carries no <c>": "</c>; null when there is no detail, or nothing left after the chain
/// is followed (callers fall back to their own error summary in both those cases).
/// </returns>
public static (string Type, string Message)? Parse(string? detail, bool shortenType)
{
if (string.IsNullOrWhiteSpace(detail)) return null;
var first = detail.Split('\n', 2)[0].TrimEnd('\r').Trim();
var inner = first.LastIndexOf(InnerMarker, StringComparison.Ordinal);
if (inner >= 0) first = first[(inner + InnerMarker.Length)..].Trim();
if (first.Length == 0) return null;
var split = first.IndexOf(": ", StringComparison.Ordinal);
if (split < 0) return (first, string.Empty);
var type = first[..split];
var message = first[(split + 2)..];
if (!shortenType) return (type, message);
// Shorten only the leading namespace: everything from a generic marker on is part of the name.
var generic = type.IndexOfAny(['`', '[']);
var head = generic < 0 ? type : type[..generic];
var dot = head.LastIndexOf('.');
if (dot >= 0 && dot < head.Length - 1)
{
type = head[(dot + 1)..] + (generic < 0 ? string.Empty : type[generic..]);
}
return (type, message);
}
}