diff --git a/src/JD.Efcpt.Build.Tasks/Config/EfcptConfigGenerator.cs b/src/JD.Efcpt.Build.Tasks/Config/EfcptConfigGenerator.cs
new file mode 100644
index 0000000..061b2ce
--- /dev/null
+++ b/src/JD.Efcpt.Build.Tasks/Config/EfcptConfigGenerator.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Threading.Tasks;
+
+namespace JD.Efcpt.Build.Tasks.Config;
+
+///
+/// Generates efcpt-config.json from the EFCorePowerTools JSON schema.
+///
+public static class EfcptConfigGenerator
+{
+ private const string PrimarySchemaUrl = "https://raw.githubusercontent.com/ErikEJ/EFCorePowerTools/master/samples/efcpt-config.schema.json";
+ private const string FallbackSchemaUrl = "https://raw.githubusercontent.com/JerrettDavis/JD.Efcpt.Build/refs/heads/main/lib/efcpt-config.schema.json";
+
+ ///
+ /// Generates a default efcpt-config.json from a schema URL.
+ ///
+ /// URL to the schema (optional, tries primary then fallback)
+ /// Optional custom DbContext name (default: "ApplicationDbContext")
+ /// Optional custom root namespace (default: "EfcptProject")
+ /// Generated JSON string
+ public static async Task GenerateFromUrlAsync(
+ string? schemaUrl = null,
+ string? dbContextName = null,
+ string? rootNamespace = null)
+ {
+ schemaUrl ??= await TryGetSchemaUrlAsync();
+
+ using var client = new HttpClient();
+ var schemaJson = await client.GetStringAsync(schemaUrl);
+ return GenerateFromSchema(schemaJson, dbContextName, rootNamespace, schemaUrl);
+ }
+
+ ///
+ /// Tries to fetch schema from primary URL, falling back to secondary if needed.
+ ///
+ private static async Task TryGetSchemaUrlAsync()
+ {
+ using var client = new HttpClient();
+ client.Timeout = TimeSpan.FromSeconds(5);
+
+ try
+ {
+ await client.GetStringAsync(PrimarySchemaUrl);
+ return PrimarySchemaUrl;
+ }
+ catch
+ {
+ return FallbackSchemaUrl;
+ }
+ }
+
+ ///
+ /// Generates a default efcpt-config.json from a local schema file.
+ ///
+ /// Path to the schema file
+ /// Optional custom DbContext name (default: "ApplicationDbContext")
+ /// Optional custom root namespace (default: "EfcptProject")
+ /// Optional schema URL to include in $schema property (default: primary schema URL)
+ /// Generated JSON string
+ public static string GenerateFromFile(
+ string schemaPath,
+ string? dbContextName = null,
+ string? rootNamespace = null,
+ string? schemaUrl = null)
+ {
+ var schemaJson = File.ReadAllText(schemaPath);
+ schemaUrl ??= PrimarySchemaUrl;
+ return GenerateFromSchema(schemaJson, dbContextName, rootNamespace, schemaUrl);
+ }
+
+ ///
+ /// Generates a default efcpt-config.json from schema JSON string.
+ ///
+ /// The JSON schema as a string
+ /// Optional custom DbContext name (default: "ApplicationDbContext")
+ /// Optional custom root namespace (default: "EfcptProject")
+ /// Optional schema URL to include in $schema property (default: primary schema URL)
+ /// Generated JSON string
+ public static string GenerateFromSchema(
+ string schemaJson,
+ string? dbContextName = null,
+ string? rootNamespace = null,
+ string? schemaUrl = null)
+ {
+ var schema = JsonNode.Parse(schemaJson);
+ if (schema is null)
+ throw new InvalidOperationException("Failed to parse schema JSON");
+
+ var config = new JsonObject();
+
+ // Add $schema property first
+ schemaUrl ??= PrimarySchemaUrl;
+ config["$schema"] = schemaUrl;
+
+ var definitions = schema["definitions"]?.AsObject();
+ if (definitions is null)
+ throw new InvalidOperationException("Schema does not contain definitions section");
+
+ // Process each top-level section - only required properties
+ ProcessCodeGeneration(config, definitions);
+ ProcessFileLayout(config, definitions);
+ ProcessNames(config, definitions, dbContextName, rootNamespace);
+ // Don't process TypeMappings as it's not required
+
+ // Serialize with indentation
+ var options = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ return JsonSerializer.Serialize(config, options);
+ }
+
+ private static void ProcessCodeGeneration(JsonObject config, JsonObject definitions)
+ {
+ var codeGenDef = definitions["CodeGeneration"]?.AsObject();
+ if (codeGenDef is null) return;
+
+ var required = GetRequiredProperties(codeGenDef);
+ var properties = codeGenDef["properties"]?.AsObject();
+ if (properties is null) return;
+
+ var codeGenConfig = new JsonObject();
+
+ // Process only required properties
+ foreach (var propName in required)
+ {
+ // Skip preview properties
+ if (propName.Contains("-preview", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ var propDef = properties[propName]?.AsObject();
+ if (propDef is null) continue;
+
+ if (TryGetDefaultValue(propDef, propName, out var defaultValue))
+ {
+ codeGenConfig[propName] = defaultValue;
+ }
+ }
+
+ if (codeGenConfig.Count > 0)
+ {
+ config["code-generation"] = codeGenConfig;
+ }
+ }
+
+ private static void ProcessNames(
+ JsonObject config,
+ JsonObject definitions,
+ string? dbContextName,
+ string? rootNamespace)
+ {
+ var namesDef = definitions["Names"]?.AsObject();
+ if (namesDef is null) return;
+
+ var required = GetRequiredProperties(namesDef);
+ var properties = namesDef["properties"]?.AsObject();
+ if (properties is null) return;
+
+ var namesConfig = new JsonObject();
+
+ // Process only required properties
+ foreach (var propName in required)
+ {
+ // Skip preview properties
+ if (propName.Contains("-preview", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ // Use custom values if provided
+ if (propName == "dbcontext-name" && !string.IsNullOrEmpty(dbContextName))
+ {
+ namesConfig[propName] = dbContextName;
+ }
+ else if (propName == "root-namespace" && !string.IsNullOrEmpty(rootNamespace))
+ {
+ namesConfig[propName] = rootNamespace;
+ }
+ else
+ {
+ var propDef = properties[propName]?.AsObject();
+ if (propDef is null) continue;
+
+ if (TryGetDefaultValue(propDef, propName, out var defaultValue))
+ {
+ namesConfig[propName] = defaultValue!;
+ }
+ else
+ {
+ // Provide sensible defaults for required string properties
+ if (propName == "dbcontext-name")
+ namesConfig[propName] = "ApplicationDbContext";
+ else if (propName == "root-namespace")
+ namesConfig[propName] = "EfcptProject";
+ }
+ }
+ }
+
+ if (namesConfig.Count > 0)
+ {
+ config["names"] = namesConfig;
+ }
+ }
+
+ private static void ProcessFileLayout(JsonObject config, JsonObject definitions)
+ {
+ var fileLayoutDef = definitions["FileLayout"]?.AsObject();
+ if (fileLayoutDef is null) return;
+
+ var required = GetRequiredProperties(fileLayoutDef);
+ var properties = fileLayoutDef["properties"]?.AsObject();
+ if (properties is null) return;
+
+ var fileLayoutConfig = new JsonObject();
+
+ // Process only required properties
+ foreach (var propName in required)
+ {
+ // Skip preview properties
+ if (propName.Contains("-preview", StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ var propDef = properties[propName]?.AsObject();
+ if (propDef is null) continue;
+
+ if (TryGetDefaultValue(propDef, propName, out var defaultValue))
+ {
+ fileLayoutConfig[propName] = defaultValue;
+ }
+ }
+
+ if (fileLayoutConfig.Count > 0)
+ {
+ config["file-layout"] = fileLayoutConfig;
+ }
+ }
+
+ private static List GetRequiredProperties(JsonObject definition)
+ {
+ var requiredArray = definition["required"]?.AsArray();
+ if (requiredArray is null)
+ return new List();
+
+ return requiredArray
+ .Select(item => item?.GetValue())
+ .Where(s => s is not null)
+ .Cast()
+ .ToList();
+ }
+
+ private static bool TryGetDefaultValue(JsonObject propertyDef, string propertyName, out JsonNode? defaultValue)
+ {
+ // Check if there's an explicit default value
+ if (propertyDef.TryGetPropertyValue("default", out defaultValue) && defaultValue is not null)
+ {
+ defaultValue = defaultValue.DeepClone();
+ return true;
+ }
+
+ // Check type to determine implicit defaults
+ var type = propertyDef["type"];
+ if (type is null)
+ {
+ defaultValue = null;
+ return false;
+ }
+
+ // Handle type as string
+ if (type is JsonValue typeValue)
+ {
+ var typeStr = typeValue.GetValue();
+ if (typeStr == "boolean")
+ {
+ defaultValue = JsonValue.Create(false);
+ return true;
+ }
+
+ defaultValue = null;
+ return false;
+ }
+
+ // Handle type as array (e.g., ["string", "null"]) - nullable types
+ if (type is JsonArray typeArray)
+ {
+ // Return null for nullable properties
+ defaultValue = JsonValue.Create(null);
+ return true;
+ }
+
+ defaultValue = null;
+ return false;
+ }
+}
diff --git a/src/JD.Efcpt.Build.Tasks/Config/README.md b/src/JD.Efcpt.Build.Tasks/Config/README.md
new file mode 100644
index 0000000..0b48e9e
--- /dev/null
+++ b/src/JD.Efcpt.Build.Tasks/Config/README.md
@@ -0,0 +1,87 @@
+# Config Generator
+
+This directory contains the `EfcptConfigGenerator` utility that generates default `efcpt-config.json` files from the EFCorePowerTools JSON schema.
+
+## Purpose
+
+The generator ensures that the default config files packaged with JD.Efcpt.Build match the structure and defaults produced by the efcpt CLI tool. This is important for:
+
+1. **Consistency**: Users get the same config structure whether they use our templates or run efcpt directly
+2. **Maintainability**: When the schema changes, we can regenerate configs rather than manually updating them
+3. **Correctness**: Automatically excludes preview properties and uses schema-defined defaults
+
+## Usage
+
+### Generating Config Files
+
+The generator can be used programmatically:
+
+```csharp
+using JD.Efcpt.Build.Tasks.Config;
+
+// From local schema file
+var config = EfcptConfigGenerator.GenerateFromFile(
+ schemaPath: "path/to/efcpt-config.schema.json",
+ dbContextName: "ApplicationDbContext",
+ rootNamespace: "EfcptProject");
+
+// From URL
+var config = await EfcptConfigGenerator.GenerateFromUrlAsync(
+ schemaUrl: "https://raw.githubusercontent.com/.../efcpt-config.schema.json",
+ dbContextName: "ApplicationDbContext",
+ rootNamespace: "EfcptProject");
+
+// Write to file
+File.WriteAllText("efcpt-config.json", config);
+```
+
+### Updating Package Config Files
+
+When the schema is updated, regenerate the packaged config files:
+
+1. Update `/lib/efcpt-config.schema.json` if needed
+2. Run the generator to update both config files:
+ - `/src/JD.Efcpt.Build/defaults/efcpt-config.json`
+ - `/src/JD.Efcpt.Build.Templates/templates/efcptbuild/efcpt-config.json`
+
+Example script:
+
+```csharp
+var schemaPath = "lib/efcpt-config.schema.json";
+var config = EfcptConfigGenerator.GenerateFromFile(
+ schemaPath,
+ dbContextName: "ApplicationDbContext",
+ rootNamespace: "EfcptProject");
+
+File.WriteAllText("src/JD.Efcpt.Build/defaults/efcpt-config.json", config);
+File.WriteAllText("src/JD.Efcpt.Build.Templates/templates/efcptbuild/efcpt-config.json", config);
+```
+
+## Generator Behavior
+
+- **Includes all properties** with defined defaults (not just required ones)
+- **Excludes preview properties** (any property containing "-preview")
+- **Uses schema defaults** where specified
+- **Provides sensible defaults** for required properties without schema defaults:
+ - `dbcontext-name`: "ApplicationDbContext"
+ - `root-namespace`: "EfcptProject"
+ - `output-path`: "Models"
+- **Sets nullable properties** to `null` by default
+
+## When to Use This
+
+This generator is **only needed at pack-time** for our own libraries. End users don't need it because:
+
+1. The efcpt CLI automatically generates a default config if one is missing
+2. Our packages include pre-generated configs that match what efcpt produces
+3. Users can customize configs via MSBuild properties without regenerating files
+
+## Testing
+
+Tests are located in `/tests/JD.Efcpt.Build.Tests/Config/EfcptConfigGeneratorTests.cs` and verify:
+
+- Valid JSON output
+- Correct structure (code-generation, names, file-layout, type-mappings sections)
+- Exclusion of preview properties
+- Custom name support
+- Schema default values
diff --git a/src/JD.Efcpt.Build.Templates/templates/efcptbuild/efcpt-config.json b/src/JD.Efcpt.Build.Templates/templates/efcptbuild/efcpt-config.json
index 63c38da..bf11da2 100644
--- a/src/JD.Efcpt.Build.Templates/templates/efcptbuild/efcpt-config.json
+++ b/src/JD.Efcpt.Build.Templates/templates/efcptbuild/efcpt-config.json
@@ -1,9 +1,26 @@
{
"$schema": "https://raw.githubusercontent.com/ErikEJ/EFCorePowerTools/master/samples/efcpt-config.schema.json",
+ "code-generation": {
+ "enable-on-configuring": false,
+ "type": "all",
+ "use-database-names": false,
+ "use-data-annotations": false,
+ "use-nullable-reference-types": false,
+ "use-inflector": true,
+ "use-legacy-inflector": false,
+ "use-many-to-many-entity": false,
+ "use-t4": false,
+ "remove-defaultsql-from-bool-properties": false,
+ "soft-delete-obsolete-files": true,
+ "use-alternate-stored-procedure-resultset-discovery": false
+ },
+ "file-layout": {
+ "output-path": "Models"
+ },
"names": {
"root-namespace": "EfcptProject",
"dbcontext-name": "ApplicationDbContext",
"dbcontext-namespace": "EfcptProject.Data",
"model-namespace": "EfcptProject.Data.Entities"
}
-}
+}
\ No newline at end of file
diff --git a/src/JD.Efcpt.Build/defaults/efcpt-config.json b/src/JD.Efcpt.Build/defaults/efcpt-config.json
index fe32bbc..d19d395 100644
--- a/src/JD.Efcpt.Build/defaults/efcpt-config.json
+++ b/src/JD.Efcpt.Build/defaults/efcpt-config.json
@@ -1,50 +1,24 @@
{
"$schema": "https://raw.githubusercontent.com/ErikEJ/EFCorePowerTools/master/samples/efcpt-config.schema.json",
- "code-generation":
- {
- "enable-on-configuring": false,
- "type": "all",
- "use-database-names": false,
- "use-data-annotations": false,
- "use-nullable-reference-types": true,
- "use-inflector": true,
- "use-legacy-inflector": false,
- "use-many-to-many-entity": false,
- "use-t4": true,
- "remove-defaultsql-from-bool-properties": false,
- "soft-delete-obsolete-files": false,
- "discover-multiple-stored-procedure-resultsets-preview": false,
- "use-alternate-stored-procedure-resultset-discovery": false,
- "t4-template-path": null
+ "code-generation": {
+ "enable-on-configuring": false,
+ "type": "all",
+ "use-database-names": false,
+ "use-data-annotations": false,
+ "use-nullable-reference-types": false,
+ "use-inflector": true,
+ "use-legacy-inflector": false,
+ "use-many-to-many-entity": false,
+ "use-t4": false,
+ "remove-defaultsql-from-bool-properties": false,
+ "soft-delete-obsolete-files": true,
+ "use-alternate-stored-procedure-resultset-discovery": false
},
- "names":
- {
- "root-namespace": "MyProject",
- "dbcontext-name": "MyDbContext",
- "dbcontext-namespace": null,
- "model-namespace": null
+ "file-layout": {
+ "output-path": "Models"
},
- "file-layout":
- {
- "output-path": "Models",
- "output-dbcontext-path": null,
- "split-dbcontext-preview": false,
- "use-schema-folders-preview": true,
- "use-schema-namespaces-preview": false
- },
- "type-mappings":
- {
- "use-DateOnly-TimeOnly": true,
- "use-HierarchyId": true,
- "use-spatial": true,
- "use-NodaTime": false
- },
- "replacements":
- {
- "preserve-casing-with-regex": false,
- "uncountable-words": [
- "Status",
- "Data"
- ]
- }
-}
+ "names": {
+ "dbcontext-name": "ApplicationDbContext",
+ "root-namespace": "EfcptProject"
+ }
+}
\ No newline at end of file
diff --git a/tests/JD.Efcpt.Build.Tests/Config/EfcptConfigGeneratorTests.cs b/tests/JD.Efcpt.Build.Tests/Config/EfcptConfigGeneratorTests.cs
new file mode 100644
index 0000000..5fca4cd
--- /dev/null
+++ b/tests/JD.Efcpt.Build.Tests/Config/EfcptConfigGeneratorTests.cs
@@ -0,0 +1,228 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using JD.Efcpt.Build.Tasks.Config;
+using Xunit;
+
+namespace JD.Efcpt.Build.Tests.Config;
+
+public class EfcptConfigGeneratorTests
+{
+ private readonly string _schemaPath;
+
+ public EfcptConfigGeneratorTests()
+ {
+ // Locate the schema file relative to the test project
+ var repoRoot = FindRepoRoot();
+ _schemaPath = Path.Combine(repoRoot, "lib", "efcpt-config.schema.json");
+
+ if (!File.Exists(_schemaPath))
+ throw new FileNotFoundException($"Schema file not found at: {_schemaPath}");
+ }
+
+ [Fact]
+ public void GenerateFromFile_ProducesValidJson()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.NotEmpty(result);
+
+ // Verify it's valid JSON
+ var parsed = JsonNode.Parse(result);
+ Assert.NotNull(parsed);
+
+ // Verify $schema property is present
+ Assert.NotNull(parsed["$schema"]);
+ Assert.Equal("https://raw.githubusercontent.com/ErikEJ/EFCorePowerTools/master/samples/efcpt-config.schema.json",
+ parsed["$schema"]?.GetValue());
+ }
+
+ [Fact]
+ public void GenerateFromFile_IncludesCodeGenerationSection()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+ var config = JsonNode.Parse(result);
+
+ // Assert
+ Assert.NotNull(config);
+ var codeGen = config["code-generation"];
+ Assert.NotNull(codeGen);
+
+ // Verify required properties exist
+ Assert.NotNull(codeGen["enable-on-configuring"]);
+ Assert.NotNull(codeGen["type"]);
+ Assert.NotNull(codeGen["use-database-names"]);
+ Assert.NotNull(codeGen["use-data-annotations"]);
+ Assert.NotNull(codeGen["use-nullable-reference-types"]);
+ Assert.NotNull(codeGen["use-inflector"]);
+ Assert.NotNull(codeGen["use-legacy-inflector"]);
+ Assert.NotNull(codeGen["use-many-to-many-entity"]);
+ Assert.NotNull(codeGen["use-t4"]);
+ Assert.NotNull(codeGen["remove-defaultsql-from-bool-properties"]);
+ Assert.NotNull(codeGen["soft-delete-obsolete-files"]);
+ Assert.NotNull(codeGen["use-alternate-stored-procedure-resultset-discovery"]);
+ }
+
+ [Fact]
+ public void GenerateFromFile_IncludesNamesSection()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+ var config = JsonNode.Parse(result);
+
+ // Assert
+ Assert.NotNull(config);
+ var names = config["names"];
+ Assert.NotNull(names);
+
+ // Verify required properties exist with defaults
+ Assert.Equal("ApplicationDbContext", names["dbcontext-name"]?.GetValue());
+ Assert.Equal("EfcptProject", names["root-namespace"]?.GetValue());
+ }
+
+ [Fact]
+ public void GenerateFromFile_IncludesFileLayoutSection()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+ var config = JsonNode.Parse(result);
+
+ // Assert
+ Assert.NotNull(config);
+ var fileLayout = config["file-layout"];
+ Assert.NotNull(fileLayout);
+
+ // Verify required properties exist
+ Assert.NotNull(fileLayout["output-path"]);
+ Assert.Equal("Models", fileLayout["output-path"]?.GetValue());
+ }
+
+ [Fact]
+ public void GenerateFromFile_ExcludesPreviewProperties()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+ var config = JsonNode.Parse(result);
+
+ // Assert - verify no preview properties are present
+ Assert.NotNull(config);
+ var jsonString = result.ToLowerInvariant();
+ Assert.DoesNotContain("-preview", jsonString);
+ }
+
+ [Fact]
+ public void GenerateFromFile_WithCustomNames()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(
+ _schemaPath,
+ dbContextName: "MyCustomContext",
+ rootNamespace: "MyCustomNamespace");
+
+ var config = JsonNode.Parse(result);
+
+ // Assert
+ Assert.NotNull(config);
+ var names = config["names"];
+ Assert.NotNull(names);
+ Assert.Equal("MyCustomContext", names["dbcontext-name"]?.GetValue());
+ Assert.Equal("MyCustomNamespace", names["root-namespace"]?.GetValue());
+ }
+
+ [Fact]
+ public void GenerateFromFile_UsesSchemaDefaults()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+ var config = JsonNode.Parse(result);
+
+ // Assert - verify defaults from schema
+ Assert.NotNull(config);
+ var codeGen = config["code-generation"];
+ Assert.NotNull(codeGen);
+
+ // Check known defaults from schema
+ Assert.Equal("all", codeGen["type"]?.GetValue());
+ Assert.True(codeGen["use-inflector"]?.GetValue());
+ Assert.True(codeGen["soft-delete-obsolete-files"]?.GetValue());
+ }
+
+ [Fact]
+ public void GenerateFromFile_ProducesExpectedStructure()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+
+ // Assert - verify the structure matches expected format
+ Assert.Contains("\"code-generation\":", result);
+ Assert.Contains("\"names\":", result);
+ Assert.Contains("\"file-layout\":", result);
+ Assert.Contains("\"$schema\":", result);
+
+ // Verify indentation (should be formatted)
+ Assert.Contains(" ", result);
+
+ // Verify type-mappings is NOT present (not required)
+ Assert.DoesNotContain("\"type-mappings\":", result);
+ }
+
+ [Fact]
+ public void GenerateFromFile_OnlyIncludesRequiredProperties()
+ {
+ // Act
+ var result = EfcptConfigGenerator.GenerateFromFile(_schemaPath);
+ var config = JsonNode.Parse(result);
+
+ // Assert
+ Assert.NotNull(config);
+
+ // Verify only required sections are present
+ Assert.NotNull(config["$schema"]);
+ Assert.NotNull(config["code-generation"]);
+ Assert.NotNull(config["names"]);
+ Assert.NotNull(config["file-layout"]);
+
+ // Verify optional sections are NOT present
+ Assert.Null(config["type-mappings"]);
+ Assert.Null(config["tables"]);
+ Assert.Null(config["views"]);
+ Assert.Null(config["stored-procedures"]);
+ Assert.Null(config["functions"]);
+ Assert.Null(config["replacements"]);
+
+ // Verify code-generation has exactly 12 required properties
+ var codeGen = config["code-generation"]?.AsObject();
+ Assert.NotNull(codeGen);
+ Assert.Equal(12, codeGen.Count);
+
+ // Verify names has exactly 2 required properties
+ var names = config["names"]?.AsObject();
+ Assert.NotNull(names);
+ Assert.Equal(2, names.Count);
+
+ // Verify file-layout has exactly 1 required property
+ var fileLayout = config["file-layout"]?.AsObject();
+ Assert.NotNull(fileLayout);
+ Assert.Single(fileLayout);
+ }
+
+ private static string FindRepoRoot()
+ {
+ var current = Directory.GetCurrentDirectory();
+ while (current != null)
+ {
+ if (Directory.Exists(Path.Combine(current, ".git")))
+ return current;
+
+ var parent = Directory.GetParent(current);
+ current = parent?.FullName;
+ }
+
+ throw new DirectoryNotFoundException("Could not find repository root");
+ }
+}