mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 00:07:21 +00:00
Refused JSON contracts built by reflection and checked all of them in the trimmed build (#1152)
This commit is contained in:
1 parent
f864359290
commit
0b42c0288e
23 files changed
+535
-99
No files matched your search
@@ -0,0 +1,60 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// Finds the options with which AI Studio reads and writes JSON.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// AI Studio keeps each kind of options in a static field or property, next to the code which uses them, so
|
||||
/// these are the options which reach System.Text.Json. A list written by hand would miss the next new one.
|
||||
/// The tests of the app compile this file, too, see Tests.csproj, so that both check the same options.
|
||||
/// </remarks>
|
||||
public static class AppJsonOptions
|
||||
{
|
||||
private const BindingFlags STATIC_MEMBERS = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the options of every static field and property of AI Studio which holds some.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reading a member runs the type initializer of its type. Hence, the static state which the start of
|
||||
/// the app fills has to be there before, Program.LOGGER_FACTORY above all.
|
||||
/// </remarks>
|
||||
/// <returns>The options by the name of their type and member, e.g., SettingsManager.JSON_OPTIONS.</returns>
|
||||
public static IReadOnlyDictionary<string, JsonSerializerOptions> FindAll()
|
||||
{
|
||||
var found = new SortedDictionary<string, JsonSerializerOptions>(StringComparer.Ordinal);
|
||||
|
||||
//
|
||||
// The Program of AI Studio, not the one of the trim smoke test, which is closer from here. Only within
|
||||
// the tests, which compile this file, too, the qualifier is redundant, and Rider reads it there:
|
||||
//
|
||||
// ReSharper disable once RedundantNameQualifier
|
||||
foreach (var type in typeof(AIStudio.Program).Assembly.GetTypes())
|
||||
{
|
||||
//
|
||||
// A context keeps options of its own for its Default instance, which never reach System.Text.Json
|
||||
// directly. The static members of an open generic type hold no value at all:
|
||||
//
|
||||
if (type.ContainsGenericParameters || type.IsSubclassOf(typeof(JsonSerializerContext)))
|
||||
continue;
|
||||
|
||||
//
|
||||
// The field behind a static auto-property is found as the property already:
|
||||
//
|
||||
foreach (var field in type.GetFields(STATIC_MEMBERS))
|
||||
if (field.FieldType == typeof(JsonSerializerOptions) && !field.IsDefined(typeof(CompilerGeneratedAttribute)) && field.GetValue(null) is JsonSerializerOptions options)
|
||||
found.Add($"{type.Name}.{field.Name}", options);
|
||||
|
||||
foreach (var property in type.GetProperties(STATIC_MEMBERS))
|
||||
if (property.PropertyType == typeof(JsonSerializerOptions) && property.GetMethod is not null && property.GetValue(null) is JsonSerializerOptions options)
|
||||
found.Add($"{type.Name}.{property.Name}", options);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,10 @@ public static class JsonContractWalker
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var property in typeInfo.Properties)
|
||||
//
|
||||
// System.Text.Json never meets the type of an ignored property, e.g., one marked with JsonIgnore:
|
||||
//
|
||||
foreach (var property in typeInfo.Properties.Where(property => property is not { Get: null, Set: null, AssociatedParameter: null }))
|
||||
Visit(property.PropertyType, $"{path} → {property.Name}", options, visited, failures);
|
||||
|
||||
if (typeInfo.KeyType is { } keyType)
|
||||
|
||||
@@ -1,65 +1,38 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Assistants.Builder;
|
||||
using AIStudio.Assistants.PromptOptimizer;
|
||||
using AIStudio.Assistants.SWOTAnalysis;
|
||||
using AIStudio.Assistants.VisualBriefing;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Settings.DataModel.PreviousModels;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.DataBackup;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AIStudio.TrimSmokeTest;
|
||||
|
||||
/// <summary>
|
||||
/// The types which AI Studio reads or writes as JSON on their own, not only as part of another type.
|
||||
/// The types which AI Studio reads or writes as JSON, each with the options it uses for them.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whatever these types hold, i.e., the types of their properties, the elements of their collections, and
|
||||
/// their derived types, gets checked along with them, so only the outermost types belong here. Add a type
|
||||
/// whenever AI Studio starts to store a new kind of file or to read a new kind of model answer.<br/><br/>
|
||||
///
|
||||
/// The answers of models are read with options of their own, which are private and only change names and
|
||||
/// leniency. The default options build the same contracts for the types of .NET they hold. A type which is
|
||||
/// private to its parser, such as the DTO of a slide deck, is out of reach here.
|
||||
/// The options of AI Studio each name the context which knows their types, so the types registered in that
|
||||
/// context are exactly those which System.Text.Json gets to read or write with these options. Whatever these
|
||||
/// types hold, i.e., the types of their properties, the elements of their collections, and their derived
|
||||
/// types, gets checked along with them. A new type, or new options, need nothing here: they are found
|
||||
/// through their context.
|
||||
/// </remarks>
|
||||
public static class JsonRoots
|
||||
{
|
||||
public static readonly IReadOnlyList<JsonRoot> ALL =
|
||||
[
|
||||
// Chats, in the workspaces, among the temporary chats, and in chat archives:
|
||||
new(typeof(ChatThread), WorkspaceBehaviour.JSON_OPTIONS),
|
||||
/// <summary>
|
||||
/// Returns every type registered in a context of the options of AI Studio, together with these options.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Options which several members share are checked once. Like AppJsonOptions.FindAll, this needs the
|
||||
/// static state of the app to be filled before.
|
||||
/// </remarks>
|
||||
public static IReadOnlyList<JsonRoot> FindAll() => AppJsonOptions.FindAll().Values
|
||||
.Distinct(ReferenceEqualityComparer.Instance)
|
||||
.Cast<JsonSerializerOptions>()
|
||||
.SelectMany(options => options.TypeInfoResolverChain
|
||||
.OfType<JsonSerializerContext>()
|
||||
.SelectMany(RegisteredTypes)
|
||||
.Select(type => new JsonRoot(type, options)))
|
||||
.ToList();
|
||||
|
||||
// The settings, and their older versions, which the migrations still read:
|
||||
new(typeof(Data), SettingsManager.JSON_OPTIONS),
|
||||
new(typeof(DataV1V3), SettingsManager.JSON_OPTIONS),
|
||||
new(typeof(DataV4), SettingsManager.JSON_OPTIONS),
|
||||
new(typeof(DataV5), SettingsManager.JSON_OPTIONS),
|
||||
|
||||
// The projects of the Visual Briefing assistant:
|
||||
new(typeof(VisualBriefingManifest), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingBuildRecord), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingEvidenceArtifact), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingPlanArtifact), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingContentArtifact), VisualBriefingJson.Persistence),
|
||||
new(typeof(VisualBriefingPresentationArtifact), VisualBriefingJson.Persistence),
|
||||
|
||||
// Backups and their restore:
|
||||
new(typeof(DataBackupManifest), DataBackupFormat.JSON_OPTIONS),
|
||||
new(typeof(DataRestorePlan), DataBackupFormat.JSON_OPTIONS),
|
||||
new(typeof(DataRestoreReport), DataBackupFormat.JSON_OPTIONS),
|
||||
|
||||
// Answers of models:
|
||||
new(typeof(LuaResponse), JsonSerializerOptions.Default),
|
||||
new(typeof(LauncherTextsResponse), JsonSerializerOptions.Default),
|
||||
new(typeof(PromptOptimizationResult), JsonSerializerOptions.Default),
|
||||
new(typeof(RetrievalContextValidationResult), JsonSerializerOptions.Default),
|
||||
new(typeof(AssistantAuditResult), JsonSerializerOptions.Default),
|
||||
new(typeof(SwotCategoryAnalysisResult), JsonSerializerOptions.Default),
|
||||
new(typeof(SwotModerationResult), JsonSerializerOptions.Default),
|
||||
];
|
||||
private static IEnumerable<Type> RegisteredTypes(JsonSerializerContext context) => context.GetType()
|
||||
.GetCustomAttributesData()
|
||||
.Where(attribute => attribute.AttributeType == typeof(JsonSerializableAttribute))
|
||||
.Select(attribute => (Type)attribute.ConstructorArguments[0].Value!);
|
||||
}
|
||||
@@ -15,14 +15,22 @@ public static class Program
|
||||
//
|
||||
AIStudio.Program.LOGGER_FACTORY = NullLoggerFactory.Instance;
|
||||
|
||||
var roots = JsonRoots.FindAll();
|
||||
var failures = new List<string>();
|
||||
foreach (var root in JsonRoots.ALL)
|
||||
|
||||
//
|
||||
// Finding no options at all means looking in the wrong place, not a build without JSON:
|
||||
//
|
||||
if (roots.Count is 0)
|
||||
failures.Add("Found no JSON options of AI Studio, see AppJsonOptions.FindAll, so nothing got checked.");
|
||||
|
||||
foreach (var root in roots)
|
||||
failures.AddRange(JsonContractWalker.FindBrokenContracts(root));
|
||||
|
||||
failures.AddRange(JsonRoundTrips.FindLosses());
|
||||
if (failures.Count is 0)
|
||||
{
|
||||
Console.WriteLine($"- All {JsonRoots.ALL.Count} JSON roots can be read and written.");
|
||||
Console.WriteLine($"- All {roots.Count} JSON roots can be read and written.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,16 @@
|
||||
|
||||
<Import Project="..\Trimming.props" />
|
||||
|
||||
<!--
|
||||
AI Studio reaches nearly all of its code from its start, and with it the assemblies it depends on. This
|
||||
program starts nowhere near that, so the trimmer would remove, e.g., the Blazor server and SQLite, and the
|
||||
types of the app which use them could not even be loaded, let alone their options be read. Rooting the
|
||||
app keeps what the app itself keeps, while code of .NET which only reflection reaches is still removed.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<TrimmerRootAssembly Include="mindworkAIStudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MindWork AI Studio\MindWork AI Studio.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in new issue
Block a user