Generated the JSON contracts of the Visual Briefing assistant at compile time

This commit is contained in:
Thorsten Sommer committed 2026-11-03 08:46:32 +01:00
1 parent c47c024399
commit 59910b2ef4
7 files changed
+118 -13

No files matched your search

@@ -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..]);
@@ -0,0 +1,62 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.VisualBriefing;
/// <summary>
/// The contracts with which System.Text.Json writes and reads the canonical JSON of visual briefings, built at
/// compile time.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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<string, JsonElement>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(JsonObject))]
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(int))]
// The payload hashes of the artifacts:
[JsonSerializable(typeof(List<VisualBriefingEvidenceFact>))]
[JsonSerializable(typeof(List<VisualBriefingEvidenceMetric>))]
[JsonSerializable(typeof(List<VisualBriefingEvidenceTable>))]
[JsonSerializable(typeof(List<VisualBriefingSourceCoverage>))]
[JsonSerializable(typeof(List<VisualBriefingAssetPlanItem>))]
[JsonSerializable(typeof(List<VisualBriefingPlanSection>))]
[JsonSerializable(typeof(List<VisualBriefingSlotValue>))]
[JsonSerializable(typeof(List<VisualBriefingChartSpec>))]
[JsonSerializable(typeof(List<VisualBriefingControlSpec>))]
[JsonSerializable(typeof(List<VisualBriefingFormulaSpec>))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(Dictionary<string, List<string>>))]
[JsonSerializable(typeof(VisualBriefingLayoutNode))]
// The prompts of the stages:
[JsonSerializable(typeof(IEnumerable<VisualBriefingSourcePrompt>))]
[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;
@@ -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.
/// </remarks>
[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,
};
/// <summary>
@@ -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.
/// </remarks>
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();
}
@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.VisualBriefing;
/// <summary>
/// The contracts with which System.Text.Json reads and writes the files of visual briefings, built at compile time.
/// </summary>
[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;
@@ -24,7 +24,7 @@ public sealed partial class VisualBriefingStore
try
{
this.LastSelectedBriefingId = briefingId;
await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize<Guid?>(briefingId), overwrite: true, token);
await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize<Guid?>(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<Guid?>(null), overwrite: true, token);
await WriteTextAtomicAsync(this.SelectionPath(), JsonSerializer.Serialize<Guid?>(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<Guid?>(serialized);
var selected = JsonSerializer.Deserialize<Guid?>(serialized, JSON_OPTIONS);
this.LastSelectedBriefingId = selected is not null &&
Directory.Exists(this.BriefingDirectory(selected.Value))
? selected
@@ -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)
@@ -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<string, List<string>>), 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<VisualBriefingSourcePrompt>), 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));