diff --git a/app/MindWork AI Studio/Chat/ChartBlockParser.cs b/app/MindWork AI Studio/Chat/ChartBlockParser.cs
new file mode 100644
index 00000000..c68ecba1
--- /dev/null
+++ b/app/MindWork AI Studio/Chat/ChartBlockParser.cs
@@ -0,0 +1,267 @@
+using System.Text;
+using System.Text.Json;
+
+namespace AIStudio.Chat;
+
+///
+/// Parses and validates versioned AI Studio chart blocks without executing their content.
+///
+public static class ChartBlockParser
+{
+ private const int MAX_JSON_BYTES = 32 * 1024;
+ private const int MAX_CATEGORIES = 50;
+ private const int MAX_SERIES = 10;
+
+ private static readonly HashSet ROOT_PROPERTIES = ["schema_version", "type", "title", "caption", "data"];
+ private static readonly HashSet DATA_PROPERTIES = ["categories", "series"];
+ private static readonly HashSet SERIES_PROPERTIES = ["name", "values"];
+
+ ///
+ /// Parses a JSON chart definition and applies the complete local version 1 validation contract.
+ ///
+ public static ChartBlockParseResult Parse(string json)
+ {
+ if (Encoding.UTF8.GetByteCount(json) > MAX_JSON_BYTES)
+ return ChartBlockParseResult.Invalid(json, "The chart JSON exceeds the 32 KB limit.");
+
+ try
+ {
+ using var document = JsonDocument.Parse(json, new JsonDocumentOptions
+ {
+ AllowTrailingCommas = false,
+ CommentHandling = JsonCommentHandling.Disallow,
+ MaxDepth = 8,
+ });
+
+ var root = document.RootElement;
+ if (root.ValueKind is not JsonValueKind.Object)
+ return ChartBlockParseResult.Invalid(json, "The chart definition must be a JSON object.");
+
+ if (!HasOnlyKnownProperties(root, ROOT_PROPERTIES, out var propertyError))
+ return ChartBlockParseResult.Invalid(json, propertyError);
+
+ if (!TryGetRequiredInt(root, "schema_version", out var schemaVersion) || schemaVersion != 1)
+ return ChartBlockParseResult.Invalid(json, "schema_version must be 1.");
+
+ if (!TryGetRequiredString(root, "type", out var typeText)
+ || !TryParseType(typeText, out var type))
+ return ChartBlockParseResult.Invalid(json, "type must be bar, stacked_bar, line, pie, or donut.");
+
+ if (!TryGetRequiredString(root, "title", out var title) || string.IsNullOrWhiteSpace(title))
+ return ChartBlockParseResult.Invalid(json, "title must be a non-empty string.");
+
+ string? caption = null;
+ if (root.TryGetProperty("caption", out var captionElement))
+ {
+ if (captionElement.ValueKind is not JsonValueKind.String
+ || string.IsNullOrWhiteSpace(captionElement.GetString()))
+ return ChartBlockParseResult.Invalid(json, "caption must be a non-empty string when provided.");
+
+ caption = captionElement.GetString();
+ }
+
+ if (!root.TryGetProperty("data", out var data) || data.ValueKind is not JsonValueKind.Object)
+ return ChartBlockParseResult.Invalid(json, "data must be a JSON object.");
+
+ if (!HasOnlyKnownProperties(data, DATA_PROPERTIES, out propertyError))
+ return ChartBlockParseResult.Invalid(json, propertyError);
+
+ if (!TryReadCategories(data, out var categories, out var error))
+ return ChartBlockParseResult.Invalid(json, error);
+
+ if (!TryReadSeries(data, categories.Count, out var series, out error))
+ return ChartBlockParseResult.Invalid(json, error);
+
+ if (type is ChartDefinitionType.PIE or ChartDefinitionType.DONUT)
+ {
+ if (series.Count != 1)
+ return ChartBlockParseResult.Invalid(json, "Pie and donut charts require exactly one series.");
+
+ if (series[0].Values.Any(value => value < 0))
+ return ChartBlockParseResult.Invalid(json, "Pie and donut chart values must not be negative.");
+ }
+
+ return ChartBlockParseResult.Valid(json, new(schemaVersion, type, title, caption, categories, series));
+ }
+ catch (JsonException exception)
+ {
+ return ChartBlockParseResult.Invalid(json, $"The chart JSON is invalid: {exception.Message}");
+ }
+ }
+
+ private static bool TryReadCategories(JsonElement data, out IReadOnlyList categories, out string error)
+ {
+ categories = [];
+ error = string.Empty;
+ if (!data.TryGetProperty("categories", out var element) || element.ValueKind is not JsonValueKind.Array)
+ {
+ error = "data.categories must be an array.";
+ return false;
+ }
+
+ var values = new List();
+ foreach (var item in element.EnumerateArray())
+ {
+ if (item.ValueKind is not JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetString()))
+ {
+ error = "Every category must be a non-empty string.";
+ return false;
+ }
+
+ values.Add(item.GetString()!);
+ if (values.Count > MAX_CATEGORIES)
+ {
+ error = $"A chart can contain at most {MAX_CATEGORIES} categories.";
+ return false;
+ }
+ }
+
+ if (values.Count == 0)
+ {
+ error = "A chart requires at least one category.";
+ return false;
+ }
+
+ categories = values;
+ return true;
+ }
+
+ private static bool TryReadSeries(JsonElement data, int categoryCount, out IReadOnlyList series, out string error)
+ {
+ series = [];
+ error = string.Empty;
+ if (!data.TryGetProperty("series", out var element) || element.ValueKind is not JsonValueKind.Array)
+ {
+ error = "data.series must be an array.";
+ return false;
+ }
+
+ var result = new List();
+ foreach (var item in element.EnumerateArray())
+ {
+ if (item.ValueKind is not JsonValueKind.Object)
+ {
+ error = "Every series must be a JSON object.";
+ return false;
+ }
+
+ if (!HasOnlyKnownProperties(item, SERIES_PROPERTIES, out error))
+ return false;
+
+ if (!TryGetRequiredString(item, "name", out var name) || string.IsNullOrWhiteSpace(name))
+ {
+ error = "Every series requires a non-empty name.";
+ return false;
+ }
+
+ if (!item.TryGetProperty("values", out var valuesElement) || valuesElement.ValueKind is not JsonValueKind.Array)
+ {
+ error = "Every series requires a values array.";
+ return false;
+ }
+
+ var values = new List();
+ foreach (var valueElement in valuesElement.EnumerateArray())
+ {
+ if (valueElement.ValueKind is not JsonValueKind.Number
+ || !valueElement.TryGetDouble(out var value)
+ || !double.IsFinite(value))
+ {
+ error = "Series values must be finite JSON numbers.";
+ return false;
+ }
+
+ values.Add(value);
+ }
+
+ if (values.Count != categoryCount)
+ {
+ error = "Every series must contain exactly one value per category.";
+ return false;
+ }
+
+ result.Add(new(name, values));
+ if (result.Count > MAX_SERIES)
+ {
+ error = $"A chart can contain at most {MAX_SERIES} series.";
+ return false;
+ }
+ }
+
+ if (result.Count == 0)
+ {
+ error = "A chart requires at least one series.";
+ return false;
+ }
+
+ series = result;
+ return true;
+ }
+
+ private static bool HasOnlyKnownProperties(JsonElement element, HashSet knownProperties, out string error)
+ {
+ var seenProperties = new HashSet(StringComparer.Ordinal);
+ foreach (var property in element.EnumerateObject())
+ {
+ if (!knownProperties.Contains(property.Name))
+ {
+ error = $"Unknown chart property: {property.Name}.";
+ return false;
+ }
+
+ if (!seenProperties.Add(property.Name))
+ {
+ error = $"Duplicate chart property: {property.Name}.";
+ return false;
+ }
+ }
+
+ error = string.Empty;
+ return true;
+ }
+
+ private static bool TryGetRequiredInt(JsonElement element, string propertyName, out int value)
+ {
+ value = 0;
+ return element.TryGetProperty(propertyName, out var property)
+ && property.ValueKind is JsonValueKind.Number
+ && property.TryGetInt32(out value);
+ }
+
+ private static bool TryGetRequiredString(JsonElement element, string propertyName, out string value)
+ {
+ value = string.Empty;
+ if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind is not JsonValueKind.String)
+ return false;
+
+ value = property.GetString() ?? string.Empty;
+ return true;
+ }
+
+ private static bool TryParseType(string value, out ChartDefinitionType type)
+ {
+ type = value switch
+ {
+ "bar" => ChartDefinitionType.BAR,
+ "stacked_bar" => ChartDefinitionType.STACKED_BAR,
+ "line" => ChartDefinitionType.LINE,
+ "pie" => ChartDefinitionType.PIE,
+ "donut" => ChartDefinitionType.DONUT,
+ _ => default,
+ };
+
+ return value is "bar" or "stacked_bar" or "line" or "pie" or "donut";
+ }
+}
+
+///
+/// The safe result of parsing a chart block, including the original JSON for fallback display.
+///
+public sealed record ChartBlockParseResult(string RawJson, ChartDefinition? Chart, string Error)
+{
+ public bool IsValid => this.Chart is not null;
+
+ public static ChartBlockParseResult Valid(string rawJson, ChartDefinition chart) => new(rawJson, chart, string.Empty);
+
+ public static ChartBlockParseResult Invalid(string rawJson, string error) => new(rawJson, null, error);
+}