feat(flow-debugger): predict the reset write + echo wait of the reset-aware handshake (#36)

Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV
This commit is contained in:
Joseph Doherty
2026-09-10 15:18:59 -04:00
parent f7504a9499
commit 13f9f73033
2 changed files with 64 additions and 1 deletions
@@ -45,6 +45,8 @@ public sealed record PredictedFlow(IReadOnlyList<PredictedNode> Nodes, IReadOnly
/// script API exposes (<c>Route.To(x).Call(y)</c>, <c>Scripts.Call(y)</c>,
/// <c>….Attributes.WriteBatchAndWaitAsync(…)</c>, <c>….Attributes[x] = v</c>, …) by text with
/// whitespace stripped, exactly as <see cref="ScriptTrustValidator"/>'s hardening walker does.
/// The nine-argument reset-aware <c>WriteBatchAndWaitAsync</c> overload (Gitea #36) additionally
/// predicts a reset attribute write + echo wait ahead of the batch write + wait pair.
/// String-literal arguments resolve to titles; anything else is flagged
/// <see cref="PredictedNode.InputDependent"/>. Branching is not modelled: every recognised call
/// is emitted once in source order, and an <c>if (…) return …;</c> guard that precedes the next
@@ -369,9 +371,18 @@ public static class FlowShapePredictor
{
case "WriteBatchAndWaitAsync":
{
// (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout)
// (batch, triggerAttr, triggerValue, waitAttr, waitValue, timeout
// [, resetAttr, resetValue, resetTimeout]) — Gitea #36: the reset-aware
// overload writes + waits on the reset attribute BEFORE the batch.
var (trigger, triggerDependent) = LiteralOrExpr(args.ElementAtOrDefault(1)?.Expression);
var (wait, waitDependent) = LiteralOrExpr(args.ElementAtOrDefault(3)?.Expression);
var resetArg = ArgAt(args, 6, "resetKey");
if (resetArg is not null)
{
var (reset, resetDependent) = LiteralOrExpr(resetArg.Expression);
_b.AddNode(FlowStepKind.AttributeWrite, FlowLane.Tags, Qualify(reset), resetDependent, "reset");
_b.AddNode(FlowStepKind.AttributeWait, FlowLane.Site, Qualify(reset), resetDependent, "reset echo");
}
_b.AddNode(FlowStepKind.AttributeWrite, FlowLane.Tags, Qualify($"batch + {trigger}"), triggerDependent, null);
_b.AddNode(FlowStepKind.AttributeWait, FlowLane.Site, Qualify(wait), waitDependent, null);
return true;
@@ -463,6 +474,16 @@ public static class FlowShapePredictor
private static string TrimAsyncSuffix(string member)
=> member.Length > 5 && member.EndsWith("Async", StringComparison.Ordinal) ? member[..^5] : member;
/// <summary>An argument by name (<c>name: expr</c>) or, failing that, by position among the unnamed arguments.</summary>
private static ArgumentSyntax? ArgAt(SeparatedSyntaxList<ArgumentSyntax> args, int index, string name)
{
var named = args.FirstOrDefault(a => a.NameColon?.Name.Identifier.ValueText == name);
if (named is not null)
return named;
var positional = args.ElementAtOrDefault(index);
return positional is { NameColon: null } ? positional : null;
}
/// <summary>A string literal resolves to its value; anything else is input-dependent and shown as its source text.</summary>
private static (string Text, bool Dependent) LiteralOrExpr(ExpressionSyntax? expr)
{