From c305542272e8ee41e3212983f08c4fc71f2cfc2a Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 11:41:06 +0200 Subject: [PATCH 1/8] remove prds --- docs/prd/prd-04-deepen-interface-generator.md | 100 ------------------ 1 file changed, 100 deletions(-) delete mode 100644 docs/prd/prd-04-deepen-interface-generator.md diff --git a/docs/prd/prd-04-deepen-interface-generator.md b/docs/prd/prd-04-deepen-interface-generator.md deleted file mode 100644 index c0eaca33c..000000000 --- a/docs/prd/prd-04-deepen-interface-generator.md +++ /dev/null @@ -1,100 +0,0 @@ -# PRD: Deepen InterfaceGenerator - -## Problem Statement - -The InterfaceGenerator module is 450 lines handling interface declaration, method generation (with all the header/attribute logic), return type derivation, dynamic querystring parameter class generation, obsolete attribute handling, and parameter aggregation. The `IInterfacePartitioning` interface already exists and is a good seam for multi-interface mode, but `InterfaceGenerator` itself is shallow — it's nearly as complex as the code it generates. Method generation has many conditional paths (headers, multipart, auth, dynamic querystring, cancellation, Apizr options) that are all entangled in the `GenerateMethod` method. - -## Solution - -Extract `IMethodGenerator` as the seam between interface assembly and method generation. Each method generation concern (signature, attributes, return type) becomes its own adapter. The generator delegates to these adapters. - -The deepened module structure: - -``` -IMethodGenerator (new seam) - ├── GenerateMethodSignature(operation, interfaceName, partitioning) → string - ├── GenerateMethodAttributes(operation, interfaceName, partitioning) → string[] - └── GenerateReturnType(operation) → string - -InterfaceGenerator (existing type, deepened) - ├── Generate(partitioning, operations) → GeneratedCode - └── Delegates to IMethodGenerator for method-level concerns - -IMethodSignatureGenerator (new module) - ├── Generate(operation, interfaceName, partitioning, knownIdentifiers) → (methodName, parameters) - └── Handles: method name deduplication, parameter aggregation, optional parameter reordering - -IMethodAttributeGenerator (new module) - ├── GenerateHeaders(operation, document, settings) → string[] - ├── GenerateMultipartAttribute(operationModel) → string? - ├── GenerateObsoleteAttribute(operation) → string? - └── Handles: Accept headers, Content-Type headers, Authorization headers, [Multipart], [Obsolete] - -IReturnTypeGenerator (new module) - ├── Generate(operation, settings) → string - ├── GenerateForApiResponse(operation) → string? - ├── GenerateForFileStream(operation) → string? - └── Handles: return type derivation, IApiResponse wrapping, Task/IObservable selection -``` - -The existing `InterfaceGenerator.Generate()` method becomes a composition of method generators: - -``` -InterfaceGenerator.Generate(partitioning) - → for each operation group: - → for each operation: - → signature = methodSignatureGenerator.Generate(...) - → attributes = methodAttributeGenerator.Generate(...) - → returnType = returnTypeGenerator.Generate(...) - → emit interface with signature, attributes, return type -``` - -## User Stories - -1. As a maintainer fixing return type derivation, I want return type logic isolated in IReturnTypeGenerator, so that I don't need to understand method signature generation to fix it. -2. As a tester, I want to test return type derivation independently of method signature generation, so that I can verify return types for all operation combinations. -3. As a developer, I want the IMethodGenerator interface to be small and stable, so that new method generation concerns can be added without changing existing code. -4. As a maintainer fixing header generation, I want header logic isolated in IMethodAttributeGenerator, so that I don't risk breaking parameter aggregation. -5. As a tester, I want to test header generation for all header combinations (Accept, Content-Type, Authorization), so that I can verify all header paths. -6. As a developer, I want the IReturnTypeGenerator interface to expose return type derivation clearly, so that I can understand what determines a method's return type. -7. As a tester, I want to test dynamic querystring parameter generation independently, so that I can verify the parameter class generation. -8. As a maintainer, I want the deletion test to pass for each generator, so that I can be confident each generator concentrates its complexity. -9. As a developer, I want the InterfaceGenerator public API to remain unchanged, so that GeneratorPipeline consumers are unaffected. -10. As a tester, I want each generator to have its own test class, so that I can verify behavior in isolation. - -## Implementation Decisions - -- **InterfaceGenerator remains the public type:** The class name and public API surface remain unchanged. The deepening is internal. -- **IMethodGenerator interface:** A small interface that generates method-level code. The interface has three methods: GenerateMethodSignature, GenerateMethodAttributes, and GenerateReturnType. -- **IMethodSignatureGenerator:** Handles method name deduplication (using IdentifierUtils.Counted), parameter aggregation (delegating to ParameterAggregator), and optional parameter reordering (delegating to OptionalParameterReorderer). -- **IMethodAttributeGenerator:** Handles all method-level attributes: Accept headers, Content-Type headers, Authorization headers, [Multipart], [Obsolete]. Each attribute type is a separate method. -- **IReturnTypeGenerator:** Handles return type derivation: operation response type, IApiResponse wrapping, Task/IObservable selection, file stream detection, response type override. -- **Dynamic querystring parameter generation:** The existing DynamicQuerystringParameterBuilder logic is preserved. It is called by IMethodSignatureGenerator when dynamic querystring mode is enabled. -- **No new dependencies:** All deepening uses existing types. No new libraries introduced. -- **Partitioning interface preserved:** The existing `IInterfacePartitioning` interface is the correct seam for multi-interface mode. It is not changed. - -## Testing Decisions - -- **What makes a good test:** Only test external behavior (generated method code), not internal generator structure. Each generator's test surface is defined by its interface. -- **Return type tests:** Tests verify return type derivation for all operation combinations: success codes, file streams, IApiResponse wrapping, Task/IObservable selection, response type override. -- **Header generation tests:** Tests verify Accept header generation, Content-Type header generation, Authorization header generation, and header combinations. Tests that ignored headers are excluded. -- **Method signature tests:** Tests verify method name deduplication, parameter aggregation, and optional parameter reordering. Tests that dynamic querystring parameter generation produces correct output. -- **Attribute generation tests:** Tests verify [Multipart] attribute generation for multipart/form-data operations. Tests that [Obsolete] attribute is generated for deprecated operations. -- **Integration tests:** Existing scenario tests under `Refitter.Tests.Scenarios` remain valid. Tests that use `RefitGenerator.CreateAsync(...).Generate()` continue to work. The generated output must remain identical. -- **Build verification:** `dotnet build -c Release src/Refitter.slnx` and `dotnet test --solution src/Refitter.slnx -c Release` must pass before commit. -- **Prior art:** Existing tests for `InterfaceGenerator` follow the pattern: OpenAPI spec → generate → assert string patterns → `BuildHelper.BuildCSharp()`. These tests remain valid after deepening. - -## Out of Scope - -- **Interface naming logic:** The interface naming logic (using IInterfacePartitioning) is preserved in InterfaceGenerator. It is not changed. -- **Interface documentation:** The interface documentation logic (delegating to XmlDocumentationGenerator) is preserved in InterfaceGenerator. It is not changed. -- **Parameter extraction:** The existing IParameterExtractor and IParameterTypeExtractor interfaces are preserved. They are not changed. -- **Adding new method attributes:** This PRD deepens existing attribute generation only. Adding new attributes is a separate effort. -- **Performance optimization:** This PRD does not address generation performance. - -## Further Notes - -- **Risk:** Low. The public API is preserved. The deepening is internal refactoring. -- **Leverage:** 450 lines → 4 modules (1 interface + 3 generators). Each generator is independently testable. -- **Deletion test:** Delete IReturnTypeGenerator → return type derivation vanishes. Delete IMethodAttributeGenerator → attribute generation vanishes. Each generator concentrates complexity that would otherwise reappear across callers. -- **The interface is the test surface:** After deepening, each generator's interface defines what tests must cover. A deep interface means fewer tests know about implementation details. From 0d635c5ee08ff4a2afa784c7d453519cba4ea4e9 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 11:51:29 +0200 Subject: [PATCH 2/8] refactor: remove underscore prefix from private fields in ApizrOptionsBuilder --- src/Refitter.Core/ApizrOptionsBuilder.cs | 56 ++++++++++++------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Refitter.Core/ApizrOptionsBuilder.cs b/src/Refitter.Core/ApizrOptionsBuilder.cs index d9e7f3370..b922dc8c6 100644 --- a/src/Refitter.Core/ApizrOptionsBuilder.cs +++ b/src/Refitter.Core/ApizrOptionsBuilder.cs @@ -5,69 +5,69 @@ namespace Refitter.Core; internal class ApizrOptionsBuilder : IApizrOptionsBuilder { - private readonly StringBuilder _optionsCode; - private readonly StringBuilder _usingsCode; - private readonly HashSet _packages = new(); - private bool _hasOptions; + private readonly StringBuilder optionsCode; + private readonly StringBuilder usingsCode; + private readonly HashSet packages = new(); + private bool hasOptions; public ApizrOptionsBuilder(string initialOptionsCode, string initialUsings) { - _optionsCode = new StringBuilder(initialOptionsCode); - _usingsCode = new StringBuilder(initialUsings); - _usingsCode.AppendLine(); + optionsCode = new StringBuilder(initialOptionsCode); + usingsCode = new StringBuilder(initialUsings); + usingsCode.AppendLine(); } - public bool HasOptions => _hasOptions; + public bool HasOptions => hasOptions; public void WithBaseAddress(string baseUrl, string duplicateStrategy) { - _optionsCode.AppendLine(); - _optionsCode.Append($" .WithBaseAddress(\"{baseUrl}\", {duplicateStrategy})"); - _hasOptions = true; + optionsCode.AppendLine(); + optionsCode.Append($" .WithBaseAddress(\"{baseUrl}\", {duplicateStrategy})"); + hasOptions = true; } public void WithDelegatingHandler(string handlerType) { - _optionsCode.AppendLine(); - _optionsCode.Append($" .WithDelegatingHandler<{handlerType}>()"); - _hasOptions = true; + optionsCode.AppendLine(); + optionsCode.Append($" .WithDelegatingHandler<{handlerType}>()"); + hasOptions = true; } public void ConfigureHttpClientBuilder(Action configure) { - _optionsCode.AppendLine(); - configure(_optionsCode); - _hasOptions = true; + optionsCode.AppendLine(); + configure(optionsCode); + hasOptions = true; } public void AppendOptionsCode(string code) { - _optionsCode.Append(code); - _hasOptions = true; + optionsCode.Append(code); + hasOptions = true; } public void AddPackage(ApizrPackages package) { - _packages.Add(package); + packages.Add(package); } public void AddUsing(string usingDirective) { - _usingsCode.Append(" "); - _usingsCode.AppendLine(usingDirective); + usingsCode.Append(" "); + usingsCode.AppendLine(usingDirective); } public string BuildOptionsCode() { - if (_hasOptions) - _optionsCode.Append(";"); + if (hasOptions) + optionsCode.Append(";"); else - _optionsCode.Clear(); + optionsCode.Clear(); - return _optionsCode.ToString(); + return optionsCode.ToString(); } - public string GetUsings() => _usingsCode.ToString(); + public string GetUsings() => usingsCode.ToString(); - public List GetPackages() => _packages.ToList(); + public List GetPackages() => packages.ToList(); } From 38d2f49989b20fb28d53617c61749b7bc152c60f Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 11:55:22 +0200 Subject: [PATCH 3/8] refactor: remove underscore prefix from private fields in RefitGeneratorSettings --- .../Settings/RefitGeneratorSettings.cs | 212 +++++++++--------- 1 file changed, 106 insertions(+), 106 deletions(-) diff --git a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs index d1581fed4..30cc81e87 100644 --- a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs +++ b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs @@ -21,14 +21,14 @@ public class RefitGeneratorSettings /// public const string DefaultNamespace = OutputConfig.DefaultNamespace; - private readonly OpenApiSourceConfig _openApiSource = new(); - private readonly CodeGenerationConfig _codeGeneration = new(); - private readonly ParameterConfig _parameterConfig = new(); - private readonly OutputConfig _outputConfig = new(); - private readonly TypeConfig _typeConfig = new(); - private readonly FilterConfig _filterConfig = new(); - private readonly SchemaConfig _schemaConfig = new(); - private readonly FeatureConfig _featureConfig = new(); + private readonly OpenApiSourceConfig openApiSource = new(); + private readonly CodeGenerationConfig codeGeneration = new(); + private readonly ParameterConfig parameterConfig = new(); + private readonly OutputConfig outputConfig = new(); + private readonly TypeConfig typeConfig = new(); + private readonly FilterConfig filterConfig = new(); + private readonly SchemaConfig schemaConfig = new(); + private readonly FeatureConfig featureConfig = new(); /// /// Gets or sets the path to the Open API. @@ -38,8 +38,8 @@ public class RefitGeneratorSettings [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? OpenApiPath { - get => _openApiSource.OpenApiPath; - set => _openApiSource.OpenApiPath = value; + get => openApiSource.OpenApiPath; + set => openApiSource.OpenApiPath = value; } /// @@ -50,8 +50,8 @@ public string? OpenApiPath [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public string[]? OpenApiPaths { - get => _openApiSource.OpenApiPaths; - set => _openApiSource.OpenApiPaths = value; + get => openApiSource.OpenApiPaths; + set => openApiSource.OpenApiPaths = value; } /// @@ -60,8 +60,8 @@ public string[]? OpenApiPaths [Description("The namespace for the generated code. Default is GeneratedCode.")] public string Namespace { - get => _outputConfig.Namespace; - set => _outputConfig.Namespace = value; + get => outputConfig.Namespace; + set => outputConfig.Namespace = value; } /// @@ -70,8 +70,8 @@ public string Namespace [Description("The namespace for the generated contracts. Default is GeneratedCode.")] public string? ContractsNamespace { - get => _outputConfig.ContractsNamespace; - set => _outputConfig.ContractsNamespace = value; + get => outputConfig.ContractsNamespace; + set => outputConfig.ContractsNamespace = value; } /// @@ -87,8 +87,8 @@ public string? ContractsNamespace [JsonConverter(typeof(JsonStringEnumConverter))] public PropertyNamingPolicy PropertyNamingPolicy { - get => _typeConfig.PropertyNamingPolicy; - set => _typeConfig.PropertyNamingPolicy = value; + get => typeConfig.PropertyNamingPolicy; + set => typeConfig.PropertyNamingPolicy = value; } /// @@ -97,8 +97,8 @@ public PropertyNamingPolicy PropertyNamingPolicy [Description("Generate contracts. Default is true.")] public bool GenerateContracts { - get => _codeGeneration.GenerateContracts; - set => _codeGeneration.GenerateContracts = value; + get => codeGeneration.GenerateContracts; + set => codeGeneration.GenerateContracts = value; } /// @@ -107,8 +107,8 @@ public bool GenerateContracts [Description("Generate clients. Default is true.")] public bool GenerateClients { - get => _codeGeneration.GenerateClients; - set => _codeGeneration.GenerateClients = value; + get => codeGeneration.GenerateClients; + set => codeGeneration.GenerateClients = value; } /// @@ -117,8 +117,8 @@ public bool GenerateClients [Description("Generate clients that implement IDisposable.")] public bool GenerateDisposableClients { - get => _codeGeneration.GenerateDisposableClients; - set => _codeGeneration.GenerateDisposableClients = value; + get => codeGeneration.GenerateDisposableClients; + set => codeGeneration.GenerateDisposableClients = value; } /// @@ -127,8 +127,8 @@ public bool GenerateDisposableClients [Description("Generate XML doc comments. Default is true.")] public bool GenerateXmlDocCodeComments { - get => _codeGeneration.GenerateXmlDocCodeComments; - set => _codeGeneration.GenerateXmlDocCodeComments = value; + get => codeGeneration.GenerateXmlDocCodeComments; + set => codeGeneration.GenerateXmlDocCodeComments = value; } /// @@ -143,8 +143,8 @@ the relevant status codes specified in the OpenAPI document. )] public bool GenerateStatusCodeComments { - get => _codeGeneration.GenerateStatusCodeComments; - set => _codeGeneration.GenerateStatusCodeComments = value; + get => codeGeneration.GenerateStatusCodeComments; + set => codeGeneration.GenerateStatusCodeComments = value; } /// @@ -153,8 +153,8 @@ public bool GenerateStatusCodeComments [Description("Add auto-generated header. Default is true.")] public bool AddAutoGeneratedHeader { - get => _codeGeneration.AddAutoGeneratedHeader; - set => _codeGeneration.AddAutoGeneratedHeader = value; + get => codeGeneration.AddAutoGeneratedHeader; + set => codeGeneration.AddAutoGeneratedHeader = value; } /// @@ -163,8 +163,8 @@ public bool AddAutoGeneratedHeader [Description("Add accept headers [Headers(\"Accept: application/json\")]. Default is true.")] public bool AddAcceptHeaders { - get => _codeGeneration.AddAcceptHeaders; - set => _codeGeneration.AddAcceptHeaders = value; + get => codeGeneration.AddAcceptHeaders; + set => codeGeneration.AddAcceptHeaders = value; } /// @@ -173,8 +173,8 @@ public bool AddAcceptHeaders [Description("Add content-type headers [Headers(\"Content-Type: application/json\")]. Default is true.")] public bool AddContentTypeHeaders { - get => _codeGeneration.AddContentTypeHeaders; - set => _codeGeneration.AddContentTypeHeaders = value; + get => codeGeneration.AddContentTypeHeaders; + set => codeGeneration.AddContentTypeHeaders = value; } /// @@ -183,8 +183,8 @@ public bool AddContentTypeHeaders [Description("Return IApiResponse objects.")] public bool ReturnIApiResponse { - get => _codeGeneration.ReturnIApiResponse; - set => _codeGeneration.ReturnIApiResponse = value; + get => codeGeneration.ReturnIApiResponse; + set => codeGeneration.ReturnIApiResponse = value; } /// @@ -193,8 +193,8 @@ public bool ReturnIApiResponse [Description("Return IObservable or Task.")] public bool ReturnIObservable { - get => _codeGeneration.ReturnIObservable; - set => _codeGeneration.ReturnIObservable = value; + get => codeGeneration.ReturnIObservable; + set => codeGeneration.ReturnIObservable = value; } /// @@ -209,8 +209,8 @@ AddAcceptHeaders dictionary of operation ids and a specific response type that t )] public Dictionary ResponseTypeOverride { - get => _codeGeneration.ResponseTypeOverride; - set => _codeGeneration.ResponseTypeOverride = value; + get => codeGeneration.ResponseTypeOverride; + set => codeGeneration.ResponseTypeOverride = value; } /// @@ -219,8 +219,8 @@ public Dictionary ResponseTypeOverride [Description("Generate operation headers. Default is true.")] public bool GenerateOperationHeaders { - get => _codeGeneration.GenerateOperationHeaders; - set => _codeGeneration.GenerateOperationHeaders = value; + get => codeGeneration.GenerateOperationHeaders; + set => codeGeneration.GenerateOperationHeaders = value; } /// @@ -229,8 +229,8 @@ public bool GenerateOperationHeaders [Description("A collection of headers to omit from operation signatures.")] public string[] IgnoredOperationHeaders { - get => _filterConfig.IgnoredOperationHeaders; - set => _filterConfig.IgnoredOperationHeaders = value; + get => filterConfig.IgnoredOperationHeaders; + set => filterConfig.IgnoredOperationHeaders = value; } /// @@ -240,8 +240,8 @@ public string[] IgnoredOperationHeaders [JsonConverter(typeof(JsonStringEnumConverter))] public TypeAccessibility TypeAccessibility { - get => _typeConfig.TypeAccessibility; - set => _typeConfig.TypeAccessibility = value; + get => typeConfig.TypeAccessibility; + set => typeConfig.TypeAccessibility = value; } /// @@ -250,8 +250,8 @@ public TypeAccessibility TypeAccessibility [Description("Enable or disable the use of cancellation tokens.")] public bool UseCancellationTokens { - get => _parameterConfig.UseCancellationTokens; - set => _parameterConfig.UseCancellationTokens = value; + get => parameterConfig.UseCancellationTokens; + set => parameterConfig.UseCancellationTokens = value; } /// @@ -266,8 +266,8 @@ Set to true to explicitly format date query string parameters )] public bool UseIsoDateFormat { - get => _parameterConfig.UseIsoDateFormat; - set => _parameterConfig.UseIsoDateFormat = value; + get => parameterConfig.UseIsoDateFormat; + set => parameterConfig.UseIsoDateFormat = value; } /// @@ -276,8 +276,8 @@ public bool UseIsoDateFormat [Description("Add additional namespace to generated types.")] public string[] AdditionalNamespaces { - get => _filterConfig.AdditionalNamespaces; - set => _filterConfig.AdditionalNamespaces = value; + get => filterConfig.AdditionalNamespaces; + set => filterConfig.AdditionalNamespaces = value; } /// @@ -286,8 +286,8 @@ public string[] AdditionalNamespaces [Description("Exclude namespaces on generated types.")] public string[] ExcludeNamespaces { - get => _filterConfig.ExcludeNamespaces; - set => _filterConfig.ExcludeNamespaces = value; + get => filterConfig.ExcludeNamespaces; + set => filterConfig.ExcludeNamespaces = value; } /// @@ -297,8 +297,8 @@ public string[] ExcludeNamespaces [JsonConverter(typeof(JsonStringEnumConverter))] public MultipleInterfaces MultipleInterfaces { - get => _codeGeneration.MultipleInterfaces; - set => _codeGeneration.MultipleInterfaces = value; + get => codeGeneration.MultipleInterfaces; + set => codeGeneration.MultipleInterfaces = value; } /// @@ -308,8 +308,8 @@ public MultipleInterfaces MultipleInterfaces [Description("Only include Paths that match the provided regular expression. May be set multiple times.")] public string[] IncludePathMatches { - get => _filterConfig.IncludePathMatches; - set => _filterConfig.IncludePathMatches = value; + get => filterConfig.IncludePathMatches; + set => filterConfig.IncludePathMatches = value; } /// @@ -319,8 +319,8 @@ public string[] IncludePathMatches [Description("Generate a Refit interface for each endpoint.")] public string[] IncludeTags { - get => _filterConfig.IncludeTags; - set => _filterConfig.IncludeTags = value; + get => filterConfig.IncludeTags; + set => filterConfig.IncludeTags = value; } /// @@ -329,8 +329,8 @@ public string[] IncludeTags [Description("Generate deprecated operations. Default is true.")] public bool GenerateDeprecatedOperations { - get => _codeGeneration.GenerateDeprecatedOperations; - set => _codeGeneration.GenerateDeprecatedOperations = value; + get => codeGeneration.GenerateDeprecatedOperations; + set => codeGeneration.GenerateDeprecatedOperations = value; } /// @@ -345,8 +345,8 @@ this is name of the Execute() method in the interface. )] public string? OperationNameTemplate { - get => _codeGeneration.OperationNameTemplate; - set => _codeGeneration.OperationNameTemplate = value; + get => codeGeneration.OperationNameTemplate; + set => codeGeneration.OperationNameTemplate = value; } /// @@ -355,8 +355,8 @@ public string? OperationNameTemplate [Description("Re-order optional parameters to the end of the parameter list.")] public bool OptionalParameters { - get => _parameterConfig.OptionalParameters; - set => _parameterConfig.OptionalParameters = value; + get => parameterConfig.OptionalParameters; + set => parameterConfig.OptionalParameters = value; } /// @@ -365,8 +365,8 @@ public bool OptionalParameters [Description("The relative path to a folder in which the output files are generated. Default is ./Generated.")] public string OutputFolder { - get => _outputConfig.OutputFolder; - set => _outputConfig.OutputFolder = value; + get => outputConfig.OutputFolder; + set => outputConfig.OutputFolder = value; } /// @@ -375,8 +375,8 @@ public string OutputFolder [Description("The relative path to a folder where to store the generated contracts. Default is ./Generated.")] public string? ContractsOutputFolder { - get => _outputConfig.ContractsOutputFolder; - set => _outputConfig.ContractsOutputFolder = value; + get => outputConfig.ContractsOutputFolder; + set => outputConfig.ContractsOutputFolder = value; } /// @@ -394,8 +394,8 @@ The filename of the generated code. )] public string? OutputFilename { - get => _outputConfig.OutputFilename; - set => _outputConfig.OutputFilename = value; + get => outputConfig.OutputFilename; + set => outputConfig.OutputFilename = value; } /// @@ -422,8 +422,8 @@ This works in conjunction with includeTags and includePathMatches. )] public bool TrimUnusedSchema { - get => _schemaConfig.TrimUnusedSchema; - set => _schemaConfig.TrimUnusedSchema = value; + get => schemaConfig.TrimUnusedSchema; + set => schemaConfig.TrimUnusedSchema = value; } /// @@ -438,8 +438,8 @@ This works in conjunction with TrimUnusedSchema. )] public string[] KeepSchemaPatterns { - get => _schemaConfig.KeepSchemaPatterns; - set => _schemaConfig.KeepSchemaPatterns = value; + get => schemaConfig.KeepSchemaPatterns; + set => schemaConfig.KeepSchemaPatterns = value; } /// @@ -456,8 +456,8 @@ This works in conjunction with TrimUnusedSchema. )] public bool IncludeInheritanceHierarchy { - get => _schemaConfig.IncludeInheritanceHierarchy; - set => _schemaConfig.IncludeInheritanceHierarchy = value; + get => schemaConfig.IncludeInheritanceHierarchy; + set => schemaConfig.IncludeInheritanceHierarchy = value; } /// @@ -467,8 +467,8 @@ public bool IncludeInheritanceHierarchy [JsonConverter(typeof(JsonStringEnumConverter))] public OperationNameGeneratorTypes OperationNameGenerator { - get => _codeGeneration.OperationNameGenerator; - set => _codeGeneration.OperationNameGenerator = value; + get => codeGeneration.OperationNameGenerator; + set => codeGeneration.OperationNameGenerator = value; } /// @@ -477,8 +477,8 @@ public OperationNameGeneratorTypes OperationNameGenerator [Description("Skip default additional properties. Default is true.")] public bool GenerateDefaultAdditionalProperties { - get => _codeGeneration.GenerateDefaultAdditionalProperties; - set => _codeGeneration.GenerateDefaultAdditionalProperties = value; + get => codeGeneration.GenerateDefaultAdditionalProperties; + set => codeGeneration.GenerateDefaultAdditionalProperties = value; } /// @@ -487,8 +487,8 @@ public bool GenerateDefaultAdditionalProperties [Description("Generate contracts as immutable records instead of classes.")] public bool ImmutableRecords { - get => _typeConfig.ImmutableRecords; - set => _typeConfig.ImmutableRecords = value; + get => typeConfig.ImmutableRecords; + set => typeConfig.ImmutableRecords = value; } /// @@ -497,8 +497,8 @@ public bool ImmutableRecords [Description("The settings describing how to configure Apizr.")] public ApizrSettings? ApizrSettings { - get => _featureConfig.ApizrSettings; - set => _featureConfig.ApizrSettings = value; + get => featureConfig.ApizrSettings; + set => featureConfig.ApizrSettings = value; } /// @@ -513,8 +513,8 @@ Wrap multiple query parameters into a single complex one. )] public bool UseDynamicQuerystringParameters { - get => _parameterConfig.UseDynamicQuerystringParameters; - set => _parameterConfig.UseDynamicQuerystringParameters = value; + get => parameterConfig.UseDynamicQuerystringParameters; + set => parameterConfig.UseDynamicQuerystringParameters = value; } /// @@ -537,8 +537,8 @@ Dependency Injection is written to a file called DependencyInjection.cs )] public bool GenerateMultipleFiles { - get => _outputConfig.GenerateMultipleFiles; - set => _outputConfig.GenerateMultipleFiles = value; + get => outputConfig.GenerateMultipleFiles; + set => outputConfig.GenerateMultipleFiles = value; } /// @@ -560,8 +560,8 @@ payloads with (yet) unknown types are offered by newer versions of an API )] public bool UsePolymorphicSerialization { - get => _featureConfig.UsePolymorphicSerialization; - set => _featureConfig.UsePolymorphicSerialization = value; + get => featureConfig.UsePolymorphicSerialization; + set => featureConfig.UsePolymorphicSerialization = value; } /// @@ -570,8 +570,8 @@ public bool UsePolymorphicSerialization [JsonIgnore] public IParameterNameGenerator? ParameterNameGenerator { - get => _parameterConfig.ParameterNameGenerator; - set => _parameterConfig.ParameterNameGenerator = value; + get => parameterConfig.ParameterNameGenerator; + set => parameterConfig.ParameterNameGenerator = value; } /// @@ -581,8 +581,8 @@ public IParameterNameGenerator? ParameterNameGenerator [JsonConverter(typeof(JsonStringEnumConverter))] public AuthenticationHeaderStyle AuthenticationHeaderStyle { - get => _featureConfig.AuthenticationHeaderStyle; - set => _featureConfig.AuthenticationHeaderStyle = value; + get => featureConfig.AuthenticationHeaderStyle; + set => featureConfig.AuthenticationHeaderStyle = value; } /// @@ -593,8 +593,8 @@ public AuthenticationHeaderStyle AuthenticationHeaderStyle [JsonConverter(typeof(JsonStringEnumConverter))] public CollectionFormat CollectionFormat { - get => _parameterConfig.CollectionFormat; - set => _parameterConfig.CollectionFormat = value; + get => parameterConfig.CollectionFormat; + set => parameterConfig.CollectionFormat = value; } /// @@ -604,8 +604,8 @@ public CollectionFormat CollectionFormat [Description("Custom directory with NSwag fluid templates for code generation. Default is null which uses the default NSwag templates. See https://github.com/RicoSuter/NSwag/wiki/Templates")] public string? CustomTemplateDirectory { - get => _codeGeneration.CustomTemplateDirectory; - set => _codeGeneration.CustomTemplateDirectory = value; + get => codeGeneration.CustomTemplateDirectory; + set => codeGeneration.CustomTemplateDirectory = value; } /// @@ -615,8 +615,8 @@ public string? CustomTemplateDirectory [Description("Security scheme for which to generate authentication headers.")] public string? SecurityScheme { - get => _featureConfig.SecurityScheme; - set => _featureConfig.SecurityScheme = value; + get => featureConfig.SecurityScheme; + set => featureConfig.SecurityScheme = value; } /// @@ -626,8 +626,8 @@ public string? SecurityScheme [Description("Generate JsonSerializerContext for AOT compilation support in the contracts namespace when contracts are generated.")] public bool GenerateJsonSerializerContext { - get => _featureConfig.GenerateJsonSerializerContext; - set => _featureConfig.GenerateJsonSerializerContext = value; + get => featureConfig.GenerateJsonSerializerContext; + set => featureConfig.GenerateJsonSerializerContext = value; } /// @@ -636,7 +636,7 @@ public bool GenerateJsonSerializerContext [Description("Suffix to append to all generated contract type names. Default is null which doesn't append any suffix.")] public string? ContractTypeSuffix { - get => _typeConfig.ContractTypeSuffix; - set => _typeConfig.ContractTypeSuffix = value; + get => typeConfig.ContractTypeSuffix; + set => typeConfig.ContractTypeSuffix = value; } } From ce0b60879651c7e22a61c6f17f1f9eb8d2aef8d5 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 11:55:48 +0200 Subject: [PATCH 4/8] refactor: remove underscore prefix from private fields in DocumentMerger --- src/Refitter.Core/DocumentMerger.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Refitter.Core/DocumentMerger.cs b/src/Refitter.Core/DocumentMerger.cs index cb976aa76..4a4f8084b 100644 --- a/src/Refitter.Core/DocumentMerger.cs +++ b/src/Refitter.Core/DocumentMerger.cs @@ -7,7 +7,7 @@ namespace Refitter.Core; internal sealed class DocumentMerger : IDocumentMerger { - private readonly DocumentEquivalenceComparer _comparer; + private readonly DocumentEquivalenceComparer comparer; /// /// Initializes a new instance of the DocumentMerger class. @@ -15,7 +15,7 @@ internal sealed class DocumentMerger : IDocumentMerger /// The comparer used to detect equivalent document elements during merging. public DocumentMerger(DocumentEquivalenceComparer comparer) { - _comparer = comparer; + comparer = comparer; } /// @@ -94,7 +94,7 @@ private void MergeIfMissingOrThrowOnConflict( return; } - if (!_comparer.AreEquivalent(existingValue, value)) + if (!comparer.AreEquivalent(existingValue, value)) throw CreateMergeConflictException(itemType, key); } From 582b825f37234b59854a325fed7dbd1a57360f95 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 11:56:24 +0200 Subject: [PATCH 5/8] refactor: remove underscore prefix from private fields in XmlDocumentationGeneratorTests --- .../XmlDocumentationGeneratorTests.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Refitter.Tests/XmlDocumentationGeneratorTests.cs b/src/Refitter.Tests/XmlDocumentationGeneratorTests.cs index ab50a0555..766d46268 100644 --- a/src/Refitter.Tests/XmlDocumentationGeneratorTests.cs +++ b/src/Refitter.Tests/XmlDocumentationGeneratorTests.cs @@ -9,7 +9,7 @@ namespace Refitter.Tests; public class XmlDocumentationGeneratorTests { - private readonly XmlDocumentationGenerator _generator = new(new() { GenerateXmlDocCodeComments = true }); + private readonly XmlDocumentationGenerator generator = new(new() { GenerateXmlDocCodeComments = true }); private static CSharpOperationModel CreateOperationModel(OpenApiOperation operation) { @@ -23,7 +23,7 @@ public void Can_Generate_Interface_Doc_Without_Linebreaks() { var docs = new StringBuilder(); var interfaceDefinition = new OpenApiOperation { Summary = "Test", }; - this._generator.AppendInterfaceDocumentationByEndpoint(interfaceDefinition, docs); + this.generator.AppendInterfaceDocumentationByEndpoint(interfaceDefinition, docs); docs.ToString().Trim().Should().Be("/// Test"); } @@ -32,7 +32,7 @@ public void Can_Generate_Interface_Doc_With_Linebreaks() { var docs = new StringBuilder(); var interfaceDefinition = new OpenApiOperation { Summary = "Test\n", }; - this._generator.AppendInterfaceDocumentationByEndpoint(interfaceDefinition, docs); + this.generator.AppendInterfaceDocumentationByEndpoint(interfaceDefinition, docs); docs.ToString().Trim().Should().NotBe("/// Test"); docs.ToString().Trim().Should().Contain("") .And.Contain("Test"); @@ -45,7 +45,7 @@ public void Can_Generate_Interface_Doc_From_Controller_Tag() var controllerTag = new OpenApiTag { Name = "TestController", Description = "TestControllerDescription" }; var document = new OpenApiDocument { Tags = [controllerTag] }; - this._generator.AppendInterfaceDocumentationByTag(document, "TestController", docs); + this.generator.AppendInterfaceDocumentationByTag(document, "TestController", docs); docs.ToString().Trim().Should().Be("/// TestControllerDescription"); } @@ -56,7 +56,7 @@ public void Can_Handle_Null_Document_Tags() var docs = new StringBuilder(); var document = new OpenApiDocument { Tags = null }; - this._generator.AppendInterfaceDocumentationByTag(document, "TestController", docs); + this.generator.AppendInterfaceDocumentationByTag(document, "TestController", docs); docs.ToString().Trim().Should().Be("/// Operations for TestController."); } @@ -68,7 +68,7 @@ public void Can_Escape_Xml_Special_Characters_In_Interface_Doc() var controllerTag = new OpenApiTag { Name = "TestController", Description = "Test & content" }; var document = new OpenApiDocument { Tags = [controllerTag] }; - this._generator.AppendInterfaceDocumentationByTag(document, "TestController", docs); + this.generator.AppendInterfaceDocumentationByTag(document, "TestController", docs); docs.ToString().Trim().Should().Be("/// Test <tag> & content"); } @@ -78,7 +78,7 @@ public void Can_Escape_Xml_Special_Characters_In_Method_Summary() { var docs = new StringBuilder(); var method = CreateOperationModel(new OpenApiOperation { Summary = "Test & content", }); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Trim().Should().StartWith("/// Test <tag> & content"); } @@ -87,7 +87,7 @@ public void Can_Generate_Method_Summary() { var docs = new StringBuilder(); var method = CreateOperationModel(new OpenApiOperation { Summary = "TestSummary", }); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Trim().Should().StartWith("/// TestSummary"); } @@ -96,7 +96,7 @@ public void Can_Generate_Method_Remarks() { var docs = new StringBuilder(); var method = CreateOperationModel(new OpenApiOperation { Description = "TestDescription", }); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Should().Contain("/// TestDescription"); } @@ -108,7 +108,7 @@ public void Can_Generate_Method_Param() { Parameters = { new OpenApiParameter { OriginalName = "testParam", Description = "TestParameter" } }, }); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Should().Contain("/// TestParameter"); } @@ -120,7 +120,7 @@ public void Can_Generate_ApizrRequestOptions_Param() { Parameters = { new OpenApiParameter { OriginalName = "testParam", Description = "TestParameter" } }, }); - this._generator.AppendMethodDocumentation(method, false, false, true, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, true, false, docs); docs.ToString().Should().Contain("/// The instance to pass through the request."); } @@ -132,7 +132,7 @@ public void Can_Generate_DynamicQuerystring_Param() { Parameters = { new OpenApiParameter { OriginalName = "testParam", Description = "TestParameter" } }, }); - this._generator.AppendMethodDocumentation(method, false, true, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, true, false, false, docs); docs.ToString().Should().Contain("/// The dynamic querystring parameter wrapping all others."); } @@ -144,7 +144,7 @@ public void Can_Generate_CancellationToken_Param() { Parameters = { new OpenApiParameter { OriginalName = "testParam", Description = "TestParameter" } }, }); - this._generator.AppendMethodDocumentation(method, false, false, false, true, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, true, docs); docs.ToString().Should().Contain("/// The cancellation token to cancel the request."); } @@ -164,7 +164,7 @@ public void Can_Generate_Method_Returns() }, Produces = ["application/json"], }); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Should().Contain("/// TestResponse"); } @@ -180,7 +180,7 @@ public void Can_Generate_Method_Returns_With_Empty_Result() }, Produces = ["application/json"], }); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Should().Contain("/// ") .And.Contain("Task"); } @@ -190,7 +190,7 @@ public void Can_Generate_Method_Returns_Without_Result() { var docs = new StringBuilder(); var method = CreateOperationModel(new OpenApiOperation()); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Should().Contain("/// ") .And.Contain("Task"); } @@ -200,7 +200,7 @@ public void Can_Generate_Method_Throws() { var docs = new StringBuilder(); var method = CreateOperationModel(new OpenApiOperation()); - this._generator.AppendMethodDocumentation(method, false, false, false, false, docs); + this.generator.AppendMethodDocumentation(method, false, false, false, false, docs); docs.ToString().Should().Contain("/// "); } From 8c80dfa4db5de7b68759707c88cb3b8d7b2521cf Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 12:02:48 +0200 Subject: [PATCH 6/8] docs: add naming rule requiring no underscore prefix for private fields --- .editorconfig | 13 +++++++++++++ docs/AGENTS.md | 1 + 2 files changed, 14 insertions(+) diff --git a/.editorconfig b/.editorconfig index b3e8ddc2d..b3cc2e1e9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -240,6 +240,10 @@ dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = pascalcase +dotnet_naming_rule.private_fields_should_be_camelcase.severity = suggestion +dotnet_naming_rule.private_fields_should_be_camelcase.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camelcase.style = camelcase_no_prefix + dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase @@ -302,6 +306,10 @@ dotnet_naming_symbols.private_constant_fields.applicable_kinds = field dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_constant_fields.required_modifiers = const +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected +dotnet_naming_symbols.private_fields.required_modifiers = + dotnet_naming_symbols.local_variables.applicable_kinds = local dotnet_naming_symbols.local_variables.applicable_accessibilities = local dotnet_naming_symbols.local_variables.required_modifiers = @@ -352,6 +360,11 @@ dotnet_naming_style._camelcase.required_suffix = dotnet_naming_style._camelcase.word_separator = dotnet_naming_style._camelcase.capitalization = camel_case +dotnet_naming_style.camelcase_no_prefix.required_prefix = +dotnet_naming_style.camelcase_no_prefix.required_suffix = +dotnet_naming_style.camelcase_no_prefix.word_separator = +dotnet_naming_style.camelcase_no_prefix.capitalization = camel_case + dotnet_naming_style.camelcase.required_prefix = dotnet_naming_style.camelcase.required_suffix = dotnet_naming_style.camelcase.word_separator = diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 426f823c9..9141b1b02 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -51,6 +51,7 @@ Run these first if source generator tests fail with missing generated types. - 4-space indentation; `crlf` line endings for C# files per `.editorconfig`. - `var` is discouraged (`csharp_style_var_* = false:silent`). - `TreatWarningsAsErrors` is enabled on most production projects. +- **Private fields must NOT be prefixed with `_`** — use camelCase without underscore prefix (e.g., `openApiPath` not `_openApiPath`). ## Key Conventions From 3df27f04a172b2cbf7911b21f266eaff4f136359 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 12:55:17 +0200 Subject: [PATCH 7/8] remove squad workflows --- .github/workflows/squad-ci.yml | 28 -- .github/workflows/squad-docs.yml | 27 -- .github/workflows/squad-heartbeat.yml | 167 ----------- .github/workflows/squad-insider-release.yml | 34 --- .github/workflows/squad-issue-assign.yml | 161 ----------- .github/workflows/squad-label-enforce.yml | 181 ------------ .github/workflows/squad-preview.yml | 30 -- .github/workflows/squad-promote.yml | 120 -------- .github/workflows/squad-release.yml | 34 --- .github/workflows/squad-triage.yml | 262 ------------------ .github/workflows/sync-squad-labels.yml | 171 ------------ .../workflows/template-source-generator.yml | 29 -- src/Refitter.Core/DocumentMerger.cs | 2 +- 13 files changed, 1 insertion(+), 1245 deletions(-) delete mode 100644 .github/workflows/squad-ci.yml delete mode 100644 .github/workflows/squad-docs.yml delete mode 100644 .github/workflows/squad-heartbeat.yml delete mode 100644 .github/workflows/squad-insider-release.yml delete mode 100644 .github/workflows/squad-issue-assign.yml delete mode 100644 .github/workflows/squad-label-enforce.yml delete mode 100644 .github/workflows/squad-preview.yml delete mode 100644 .github/workflows/squad-promote.yml delete mode 100644 .github/workflows/squad-release.yml delete mode 100644 .github/workflows/squad-triage.yml delete mode 100644 .github/workflows/sync-squad-labels.yml delete mode 100644 .github/workflows/template-source-generator.yml diff --git a/.github/workflows/squad-ci.yml b/.github/workflows/squad-ci.yml deleted file mode 100644 index 3b214346b..000000000 --- a/.github/workflows/squad-ci.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Squad CI -# Project type was not detected — configure build/test commands below - -on: - pull_request: - branches: [dev, preview, main, insider] - types: [opened, synchronize, reopened] - push: - branches: [dev, insider] - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Build and test - run: | - # TODO: Project type was not detected — add your build/test commands here - # Go: go test ./... - # Python: pip install -r requirements.txt && pytest - # .NET: dotnet test - # Java (Maven): mvn test - # Java (Gradle): ./gradlew test - echo "No build commands configured — update squad-ci.yml" diff --git a/.github/workflows/squad-docs.yml b/.github/workflows/squad-docs.yml deleted file mode 100644 index a6ecefdeb..000000000 --- a/.github/workflows/squad-docs.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Squad Docs — Build & Deploy -# Project type was not detected — configure documentation build commands below - -on: - workflow_dispatch: - push: - branches: [preview] - paths: - - 'docs/**' - - '.github/workflows/squad-docs.yml' - -permissions: - contents: read - pages: write - id-token: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Build docs - run: | - # TODO: Add your documentation build commands here - # This workflow is optional — remove or customize it for your project - echo "No docs build commands configured — update or remove squad-docs.yml" diff --git a/.github/workflows/squad-heartbeat.yml b/.github/workflows/squad-heartbeat.yml deleted file mode 100644 index cee4d7c0a..000000000 --- a/.github/workflows/squad-heartbeat.yml +++ /dev/null @@ -1,167 +0,0 @@ -name: Squad Heartbeat (Ralph) -# ⚠️ SYNC: This workflow is maintained in 4 locations. Changes must be applied to all: -# - templates/workflows/squad-heartbeat.yml (source template) -# - packages/squad-cli/templates/workflows/squad-heartbeat.yml (CLI package) -# - .squad/templates/workflows/squad-heartbeat.yml (installed template) -# - .github/workflows/squad-heartbeat.yml (active workflow) -# Run 'squad upgrade' to sync installed copies from source templates. - -on: - # React to completed work or new squad work - issues: - types: [closed, labeled] - pull_request: - types: [closed] - - # Manual trigger - workflow_dispatch: - -permissions: - issues: write - contents: read - pull-requests: read - -jobs: - heartbeat: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Check triage script - id: check-script - run: | - if [ -f ".squad/templates/ralph-triage.js" ]; then - echo "has_script=true" >> $GITHUB_OUTPUT - else - echo "has_script=false" >> $GITHUB_OUTPUT - echo "⚠️ ralph-triage.js not found — run 'squad upgrade' to install" - fi - - - name: Ralph — Smart triage - if: steps.check-script.outputs.has_script == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - node .squad/templates/ralph-triage.js \ - --squad-dir .squad \ - --output triage-results.json - - - name: Ralph — Apply triage decisions - if: steps.check-script.outputs.has_script == 'true' && hashFiles('triage-results.json') != '' - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const path = 'triage-results.json'; - if (!fs.existsSync(path)) { - core.info('No triage results — board is clear'); - return; - } - - const results = JSON.parse(fs.readFileSync(path, 'utf8')); - if (results.length === 0) { - core.info('📋 Board is clear — Ralph found no untriaged issues'); - return; - } - - for (const decision of results) { - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: decision.issueNumber, - labels: [decision.label] - }); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: decision.issueNumber, - body: [ - '### 🔄 Ralph — Auto-Triage', - '', - `**Assigned to:** ${decision.assignTo}`, - `**Reason:** ${decision.reason}`, - `**Source:** ${decision.source}`, - '', - '> Ralph auto-triaged this issue using routing rules.', - '> To reassign, swap the `squad:*` label.' - ].join('\n') - }); - - core.info(`Triaged #${decision.issueNumber} → ${decision.assignTo} (${decision.source})`); - } catch (e) { - core.warning(`Failed to triage #${decision.issueNumber}: ${e.message}`); - } - } - - core.info(`🔄 Ralph triaged ${results.length} issue(s)`); - - # Copilot auto-assign step (uses PAT if available) - - name: Ralph — Assign @copilot issues - if: success() - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN || secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - - let teamFile = '.squad/team.md'; - if (!fs.existsSync(teamFile)) { - teamFile = '.ai-team/team.md'; - } - if (!fs.existsSync(teamFile)) return; - - const content = fs.readFileSync(teamFile, 'utf8'); - - // Check if @copilot is on the team with auto-assign - const hasCopilot = content.includes('🤖 Coding Agent') || content.includes('@copilot'); - const autoAssign = content.includes(''); - if (!hasCopilot || !autoAssign) return; - - // Find issues labeled squad:copilot with no assignee - try { - const { data: copilotIssues } = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: 'squad:copilot', - state: 'open', - per_page: 5 - }); - - const unassigned = copilotIssues.filter(i => - !i.assignees || i.assignees.length === 0 - ); - - if (unassigned.length === 0) { - core.info('No unassigned squad:copilot issues'); - return; - } - - // Get repo default branch - const { data: repoData } = await github.rest.repos.get({ - owner: context.repo.owner, - repo: context.repo.repo - }); - - for (const issue of unassigned) { - try { - await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - assignees: ['copilot-swe-agent[bot]'], - agent_assignment: { - target_repo: `${context.repo.owner}/${context.repo.repo}`, - base_branch: repoData.default_branch, - custom_instructions: `Read .squad/team.md (or .ai-team/team.md) for team context and .squad/routing.md (or .ai-team/routing.md) for routing rules.` - } - }); - core.info(`Assigned copilot-swe-agent[bot] to #${issue.number}`); - } catch (e) { - core.warning(`Failed to assign @copilot to #${issue.number}: ${e.message}`); - } - } - } catch (e) { - core.info(`No squad:copilot label found or error: ${e.message}`); - } diff --git a/.github/workflows/squad-insider-release.yml b/.github/workflows/squad-insider-release.yml deleted file mode 100644 index 7b84c3fba..000000000 --- a/.github/workflows/squad-insider-release.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Squad Insider Release -# Project type was not detected — configure build, test, and insider release commands below - -on: - push: - branches: [insider] - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Build and test - run: | - # TODO: Project type was not detected — add your build/test commands here - # Go: go test ./... - # Python: pip install -r requirements.txt && pytest - # .NET: dotnet test - # Java (Maven): mvn test - # Java (Gradle): ./gradlew test - echo "No build commands configured — update squad-insider-release.yml" - - - name: Create insider release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # TODO: Add your insider/pre-release commands here - echo "No release commands configured — update squad-insider-release.yml" diff --git a/.github/workflows/squad-issue-assign.yml b/.github/workflows/squad-issue-assign.yml deleted file mode 100644 index a31a59c07..000000000 --- a/.github/workflows/squad-issue-assign.yml +++ /dev/null @@ -1,161 +0,0 @@ -name: Squad Issue Assign - -on: - issues: - types: [labeled] - -permissions: - issues: write - contents: read - -jobs: - assign-work: - # Only trigger on squad:{member} labels (not the base "squad" label) - if: startsWith(github.event.label.name, 'squad:') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Identify assigned member and trigger work - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const issue = context.payload.issue; - const label = context.payload.label.name; - - // Extract member name from label (e.g., "squad:ripley" → "ripley") - const memberName = label.replace('squad:', '').toLowerCase(); - - // Read team roster — check .squad/ first, fall back to .ai-team/ - let teamFile = '.squad/team.md'; - if (!fs.existsSync(teamFile)) { - teamFile = '.ai-team/team.md'; - } - if (!fs.existsSync(teamFile)) { - core.warning('No .squad/team.md or .ai-team/team.md found — cannot assign work'); - return; - } - - const content = fs.readFileSync(teamFile, 'utf8'); - const lines = content.split('\n'); - - // Check if this is a coding agent assignment - const isCopilotAssignment = memberName === 'copilot'; - - let assignedMember = null; - if (isCopilotAssignment) { - assignedMember = { name: '@copilot', role: 'Coding Agent' }; - } else { - let inMembersTable = false; - for (const line of lines) { - if (line.match(/^##\s+(Members|Team Roster)/i)) { - inMembersTable = true; - continue; - } - if (inMembersTable && line.startsWith('## ')) { - break; - } - if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { - const cells = line.split('|').map(c => c.trim()).filter(Boolean); - if (cells.length >= 2 && cells[0].toLowerCase() === memberName) { - assignedMember = { name: cells[0], role: cells[1] }; - break; - } - } - } - } - - if (!assignedMember) { - core.warning(`No member found matching label "${label}"`); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `⚠️ No squad member found matching label \`${label}\`. Check \`.squad/team.md\` (or \`.ai-team/team.md\`) for valid member names.` - }); - return; - } - - // Post assignment acknowledgment - let comment; - if (isCopilotAssignment) { - comment = [ - `### 🤖 Routed to @copilot (Coding Agent)`, - '', - `**Issue:** #${issue.number} — ${issue.title}`, - '', - `@copilot has been assigned and will pick this up automatically.`, - '', - `> The coding agent will create a \`copilot/*\` branch and open a draft PR.`, - `> Review the PR as you would any team member's work.`, - ].join('\n'); - } else { - comment = [ - `### 📋 Assigned to ${assignedMember.name} (${assignedMember.role})`, - '', - `**Issue:** #${issue.number} — ${issue.title}`, - '', - `${assignedMember.name} will pick this up in the next Copilot session.`, - '', - `> **For Copilot coding agent:** If enabled, this issue will be worked automatically.`, - `> Otherwise, start a Copilot session and say:`, - `> \`${assignedMember.name}, work on issue #${issue.number}\``, - ].join('\n'); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: comment - }); - - core.info(`Issue #${issue.number} assigned to ${assignedMember.name} (${assignedMember.role})`); - - # Separate step: assign @copilot using PAT (required for coding agent) - - name: Assign @copilot coding agent - if: github.event.label.name == 'squad:copilot' - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.COPILOT_ASSIGN_TOKEN }} - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const issue_number = context.payload.issue.number; - - // Get the default branch name (main, master, etc.) - const { data: repoData } = await github.rest.repos.get({ owner, repo }); - const baseBranch = repoData.default_branch; - - try { - await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', { - owner, - repo, - issue_number, - assignees: ['copilot-swe-agent[bot]'], - agent_assignment: { - target_repo: `${owner}/${repo}`, - base_branch: baseBranch, - custom_instructions: '', - custom_agent: '', - model: '' - }, - headers: { - 'X-GitHub-Api-Version': '2022-11-28' - } - }); - core.info(`Assigned copilot-swe-agent to issue #${issue_number} (base: ${baseBranch})`); - } catch (err) { - core.warning(`Assignment with agent_assignment failed: ${err.message}`); - // Fallback: try without agent_assignment - try { - await github.rest.issues.addAssignees({ - owner, repo, issue_number, - assignees: ['copilot-swe-agent'] - }); - core.info(`Fallback assigned copilot-swe-agent to issue #${issue_number}`); - } catch (err2) { - core.warning(`Fallback also failed: ${err2.message}`); - } - } diff --git a/.github/workflows/squad-label-enforce.yml b/.github/workflows/squad-label-enforce.yml deleted file mode 100644 index 33d57e7cf..000000000 --- a/.github/workflows/squad-label-enforce.yml +++ /dev/null @@ -1,181 +0,0 @@ -name: Squad Label Enforce - -on: - issues: - types: [labeled] - -permissions: - issues: write - contents: read - -jobs: - enforce: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Enforce mutual exclusivity - uses: actions/github-script@v9 - with: - script: | - const issue = context.payload.issue; - const appliedLabel = context.payload.label.name; - - // Namespaces with mutual exclusivity rules - const EXCLUSIVE_PREFIXES = ['go:', 'release:', 'type:', 'priority:']; - - // Skip if not a managed namespace label - if (!EXCLUSIVE_PREFIXES.some(p => appliedLabel.startsWith(p))) { - core.info(`Label ${appliedLabel} is not in a managed namespace — skipping`); - return; - } - - const allLabels = issue.labels.map(l => l.name); - - // Handle go: namespace (mutual exclusivity) - if (appliedLabel.startsWith('go:')) { - const otherGoLabels = allLabels.filter(l => - l.startsWith('go:') && l !== appliedLabel - ); - - if (otherGoLabels.length > 0) { - // Remove conflicting go: labels - for (const label of otherGoLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - // Post update comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Triage verdict updated → \`${appliedLabel}\`` - }); - } - - // Auto-apply release:backlog if go:yes and no release target - if (appliedLabel === 'go:yes') { - const hasReleaseLabel = allLabels.some(l => l.startsWith('release:')); - if (!hasReleaseLabel) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ['release:backlog'] - }); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `📋 Marked as \`release:backlog\` — assign a release target when ready.` - }); - - core.info('Applied release:backlog for go:yes issue'); - } - } - - // Remove release: labels if go:no - if (appliedLabel === 'go:no') { - const releaseLabels = allLabels.filter(l => l.startsWith('release:')); - if (releaseLabels.length > 0) { - for (const label of releaseLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed release label from go:no issue: ${label}`); - } - } - } - } - - // Handle release: namespace (mutual exclusivity) - if (appliedLabel.startsWith('release:')) { - const otherReleaseLabels = allLabels.filter(l => - l.startsWith('release:') && l !== appliedLabel - ); - - if (otherReleaseLabels.length > 0) { - // Remove conflicting release: labels - for (const label of otherReleaseLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - // Post update comment - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Release target updated → \`${appliedLabel}\`` - }); - } - } - - // Handle type: namespace (mutual exclusivity) - if (appliedLabel.startsWith('type:')) { - const otherTypeLabels = allLabels.filter(l => - l.startsWith('type:') && l !== appliedLabel - ); - - if (otherTypeLabels.length > 0) { - for (const label of otherTypeLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Issue type updated → \`${appliedLabel}\`` - }); - } - } - - // Handle priority: namespace (mutual exclusivity) - if (appliedLabel.startsWith('priority:')) { - const otherPriorityLabels = allLabels.filter(l => - l.startsWith('priority:') && l !== appliedLabel - ); - - if (otherPriorityLabels.length > 0) { - for (const label of otherPriorityLabels) { - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - name: label - }); - core.info(`Removed conflicting label: ${label}`); - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: `🏷️ Priority updated → \`${appliedLabel}\`` - }); - } - } - - core.info(`Label enforcement complete for ${appliedLabel}`); diff --git a/.github/workflows/squad-preview.yml b/.github/workflows/squad-preview.yml deleted file mode 100644 index 9015a914b..000000000 --- a/.github/workflows/squad-preview.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Squad Preview Validation -# Project type was not detected — configure build, test, and validation commands below - -on: - push: - branches: [preview] - -permissions: - contents: read - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Build and test - run: | - # TODO: Project type was not detected — add your build/test commands here - # Go: go test ./... - # Python: pip install -r requirements.txt && pytest - # .NET: dotnet test - # Java (Maven): mvn test - # Java (Gradle): ./gradlew test - echo "No build commands configured — update squad-preview.yml" - - - name: Validate - run: | - # TODO: Add pre-release validation commands here - echo "No validation commands configured — update squad-preview.yml" diff --git a/.github/workflows/squad-promote.yml b/.github/workflows/squad-promote.yml deleted file mode 100644 index 2406d51b4..000000000 --- a/.github/workflows/squad-promote.yml +++ /dev/null @@ -1,120 +0,0 @@ -name: Squad Promote - -on: - workflow_dispatch: - inputs: - dry_run: - description: 'Dry run — show what would happen without pushing' - required: false - default: 'false' - type: choice - options: ['false', 'true'] - -permissions: - contents: write - -jobs: - dev-to-preview: - name: Promote dev → preview - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Fetch all branches - run: git fetch --all - - - name: Show current state (dry run info) - run: | - echo "=== dev HEAD ===" && git log origin/dev -1 --oneline - echo "=== preview HEAD ===" && git log origin/preview -1 --oneline - echo "=== Files that would be stripped ===" - git diff origin/preview..origin/dev --name-only | grep -E "^(\.(ai-team|squad|ai-team-templates)|team-docs/|docs/proposals/)" || echo "(none)" - - - name: Merge dev → preview (strip forbidden paths) - if: ${{ inputs.dry_run == 'false' }} - run: | - git checkout preview - git merge origin/dev --no-commit --no-ff -X theirs || true - - # Strip forbidden paths from merge commit - git rm -rf --cached --ignore-unmatch \ - .ai-team/ \ - .squad/ \ - .ai-team-templates/ \ - team-docs/ \ - "docs/proposals/" || true - - # Commit if there are staged changes - if ! git diff --cached --quiet; then - git commit -m "chore: promote dev → preview (v$(node -e "console.log(require('./package.json').version)"))" - git push origin preview - echo "✅ Pushed preview branch" - else - echo "ℹ️ Nothing to commit — preview is already up to date" - fi - - - name: Dry run complete - if: ${{ inputs.dry_run == 'true' }} - run: echo "🔍 Dry run complete — no changes pushed." - - preview-to-main: - name: Promote preview → main (release) - needs: dev-to-preview - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure git - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: Fetch all branches - run: git fetch --all - - - name: Show current state - run: | - echo "=== preview HEAD ===" && git log origin/preview -1 --oneline - echo "=== main HEAD ===" && git log origin/main -1 --oneline - echo "=== Version ===" && node -e "console.log('v' + require('./package.json').version)" - - - name: Validate preview is release-ready - run: | - git checkout preview - VERSION=$(node -e "console.log(require('./package.json').version)") - if ! grep -q "## \[$VERSION\]" CHANGELOG.md 2>/dev/null; then - echo "::error::Version $VERSION not found in CHANGELOG.md — update before releasing" - exit 1 - fi - echo "✅ Version $VERSION has CHANGELOG entry" - - # Verify no forbidden files on preview - FORBIDDEN=$(git ls-files | grep -E "^(\.(ai-team|squad|ai-team-templates)/|team-docs/|docs/proposals/)" || true) - if [ -n "$FORBIDDEN" ]; then - echo "::error::Forbidden files found on preview: $FORBIDDEN" - exit 1 - fi - echo "✅ No forbidden files on preview" - - - name: Merge preview → main - if: ${{ inputs.dry_run == 'false' }} - run: | - git checkout main - git merge origin/preview --no-ff -m "chore: promote preview → main (v$(node -e "console.log(require('./package.json').version)"))" - git push origin main - echo "✅ Pushed main — squad-release.yml will tag and publish the release" - - - name: Dry run complete - if: ${{ inputs.dry_run == 'true' }} - run: echo "🔍 Dry run complete — no changes pushed." diff --git a/.github/workflows/squad-release.yml b/.github/workflows/squad-release.yml deleted file mode 100644 index cc000894c..000000000 --- a/.github/workflows/squad-release.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Squad Release -# Project type was not detected — configure build, test, and release commands below - -on: - push: - branches: [main] - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Build and test - run: | - # TODO: Project type was not detected — add your build/test commands here - # Go: go test ./... - # Python: pip install -r requirements.txt && pytest - # .NET: dotnet test - # Java (Maven): mvn test - # Java (Gradle): ./gradlew test - echo "No build commands configured — update squad-release.yml" - - - name: Create release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # TODO: Add your release commands here (e.g., git tag, gh release create) - echo "No release commands configured — update squad-release.yml" diff --git a/.github/workflows/squad-triage.yml b/.github/workflows/squad-triage.yml deleted file mode 100644 index 012da8863..000000000 --- a/.github/workflows/squad-triage.yml +++ /dev/null @@ -1,262 +0,0 @@ -name: Squad Triage - -on: - issues: - types: [labeled] - -permissions: - issues: write - contents: read - -jobs: - triage: - if: github.event.label.name == 'squad' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Triage issue via Lead agent - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const issue = context.payload.issue; - - // Read team roster — check .squad/ first, fall back to .ai-team/ - let teamFile = '.squad/team.md'; - if (!fs.existsSync(teamFile)) { - teamFile = '.ai-team/team.md'; - } - if (!fs.existsSync(teamFile)) { - core.warning('No .squad/team.md or .ai-team/team.md found — cannot triage'); - return; - } - - const content = fs.readFileSync(teamFile, 'utf8'); - const lines = content.split('\n'); - - // Check if @copilot is on the team - const hasCopilot = content.includes('🤖 Coding Agent'); - const copilotAutoAssign = content.includes(''); - - // Parse @copilot capability profile - let goodFitKeywords = []; - let needsReviewKeywords = []; - let notSuitableKeywords = []; - - if (hasCopilot) { - // Extract capability tiers from team.md - const goodFitMatch = content.match(/🟢\s*Good fit[^:]*:\s*(.+)/i); - const needsReviewMatch = content.match(/🟡\s*Needs review[^:]*:\s*(.+)/i); - const notSuitableMatch = content.match(/🔴\s*Not suitable[^:]*:\s*(.+)/i); - - if (goodFitMatch) { - goodFitKeywords = goodFitMatch[1].toLowerCase().split(',').map(s => s.trim()); - } else { - goodFitKeywords = ['bug fix', 'test coverage', 'lint', 'format', 'dependency update', 'small feature', 'scaffolding', 'doc fix', 'documentation']; - } - if (needsReviewMatch) { - needsReviewKeywords = needsReviewMatch[1].toLowerCase().split(',').map(s => s.trim()); - } else { - needsReviewKeywords = ['medium feature', 'refactoring', 'api endpoint', 'migration']; - } - if (notSuitableMatch) { - notSuitableKeywords = notSuitableMatch[1].toLowerCase().split(',').map(s => s.trim()); - } else { - notSuitableKeywords = ['architecture', 'system design', 'security', 'auth', 'encryption', 'performance']; - } - } - - const members = []; - let inMembersTable = false; - for (const line of lines) { - if (line.match(/^##\s+(Members|Team Roster)/i)) { - inMembersTable = true; - continue; - } - if (inMembersTable && line.startsWith('## ')) { - break; - } - if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { - const cells = line.split('|').map(c => c.trim()).filter(Boolean); - if (cells.length >= 2 && cells[0] !== 'Scribe') { - members.push({ - name: cells[0], - role: cells[1] - }); - } - } - } - - // Read routing rules — check .squad/ first, fall back to .ai-team/ - let routingFile = '.squad/routing.md'; - if (!fs.existsSync(routingFile)) { - routingFile = '.ai-team/routing.md'; - } - let routingContent = ''; - if (fs.existsSync(routingFile)) { - routingContent = fs.readFileSync(routingFile, 'utf8'); - } - - // Find the Lead - const lead = members.find(m => - m.role.toLowerCase().includes('lead') || - m.role.toLowerCase().includes('architect') || - m.role.toLowerCase().includes('coordinator') - ); - - if (!lead) { - core.warning('No Lead role found in team roster — cannot triage'); - return; - } - - function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } - - // Build triage context - const memberList = members.map(m => - `- **${m.name}** (${m.role}) → label: \`squad:${slugify(m.name)}\`` - ).join('\n'); - - // Determine best assignee based on issue content and routing - const issueText = `${issue.title}\n${issue.body || ''}`.toLowerCase(); - - let assignedMember = null; - let triageReason = ''; - let copilotTier = null; - - // First, evaluate @copilot fit if enabled - if (hasCopilot) { - const isNotSuitable = notSuitableKeywords.some(kw => issueText.includes(kw)); - const isGoodFit = !isNotSuitable && goodFitKeywords.some(kw => issueText.includes(kw)); - const isNeedsReview = !isNotSuitable && !isGoodFit && needsReviewKeywords.some(kw => issueText.includes(kw)); - - if (isGoodFit) { - copilotTier = 'good-fit'; - assignedMember = { name: '@copilot', role: 'Coding Agent' }; - triageReason = '🟢 Good fit for @copilot — matches capability profile'; - } else if (isNeedsReview) { - copilotTier = 'needs-review'; - assignedMember = { name: '@copilot', role: 'Coding Agent' }; - triageReason = '🟡 Routing to @copilot (needs review) — a squad member should review the PR'; - } else if (isNotSuitable) { - copilotTier = 'not-suitable'; - // Fall through to normal routing - } - } - - // If not routed to @copilot, use keyword-based routing - if (!assignedMember) { - for (const member of members) { - const role = member.role.toLowerCase(); - if ((role.includes('frontend') || role.includes('ui')) && - (issueText.includes('ui') || issueText.includes('frontend') || - issueText.includes('css') || issueText.includes('component') || - issueText.includes('button') || issueText.includes('page') || - issueText.includes('layout') || issueText.includes('design'))) { - assignedMember = member; - triageReason = 'Issue relates to frontend/UI work'; - break; - } - if ((role.includes('backend') || role.includes('api') || role.includes('server')) && - (issueText.includes('api') || issueText.includes('backend') || - issueText.includes('database') || issueText.includes('endpoint') || - issueText.includes('server') || issueText.includes('auth'))) { - assignedMember = member; - triageReason = 'Issue relates to backend/API work'; - break; - } - if ((role.includes('test') || role.includes('qa') || role.includes('quality')) && - (issueText.includes('test') || issueText.includes('bug') || - issueText.includes('fix') || issueText.includes('regression') || - issueText.includes('coverage'))) { - assignedMember = member; - triageReason = 'Issue relates to testing/quality work'; - break; - } - if ((role.includes('devops') || role.includes('infra') || role.includes('ops')) && - (issueText.includes('deploy') || issueText.includes('ci') || - issueText.includes('pipeline') || issueText.includes('docker') || - issueText.includes('infrastructure'))) { - assignedMember = member; - triageReason = 'Issue relates to DevOps/infrastructure work'; - break; - } - } - } - - // Default to Lead if no routing match - if (!assignedMember) { - assignedMember = lead; - triageReason = 'No specific domain match — assigned to Lead for further analysis'; - } - - const isCopilot = assignedMember.name === '@copilot'; - const assignLabel = isCopilot ? 'squad:copilot' : `squad:${slugify(assignedMember.name)}`; - - // Add the member-specific label - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: [assignLabel] - }); - - // Apply default triage verdict - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - labels: ['go:needs-research'] - }); - - // Auto-assign @copilot if enabled - if (isCopilot && copilotAutoAssign) { - try { - await github.rest.issues.addAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - assignees: ['copilot'] - }); - } catch (err) { - core.warning(`Could not auto-assign @copilot: ${err.message}`); - } - } - - // Build copilot evaluation note - let copilotNote = ''; - if (hasCopilot && !isCopilot) { - if (copilotTier === 'not-suitable') { - copilotNote = `\n\n**@copilot evaluation:** 🔴 Not suitable — issue involves work outside the coding agent's capability profile.`; - } else { - copilotNote = `\n\n**@copilot evaluation:** No strong capability match — routed to squad member.`; - } - } - - // Post triage comment - const comment = [ - `### 🏗️ Squad Triage — ${lead.name} (${lead.role})`, - '', - `**Issue:** #${issue.number} — ${issue.title}`, - `**Assigned to:** ${assignedMember.name} (${assignedMember.role})`, - `**Reason:** ${triageReason}`, - copilotTier === 'needs-review' ? `\n⚠️ **PR review recommended** — a squad member should review @copilot's work on this one.` : '', - copilotNote, - '', - `---`, - '', - `**Team roster:**`, - memberList, - hasCopilot ? `- **@copilot** (Coding Agent) → label: \`squad:copilot\`` : '', - '', - `> To reassign, remove the current \`squad:*\` label and add the correct one.`, - ].filter(Boolean).join('\n'); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue.number, - body: comment - }); - - core.info(`Triaged issue #${issue.number} → ${assignedMember.name} (${assignLabel})`); diff --git a/.github/workflows/sync-squad-labels.yml b/.github/workflows/sync-squad-labels.yml deleted file mode 100644 index 1c7fbdf1c..000000000 --- a/.github/workflows/sync-squad-labels.yml +++ /dev/null @@ -1,171 +0,0 @@ -name: Sync Squad Labels - -on: - push: - paths: - - '.squad/team.md' - - '.ai-team/team.md' - workflow_dispatch: - -permissions: - issues: write - contents: read - -jobs: - sync-labels: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Parse roster and sync labels - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - let teamFile = '.squad/team.md'; - if (!fs.existsSync(teamFile)) { - teamFile = '.ai-team/team.md'; - } - - if (!fs.existsSync(teamFile)) { - core.info('No .squad/team.md or .ai-team/team.md found — skipping label sync'); - return; - } - - const content = fs.readFileSync(teamFile, 'utf8'); - const lines = content.split('\n'); - - // Parse the Members table for agent names - const members = []; - let inMembersTable = false; - for (const line of lines) { - if (line.match(/^##\s+(Members|Team Roster)/i)) { - inMembersTable = true; - continue; - } - if (inMembersTable && line.startsWith('## ')) { - break; - } - if (inMembersTable && line.startsWith('|') && !line.includes('---') && !line.includes('Name')) { - const cells = line.split('|').map(c => c.trim()).filter(Boolean); - if (cells.length >= 2 && cells[0] !== 'Scribe') { - members.push({ - name: cells[0], - role: cells[1] - }); - } - } - } - - core.info(`Found ${members.length} squad members: ${members.map(m => m.name).join(', ')}`); - - // Check if @copilot is on the team - const hasCopilot = content.includes('🤖 Coding Agent'); - - // Define label color palette for squad labels - const SQUAD_COLOR = '9B8FCC'; - const MEMBER_COLOR = '9B8FCC'; - const COPILOT_COLOR = '10b981'; - - // Define go: and release: labels (static) - const GO_LABELS = [ - { name: 'go:yes', color: '0E8A16', description: 'Ready to implement' }, - { name: 'go:no', color: 'B60205', description: 'Not pursuing' }, - { name: 'go:needs-research', color: 'FBCA04', description: 'Needs investigation' } - ]; - - const RELEASE_LABELS = [ - { name: 'release:v0.4.0', color: '6B8EB5', description: 'Targeted for v0.4.0' }, - { name: 'release:v0.5.0', color: '6B8EB5', description: 'Targeted for v0.5.0' }, - { name: 'release:v0.6.0', color: '8B7DB5', description: 'Targeted for v0.6.0' }, - { name: 'release:v1.0.0', color: '8B7DB5', description: 'Targeted for v1.0.0' }, - { name: 'release:backlog', color: 'D4E5F7', description: 'Not yet targeted' } - ]; - - const TYPE_LABELS = [ - { name: 'type:feature', color: 'DDD1F2', description: 'New capability' }, - { name: 'type:bug', color: 'FF0422', description: 'Something broken' }, - { name: 'type:spike', color: 'F2DDD4', description: 'Research/investigation — produces a plan, not code' }, - { name: 'type:docs', color: 'D4E5F7', description: 'Documentation work' }, - { name: 'type:chore', color: 'D4E5F7', description: 'Maintenance, refactoring, cleanup' }, - { name: 'type:epic', color: 'CC4455', description: 'Parent issue that decomposes into sub-issues' } - ]; - - // High-signal labels — these MUST visually dominate all others - const SIGNAL_LABELS = [ - { name: 'bug', color: 'FF0422', description: 'Something isn\'t working' }, - { name: 'feedback', color: '00E5FF', description: 'User feedback — high signal, needs attention' } - ]; - - const PRIORITY_LABELS = [ - { name: 'priority:p0', color: 'B60205', description: 'Blocking release' }, - { name: 'priority:p1', color: 'D93F0B', description: 'This sprint' }, - { name: 'priority:p2', color: 'FBCA04', description: 'Next sprint' } - ]; - - function slugify(t) { return t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); } - - // Ensure the base "squad" triage label exists - const labels = [ - { name: 'squad', color: SQUAD_COLOR, description: 'Squad triage inbox — Lead will assign to a member' } - ]; - - for (const member of members) { - labels.push({ - name: `squad:${slugify(member.name)}`, - color: MEMBER_COLOR, - description: `Assigned to ${member.name} (${member.role})` - }); - } - - // Add @copilot label if coding agent is on the team - if (hasCopilot) { - labels.push({ - name: 'squad:copilot', - color: COPILOT_COLOR, - description: 'Assigned to @copilot (Coding Agent) for autonomous work' - }); - } - - // Add go:, release:, type:, priority:, and high-signal labels - labels.push(...GO_LABELS); - labels.push(...RELEASE_LABELS); - labels.push(...TYPE_LABELS); - labels.push(...PRIORITY_LABELS); - labels.push(...SIGNAL_LABELS); - - // Sync labels (create or update) - for (const label of labels) { - try { - await github.rest.issues.getLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name - }); - // Label exists — update it - await github.rest.issues.updateLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description - }); - core.info(`Updated label: ${label.name}`); - } catch (err) { - if (err.status === 404) { - // Label doesn't exist — create it - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label.name, - color: label.color, - description: label.description - }); - core.info(`Created label: ${label.name}`); - } else { - throw err; - } - } - } - - core.info(`Label sync complete: ${labels.length} labels synced`); diff --git a/.github/workflows/template-source-generator.yml b/.github/workflows/template-source-generator.yml deleted file mode 100644 index fe0cf2567..000000000 --- a/.github/workflows/template-source-generator.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Run Tests - -on: - workflow_call: - inputs: - os: - type: string - default: ubuntu-latest - -jobs: - - test: - - name: ${{ inputs.os }} - runs-on: ${{ inputs.os }} - timeout-minutes: 5 - - steps: - - name: 🛒 Checkout repository - uses: actions/checkout@v6 - - - name: 🛠️ Generate Code - run: dotnet build - working-directory: test/SourceGenerator/Net8 - continue-on-error: true - - - name: 🛠️ Build - run: dotnet build - working-directory: test/SourceGenerator/Net8 diff --git a/src/Refitter.Core/DocumentMerger.cs b/src/Refitter.Core/DocumentMerger.cs index 4a4f8084b..6e9eba6b6 100644 --- a/src/Refitter.Core/DocumentMerger.cs +++ b/src/Refitter.Core/DocumentMerger.cs @@ -15,7 +15,7 @@ internal sealed class DocumentMerger : IDocumentMerger /// The comparer used to detect equivalent document elements during merging. public DocumentMerger(DocumentEquivalenceComparer comparer) { - comparer = comparer; + this.comparer = comparer; } /// From 57a06826c6d6db14d5ef3e64ec7f4361edcf2222 Mon Sep 17 00:00:00 2001 From: Christian Helle Date: Mon, 15 Jun 2026 13:07:09 +0200 Subject: [PATCH 8/8] fix: remove duplicate private_fields symbol spec in .editorconfig --- .editorconfig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index b3cc2e1e9..709778b20 100644 --- a/.editorconfig +++ b/.editorconfig @@ -306,10 +306,6 @@ dotnet_naming_symbols.private_constant_fields.applicable_kinds = field dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected dotnet_naming_symbols.private_constant_fields.required_modifiers = const -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_fields.required_modifiers = - dotnet_naming_symbols.local_variables.applicable_kinds = local dotnet_naming_symbols.local_variables.applicable_accessibilities = local dotnet_naming_symbols.local_variables.required_modifiers =