diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Analyzer/Emission/CanonicalJsonFragmentParser.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Analyzer/Emission/CanonicalJsonFragmentParser.cs
new file mode 100644
index 000000000..0050f5c9e
--- /dev/null
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Analyzer/Emission/CanonicalJsonFragmentParser.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace StellaOps.Workflow.Analyzer.Emission;
+
+///
+/// Minimal recursive-descent JSON parser that produces nodes
+/// matching the polymorphic $type discriminators used in the runtime contract and in
+/// serialised canonical fragments.
+///
+///
+///
+/// This parser is intentionally narrow. It only accepts the shape that the canonical JSON
+/// writer itself emits: every expression-object has a $type field and the shape that
+/// type dictates (see WorkflowExpressionDefinition's JsonDerivedType attributes).
+/// Anything else throws .
+///
+///
+/// The parser runs inside the Roslyn source generator, which targets netstandard2.0 without
+/// System.Text.Json available. Hand-rolling keeps dependencies zero.
+///
+///
+internal static class CanonicalJsonFragmentParser
+{
+ public static CanonicalExpr Parse(string json)
+ {
+ var reader = new JsonReader(json);
+ reader.SkipWhitespace();
+ var value = ReadExpression(ref reader);
+ reader.SkipWhitespace();
+ if (!reader.IsAtEnd)
+ {
+ throw new FragmentParseException($"unexpected trailing content at position {reader.Position}");
+ }
+ return value;
+ }
+
+ private static CanonicalExpr ReadExpression(ref JsonReader reader)
+ {
+ reader.SkipWhitespace();
+ reader.Expect('{');
+ var members = new Dictionary(StringComparer.Ordinal);
+ var first = true;
+ while (true)
+ {
+ reader.SkipWhitespace();
+ if (reader.Peek() == '}')
+ {
+ reader.Advance();
+ break;
+ }
+ if (!first)
+ {
+ reader.Expect(',');
+ reader.SkipWhitespace();
+ }
+ var key = reader.ReadString();
+ reader.SkipWhitespace();
+ reader.Expect(':');
+ reader.SkipWhitespace();
+ members[key] = ReadValue(ref reader);
+ first = false;
+ }
+
+ if (!members.TryGetValue("$type", out var typeValue) || typeValue is not string typeName)
+ {
+ throw new FragmentParseException("expression object missing '$type' discriminator");
+ }
+
+ return typeName switch
+ {
+ "null" => new NullExpr(),
+ "string" => new StringExpr(RequireString(members, "value")),
+ "number" => new NumberExpr(RequireString(members, "value")),
+ "boolean" => new BoolExpr(RequireBool(members, "value")),
+ "path" => new PathExpr(RequireString(members, "path")),
+ "object" => new ObjectExpr(ReadProperties(RequireArray(members, "properties"))),
+ "array" => new ArrayExpr(CastItems(RequireArray(members, "items"))),
+ "function" => new FunctionExpr(
+ RequireString(members, "functionName"),
+ CastItems(RequireArray(members, "arguments"))),
+ "group" => new GroupExpr(RequireExpr(members, "expression")),
+ "unary" => new UnaryExpr(RequireString(members, "operator"), RequireExpr(members, "operand")),
+ "binary" => new BinaryExpr(
+ RequireString(members, "operator"),
+ RequireExpr(members, "left"),
+ RequireExpr(members, "right")),
+ _ => throw new FragmentParseException($"unsupported canonical expression $type '{typeName}'"),
+ };
+ }
+
+ private static object? ReadValue(ref JsonReader reader)
+ {
+ reader.SkipWhitespace();
+ var c = reader.Peek();
+ return c switch
+ {
+ '{' => ReadObjectOrExpression(ref reader),
+ '[' => ReadArray(ref reader),
+ '"' => reader.ReadString(),
+ 't' or 'f' => reader.ReadBoolLiteral(),
+ 'n' => reader.ReadNullLiteral(),
+ _ when c == '-' || (c >= '0' && c <= '9') => reader.ReadNumberLiteral(),
+ _ => throw new FragmentParseException($"unexpected character '{c}' at position {reader.Position}"),
+ };
+ }
+
+ ///
+ /// Either a canonical expression (when the object has $type) or a plain property
+ /// carrier (used for properties[] entries which are {name, expression}).
+ ///
+ private static object ReadObjectOrExpression(ref JsonReader reader)
+ {
+ var start = reader.Position;
+ reader.Advance(); // consume '{'
+ var members = new Dictionary(StringComparer.Ordinal);
+ var first = true;
+ while (true)
+ {
+ reader.SkipWhitespace();
+ if (reader.Peek() == '}')
+ {
+ reader.Advance();
+ break;
+ }
+ if (!first)
+ {
+ reader.Expect(',');
+ reader.SkipWhitespace();
+ }
+ var key = reader.ReadString();
+ reader.SkipWhitespace();
+ reader.Expect(':');
+ reader.SkipWhitespace();
+ members[key] = ReadValue(ref reader);
+ first = false;
+ }
+
+ // Canonical expression — has $type.
+ if (members.TryGetValue("$type", out var typeValue) && typeValue is string typeName)
+ {
+ return DispatchExpression(typeName, members, start);
+ }
+
+ // Plain carrier: either {"name": "...", "expression": {...}} (ObjectExpr property) or
+ // a free-form bag we hand back for the caller to interpret.
+ return members;
+ }
+
+ private static CanonicalExpr DispatchExpression(string typeName, Dictionary members, int startPos)
+ {
+ return typeName switch
+ {
+ "null" => new NullExpr(),
+ "string" => new StringExpr(RequireString(members, "value")),
+ "number" => new NumberExpr(RequireString(members, "value")),
+ "boolean" => new BoolExpr(RequireBool(members, "value")),
+ "path" => new PathExpr(RequireString(members, "path")),
+ "object" => new ObjectExpr(ReadProperties(RequireArray(members, "properties"))),
+ "array" => new ArrayExpr(CastItems(RequireArray(members, "items"))),
+ "function" => new FunctionExpr(
+ RequireString(members, "functionName"),
+ CastItems(RequireArray(members, "arguments"))),
+ "group" => new GroupExpr(RequireExpr(members, "expression")),
+ "unary" => new UnaryExpr(RequireString(members, "operator"), RequireExpr(members, "operand")),
+ "binary" => new BinaryExpr(
+ RequireString(members, "operator"),
+ RequireExpr(members, "left"),
+ RequireExpr(members, "right")),
+ _ => throw new FragmentParseException($"unsupported canonical expression $type '{typeName}' at position {startPos}"),
+ };
+ }
+
+ private static List