diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Bindings.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Bindings.cs
index 4c857fddf..7714c4353 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Bindings.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Bindings.cs
@@ -204,7 +204,7 @@ public sealed partial class VisualBriefingArtifactService
return context;
if (path is "$index")
- return JsonSerializer.SerializeToElement(0);
+ return JsonSerializer.SerializeToElement(0, JSON_OPTIONS);
if (path.StartsWith(".", StringComparison.Ordinal))
return GetDataAtPath(context, path[1..]);
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingCanonicalJsonContext.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingCanonicalJsonContext.cs
new file mode 100644
index 000000000..6bad49573
--- /dev/null
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingCanonicalJsonContext.cs
@@ -0,0 +1,62 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Assistants.VisualBriefing;
+
+///
+/// The contracts with which System.Text.Json writes and reads the canonical JSON of visual briefings, built at
+/// compile time.
+///
+///
+/// Much of what these contracts write goes into the hashes of stored briefings, see VisualBriefingJson.Canonical.
+/// The hashes of the artifacts are generic: every list they hash needs a contract of its own. A test fails for a type
+/// which is missing here.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+
+// The header and the data of an exported briefing, and the build which the progress view shows:
+[JsonSerializable(typeof(VisualBriefingExportManifest))]
+[JsonSerializable(typeof(VisualBriefingBuildRecord))]
+[JsonSerializable(typeof(VisualBriefingCompiledData))]
+[JsonSerializable(typeof(VisualBriefingContentData))]
+[JsonSerializable(typeof(VisualBriefingInteractionData))]
+[JsonSerializable(typeof(VisualBriefingProtectedData))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(JsonElement))]
+[JsonSerializable(typeof(JsonObject))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(int))]
+
+// The payload hashes of the artifacts:
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(Dictionary))]
+[JsonSerializable(typeof(Dictionary>))]
+[JsonSerializable(typeof(VisualBriefingLayoutNode))]
+
+// The prompts of the stages:
+[JsonSerializable(typeof(IEnumerable))]
+[JsonSerializable(typeof(string[]))]
+[JsonSerializable(typeof(VisualBriefingRequiredSlot[]))]
+[JsonSerializable(typeof(VisualBriefingControlRequirement[]))]
+[JsonSerializable(typeof(VisualBriefingEvidencePrompt))]
+[JsonSerializable(typeof(VisualBriefingParentDesign))]
+
+// The answers of the models:
+[JsonSerializable(typeof(VisualBriefingEvidenceResponse))]
+[JsonSerializable(typeof(VisualBriefingPlanResponse))]
+[JsonSerializable(typeof(VisualBriefingContentResponse))]
+[JsonSerializable(typeof(VisualBriefingDesignResponse))]
+
+// The fingerprint which decides whether the editor holds unsaved changes:
+[JsonSerializable(typeof(VisualBriefingPersistenceFingerprint))]
+internal sealed partial class VisualBriefingCanonicalJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingJson.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingJson.cs
index b6a181774..c1a8885b2 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingJson.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingJson.cs
@@ -28,7 +28,9 @@ internal static class VisualBriefingJson
/// and a mismatch makes the store discard the stored artifact. Any change here — a converter, a
/// naming policy, an encoder — therefore invalidates every briefing that was ever written, which
/// surfaces as a failed integrity check rather than as a build error. This is why enums stay numeric
- /// here even though the persisted manifest writes their member names.
+ /// here even though the persisted manifest writes their member names. Unlike other options of the app,
+ /// these are not made read-only right here, because MWAIS0010 wants nothing but the object creation;
+ /// System.Text.Json locks them with their first use anyway.
///
[CanonicalJsonConfiguration]
internal static JsonSerializerOptions Canonical { get; } = new()
@@ -38,6 +40,7 @@ internal static class VisualBriefingJson
WriteIndented = false,
Encoder = JavaScriptEncoder.Default,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
+ TypeInfoResolver = VisualBriefingCanonicalJsonContext.Default,
};
///
@@ -52,7 +55,7 @@ internal static class VisualBriefingJson
/// defined outside the feature, such as the target language and the audience enums. Reading accepts
/// numbers as well, so manifests written before this distinction existed keep loading.
///
- internal static JsonSerializerOptions Persistence { get; } = new()
+ internal static JsonSerializerOptions Persistence { get; } = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = false,
@@ -60,5 +63,6 @@ internal static class VisualBriefingJson
Encoder = JavaScriptEncoder.Default,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
Converters = { new JsonStringEnumConverter() },
- };
+ TypeInfoResolver = VisualBriefingPersistenceJsonContext.Default,
+ }.AsReadOnly();
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPersistenceJsonContext.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPersistenceJsonContext.cs
new file mode 100644
index 000000000..b7e92b637
--- /dev/null
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPersistenceJsonContext.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Assistants.VisualBriefing;
+
+///
+/// The contracts with which System.Text.Json reads and writes the files of visual briefings, built at compile time.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+[JsonSerializable(typeof(VisualBriefingManifest))]
+[JsonSerializable(typeof(VisualBriefingEvidenceArtifact))]
+[JsonSerializable(typeof(VisualBriefingPlanArtifact))]
+[JsonSerializable(typeof(VisualBriefingContentArtifact))]
+[JsonSerializable(typeof(VisualBriefingPresentationArtifact))]
+[JsonSerializable(typeof(VisualBriefingBuildRecord))]
+[JsonSerializable(typeof(VisualBriefingOperationDiagnostics))]
+
+// The briefing selected last:
+[JsonSerializable(typeof(Guid?))]
+internal sealed partial class VisualBriefingPersistenceJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Projects.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Projects.cs
index d17986e4b..5bab50640 100644
--- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Projects.cs
+++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingStore.Projects.cs
@@ -24,7 +24,7 @@ public sealed partial class VisualBriefingStore
try
{
this.LastSelectedBriefingId = briefingId;
- await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize(briefingId), overwrite: true, token);
+ await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize(briefingId, JSON_OPTIONS), overwrite: true, token);
}
finally
{
@@ -47,7 +47,7 @@ public sealed partial class VisualBriefingStore
return;
this.LastSelectedBriefingId = null;
- await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize(null), overwrite: true, token);
+ await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize(null, JSON_OPTIONS), overwrite: true, token);
}
finally
{
@@ -67,7 +67,7 @@ public sealed partial class VisualBriefingStore
try
{
var serialized = await File.ReadAllTextAsync(path, token);
- var selected = JsonSerializer.Deserialize(serialized);
+ var selected = JsonSerializer.Deserialize(serialized, JSON_OPTIONS);
this.LastSelectedBriefingId = selected is not null &&
Directory.Exists(this.BriefingDirectory(selected.Value))
? selected
diff --git a/app/Tests/Serialization/JsonCoverageTests.cs b/app/Tests/Serialization/JsonCoverageTests.cs
index 716585793..8c5e8d46b 100644
--- a/app/Tests/Serialization/JsonCoverageTests.cs
+++ b/app/Tests/Serialization/JsonCoverageTests.cs
@@ -20,6 +20,9 @@ public sealed class JsonCoverageTests
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceRequests))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ServiceAnswers))]
+ [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingStored))]
+ [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingHashed))]
+ [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))]
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
{
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
diff --git a/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs b/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs
index 2b4f319ac..735725f8b 100644
--- a/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs
+++ b/app/Tests/Serialization/JsonTestRoots.VisualBriefing.cs
@@ -1,4 +1,5 @@
using System.Text.Json;
+using System.Text.Json.Nodes;
using AIStudio.Assistants.VisualBriefing;
@@ -22,8 +23,8 @@ public static partial class JsonTestRoots
yield return Case(new(typeof(VisualBriefingPresentationArtifact), options));
yield return Case(new(typeof(VisualBriefingBuildRecord), options));
- // The briefing selected last, which gets written without options of its own:
- yield return Case(new(typeof(Guid?), JsonSerializerOptions.Default), "selected briefing");
+ // The briefing selected last:
+ yield return Case(new(typeof(Guid?), options), "selected briefing");
//
// The canonical JSON which gets read back: the header and the data of an exported briefing, the version
@@ -60,12 +61,28 @@ public static partial class JsonTestRoots
yield return Case(new(typeof(Dictionary>), options));
yield return Case(new(typeof(VisualBriefingLayoutNode), options));
- // The data of a briefing, and the IDs the prompts list:
+ // The data of a briefing, with what the compilers and the stages put into it:
yield return Case(new(typeof(JsonElement), options));
- yield return Case(new(typeof(string[]), options));
+ yield return Case(new(typeof(JsonObject), options));
+ yield return Case(new(typeof(VisualBriefingCompiledData), options));
+ yield return Case(new(typeof(VisualBriefingContentData), options));
+ yield return Case(new(typeof(VisualBriefingInteractionData), options));
+ yield return Case(new(typeof(VisualBriefingProtectedData), options));
+ yield return Case(new(typeof(string), options), "value of a control");
- // The position inside a repeated element, which the data bindings write without options of their own:
- yield return Case(new(typeof(int), JsonSerializerOptions.Default), "index");
+ // The position inside a repeated element, which the data bindings write:
+ yield return Case(new(typeof(int), options), "index");
+
+ // What the prompts of the stages carry:
+ yield return Case(new(typeof(IEnumerable), options));
+ yield return Case(new(typeof(string[]), options));
+ yield return Case(new(typeof(VisualBriefingRequiredSlot[]), options));
+ yield return Case(new(typeof(VisualBriefingControlRequirement[]), options));
+ yield return Case(new(typeof(VisualBriefingEvidencePrompt), options));
+ yield return Case(new(typeof(VisualBriefingParentDesign), options));
+
+ // The fingerprint which decides whether the editor holds unsaved changes:
+ yield return Case(new(typeof(VisualBriefingPersistenceFingerprint), options));
// The diagnostics a user copies to the clipboard:
yield return Case(new(typeof(VisualBriefingOperationDiagnostics), VisualBriefingJson.Persistence));