fix(adminui): bound the diagnostics marker payload on both the endpoint and the circuit (D-a review r1)

I-1 - a non-C# paste produced thousands of Roslyn markers. The Problems-list report sent
them all as one JS->.NET message, which could exceed the 1 MB hub limit and kill the
circuit. The JS now sends at most MaxReportedMarkers (200) markers, errors first, each
message cut to 300 chars, plus the total. ScriptProblemReport.FromEditor clamps again
on arrival, and the Problems list shows "showing N of Total" / "+N more". The diagnostics
endpoint also bounds its response: Distinct, errors first, MaxDiagnosticMarkers (1000),
messages cut to 1000 chars. An error is always kept, so editor-accepts == publish-accepts.

M-1 - drop the per-instance compilation lineage; analyses derive from BaseCompilation.
M-2 - the VirtualTag modal clears its problems (and editor ref) on close and on a script
reload.
M-5 - an over-limit text is announced at blur (OnSyncRefused), not only at Save.

Refs #745

Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 17:51:35 -04:00
parent 17c7d05696
commit a9e8f5abfe
10 changed files with 305 additions and 34 deletions
@@ -50,5 +50,54 @@ public static class ScriptProblems
return string.Join(", ", parts);
}
/// <summary>A one-line count for a (possibly capped) report, e.g. <c>"200 errors (showing 200 of 4321)"</c>.</summary>
/// <param name="report">The report the editor sent.</param>
/// <returns>The summary of the shown markers, with the shown/total suffix when some were left out.</returns>
public static string Summary(ScriptProblemReport report)
{
var summary = Summary(report.Markers.ToList());
return report.Omitted > 0 ? $"{summary} (showing {report.Markers.Count} of {report.Total})" : summary;
}
/// <summary>Cuts <paramref name="text"/> to at most <paramref name="maxChars"/> characters, ending in an ellipsis.</summary>
/// <param name="text">The text.</param>
/// <param name="maxChars">The maximum length, ellipsis included (at least 1).</param>
/// <returns>The text, unchanged when it already fits.</returns>
public static string Truncate(string text, int maxChars)
=> text.Length <= maxChars ? text : string.Concat(text.AsSpan(0, Math.Max(0, maxChars - 1)), "…");
private static string Plural(int n, string noun) => n == 1 ? $"1 {noun}" : $"{n} {noun}s";
}
/// <summary>
/// What the editor reported for the Problems list: at most <see cref="ScriptEditorLimits.MaxReportedMarkers"/>
/// markers in display order (errors first) plus the <see cref="Total"/> the diagnostics pass produced. The JS caps
/// the report before it crosses the circuit — one hub message, bounded by the 1 MB receive limit — and
/// <see cref="FromEditor"/> clamps it again on arrival.
/// </summary>
/// <param name="Markers">The markers shown.</param>
/// <param name="Total">How many markers the diagnostics pass produced (≥ <c>Markers.Count</c>).</param>
public sealed record ScriptProblemReport(IReadOnlyList<DiagnosticMarker> Markers, int Total)
{
/// <summary>No problems.</summary>
public static readonly ScriptProblemReport Empty = new(Array.Empty<DiagnosticMarker>(), 0);
/// <summary>How many markers were left out of <see cref="Markers"/>.</summary>
public int Omitted => Math.Max(0, Total - Markers.Count);
/// <summary>Builds a report from what the editor JS sent, re-applying the circuit caps defensively.</summary>
/// <param name="markers">The markers sent (already capped by the JS).</param>
/// <param name="total">The total the JS counted.</param>
/// <returns>The clamped report.</returns>
public static ScriptProblemReport FromEditor(IReadOnlyList<DiagnosticMarker>? markers, int total)
{
if (markers is null || markers.Count == 0) return total <= 0 ? Empty : new(Array.Empty<DiagnosticMarker>(), total);
var shown = ScriptProblems.Order(markers)
.Take(ScriptEditorLimits.MaxReportedMarkers)
.Select(m => m.Message.Length <= ScriptEditorLimits.MaxReportedMessageChars
? m
: m with { Message = ScriptProblems.Truncate(m.Message, ScriptEditorLimits.MaxReportedMessageChars) })
.ToList();
return new ScriptProblemReport(shown, Math.Max(total, markers.Count));
}
}