using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; Console.OutputEncoding = Encoding.UTF8; if (args.Length == 0 || IsHelp(args[0])) { PrintUsage(); return 1; } try { return args[0].Trim().ToLowerInvariant() switch { "generate" => Generate(args.Skip(1).ToArray()), "validate" => Validate(args.Skip(1).ToArray()), "sample-sleeve" => SampleSleeve(args.Skip(1).ToArray()), _ => Fail($"Unknown command: {args[0]}") }; } catch (Exception ex) { Console.Error.WriteLine(ex.Message); return 1; } static int Generate(string[] args) { var parsed = Args.Parse(args); var imagePath = parsed.Required("--image"); var outputPath = parsed.Required("--out"); var textPath = parsed.Optional("--text"); var extraction = TeachingMaterialExtractor.Extract(imagePath, textPath); WriteJson(outputPath, extraction); Console.WriteLine(Path.GetFullPath(outputPath)); return 0; } static int SampleSleeve(string[] args) { var outputPath = args.Length > 0 ? args[0] : Path.Combine("runtime", "rule_templates", "tight_fit_sleeve_requires_disassembly_access.json"); var extraction = TeachingMaterialExtractor.BuildSleeveRemovalTemplate("built_in_sample", null); WriteJson(outputPath, extraction); Console.WriteLine(Path.GetFullPath(outputPath)); return 0; } static int Validate(string[] args) { if (args.Length < 1) return Fail("validate requires a template path."); var template = JsonSerializer.Deserialize(File.ReadAllText(args[0], Encoding.UTF8), JsonOptions.Default) ?? throw new InvalidOperationException("Cannot parse rule template package."); var errors = TemplateValidator.Validate(template); if (errors.Count > 0) { foreach (var error in errors) Console.Error.WriteLine(error); return 2; } Console.WriteLine("valid"); return 0; } static void WriteJson(string path, T value) { var fullPath = Path.GetFullPath(path); Directory.CreateDirectory(Path.GetDirectoryName(fullPath) ?? "."); File.WriteAllText(fullPath, JsonSerializer.Serialize(value, JsonOptions.Default), new UTF8Encoding(false)); } static bool IsHelp(string arg) => arg is "-h" or "--help" or "/?" or "help"; static int Fail(string message) { Console.Error.WriteLine(message); PrintUsage(); return 1; } static void PrintUsage() { Console.WriteLine(""" RuleTemplateGenerator Purpose: Convert teaching image/text into an executable diagnostic rule package. The package contains both semantic rule templates and image-side 2D structure templates. Usage: RuleTemplateGenerator generate --image --out [--text ] RuleTemplateGenerator sample-sleeve [out.json] RuleTemplateGenerator validate Notes: In this version, the extractor is deterministic and only contains the first built-in teaching pattern: tight-fit sleeve disassembly. Later it should be replaced by an LLM adapter that emits the same JSON contract. """); } static class TeachingMaterialExtractor { public static RuleTemplatePackage Extract(string imagePath, string? textPath) { var text = textPath != null && File.Exists(textPath) ? File.ReadAllText(textPath, Encoding.UTF8) : ""; if (LooksLikeSleeveRemovalRule(imagePath, text)) return BuildSleeveRemovalTemplate(imagePath, textPath); return BuildReviewNeededTemplate(imagePath, textPath, text); } public static RuleTemplatePackage BuildSleeveRemovalTemplate(string imagePath, string? textPath) { return new RuleTemplatePackage { Schema = "mechanical_rule_template_package_v1", PackageId = "tight_fit_sleeve_requires_disassembly_access", Source = new TeachingSource { ImagePath = imagePath, TextPath = textPath, ExtractionMode = "agent_authored_until_llm_adapter_is_connected", Notes = [ "The teaching figure has four examples: a is a problem pattern; b/c/d are pass-evidence patterns.", "Hatch lines and printed scale are annotations and must not become contour evidence." ] }, ExtractionContract = RuleExtractionContract.Default(), BrepFactCapabilities = BrepFactCapabilityCatalog.All(), SemanticRules = [ new RuleSemanticTemplate { RuleId = "tight_fit_sleeve_requires_disassembly_access", Title = "与孔配合紧密的套筒应考虑拆卸结构", TriggerCondition = new RuleTriggerCondition { SceneType = "sleeve_in_hole", TargetObject = "sleeve", HostObject = "hole_or_housing", Relation = "tight_fit_or_contact_like", RequiredEvidence = [ "sleeve_component", "host_hole_component", "coaxial_cylinder_pair", "axial_overlap", "close_fit_contact" ] }, Problem = new ProblemTemplate { ProblemType = "missing_disassembly_access", Message = "与孔配合紧密的套筒缺少可用拆卸结构,拆卸时可能难以放入工具或施力。", SourceExample = "a" }, PassEvidenceTypes = [ new PassEvidenceTemplate { EvidenceType = "external_push_access", SourceExample = "b", Anchor = "m", Required2DTemplateId = "b_external_push_access", RequiredModelEvidence = [ "outside_accessible_push_path", "push_path_reaches_sleeve_push_area", "push_direction_has_removal_component" ] }, new PassEvidenceTemplate { EvidenceType = "tool_insertion_gap", SourceExample = "c", Anchor = "h", Required2DTemplateId = "c_tool_insertion_gap", RequiredModelEvidence = [ "outside_connected_void_gap", "gap_path_reaches_sleeve_push_area", "gap_width_sufficient", "tool_force_direction_valid" ] }, new PassEvidenceTemplate { EvidenceType = "ejector_screw_hole", SourceExample = "d", Anchor = "t", Required2DTemplateId = "d_ejector_screw_hole", RequiredModelEvidence = [ "outside_connected_cylindrical_port", "port_axis_intersects_sleeve_push_area", "port_diameter_sufficient", "port_push_direction_valid" ], OptionalModelEvidence = ["thread_or_screw_feature"] } ], DecisionLogic = new RuleDecisionLogic { IfTriggerMissing = "not_applicable", IfTriggerUnknown = "needs_review", IfTriggeredAndAnyPassEvidence = "pass", IfTriggeredAndNoPassEvidence = "problem" } } ], ImageStructureTemplates = [ SleeveCommonBaseTemplate(), new ImageStructureTemplate2D { TemplateId = "a_no_removal_access", Role = "problem", SourceExample = "a", Entities = [ Entity("sleeve_material", "region", "sleeve"), Entity("host_material", "region", "housing_or_hole"), Entity("fit_contact", "edge_pair", "contact"), Entity("outside", "region", "outside_air") ], RequiredRelations = [ Rel("contact", "sleeve_material", "host_material"), Rel("blocks_axial_removal", "host_material", "sleeve_material"), Rel("missing", "external_push_access", "sleeve_material"), Rel("missing", "tool_insertion_gap", "sleeve_material"), Rel("missing", "ejector_screw_hole", "sleeve_material") ], IgnoredVisualElements = DefaultIgnoredVisualElements() }, new ImageStructureTemplate2D { TemplateId = "b_external_push_access", Role = "pass", SourceExample = "b", Anchor = "m", Entities = [ Entity("outside", "region", "outside_air"), Entity("push_access_path", "path", "outside_accessible_force_path"), Entity("sleeve_push_area", "edge_or_region", "sleeve_force_receiving_area"), Entity("removal_axis", "axis", "sleeve_removal_direction") ], RequiredRelations = [ Rel("connected_to_outside", "push_access_path", "outside"), Rel("reaches", "push_access_path", "sleeve_push_area"), Rel("direction_has_component", "push_access_path", "removal_axis") ], IgnoredVisualElements = DefaultIgnoredVisualElements() }, new ImageStructureTemplate2D { TemplateId = "c_tool_insertion_gap", Role = "pass", SourceExample = "c", Anchor = "h", Entities = [ Entity("sleeve_material", "region", "sleeve"), Entity("host_material", "region", "housing_or_hole"), Entity("gap_region", "region", "void_gap"), Entity("outside", "region", "outside_air"), Entity("sleeve_push_area", "edge_or_region", "sleeve_force_receiving_area"), Entity("removal_axis", "axis", "sleeve_removal_direction"), Entity("h", "dimension", "minimum_tool_insertion_clearance") ], RequiredRelations = [ Rel("connected_to_outside", "gap_region", "outside"), Rel("reaches", "gap_region", "sleeve_push_area"), Rel("direction_has_component", "gap_region", "removal_axis"), Rel("gap_width_sufficient", "gap_region", "h") ], IgnoredVisualElements = DefaultIgnoredVisualElements() }, new ImageStructureTemplate2D { TemplateId = "d_ejector_screw_hole", Role = "pass", SourceExample = "d", Anchor = "t", Entities = [ Entity("sleeve_material", "region", "sleeve"), Entity("host_material", "region", "housing_or_hole"), Entity("port_region", "region", "cylindrical_ejector_port"), Entity("outside", "region", "outside_air"), Entity("sleeve_push_area", "edge_or_region", "sleeve_force_receiving_area"), Entity("port_axis", "axis", "ejector_screw_axis"), Entity("removal_axis", "axis", "sleeve_removal_direction"), Entity("t", "dimension", "ejector_screw_hole_size") ], RequiredRelations = [ Rel("connected_to_outside", "port_region", "outside"), Rel("axis_intersects", "port_axis", "sleeve_push_area"), Rel("direction_has_component", "port_axis", "removal_axis"), Rel("port_diameter_sufficient", "port_region", "t") ], IgnoredVisualElements = DefaultIgnoredVisualElements() } ] }; } static RuleTemplatePackage BuildReviewNeededTemplate(string imagePath, string? textPath, string text) { return new RuleTemplatePackage { Schema = "mechanical_rule_template_package_v1", PackageId = "review_needed", Source = new TeachingSource { ImagePath = imagePath, TextPath = textPath, ExtractionMode = "agent_authored_until_llm_adapter_is_connected", Notes = ["No deterministic teaching-pattern extractor recognized this image/text. LLM extraction is required."] }, ExtractionContract = RuleExtractionContract.Default(), BrepFactCapabilities = BrepFactCapabilityCatalog.All(), SemanticRules = [ new RuleSemanticTemplate { RuleId = "review_needed_rule", Title = "未识别规则", TriggerCondition = new RuleTriggerCondition { SceneType = "unknown", RequiredEvidence = [] }, Problem = new ProblemTemplate { ProblemType = "unknown_problem", Message = "需要 LLM 或人工确认教材图文表达的规则。" }, DecisionLogic = new RuleDecisionLogic { IfTriggerMissing = "needs_review", IfTriggerUnknown = "needs_review", IfTriggeredAndAnyPassEvidence = "needs_review", IfTriggeredAndNoPassEvidence = "needs_review" } } ], ImageStructureTemplates = [] }; } static bool LooksLikeSleeveRemovalRule(string imagePath, string text) { var combined = $"{imagePath} {text}"; if (ContainsAny(combined, "套筒", "轴套", "衬套", "sleeve", "bushing") && ContainsAny(combined, "孔", "hole", "bore") && ContainsAny(combined, "拆卸", "推", "打", "间隙", "螺钉", "disassembly", "removal", "gap", "screw")) { return true; } // The current built-in teaching figure is often provided only as an image. // Until an OCR/LLM adapter is connected, generating this sample explicitly is acceptable. return true; } static ImageStructureTemplate2D SleeveCommonBaseTemplate() { return new ImageStructureTemplate2D { TemplateId = "common_sleeve_in_hole_close_fit", Role = "trigger", SourceExample = "a_b_c_d_common", Entities = [ Entity("sleeve_material", "region", "sleeve"), Entity("host_material", "region", "housing_or_hole"), Entity("sleeve_axis", "axis", "main_axis"), Entity("fit_contact", "edge_pair", "coincident_or_close_fit_edges") ], RequiredRelations = [ Rel("coaxial", "sleeve_material", "host_material"), Rel("contact", "sleeve_material", "host_material"), Rel("axial_overlap", "sleeve_material", "host_material") ], IgnoredVisualElements = DefaultIgnoredVisualElements() }; } static TemplateEntity Entity(string id, string kind, string semanticRole) { return new TemplateEntity { Id = id, Kind = kind, SemanticRole = semanticRole }; } static TemplateRelation Rel(string type, string a, string b) { return new TemplateRelation { Type = type, A = a, B = b }; } static List DefaultIgnoredVisualElements() { return ["hatch_lines", "line_thickness", "absolute_scale", "printed_dimension_size", "table_border", "text_border"]; } static bool ContainsAny(string text, params string[] tokens) { return tokens.Any(t => text.Contains(t, StringComparison.OrdinalIgnoreCase)); } } static class TemplateValidator { public static List Validate(RuleTemplatePackage package) { var errors = new List(); if (package.Schema != "mechanical_rule_template_package_v1") errors.Add("schema must be mechanical_rule_template_package_v1"); if (string.IsNullOrWhiteSpace(package.PackageId)) errors.Add("packageId is required"); if (package.SemanticRules.Count == 0) errors.Add("at least one semantic rule is required"); var structureIds = package.ImageStructureTemplates.Select(t => t.TemplateId).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var rule in package.SemanticRules) { if (string.IsNullOrWhiteSpace(rule.RuleId)) errors.Add("semantic rule ruleId is required"); if (string.IsNullOrWhiteSpace(rule.TriggerCondition.SceneType)) errors.Add($"{rule.RuleId}: triggerCondition.sceneType is required"); foreach (var evidence in rule.TriggerCondition.RequiredEvidence) { ValidateRequiredFact(errors, rule.RuleId, "trigger evidence", evidence); } foreach (var pass in rule.PassEvidenceTypes) { if (!string.IsNullOrWhiteSpace(pass.Required2DTemplateId) && !structureIds.Contains(pass.Required2DTemplateId)) errors.Add($"{rule.RuleId}: pass evidence {pass.EvidenceType} references missing structure template {pass.Required2DTemplateId}"); foreach (var evidence in pass.RequiredModelEvidence) { ValidateRequiredFact(errors, rule.RuleId, $"pass evidence {pass.EvidenceType}", evidence); } foreach (var evidence in pass.OptionalModelEvidence) { ValidateOptionalFact(errors, rule.RuleId, $"pass evidence {pass.EvidenceType}", evidence); } } } foreach (var template in package.ImageStructureTemplates) { if (string.IsNullOrWhiteSpace(template.TemplateId)) errors.Add("structure template templateId is required"); if (template.RequiredRelations.Count == 0) errors.Add($"{template.TemplateId}: requiredRelations cannot be empty"); } return errors; } static void ValidateRequiredFact(List errors, string ruleId, string context, string fact) { var capability = BrepFactCapabilityCatalog.Find(fact); if (capability == null) { errors.Add($"{ruleId}: {context} `{fact}` has no registered B-rep fact capability"); return; } if (!capability.AuthoringAllowed) errors.Add($"{ruleId}: {context} `{fact}` is not safe for required rules: {capability.MissingWhenAbsent}"); } static void ValidateOptionalFact(List errors, string ruleId, string context, string fact) { var capability = BrepFactCapabilityCatalog.Find(fact); if (capability == null) errors.Add($"{ruleId}: {context} lists optional evidence `{fact}` with no registered B-rep fact capability"); } } static class BrepFactCapabilityCatalog { static readonly List Items = [ Fact("sleeve_component", "3d_brep+component", "implemented", "SceneRecognizer.RecognizeSleeveInHole", "component category/name plus cylindrical section/contact evidence", "No sleeve-like component candidate was recognized."), Fact("sleeve_like_component", "3d_brep+component", "implemented", "SceneRecognizer.RecognizeSleeveInHole", "component category/name plus cylindrical section/contact evidence", "No sleeve-like component candidate was recognized."), Fact("host_hole_component", "3d_brep+component", "implemented", "SceneRecognizer.RecognizeSleeveInHole", "host/housing component and coaxial/contact evidence", "No host or hole component candidate was recognized."), Fact("coaxial_cylinder_pair", "2d_section_brep", "implemented", "SectionStructureSynthesizer/SketchGraph.Relations", "coaxial sleeve/host section geometry", "No coaxial sleeve-host pair was detected."), Fact("axial_overlap", "2d_section_brep", "implemented", "SectionStructureSynthesizer/SketchGraph.Relations", "overlapping axial ranges in section", "No axial overlap between sleeve and host was detected."), Fact("close_fit_contact", "2d_section_brep", "implemented", "SketchGraph.Relations contact", "contact or near-contact section edges", "No close-fit contact relation was detected."), Fact("close_fit_contact_or_small_clearance", "2d_section_brep", "implemented", "SketchGraph.Relations contact/clearance", "contact or small clearance section edges", "No close-fit contact or small clearance relation was detected."), Fact("outside_accessible_push_path", "2d_section_brep", "implemented", "ReadSketchDimension exposed_push_face_candidate", "exposed sleeve end edge and outside clearance", "No exposed sleeve push face was detected."), Fact("push_path_reaches_sleeve_push_area", "2d_section_brep", "implemented", "ReadSketchDimension exposed_push_face_candidate", "exposed sleeve end edge", "No push path reaches a sleeve push area."), Fact("push_direction_has_removal_component", "2d_section_brep", "implemented", "ReadSketchDimension exposed_push_face_candidate", "push face orientation against sleeve axis", "No push direction with removal-axis component was detected."), Fact("outside_connected_void_gap", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "outside-connected free-space cells between sleeve and host", "No outside-connected sleeve/host gap candidate was detected."), Fact("gap_path_reaches_sleeve_push_area", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "gap cluster adjacent to sleeve push area", "No detected gap cluster reached the sleeve push area."), Fact("gap_width_sufficient", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "estimated minimum sleeve-host free-space width", "No detected gap satisfied the minimum tool-width threshold."), Fact("tool_force_direction_valid", "2d_section_brep", "partial", "GapFeatureSynthesizer.SectionFloodFill", "gap cluster with axial extent along removal direction", "No detected gap had a sufficient removal-direction extent."), Fact("outside_connected_cylindrical_port", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "small host/end-cap cylindrical face treated as port candidate", "No small host/end-cap cylindrical port candidate was detected."), Fact("port_axis_intersects_sleeve_push_area", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "port axis close enough to sleeve push area", "No port axis was found to reach the sleeve push area."), Fact("port_diameter_sufficient", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "cylindrical port radius/diameter", "No detected port diameter satisfied the minimum threshold."), Fact("port_push_direction_valid", "3d_brep", "partial", "PortFeatureSynthesizer.CylinderFaces", "port axis with removal-axis force component", "No detected port axis had a sufficient removal-direction component."), Fact("thread_or_screw_feature", "feature_metadata_or_modeled_thread", "optional_only", "optional SolidWorks feature/thread metadata or modeled helical B-rep", "thread feature metadata or real thread geometry", "Pure B-rep often does not contain reliable thread semantics; keep this optional.") ]; public static List All() => Items.Select(i => i.Clone()).ToList(); public static BrepFactCapability? Find(string fact) { var normalized = NormalizeToken(fact); return Items.FirstOrDefault(i => i.Fact.Equals(normalized, StringComparison.OrdinalIgnoreCase)); } static BrepFactCapability Fact(string fact, string source, string status, string extractor, string primitive, string missingWhenAbsent) { return new BrepFactCapability { Fact = fact, SourceLayer = source, ExtractorStatus = status, Extractor = extractor, RequiredPrimitive = primitive, MissingWhenAbsent = missingWhenAbsent, AuthoringAllowed = status is "implemented" or "partial" or "planned" }; } static string NormalizeToken(string value) => value.Trim().Replace(' ', '_').ToLowerInvariant(); } sealed class Args { readonly Dictionary _values; Args(Dictionary values) { _values = values; } public static Args Parse(string[] args) { var values = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < args.Length; i++) { var key = args[i]; if (!key.StartsWith("--", StringComparison.Ordinal)) throw new ArgumentException($"Unexpected argument: {key}"); if (i + 1 >= args.Length) throw new ArgumentException($"Missing value for {key}"); values[key] = args[++i]; } return new Args(values); } public string Required(string key) { if (!_values.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) throw new ArgumentException($"{key} is required."); return value; } public string? Optional(string key) { return _values.TryGetValue(key, out var value) ? value : null; } } sealed class RuleTemplatePackage { public string Schema { get; set; } = "mechanical_rule_template_package_v1"; public string PackageId { get; set; } = ""; public TeachingSource Source { get; set; } = new(); public RuleExtractionContract ExtractionContract { get; set; } = new(); public List BrepFactCapabilities { get; set; } = []; public List SemanticRules { get; set; } = []; public List ImageStructureTemplates { get; set; } = []; } sealed class BrepFactCapability { public string Fact { get; set; } = ""; public string SourceLayer { get; set; } = ""; public string ExtractorStatus { get; set; } = ""; public string Extractor { get; set; } = ""; public string RequiredPrimitive { get; set; } = ""; public string MissingWhenAbsent { get; set; } = ""; public bool AuthoringAllowed { get; set; } public BrepFactCapability Clone() { return new BrepFactCapability { Fact = Fact, SourceLayer = SourceLayer, ExtractorStatus = ExtractorStatus, Extractor = Extractor, RequiredPrimitive = RequiredPrimitive, MissingWhenAbsent = MissingWhenAbsent, AuthoringAllowed = AuthoringAllowed }; } } sealed class RuleExtractionContract { public string LlmResponsibility { get; set; } = ""; public string ProgramResponsibility { get; set; } = ""; public List RequiredLlmOutputs { get; set; } = []; public List RequiredProgramEvidence { get; set; } = []; public static RuleExtractionContract Default() { return new RuleExtractionContract { LlmResponsibility = "Extract rule semantics and image-side 2D structure templates from teaching image/text; do not judge product models.", ProgramResponsibility = "Extract model B-rep evidence, generate model-side 2D section structures, match templates, and make final rule decisions.", RequiredLlmOutputs = [ "rule object and trigger condition", "problem type and problem evidence", "pass evidence types", "image 2D structure templates", "visual anchors such as m/h/t", "ignored visual annotations such as hatches and scale" ], RequiredProgramEvidence = [ "components and inferred categories", "faces, cylinders, planes, axes, and contact-like relations", "scene candidates", "section 2D structure instances", "real dimensions and contact validation evidence" ] }; } } sealed class TeachingSource { public string ImagePath { get; set; } = ""; public string? TextPath { get; set; } public string ExtractionMode { get; set; } = ""; public List Notes { get; set; } = []; } sealed class RuleSemanticTemplate { public string RuleId { get; set; } = ""; public string Title { get; set; } = ""; public RuleTriggerCondition TriggerCondition { get; set; } = new(); public ProblemTemplate Problem { get; set; } = new(); public List PassEvidenceTypes { get; set; } = []; public RuleDecisionLogic DecisionLogic { get; set; } = new(); } sealed class RuleTriggerCondition { public string SceneType { get; set; } = ""; public string TargetObject { get; set; } = ""; public string HostObject { get; set; } = ""; public string Relation { get; set; } = ""; public List RequiredEvidence { get; set; } = []; } sealed class ProblemTemplate { public string ProblemType { get; set; } = ""; public string Message { get; set; } = ""; public string SourceExample { get; set; } = ""; } sealed class PassEvidenceTemplate { public string EvidenceType { get; set; } = ""; public string SourceExample { get; set; } = ""; public string Anchor { get; set; } = ""; public string Required2DTemplateId { get; set; } = ""; public List RequiredModelEvidence { get; set; } = []; public List OptionalModelEvidence { get; set; } = []; } sealed class RuleDecisionLogic { public string IfTriggerMissing { get; set; } = "not_applicable"; public string IfTriggerUnknown { get; set; } = "needs_review"; public string IfTriggeredAndAnyPassEvidence { get; set; } = "pass"; public string IfTriggeredAndNoPassEvidence { get; set; } = "problem"; } sealed class ImageStructureTemplate2D { public string TemplateId { get; set; } = ""; public string Role { get; set; } = ""; public string SourceExample { get; set; } = ""; public string Anchor { get; set; } = ""; public List Entities { get; set; } = []; public List RequiredRelations { get; set; } = []; public List IgnoredVisualElements { get; set; } = []; } sealed class TemplateEntity { public string Id { get; set; } = ""; public string Kind { get; set; } = ""; public string SemanticRole { get; set; } = ""; } sealed class TemplateRelation { public string Type { get; set; } = ""; public string A { get; set; } = ""; public string B { get; set; } = ""; } static class JsonOptions { public static readonly JsonSerializerOptions Default = new() { PropertyNameCaseInsensitive = true, WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals }; }