@phuc1403/musketeer 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/INSTALLATION.md +1 -1
  2. package/README.md +2 -2
  3. package/manifest.json +3 -3
  4. package/package.json +1 -1
  5. package/template/.claude/skills/logical-components/SKILL.md +165 -94
  6. package/template/.claude/skills/logical-components/assets/component-diagram-template.html +104 -0
  7. package/template/.claude/skills/logical-components/references/component-analyst-prompt.md +88 -0
  8. package/template/.claude/skills/logical-components/references/component-level-detection.md +95 -0
  9. package/template/.claude/skills/logical-components/references/logical-component-concepts.md +92 -0
  10. package/template/.claude/skills/logical-components/scripts/build-component-model.cjs +120 -0
  11. package/template/.claude/skills/logical-components/scripts/index-declared-symbols.cjs +106 -0
  12. package/template/.claude/skills/logical-components/scripts/lib/coupling-metrics.cjs +48 -0
  13. package/template/.claude/skills/logical-components/scripts/lib/declared-symbol-patterns.cjs +57 -0
  14. package/template/.claude/skills/logical-components/scripts/lib/html-sections.cjs +91 -0
  15. package/template/.claude/skills/logical-components/scripts/lib/mermaid-graph.cjs +55 -0
  16. package/template/.claude/skills/logical-components/scripts/lib/read-json-input.cjs +109 -0
  17. package/template/.claude/skills/logical-components/scripts/lib/repo-walk.cjs +61 -0
  18. package/template/.claude/skills/logical-components/scripts/lib/verify-claims.cjs +159 -0
  19. package/template/.claude/skills/logical-components/scripts/render-component-diagram-html.cjs +112 -0
  20. package/template/.claude/skills/logical-components/scripts/scan-folder-tree.cjs +88 -0
  21. package/template/.claude/skills/logical-components/scripts/select-stale-components.cjs +129 -0
  22. package/template/.claude/statusline.cjs +26 -22
  23. package/template/.claude/skills/logical-components/.gitattributes +0 -2
  24. package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +0 -53
  25. package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +0 -67
  26. package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +0 -39
  27. package/template/.claude/skills/logical-components/scripts/Directory.Build.props +0 -7
  28. package/template/.claude/skills/logical-components/scripts/Directory.Build.rsp +0 -1
  29. package/template/.claude/skills/logical-components/scripts/Directory.Build.targets +0 -6
  30. package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +0 -8
  31. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +0 -70
  32. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +0 -47
  33. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +0 -98
  34. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +0 -54
  35. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +0 -37
  36. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +0 -86
  37. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +0 -219
  38. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +0 -180
  39. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +0 -130
  40. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +0 -101
  41. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +0 -251
  42. package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +0 -14
  43. package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +0 -111
  44. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +0 -87
  45. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +0 -79
  46. package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +0 -15
  47. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs +0 -72
  48. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +0 -78
  49. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +0 -272
  50. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +0 -231
  51. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PlainText.cs +0 -55
  52. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +0 -75
  53. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +0 -27
  54. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +0 -77
  55. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +0 -164
  56. package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +0 -234
  57. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +0 -84
  58. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +0 -197
  59. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +0 -115
  60. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SourceCodeLines.cs +0 -79
  61. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +0 -53
  62. package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +0 -106
  63. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +0 -63
  64. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +0 -297
  65. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +0 -41
  66. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +0 -90
  67. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchesCommandTests.cs +0 -37
  68. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +0 -59
  69. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +0 -42
  70. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +0 -249
  71. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +0 -253
  72. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppExtractionTests.cs +0 -188
  73. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppWorkspace.cs +0 -30
  74. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +0 -174
  75. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +0 -17
  76. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +0 -72
  77. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +0 -36
  78. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +0 -23
  79. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +0 -93
  80. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +0 -69
  81. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +0 -149
  82. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +0 -225
  83. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +0 -34
  84. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +0 -55
  85. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +0 -315
  86. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +0 -27
  87. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +0 -266
  88. package/template/.claude/skills/logical-components/scripts/NuGet.config +0 -17
  89. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +0 -7
  90. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +0 -7
  91. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/CompositionRootApp.slnx +0 -5
  92. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderExtensions.cs +0 -15
  93. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderShims.cs +0 -25
  94. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/NativeInterop.cs +0 -17
  95. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/Root.Application.csproj +0 -7
  96. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootAutofacModule.cs +0 -11
  97. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootGreeter.cs +0 -7
  98. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/logical-components.json +0 -48
  99. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AbstractMemberCases.cs +0 -27
  100. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AttributeCases.cs +0 -23
  101. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/CouplingApp.Application.csproj +0 -7
  102. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericBaseCases.cs +0 -14
  103. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericSeamBindingCases.cs +0 -59
  104. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ImplicitCallCases.cs +0 -230
  105. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.First.cs +0 -11
  106. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.Second.cs +0 -14
  107. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/StackOverflowCases.cs +0 -14
  108. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ViaDisambiguationCases.cs +0 -36
  109. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +0 -7
  110. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +0 -8
  111. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +0 -5
  112. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +0 -7
  113. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +0 -14
  114. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/Multi.Infrastructure.csproj +0 -12
  115. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/SchedulingClient.cs +0 -12
  116. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +0 -7
  117. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +0 -7
  118. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +0 -6
  119. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +0 -11
  120. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/ObjOutputApp.slnx +0 -5
  121. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Greeting.cs +0 -7
  122. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Obj.Application.csproj +0 -14
  123. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +0 -67
  124. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +0 -28
  125. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +0 -16
  126. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +0 -10
  127. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +0 -18
  128. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +0 -10
  129. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +0 -78
  130. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +0 -37
  131. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +0 -33
  132. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +0 -18
  133. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +0 -63
  134. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +0 -5
  135. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +0 -7
  136. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +0 -17
  137. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +0 -9
  138. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +0 -7
  139. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +0 -4
  140. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +0 -18
  141. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +0 -6
  142. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +0 -19
  143. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +0 -7
  144. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +0 -6
  145. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +0 -7
  146. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +0 -107
  147. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +0 -143
  148. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +0 -12
  149. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +0 -10
  150. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +0 -89
  151. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +0 -7
  152. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +0 -11
  153. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +0 -8
  154. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +0 -16
  155. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +0 -6
  156. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +0 -11
  157. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +0 -4
  158. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +0 -8
  159. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +0 -35
  160. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +0 -13
  161. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +0 -13
  162. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +0 -18
  163. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +0 -7
  164. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +0 -11
  165. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +0 -11
  166. package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +0 -7
  167. package/template/.claude/skills/logical-components/scripts/global.json +0 -5
  168. package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +0 -26
@@ -1,55 +0,0 @@
1
- using System.Globalization;
2
- using System.Text;
3
-
4
- namespace LogicalComponents;
5
-
6
- /// <summary>
7
- /// The unicode rule shared by every short string an agent hands back that is stored verbatim or shown at a review
8
- /// gate: a classify verdict's <c>reason</c> (<see cref="ClassifyMergeCommand"/>) and a responsibility's <c>text</c>
9
- /// (<see cref="ResponsibilityValidation"/>). Neither may contain a character that could hide something from a
10
- /// reviewer or a renderer - a bidi override, a zero-width character, or one that fakes a line break.
11
- /// </summary>
12
- public static class PlainText
13
- {
14
- /// <summary>
15
- /// True for text containing a Unicode category that must never appear in plain text stored or shown verbatim:
16
- /// control (Cc), format (Cf - includes bidi overrides like U+202E and zero-width characters), line/paragraph
17
- /// separators (Zl/Zp - could fake a second line), private-use (Co), or a lone surrogate (Cs).
18
- /// </summary>
19
- public static bool HasDisallowedUnicode(string text)
20
- {
21
- for (var i = 0; i < text.Length; i++)
22
- {
23
- var c = text[i];
24
- if (char.IsHighSurrogate(c))
25
- {
26
- if (i + 1 >= text.Length || !char.IsLowSurrogate(text[i + 1]))
27
- {
28
- return true; // lone high surrogate
29
- }
30
-
31
- if (IsBannedCategory(Rune.GetUnicodeCategory(new Rune(c, text[i + 1]))))
32
- {
33
- return true;
34
- }
35
-
36
- i++;
37
- continue;
38
- }
39
-
40
- if (char.IsLowSurrogate(c) || IsDisallowed(c))
41
- {
42
- return true;
43
- }
44
- }
45
-
46
- return false;
47
- }
48
-
49
- /// <summary>True for one UTF-16 code unit in a category <see cref="HasDisallowedUnicode"/> rejects.</summary>
50
- public static bool IsDisallowed(char c) => IsBannedCategory(CharUnicodeInfo.GetUnicodeCategory(c));
51
-
52
- private static bool IsBannedCategory(UnicodeCategory category) => category is UnicodeCategory.Control
53
- or UnicodeCategory.Format or UnicodeCategory.LineSeparator or UnicodeCategory.ParagraphSeparator
54
- or UnicodeCategory.PrivateUse or UnicodeCategory.Surrogate;
55
- }
@@ -1,75 +0,0 @@
1
- using Microsoft.CodeAnalysis;
2
- using Microsoft.CodeAnalysis.FindSymbols;
3
-
4
- namespace LogicalComponents;
5
-
6
- /// <summary>
7
- /// Maps a seam (an interface or abstract class) to the concrete classes behind it in the loaded solution. Test
8
- /// projects were removed at load time, so test doubles never appear. Asking the type system rather than parsing DI
9
- /// registrations covers every registration style (instance, factory, forwarding) at once.
10
- /// A constructed generic seam (<c>IHandler&lt;Order&gt;</c>) resolves only to classes implementing exactly that construction,
11
- /// or an open generic one (<c>LoggingHandler&lt;T&gt; : IHandler&lt;T&gt;</c>). Results are cached per construction and sorted.
12
- /// </summary>
13
- public sealed class PortResolution(Solution solution)
14
- {
15
- private readonly Dictionary<string, IReadOnlyList<INamedTypeSymbol>> _cache = new(StringComparer.Ordinal);
16
-
17
- public async Task<IReadOnlyList<INamedTypeSymbol>> ImplementationsAsync(
18
- INamedTypeSymbol seam, CancellationToken cancellationToken)
19
- {
20
- var key = Display(seam);
21
- if (_cache.TryGetValue(key, out var cached))
22
- {
23
- return cached;
24
- }
25
-
26
- var definition = seam.OriginalDefinition;
27
- IEnumerable<INamedTypeSymbol> found = definition.TypeKind == TypeKind.Interface
28
- ? (await SymbolFinder.FindImplementationsAsync(definition, solution, cancellationToken: cancellationToken))
29
- .OfType<INamedTypeSymbol>()
30
- : await SymbolFinder.FindDerivedClassesAsync(definition, solution, transitive: true, cancellationToken: cancellationToken);
31
-
32
- var implementations = found
33
- .Where(t => t.TypeKind == TypeKind.Class && !t.IsAbstract)
34
- .Select(t => t.OriginalDefinition)
35
- .Where(t => ImplementsConstruction(t, seam))
36
- .DistinctBy(SymbolNames.FullName)
37
- .OrderBy(SymbolNames.FullName, StringComparer.Ordinal)
38
- .ToList();
39
- _cache[key] = implementations;
40
- return implementations;
41
- }
42
-
43
- private static bool ImplementsConstruction(INamedTypeSymbol implementation, INamedTypeSymbol seam)
44
- {
45
- if (!seam.IsGenericType || seam.TypeArguments.All(a => a is ITypeParameterSymbol))
46
- {
47
- return true;
48
- }
49
-
50
- var definition = SymbolNames.FullName(seam.OriginalDefinition);
51
- var wanted = Display(seam);
52
- return Supertypes(implementation, seam.TypeKind).Any(s =>
53
- SymbolNames.FullName(s.OriginalDefinition) == definition
54
- && (Display(s) == wanted || s.TypeArguments.Any(a => a is ITypeParameterSymbol)));
55
- }
56
-
57
- private static IEnumerable<INamedTypeSymbol> Supertypes(INamedTypeSymbol type, TypeKind seamKind)
58
- {
59
- if (seamKind == TypeKind.Interface)
60
- {
61
- return type.AllInterfaces;
62
- }
63
-
64
- var bases = new List<INamedTypeSymbol>();
65
- for (var current = type.BaseType; current is not null; current = current.BaseType)
66
- {
67
- bases.Add(current);
68
- }
69
-
70
- return bases;
71
- }
72
-
73
- // Compared as text: symbols of the same type seen from two projects' compilations are not always the same instance.
74
- private static string Display(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
75
- }
@@ -1,27 +0,0 @@
1
- using LogicalComponents;
2
-
3
- // Entry point: dispatches to one command per pipeline step. Anything unknown prints usage and exits 1.
4
- // candidates, extract and render report on stderr (their artifact is a file); the others report on stdout.
5
- return args switch
6
- {
7
- ["candidates", .. var rest] => await CandidatesCommand.RunAsync(rest, Console.Error),
8
- ["classify-batches", .. var rest] => await ClassifyBatchesCommand.RunAsync(rest, Console.Out),
9
- ["classify-merge", .. var rest] => await ClassifyMergeCommand.RunAsync(rest, Console.Out),
10
- ["extract", .. var rest] => await ExtractCommand.RunAsync(rest, Console.Error),
11
- ["batches", .. var rest] => await BatchesCommand.RunAsync(rest, Console.Out),
12
- ["prune", .. var rest] => await PruneCommand.RunAsync(rest, Console.Out),
13
- ["render", .. var rest] => await RenderCommand.RunAsync(rest, Console.Error),
14
- _ => PrintUsage(),
15
- };
16
-
17
- static int PrintUsage()
18
- {
19
- Console.Error.WriteLine(CandidatesCommand.Usage);
20
- Console.Error.WriteLine(ClassifyBatchesCommand.Usage);
21
- Console.Error.WriteLine(ClassifyMergeCommand.Usage);
22
- Console.Error.WriteLine(ExtractCommand.Usage);
23
- Console.Error.WriteLine(BatchesCommand.Usage);
24
- Console.Error.WriteLine(PruneCommand.Usage);
25
- Console.Error.WriteLine(RenderCommand.Usage);
26
- return 1;
27
- }
@@ -1,77 +0,0 @@
1
- using System.Text.RegularExpressions;
2
-
3
- namespace LogicalComponents;
4
-
5
- /// <summary>
6
- /// Pure rules for which loaded projects the extractor keeps: no test projects, and one project per
7
- /// multi-targeted csproj. Kept free of Roslyn types so they are unit-testable from plain strings.
8
- /// </summary>
9
- public static partial class ProjectFilters
10
- {
11
- /// <summary>
12
- /// A test project lives under a <c>tests</c>/<c>test</c> folder, is named like one
13
- /// (<c>.Tests</c>, <c>.UnitTests</c>, <c>.IntegrationTests</c>, <c>.Test</c>, <c>.Specs</c>), or its project
14
- /// file carries a test marker (test SDK, <c>IsTestProject</c>, or a test-framework package).
15
- /// Test-support libraries such as <c>Foo.Testing</c> are deliberately kept: nothing here can tell them apart
16
- /// from source, and their types can be excluded through config.
17
- /// </summary>
18
- public static bool IsTestProject(string relativePath, string projectFileText)
19
- {
20
- var segments = relativePath.Split('/', '\\');
21
- if (segments.Any(s => s.Equals("tests", StringComparison.OrdinalIgnoreCase)
22
- || s.Equals("test", StringComparison.OrdinalIgnoreCase)))
23
- {
24
- return true;
25
- }
26
-
27
- return TestProjectName().IsMatch(Path.GetFileNameWithoutExtension(relativePath))
28
- || TestProjectMarker().IsMatch(projectFileText);
29
- }
30
-
31
- /// <summary>
32
- /// Orders the target framework in a loaded multi-target project name such as <c>Foo(net10.0)</c>.
33
- /// Families rank .NET Framework &lt; netstandard &lt; netcoreapp &lt; net5+; versions order within a family.
34
- /// </summary>
35
- public static (int Family, Version Version) TargetFrameworkRank(string loadedProjectName)
36
- {
37
- var match = TargetFrameworkSuffix().Match(loadedProjectName);
38
- if (!match.Success)
39
- {
40
- return (0, new Version(0, 0));
41
- }
42
-
43
- // Drop an OS suffix: net9.0-windows ranks as net9.0.
44
- var tfm = match.Groups["tfm"].Value.ToLowerInvariant().Split('-')[0];
45
- return tfm switch
46
- {
47
- _ when tfm.StartsWith("netstandard") => (1, ParseVersion(tfm["netstandard".Length..])),
48
- _ when tfm.StartsWith("netcoreapp") => (2, ParseVersion(tfm["netcoreapp".Length..])),
49
- _ when tfm.Contains('.') => (3, ParseVersion(tfm["net".Length..])),
50
- // .NET Framework monikers pack the version into digits: net472 is 4.7.2.
51
- _ => (0, ParseVersion(string.Join('.', tfm["net".Length..].ToCharArray()))),
52
- };
53
- }
54
-
55
- /// <summary><c>Foo(net10.0)</c> becomes <c>Foo</c>; a name without a framework suffix is returned as-is.</summary>
56
- public static string StripTargetFramework(string loadedProjectName)
57
- {
58
- var match = TargetFrameworkSuffix().Match(loadedProjectName);
59
- return match.Success ? match.Groups["name"].Value : loadedProjectName;
60
- }
61
-
62
- private static Version ParseVersion(string text) =>
63
- Version.TryParse(text.Contains('.') ? text : text + ".0", out var version) ? version : new Version(0, 0);
64
-
65
- // Segment-based and case-sensitive on purpose: "Foo.Contest", "Latest.Feeds", "Net.SpeedTest" and
66
- // "Experiments.ABTest" must not look like test projects; only these exact last-segment suffixes do.
67
- [GeneratedRegex(@"\.(Test|Tests|UnitTests|IntegrationTests|FunctionalTests|Spec|Specs)$")]
68
- private static partial Regex TestProjectName();
69
-
70
- [GeneratedRegex(
71
- @"<IsTestProject>\s*true\s*</IsTestProject>|Sdk=""MSTest\.Sdk|Include=""(Microsoft\.NET\.Test\.Sdk|xunit|xunit\.v3|xunit\.core|xunit\.v3\.core|NUnit|MSTest|MSTest\.TestFramework|Microsoft\.Testing\.Platform)""",
72
- RegexOptions.IgnoreCase)]
73
- private static partial Regex TestProjectMarker();
74
-
75
- [GeneratedRegex(@"^(?<name>.+)\((?<tfm>net[^)]*)\)$")]
76
- private static partial Regex TargetFrameworkSuffix();
77
- }
@@ -1,164 +0,0 @@
1
- using System.Text.Json;
2
-
3
- namespace LogicalComponents;
4
-
5
- /// <summary>
6
- /// <c>prune --resp &lt;dir&gt; --verify &lt;dir&gt; --out &lt;dir&gt;</c>: applies the verifier agents' verdicts. For every
7
- /// <c>resp/batch-NN.json</c> it reads <c>verify/batch-NN.json</c> — <c>{ "&lt;id&gt;": [ { "item": 1, "supported": true,
8
- /// "reason": "..." } ] }</c> — and writes the batch without its unsupported items to <c>--out</c>. The agents' files are
9
- /// never edited, so pruning is repeatable. Every item needs exactly one verdict: an unjudged claim is not verified, a
10
- /// null verdict entry or one missing <c>supported</c> is malformed (never silently "not supported"), so any gap or
11
- /// malformed entry fails the step (exit 3) before anything is written. Exit codes: 0 written, 1 usage, 3 verdicts
12
- /// incomplete or malformed.
13
- /// </summary>
14
- public static class PruneCommand
15
- {
16
- public const string Usage = "usage: LogicalComponents prune --resp <dir> --verify <dir> --out <dir>";
17
-
18
- public sealed record Verdict(int Item, bool Supported, string? Reason);
19
-
20
- // Supported is nullable only for reading: a verifier file missing it, or a null array entry, is malformed input
21
- // (exit 3) rather than a silent "not supported". Verdict above keeps Supported non-nullable for every other use,
22
- // once ReadVerdicts has confirmed it is actually there.
23
- private sealed record RawVerdict(int Item, bool? Supported, string? Reason);
24
-
25
- private static readonly JsonSerializerOptions ReadOptions = new()
26
- {
27
- PropertyNameCaseInsensitive = true,
28
- ReadCommentHandling = JsonCommentHandling.Skip,
29
- AllowTrailingCommas = true,
30
- AllowDuplicateProperties = false,
31
- };
32
-
33
- public static async Task<int> RunAsync(string[] args, TextWriter output)
34
- {
35
- if (args is not ["--resp", var respDir, "--verify", var verifyDir, "--out", var outDir]
36
- || !Directory.Exists(respDir) || !Directory.Exists(verifyDir))
37
- {
38
- await output.WriteLineAsync(Usage);
39
- return 1;
40
- }
41
-
42
- var problems = new List<string>();
43
- var pruned = new List<(string File, SortedDictionary<string, List<Responsibility>> Kept)>();
44
- var removed = new List<string>();
45
- var emptied = new List<string>();
46
-
47
- foreach (var respFile in Directory.EnumerateFiles(respDir, "batch-*.json").Order(StringComparer.Ordinal))
48
- {
49
- var name = Path.GetFileName(respFile);
50
- var batch = ResponsibilityBatches.Load([respFile], ReadOptions);
51
- problems.AddRange(batch.Problems);
52
- var verdicts = ReadVerdicts(Path.Combine(verifyDir, name), name, problems);
53
- if (verdicts is null)
54
- {
55
- continue;
56
- }
57
-
58
- var kept = new SortedDictionary<string, List<Responsibility>>(StringComparer.Ordinal);
59
- foreach (var (id, items) in batch.Responsibilities.OrderBy(r => r.Key, StringComparer.Ordinal))
60
- {
61
- var judged = verdicts.GetValueOrDefault(id) ?? [];
62
- problems.AddRange(judged.Where(v => v.Item < 1 || v.Item > items.Count)
63
- .Select(v => $"{id}[{v.Item}]: verdict for an item that does not exist ({name})"));
64
-
65
- kept[id] = [];
66
- for (var item = 1; item <= items.Count; item++)
67
- {
68
- var forItem = judged.Where(v => v.Item == item).ToList();
69
- if (forItem.Count != 1)
70
- {
71
- problems.Add($"{id}[{item}]: {(forItem.Count == 0 ? "no verdict" : "more than one verdict")} ({name})");
72
- }
73
- else if (forItem[0].Supported)
74
- {
75
- kept[id].Add(items[item - 1]);
76
- }
77
- else
78
- {
79
- removed.Add($"removed {id}[{item}]: {forItem[0].Reason ?? "(no reason given)"}");
80
- }
81
- }
82
-
83
- if (items.Count > 0 && kept[id].Count == 0)
84
- {
85
- emptied.Add($"empty {id}: every responsibility was unsupported");
86
- }
87
- }
88
-
89
- problems.AddRange(verdicts.Keys.Where(id => !batch.Responsibilities.ContainsKey(id))
90
- .Order(StringComparer.Ordinal)
91
- .Select(id => $"{id}: verdicts for a component the batch does not answer ({name})"));
92
- pruned.Add((name, kept));
93
- }
94
-
95
- if (problems.Count > 0)
96
- {
97
- foreach (var problem in problems)
98
- {
99
- await output.WriteLineAsync(problem);
100
- }
101
-
102
- return 3;
103
- }
104
-
105
- Directory.CreateDirectory(outDir);
106
- foreach (var (file, kept) in pruned)
107
- {
108
- await File.WriteAllBytesAsync(Path.Combine(outDir, file), ComponentsFile.ToJson(kept));
109
- }
110
-
111
- foreach (var line in removed.Concat(emptied))
112
- {
113
- await output.WriteLineAsync(line);
114
- }
115
-
116
- await output.WriteLineAsync($"{removed.Count} removed, {emptied.Count} empty");
117
- return 0;
118
- }
119
-
120
- private static Dictionary<string, List<Verdict>>? ReadVerdicts(string path, string name, List<string> problems)
121
- {
122
- if (!File.Exists(path))
123
- {
124
- problems.Add($"{name}: no verdict file");
125
- return null;
126
- }
127
-
128
- Dictionary<string, List<RawVerdict?>?>? raw;
129
- try
130
- {
131
- raw = JsonSerializer.Deserialize<Dictionary<string, List<RawVerdict?>?>>(File.ReadAllText(path), ReadOptions);
132
- }
133
- catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
134
- {
135
- problems.Add($"{name}: not a valid verdict file ({e.Message})");
136
- return null;
137
- }
138
-
139
- var clean = new Dictionary<string, List<Verdict>>(StringComparer.Ordinal);
140
- foreach (var (id, items) in raw ?? [])
141
- {
142
- var kept = new List<Verdict>();
143
- foreach (var verdict in items ?? [])
144
- {
145
- if (verdict is null)
146
- {
147
- problems.Add($"{id}: a null verdict entry ({name})");
148
- }
149
- else if (verdict.Supported is not { } supported)
150
- {
151
- problems.Add($"{id}[{verdict.Item}]: verdict is missing 'supported' ({name})");
152
- }
153
- else
154
- {
155
- kept.Add(new Verdict(verdict.Item, supported, verdict.Reason));
156
- }
157
- }
158
-
159
- clean[id] = kept;
160
- }
161
-
162
- return clean;
163
- }
164
- }
@@ -1,234 +0,0 @@
1
- using System.Text;
2
- using System.Text.Json;
3
- using Microsoft.CodeAnalysis.Text;
4
-
5
- namespace LogicalComponents;
6
-
7
- /// <summary>
8
- /// <c>render --components &lt;components.json&gt; --resp &lt;dir|file&gt;... --repo &lt;repoPath&gt; --out &lt;md&gt;</c>.
9
- /// Merges the agents' batch files (a directory means every <c>batch-*.json</c> in it, read in name order), validates them
10
- /// against the extraction and the repo's files, and writes the markdown only when everything is valid. The markdown is
11
- /// written to a temp file beside --out and renamed over it, so a failed or rejected run never deletes or corrupts
12
- /// whatever --out already held.
13
- /// Exit codes: 0 written; 1 usage; 2 inputs unusable (unreadable components.json, a missing path, components.json has
14
- /// no file-hash map from an old extract, a file the extraction cites changed or went missing since extract, an
15
- /// unwritable --out); 3 responsibilities rejected, every problem on stderr, --out left exactly as it was.
16
- /// </summary>
17
- public static class RenderCommand
18
- {
19
- public const string Usage =
20
- "usage: LogicalComponents render --components <components.json> --resp <dir|file>... --repo <repoPath> --out <md>";
21
-
22
- private static readonly JsonSerializerOptions ReadOptions = new()
23
- {
24
- PropertyNameCaseInsensitive = true,
25
- ReadCommentHandling = JsonCommentHandling.Skip,
26
- AllowTrailingCommas = true,
27
- // The same component twice in one file must not silently keep the last list.
28
- AllowDuplicateProperties = false,
29
- };
30
-
31
- private sealed record Arguments(string Components, IReadOnlyList<string> Resp, string Repo, string Out);
32
-
33
- public static async Task<int> RunAsync(string[] args, TextWriter errors)
34
- {
35
- var arguments = ParseArgs(args);
36
- if (arguments is null)
37
- {
38
- await errors.WriteLineAsync(Usage);
39
- return 1;
40
- }
41
-
42
- var extraction = await ReadExtractionAsync(arguments.Components, errors);
43
- if (extraction is null)
44
- {
45
- return 2;
46
- }
47
-
48
- var missing = arguments.Resp.Where(p => !Directory.Exists(p) && !File.Exists(p))
49
- .Concat(Directory.Exists(arguments.Repo) ? [] : [arguments.Repo])
50
- .ToList();
51
- if (missing.Count > 0)
52
- {
53
- await errors.WriteLineAsync($"Not found: {string.Join(", ", missing)}");
54
- return 2;
55
- }
56
-
57
- // Checked before any citation: a repo that no longer matches components.json is not the agents' fault.
58
- if (extraction.FileHashes is null)
59
- {
60
- await errors.WriteLineAsync($"{arguments.Components} has no file hashes (from an old extract); re-run extract");
61
- return 2;
62
- }
63
-
64
- var changed = ChangedFiles(extraction.FileHashes, arguments.Repo);
65
- if (changed.Count > 0)
66
- {
67
- foreach (var file in changed)
68
- {
69
- await errors.WriteLineAsync($"{file}: changed since extract");
70
- }
71
-
72
- return 2;
73
- }
74
-
75
- var sourceLines = ReadSourceLines(extraction, arguments.Repo, out var missingSources);
76
- if (missingSources.Count > 0)
77
- {
78
- // Defensive only: every span/reaches file is already covered by the file-hash check above.
79
- await errors.WriteLineAsync(
80
- $"The repo does not match components.json (re-run extract); missing: {string.Join(", ", missingSources)}");
81
- return 2;
82
- }
83
-
84
- var batches = ResponsibilityBatches.Load(arguments.Resp, ReadOptions);
85
- var problems = batches.Problems
86
- .Concat(ResponsibilityValidation.Validate(extraction, batches.Responsibilities, sourceLines, batches.FileOf))
87
- .ToList();
88
- if (problems.Count > 0)
89
- {
90
- foreach (var problem in problems)
91
- {
92
- await errors.WriteLineAsync(problem);
93
- }
94
-
95
- // A rejected run is the agents' problem, not the user's: an --out from an earlier successful run is left
96
- // exactly as it was, so a failed retry can never look like data loss.
97
- return 3;
98
- }
99
-
100
- return await WriteAsync(arguments.Out, extraction, batches.Responsibilities, errors);
101
- }
102
-
103
- /// <summary>
104
- /// Writes the markdown to a temp file beside <paramref name="outPath"/> and renames it over the target only once
105
- /// the write succeeds, so a failure (disk full, an unwritable path) never leaves a partial or truncated --out, and
106
- /// never touches whatever was already there.
107
- /// </summary>
108
- private static async Task<int> WriteAsync(
109
- string outPath, ExtractResult extraction, IReadOnlyDictionary<string, IReadOnlyList<Responsibility>> responsibilities,
110
- TextWriter errors)
111
- {
112
- var fullPath = Path.GetFullPath(outPath);
113
- var directory = Path.GetDirectoryName(fullPath)!;
114
- var temp = Path.Combine(directory, $".{Path.GetFileName(fullPath)}.{Guid.NewGuid():N}.tmp");
115
- try
116
- {
117
- Directory.CreateDirectory(directory);
118
- var markdown = MarkdownRenderer.Render(extraction, responsibilities);
119
- await File.WriteAllBytesAsync(temp, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(markdown));
120
- File.Move(temp, outPath, overwrite: true);
121
- return 0;
122
- }
123
- catch (Exception e) when (e is IOException or UnauthorizedAccessException)
124
- {
125
- await errors.WriteLineAsync($"Cannot write {outPath}: {e.Message}");
126
- return 2;
127
- }
128
- finally
129
- {
130
- if (File.Exists(temp))
131
- {
132
- File.Delete(temp);
133
- }
134
- }
135
- }
136
-
137
- /// <summary>Every file in <paramref name="hashes"/> whose current content hash differs, in file order, or that is missing.</summary>
138
- private static List<string> ChangedFiles(IReadOnlyDictionary<string, string> hashes, string repo) =>
139
- hashes.OrderBy(h => h.Key, StringComparer.Ordinal)
140
- .Where(h => IsChanged(Path.Combine(repo, h.Key), h.Value))
141
- .Select(h => h.Key)
142
- .ToList();
143
-
144
- private static bool IsChanged(string path, string expectedHash)
145
- {
146
- try
147
- {
148
- return !File.Exists(path) || ComponentsFile.HashFile(path) != expectedHash;
149
- }
150
- catch (Exception e) when (e is IOException or UnauthorizedAccessException)
151
- {
152
- // Became unreadable between the existence check and the hash read: treat it the same as changed, since
153
- // render cannot trust an uncheckable citation either.
154
- return true;
155
- }
156
- }
157
-
158
- private static async Task<ExtractResult?> ReadExtractionAsync(string path, TextWriter errors)
159
- {
160
- try
161
- {
162
- var extraction = ComponentsFile.Deserialize(await File.ReadAllBytesAsync(path));
163
- // Deserialization leaves absent lists null despite the non-nullable types: `{}` is not a components.json.
164
- if (extraction.Components is not null && extraction.ExternalNodes is not null
165
- && extraction.Edges is not null && extraction.Warnings is not null)
166
- {
167
- return extraction;
168
- }
169
-
170
- await errors.WriteLineAsync($"{path} is not a components.json written by extract");
171
- }
172
- catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
173
- {
174
- await errors.WriteLineAsync($"Cannot read {path}: {e.Message}");
175
- }
176
-
177
- return null;
178
- }
179
-
180
- /// <summary>
181
- /// The lines of every file a component is declared in, split the way Roslyn splits them (so U+2028 and friends count
182
- /// exactly as they did when extract measured the spans). Files missing from the repo are reported, not read.
183
- /// </summary>
184
- private static Dictionary<string, IReadOnlyList<string>> ReadSourceLines(
185
- ExtractResult extraction, string repo, out List<string> missing)
186
- {
187
- var lines = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
188
- missing = [];
189
- var files = extraction.Components.SelectMany(c => c.Spans.Concat(c.Reaches ?? []).Select(s => s.File));
190
- foreach (var file in files.Distinct().Order(StringComparer.Ordinal))
191
- {
192
- var path = Path.Combine(repo, file);
193
- if (!File.Exists(path))
194
- {
195
- missing.Add(file);
196
- continue;
197
- }
198
-
199
- var text = SourceText.From(File.ReadAllText(path));
200
- lines[file] = text.Lines.Select(l => text.ToString(l.Span)).ToList();
201
- }
202
-
203
- return lines;
204
- }
205
-
206
- /// <summary>Each flag exactly once; <c>--resp</c> takes every following value up to the next flag.</summary>
207
- private static Arguments? ParseArgs(string[] args)
208
- {
209
- var values = new Dictionary<string, List<string>>(StringComparer.Ordinal);
210
- for (var i = 0; i < args.Length; i++)
211
- {
212
- var flag = args[i];
213
- if (flag is not ("--components" or "--resp" or "--repo" or "--out") || values.ContainsKey(flag))
214
- {
215
- return null;
216
- }
217
-
218
- var taken = new List<string>();
219
- while (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
220
- {
221
- taken.Add(args[++i]);
222
- }
223
-
224
- values[flag] = taken;
225
- }
226
-
227
- string? Single(string flag) => values.TryGetValue(flag, out var v) && v.Count == 1 ? v[0] : null;
228
- var resp = values.GetValueOrDefault("--resp") ?? [];
229
- return Single("--components") is { } components && Single("--repo") is { } repo && Single("--out") is { } output
230
- && resp.Count > 0
231
- ? new Arguments(components, resp, repo, output)
232
- : null;
233
- }
234
- }