@phuc1403/musketeer 0.10.0 → 0.12.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.
- package/INSTALLATION.md +1 -0
- package/README.md +2 -2
- package/manifest.json +29 -3
- package/package.json +1 -1
- package/src/provisioner/detect.js +17 -2
- package/src/resolver.js +8 -3
- package/src/schema.js +1 -0
- package/src/settings-merger.js +0 -0
- package/template/.claude/hooks/inject-naming-rule-into-subagents.cjs +44 -0
- package/template/.claude/hooks/validate-cml-hook.js +6 -5
- package/template/.claude/rules/csharp-identifier-naming.md +77 -0
- package/template/.claude/skills/code-review/SKILL.md +4 -1
- package/template/.claude/skills/code-review/references/naming-rule-review.md +41 -0
- package/template/.claude/skills/logical-components/.gitattributes +2 -0
- package/template/.claude/skills/logical-components/SKILL.md +107 -0
- package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +53 -0
- package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +67 -0
- package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +39 -0
- package/template/.claude/skills/logical-components/scripts/Directory.Build.props +7 -0
- package/template/.claude/skills/logical-components/scripts/Directory.Build.rsp +1 -0
- package/template/.claude/skills/logical-components/scripts/Directory.Build.targets +6 -0
- package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +8 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +70 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +47 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +98 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +54 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +37 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +86 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +219 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +180 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +130 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +101 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +251 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +111 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +87 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +79 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +15 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs +72 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +78 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +272 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +231 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PlainText.cs +55 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +75 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +27 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +77 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +164 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +234 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +84 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +197 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +115 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SourceCodeLines.cs +79 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +53 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +106 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +63 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +297 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +41 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +90 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchesCommandTests.cs +37 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +59 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +42 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +249 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +253 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppExtractionTests.cs +188 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppWorkspace.cs +30 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +174 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +17 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +72 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +36 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +23 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +93 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +69 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +149 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +225 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +34 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +55 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +315 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +27 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +266 -0
- package/template/.claude/skills/logical-components/scripts/NuGet.config +17 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/CompositionRootApp.slnx +5 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderExtensions.cs +15 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderShims.cs +25 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/NativeInterop.cs +17 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/Root.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootAutofacModule.cs +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootGreeter.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/logical-components.json +48 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AbstractMemberCases.cs +27 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AttributeCases.cs +23 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/CouplingApp.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericBaseCases.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericSeamBindingCases.cs +59 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ImplicitCallCases.cs +230 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.First.cs +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.Second.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/StackOverflowCases.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ViaDisambiguationCases.cs +36 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +8 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +5 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +14 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/Multi.Infrastructure.csproj +12 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/SchedulingClient.cs +12 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/ObjOutputApp.slnx +5 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Greeting.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Obj.Application.csproj +14 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +67 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +28 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +16 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +10 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +10 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +78 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +37 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +33 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +63 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +5 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +17 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +9 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +4 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +19 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +107 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +143 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +12 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +10 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +89 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +8 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +16 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +6 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +4 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +8 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +35 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +13 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +13 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +18 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +11 -0
- package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +7 -0
- package/template/.claude/skills/logical-components/scripts/global.json +5 -0
- package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +26 -0
package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
using System.Diagnostics;
|
|
2
|
+
using Microsoft.CodeAnalysis;
|
|
3
|
+
using Microsoft.CodeAnalysis.MSBuild;
|
|
4
|
+
|
|
5
|
+
namespace LogicalComponents;
|
|
6
|
+
|
|
7
|
+
/// <summary>A target repo that could not be restored or loaded into a Roslyn workspace.</summary>
|
|
8
|
+
public sealed class WorkspaceLoadException(string message) : Exception(message);
|
|
9
|
+
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// A loaded repo: the Roslyn solution reduced to one C# source project per project file, plus where it came from.
|
|
12
|
+
/// Owns the workspace (and its out-of-process build host), so dispose it when done.
|
|
13
|
+
/// </summary>
|
|
14
|
+
public sealed class LoadedWorkspace(MSBuildWorkspace workspace, Solution solution, string repoRoot, string source)
|
|
15
|
+
: IDisposable
|
|
16
|
+
{
|
|
17
|
+
public Solution Solution { get; } = solution;
|
|
18
|
+
|
|
19
|
+
/// <summary>Absolute repo root; every path written to output is made relative to it.</summary>
|
|
20
|
+
public string RepoRoot { get; } = repoRoot;
|
|
21
|
+
|
|
22
|
+
/// <summary>The solution file name, or the glob the loader fell back to.</summary>
|
|
23
|
+
public string Source { get; } = source;
|
|
24
|
+
|
|
25
|
+
public IReadOnlyList<string> ProjectNames { get; } =
|
|
26
|
+
solution.Projects.Select(p => p.Name).Order(StringComparer.Ordinal).ToList();
|
|
27
|
+
|
|
28
|
+
public void Dispose() => workspace.Dispose();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// <summary>
|
|
32
|
+
/// Loads a repo's source projects into a Roslyn workspace. The top-level solution is preferred; the csproj files
|
|
33
|
+
/// are used only when there is no solution or it contains no source projects. A solution that exists but fails
|
|
34
|
+
/// to open is an error, never a silent switch to a different project set.
|
|
35
|
+
/// </summary>
|
|
36
|
+
public static class WorkspaceLoader
|
|
37
|
+
{
|
|
38
|
+
private static readonly TimeSpan RestoreTimeout = TimeSpan.FromMinutes(10);
|
|
39
|
+
|
|
40
|
+
/// <param name="solution">
|
|
41
|
+
/// An explicit solution file, relative to <paramref name="repoPath"/> or absolute, used instead of searching the
|
|
42
|
+
/// repo root. Use it to resolve the "more than one solution" error, or to pick a non-top-level solution on purpose.
|
|
43
|
+
/// </param>
|
|
44
|
+
public static async Task<LoadedWorkspace> LoadAsync(
|
|
45
|
+
string repoPath, bool restore = true, string? solution = null, TextWriter? log = null)
|
|
46
|
+
{
|
|
47
|
+
var repoRoot = Path.GetFullPath(repoPath);
|
|
48
|
+
if (!Directory.Exists(repoRoot))
|
|
49
|
+
{
|
|
50
|
+
throw new DirectoryNotFoundException($"Repository path not found: {repoRoot}");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log ??= TextWriter.Null;
|
|
54
|
+
|
|
55
|
+
var solutionFile = solution is null ? FindSolutionFile(repoRoot) : ResolveGivenSolution(repoRoot, solution);
|
|
56
|
+
if (solutionFile is not null)
|
|
57
|
+
{
|
|
58
|
+
var name = Path.GetFileName(solutionFile);
|
|
59
|
+
var loaded = await LoadAsync(repoRoot, name, [solutionFile], restore, log,
|
|
60
|
+
(workspace, file) => workspace.OpenSolutionAsync(file));
|
|
61
|
+
if (loaded.Solution.Projects.Any())
|
|
62
|
+
{
|
|
63
|
+
return loaded;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
loaded.Dispose();
|
|
67
|
+
log.WriteLine($"{name} contains no C# source projects; falling back to project files.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var (projectFiles, glob) = FindProjectFiles(repoRoot, log);
|
|
71
|
+
if (projectFiles.Count == 0)
|
|
72
|
+
{
|
|
73
|
+
throw new WorkspaceLoadException($"No solution and no C# source project found under {repoRoot}");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return await LoadAsync(repoRoot, glob, projectFiles, restore, log, async (workspace, file) =>
|
|
77
|
+
{
|
|
78
|
+
// Opening a project also loads its references; skip ones already pulled in that way.
|
|
79
|
+
if (workspace.CurrentSolution.Projects.All(p => !PathsEqual(p.FilePath, file)))
|
|
80
|
+
{
|
|
81
|
+
await workspace.OpenProjectAsync(file);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// <summary>Restores and opens each entry file, then reduces the result to source projects.</summary>
|
|
87
|
+
private static async Task<LoadedWorkspace> LoadAsync(
|
|
88
|
+
string repoRoot, string source, IReadOnlyList<string> entryFiles, bool restore, TextWriter log,
|
|
89
|
+
Func<MSBuildWorkspace, string, Task> open)
|
|
90
|
+
{
|
|
91
|
+
var workspace = MSBuildWorkspace.Create();
|
|
92
|
+
try
|
|
93
|
+
{
|
|
94
|
+
var failures = new List<string>();
|
|
95
|
+
workspace.RegisterWorkspaceFailedHandler(e =>
|
|
96
|
+
{
|
|
97
|
+
if (e.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure)
|
|
98
|
+
{
|
|
99
|
+
failures.Add(e.Diagnostic.Message);
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
{
|
|
103
|
+
log.WriteLine($"warning: {e.Diagnostic.Message}");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
foreach (var file in entryFiles)
|
|
108
|
+
{
|
|
109
|
+
if (restore)
|
|
110
|
+
{
|
|
111
|
+
await RestoreAsync(file, repoRoot);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await open(workspace, file);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (failures.Count > 0)
|
|
118
|
+
{
|
|
119
|
+
throw new WorkspaceLoadException(
|
|
120
|
+
$"Loading {source} failed:{Environment.NewLine} " + string.Join(Environment.NewLine + " ", failures));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return new LoadedWorkspace(workspace, KeepSourceProjects(workspace.CurrentSolution, repoRoot, log), repoRoot, source);
|
|
124
|
+
}
|
|
125
|
+
catch (Exception e)
|
|
126
|
+
{
|
|
127
|
+
workspace.Dispose();
|
|
128
|
+
throw e is WorkspaceLoadException ? e : new WorkspaceLoadException($"Loading {source} failed: {e.Message}");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// <summary>
|
|
133
|
+
/// Drops non-C# and test projects, and keeps one project per multi-targeted project file: the highest target
|
|
134
|
+
/// framework, renamed without its <c>(tfm)</c> suffix, so every type is seen exactly once. A project referencing a
|
|
135
|
+
/// dropped copy (another TFM of the same file resolved by a referencing project that targets a lower framework)
|
|
136
|
+
/// is redirected to the kept copy first, so it never silently loses that reference.
|
|
137
|
+
/// </summary>
|
|
138
|
+
private static Solution KeepSourceProjects(Solution solution, string repoRoot, TextWriter log)
|
|
139
|
+
{
|
|
140
|
+
foreach (var byFile in solution.Projects.GroupBy(p => p.FilePath ?? "", StringComparer.OrdinalIgnoreCase))
|
|
141
|
+
{
|
|
142
|
+
var projects = byFile.ToList();
|
|
143
|
+
var isTest = byFile.Key != ""
|
|
144
|
+
&& projects[0].Language == LanguageNames.CSharp
|
|
145
|
+
&& ProjectFilters.IsTestProject(Path.GetRelativePath(repoRoot, byFile.Key), File.ReadAllText(byFile.Key));
|
|
146
|
+
if (isTest)
|
|
147
|
+
{
|
|
148
|
+
log.WriteLine($"dropped test project: {Path.GetRelativePath(repoRoot, byFile.Key)}");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var isSource = byFile.Key != "" && projects[0].Language == LanguageNames.CSharp && !isTest;
|
|
152
|
+
var kept = isSource
|
|
153
|
+
? projects.MaxBy(p => ProjectFilters.TargetFrameworkRank(p.Name))
|
|
154
|
+
: null;
|
|
155
|
+
|
|
156
|
+
var dropped = projects.Where(p => p != kept).Select(p => p.Id).ToList();
|
|
157
|
+
if (dropped.Count > 0)
|
|
158
|
+
{
|
|
159
|
+
solution = RedirectProjectReferences(solution, dropped, kept?.Id);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
foreach (var id in dropped)
|
|
163
|
+
{
|
|
164
|
+
solution = solution.RemoveProject(id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (kept is not null)
|
|
168
|
+
{
|
|
169
|
+
solution = solution.WithProjectName(kept.Id, ProjectFilters.StripTargetFramework(kept.Name));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return solution;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// <summary>
|
|
177
|
+
/// Points every reference to one of <paramref name="droppedIds"/> at <paramref name="keptId"/> instead (deduped
|
|
178
|
+
/// against a reference the project already has), or drops it if there is no kept copy to redirect to (the whole
|
|
179
|
+
/// group is being dropped, e.g. a test or non-C# project).
|
|
180
|
+
/// </summary>
|
|
181
|
+
private static Solution RedirectProjectReferences(
|
|
182
|
+
Solution solution, IReadOnlyCollection<ProjectId> droppedIds, ProjectId? keptId)
|
|
183
|
+
{
|
|
184
|
+
foreach (var project in solution.Projects.ToList())
|
|
185
|
+
{
|
|
186
|
+
if (droppedIds.Contains(project.Id) || project.ProjectReferences.All(r => !droppedIds.Contains(r.ProjectId)))
|
|
187
|
+
{
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
var references = project.ProjectReferences.Where(r => !droppedIds.Contains(r.ProjectId)).ToList();
|
|
192
|
+
if (keptId is not null && project.Id != keptId && references.All(r => r.ProjectId != keptId))
|
|
193
|
+
{
|
|
194
|
+
references.Add(new ProjectReference(keptId));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
solution = solution.WithProjectReferences(project.Id, references);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return solution;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static string? FindSolutionFile(string repoRoot)
|
|
204
|
+
{
|
|
205
|
+
// Top-level only: a nested solution usually belongs to a sample or a vendored dependency.
|
|
206
|
+
var candidates = Directory.EnumerateFiles(repoRoot, "*.slnx")
|
|
207
|
+
.Concat(Directory.EnumerateFiles(repoRoot, "*.sln"))
|
|
208
|
+
.Order(StringComparer.Ordinal)
|
|
209
|
+
.ToList();
|
|
210
|
+
|
|
211
|
+
return candidates.Count switch
|
|
212
|
+
{
|
|
213
|
+
0 => null,
|
|
214
|
+
1 => candidates[0],
|
|
215
|
+
_ => throw new WorkspaceLoadException(
|
|
216
|
+
$"More than one solution in {repoRoot}: {string.Join(", ", candidates.Select(Path.GetFileName))}"
|
|
217
|
+
+ "; pass --solution <file> to pick one."),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// <summary>Resolves an explicit <c>--solution</c> file (relative to the repo root, or absolute); errors if missing.</summary>
|
|
222
|
+
private static string ResolveGivenSolution(string repoRoot, string solution)
|
|
223
|
+
{
|
|
224
|
+
var path = Path.IsPathRooted(solution) ? solution : Path.Combine(repoRoot, solution);
|
|
225
|
+
if (!File.Exists(path))
|
|
226
|
+
{
|
|
227
|
+
throw new WorkspaceLoadException($"Solution not found: {Path.GetFullPath(path)}");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return Path.GetFullPath(path);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// <summary>C# project files under <c>src/</c>, or under the repo root minus build output and dot folders.</summary>
|
|
234
|
+
private static (List<string> Files, string Glob) FindProjectFiles(string repoRoot, TextWriter log)
|
|
235
|
+
{
|
|
236
|
+
var src = Path.Combine(repoRoot, "src");
|
|
237
|
+
var (searchRoot, glob) = Directory.Exists(src) ? (src, "src/**/*.csproj") : (repoRoot, "**/*.csproj");
|
|
238
|
+
var files = Directory
|
|
239
|
+
.EnumerateFiles(searchRoot, "*.csproj", SearchOption.AllDirectories)
|
|
240
|
+
.Where(file => !CitableSource.IsBuildOutputPath(Path.GetRelativePath(repoRoot, file))
|
|
241
|
+
&& !Path.GetRelativePath(repoRoot, file)
|
|
242
|
+
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
|
243
|
+
.Any(segment => segment.StartsWith('.')))
|
|
244
|
+
.Where(file =>
|
|
245
|
+
{
|
|
246
|
+
var relative = Path.GetRelativePath(repoRoot, file);
|
|
247
|
+
if (!ProjectFilters.IsTestProject(relative, File.ReadAllText(file)))
|
|
248
|
+
{
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
log.WriteLine($"dropped test project: {relative}");
|
|
253
|
+
return false;
|
|
254
|
+
})
|
|
255
|
+
.Order(StringComparer.Ordinal)
|
|
256
|
+
.ToList();
|
|
257
|
+
return (files, glob);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// <summary>
|
|
261
|
+
/// MSBuildWorkspace needs restored projects (project.assets.json) to resolve references. Runs from the repo root
|
|
262
|
+
/// so the target's own global.json picks the SDK.
|
|
263
|
+
/// </summary>
|
|
264
|
+
private static async Task RestoreAsync(string path, string repoRoot)
|
|
265
|
+
{
|
|
266
|
+
var start = new ProcessStartInfo("dotnet", ["restore", path, "--nologo", "-v", "q"])
|
|
267
|
+
{
|
|
268
|
+
WorkingDirectory = repoRoot,
|
|
269
|
+
RedirectStandardOutput = true,
|
|
270
|
+
RedirectStandardError = true,
|
|
271
|
+
};
|
|
272
|
+
using var process = Process.Start(start)
|
|
273
|
+
?? throw new WorkspaceLoadException("Could not start `dotnet restore`.");
|
|
274
|
+
var stdout = process.StandardOutput.ReadToEndAsync();
|
|
275
|
+
var stderr = process.StandardError.ReadToEndAsync();
|
|
276
|
+
|
|
277
|
+
using var timeout = new CancellationTokenSource(RestoreTimeout);
|
|
278
|
+
try
|
|
279
|
+
{
|
|
280
|
+
await process.WaitForExitAsync(timeout.Token);
|
|
281
|
+
}
|
|
282
|
+
catch (OperationCanceledException)
|
|
283
|
+
{
|
|
284
|
+
process.Kill(entireProcessTree: true);
|
|
285
|
+
throw new WorkspaceLoadException($"`dotnet restore {path}` did not finish within {RestoreTimeout.TotalMinutes} minutes.");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (process.ExitCode != 0)
|
|
289
|
+
{
|
|
290
|
+
throw new WorkspaceLoadException(
|
|
291
|
+
$"`dotnet restore {path}` failed ({process.ExitCode}):{Environment.NewLine}{await stdout}{await stderr}");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private static bool PathsEqual(string? a, string b) =>
|
|
296
|
+
a is not null && string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
|
|
297
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class AssignIdsTests
|
|
4
|
+
{
|
|
5
|
+
private static Component Unnamed(string type, string module) =>
|
|
6
|
+
new(Id: "", Type: type, Name: "", Module: module, Layer: "Application", Spans: []);
|
|
7
|
+
|
|
8
|
+
[Fact]
|
|
9
|
+
public void A_three_way_clash_across_modules_prefixes_every_member_with_its_module()
|
|
10
|
+
{
|
|
11
|
+
var named = ComponentDiscovery.AssignIds(
|
|
12
|
+
[
|
|
13
|
+
Unnamed("A.Export.Application.Reporting", "Export"),
|
|
14
|
+
Unnamed("A.Import.Application.Reporting", "Import"),
|
|
15
|
+
Unnamed("A.Jobs.Reporting", "BuildingBlocks"),
|
|
16
|
+
Unnamed("A.Export.Application.Framing", "Export"),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
Assert.Equal(
|
|
20
|
+
[
|
|
21
|
+
("A.Export.Application.Framing", "Framing", "Framing"),
|
|
22
|
+
("A.Export.Application.Reporting", "Export_Reporting", "Reporting (Export)"),
|
|
23
|
+
("A.Import.Application.Reporting", "Import_Reporting", "Reporting (Import)"),
|
|
24
|
+
("A.Jobs.Reporting", "BuildingBlocks_Reporting", "Reporting (BuildingBlocks)"),
|
|
25
|
+
],
|
|
26
|
+
named.Select(c => (c.Type, c.Id, c.Name)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[Fact]
|
|
30
|
+
public void The_same_type_twice_is_an_error_rather_than_two_identical_ids()
|
|
31
|
+
{
|
|
32
|
+
// A source file linked into two projects declares the same full type name in both.
|
|
33
|
+
var error = Assert.Throws<InvalidOperationException>(() => ComponentDiscovery.AssignIds(
|
|
34
|
+
[
|
|
35
|
+
Unnamed("A.Shared.Clock", "Export"),
|
|
36
|
+
Unnamed("A.Shared.Clock", "Import"),
|
|
37
|
+
]));
|
|
38
|
+
|
|
39
|
+
Assert.Contains("A.Shared.Clock", error.Message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class BatchPlanningTests
|
|
4
|
+
{
|
|
5
|
+
private static ExtractResult WithComponents(int count) => RenderTestData.Extraction with
|
|
6
|
+
{
|
|
7
|
+
Components = [.. Enumerable.Range(1, count).Select(i =>
|
|
8
|
+
new Component($"C{i:D3}", $"A.C{i:D3}", $"C {i:D3}", "A", "Application", [new SourceSpan($"src/C{i:D3}.cs", 1, 9)]))],
|
|
9
|
+
ExternalNodes = [],
|
|
10
|
+
Edges = [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
[Theory]
|
|
14
|
+
[InlineData(82, 9, new[] { 10, 10, 10, 10, 10, 10, 10, 10, 2 })]
|
|
15
|
+
[InlineData(20, 9, new[] { 3, 3, 3, 3, 3, 3, 2 })]
|
|
16
|
+
[InlineData(5, 9, new[] { 1, 1, 1, 1, 1 })]
|
|
17
|
+
public void Components_are_split_into_at_most_the_agent_count_of_equal_batches_in_extraction_order(
|
|
18
|
+
int components, int agents, int[] sizes)
|
|
19
|
+
{
|
|
20
|
+
var batches = BatchPlanning.Plan(WithComponents(components), agents);
|
|
21
|
+
|
|
22
|
+
Assert.Equal(sizes, batches.Select(b => b.Components.Count));
|
|
23
|
+
Assert.Equal(Enumerable.Range(1, sizes.Length), batches.Select(b => b.Batch));
|
|
24
|
+
Assert.Equal(
|
|
25
|
+
WithComponents(components).Components.Select(c => c.Id),
|
|
26
|
+
batches.SelectMany(b => b.Components).Select(c => c.Id));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[Fact]
|
|
30
|
+
public void Each_component_carries_its_spans_and_what_it_uses_by_name()
|
|
31
|
+
{
|
|
32
|
+
var batch = BatchPlanning.Plan(RenderTestData.Extraction, 9)
|
|
33
|
+
.SelectMany(b => b.Components)
|
|
34
|
+
.Single(c => c.Id == "OrderAcceptance");
|
|
35
|
+
|
|
36
|
+
Assert.Equal([new SourceSpan("src/App/OrderAcceptance.cs", 4, 17)], batch.Spans);
|
|
37
|
+
// Records compare their list members by reference, so compare the contents.
|
|
38
|
+
Assert.Equal(
|
|
39
|
+
[
|
|
40
|
+
("IAuditTrail", "I Audit Trail", "di", ""),
|
|
41
|
+
("OrderQueue", "Order Queue", "di,new", "IOrderQueue,IQueue"),
|
|
42
|
+
],
|
|
43
|
+
batch.Uses.Select(u => (u.Id, u.Name, string.Join(",", u.Kinds), string.Join(",", u.Via))));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[Fact]
|
|
47
|
+
public void Each_component_carries_the_helper_code_it_runs()
|
|
48
|
+
{
|
|
49
|
+
var reaches = new List<SourceSpan> { new("src/Infra/OrderQueue.cs", 10, 10) };
|
|
50
|
+
var extraction = RenderTestData.Extraction with
|
|
51
|
+
{
|
|
52
|
+
Components = [RenderTestData.Extraction.Components[0] with { Reaches = reaches }, .. RenderTestData.Extraction.Components.Skip(1)],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
var batch = BatchPlanning.Plan(extraction, 9).SelectMany(b => b.Components).Single(c => c.Id == "OrderAcceptance");
|
|
56
|
+
|
|
57
|
+
Assert.Equal(reaches, batch.Reaches);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
[Fact]
|
|
61
|
+
public void An_enormous_agent_count_does_not_overflow_the_batch_size()
|
|
62
|
+
{
|
|
63
|
+
var batches = BatchPlanning.Plan(WithComponents(3), int.MaxValue);
|
|
64
|
+
|
|
65
|
+
Assert.Equal([1, 1, 1], batches.Select(b => b.Components.Count));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
[Fact]
|
|
69
|
+
public void Batch_files_are_written_identically_every_time()
|
|
70
|
+
{
|
|
71
|
+
var first = Directory.CreateTempSubdirectory("lc-batches-");
|
|
72
|
+
var second = Directory.CreateTempSubdirectory("lc-batches-");
|
|
73
|
+
try
|
|
74
|
+
{
|
|
75
|
+
BatchPlanning.Write(BatchPlanning.Plan(WithComponents(20), 9), first.FullName);
|
|
76
|
+
BatchPlanning.Write(BatchPlanning.Plan(WithComponents(20), 9), second.FullName);
|
|
77
|
+
|
|
78
|
+
var names = Directory.GetFiles(first.FullName).Select(Path.GetFileName).Order().ToList();
|
|
79
|
+
Assert.Equal(Enumerable.Range(1, 7).Select(i => $"batch-{i:D2}.json"), names);
|
|
80
|
+
Assert.All(names, n => Assert.Equal(
|
|
81
|
+
File.ReadAllBytes(Path.Combine(first.FullName, n!)),
|
|
82
|
+
File.ReadAllBytes(Path.Combine(second.FullName, n!))));
|
|
83
|
+
}
|
|
84
|
+
finally
|
|
85
|
+
{
|
|
86
|
+
first.Delete(recursive: true);
|
|
87
|
+
second.Delete(recursive: true);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
/// <summary>The batches command end to end: malformed or extreme input fails cleanly instead of crashing.</summary>
|
|
4
|
+
public class BatchesCommandTests : IDisposable
|
|
5
|
+
{
|
|
6
|
+
private readonly DirectoryInfo _work = Directory.CreateTempSubdirectory("lc-batches-cmd-");
|
|
7
|
+
|
|
8
|
+
public void Dispose() => _work.Delete(recursive: true);
|
|
9
|
+
|
|
10
|
+
private string PathOf(string relative) => Path.Combine(_work.FullName, relative);
|
|
11
|
+
|
|
12
|
+
[Fact]
|
|
13
|
+
public async Task An_empty_components_json_is_an_error_not_a_crash()
|
|
14
|
+
{
|
|
15
|
+
File.WriteAllText(PathOf("components.json"), "{}");
|
|
16
|
+
var output = new StringWriter();
|
|
17
|
+
|
|
18
|
+
var exit = await BatchesCommand.RunAsync(
|
|
19
|
+
["--components", PathOf("components.json"), "--agents", "9", "--out", PathOf("batches")], output);
|
|
20
|
+
|
|
21
|
+
Assert.Equal(2, exit);
|
|
22
|
+
Assert.Contains(PathOf("components.json"), output.ToString());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
[Fact]
|
|
26
|
+
public async Task An_enormous_agent_count_does_not_crash()
|
|
27
|
+
{
|
|
28
|
+
File.WriteAllBytes(PathOf("components.json"), ComponentsFile.Serialize(RenderTestData.Extraction));
|
|
29
|
+
var output = new StringWriter();
|
|
30
|
+
|
|
31
|
+
var exit = await BatchesCommand.RunAsync(
|
|
32
|
+
["--components", PathOf("components.json"), "--agents", int.MaxValue.ToString(), "--out", PathOf("batches")], output);
|
|
33
|
+
|
|
34
|
+
Assert.Equal(0, exit);
|
|
35
|
+
Assert.True(File.Exists(Path.Combine(PathOf("batches"), "batch-01.json")));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
[Collection(FixtureSolutions.Name)]
|
|
4
|
+
public class CandidatesCommandTests(SampleAppWorkspace sample)
|
|
5
|
+
{
|
|
6
|
+
private static readonly ExtractConfig FixtureConfig = ExtractConfig.Load(FixturePaths.SampleApp);
|
|
7
|
+
|
|
8
|
+
[Fact]
|
|
9
|
+
public async Task Each_candidate_carries_its_spans_and_how_the_config_classifies_it()
|
|
10
|
+
{
|
|
11
|
+
var config = FixtureConfig with { Components = FixtureConfig.Components.Where(c => c != "Sample.Infrastructure.Mailer").ToList() };
|
|
12
|
+
|
|
13
|
+
var file = await CandidatesCommand.ListAsync(sample.Loaded, config, TestContext.Current.CancellationToken);
|
|
14
|
+
var byType = file.Candidates.ToDictionary(c => c.Type);
|
|
15
|
+
|
|
16
|
+
var acceptance = byType["Sample.Application.OrderAcceptance"];
|
|
17
|
+
Assert.Equal(("Application", "Sample", "component"), (acceptance.Layer, acceptance.Module, acceptance.Status));
|
|
18
|
+
Assert.Equal([new SourceSpan("src/Sample.Application/OrderAcceptance.cs", 4, 17)], acceptance.Spans);
|
|
19
|
+
Assert.Equal("helper", byType["Sample.Application.OrderNumber"].Status);
|
|
20
|
+
Assert.Equal("unclassified", byType["Sample.Infrastructure.Mailer"].Status);
|
|
21
|
+
Assert.Equal(file.Candidates.OrderBy(c => c.Type, StringComparer.Ordinal).Select(c => c.Type), file.Candidates.Select(c => c.Type));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
[Fact]
|
|
25
|
+
public async Task Extract_refuses_unclassified_candidates_naming_each_one()
|
|
26
|
+
{
|
|
27
|
+
var config = FixtureConfig with { Components = FixtureConfig.Components.Where(c => c != "Sample.Infrastructure.Mailer").ToList() };
|
|
28
|
+
|
|
29
|
+
var error = await Assert.ThrowsAsync<UnclassifiedCandidatesException>(() =>
|
|
30
|
+
CouplingResolver.ExtractAsync(sample.Loaded, config, TestContext.Current.CancellationToken));
|
|
31
|
+
|
|
32
|
+
Assert.Equal(["Sample.Infrastructure.Mailer"], error.Types);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[Fact]
|
|
36
|
+
public async Task Unknown_option_prints_usage_and_exits_1()
|
|
37
|
+
{
|
|
38
|
+
var errors = new StringWriter();
|
|
39
|
+
|
|
40
|
+
var exitCode = await CandidatesCommand.RunAsync(["repo", "--bogus"], errors);
|
|
41
|
+
|
|
42
|
+
Assert.Equal(1, exitCode);
|
|
43
|
+
Assert.Contains(CandidatesCommand.Usage, errors.ToString());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[Fact]
|
|
47
|
+
public async Task Solution_option_is_parsed_and_passed_through_to_the_loader()
|
|
48
|
+
{
|
|
49
|
+
var errors = new StringWriter();
|
|
50
|
+
var missing = Path.Combine(Path.GetTempPath(), "lc-does-not-exist-" + Guid.NewGuid());
|
|
51
|
+
|
|
52
|
+
var exitCode = await CandidatesCommand.RunAsync([missing, "--solution", "custom.slnx", "--out", "out.json"], errors);
|
|
53
|
+
|
|
54
|
+
// --solution and --out both parsed fine (no usage error); it fails later because repoPath itself is missing,
|
|
55
|
+
// proving the flag reached WorkspaceLoader.LoadAsync rather than being rejected as an unknown option.
|
|
56
|
+
Assert.Equal(2, exitCode);
|
|
57
|
+
Assert.Contains(missing, errors.ToString());
|
|
58
|
+
}
|
|
59
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
namespace LogicalComponents.Tests;
|
|
2
|
+
|
|
3
|
+
public class CitationTests
|
|
4
|
+
{
|
|
5
|
+
[Theory]
|
|
6
|
+
[InlineData("src/A/B.cs:12-30", "src/A/B.cs", 12, 30)]
|
|
7
|
+
[InlineData("src/A/B.cs:7", "src/A/B.cs", 7, 7)]
|
|
8
|
+
[InlineData("src/My Folder/C.cs:1-2", "src/My Folder/C.cs", 1, 2)]
|
|
9
|
+
[InlineData("src/A/B.cs:0007-007", "src/A/B.cs", 7, 7)]
|
|
10
|
+
public void Parses_a_file_with_a_line_or_a_line_range(string cite, string file, int start, int end)
|
|
11
|
+
{
|
|
12
|
+
Assert.Equal(new Citation(file, start, end), Citation.Parse(cite));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
[Theory]
|
|
16
|
+
[InlineData("")]
|
|
17
|
+
[InlineData("src/A/B.cs")]
|
|
18
|
+
[InlineData("src/A/B.cs:")]
|
|
19
|
+
[InlineData("src/A/B.cs:0-3")]
|
|
20
|
+
[InlineData("src/A/B.cs:9-3")]
|
|
21
|
+
[InlineData("src/A/B.cs:a-b")]
|
|
22
|
+
[InlineData(":3-4")]
|
|
23
|
+
[InlineData("src\\A\\B.cs:3-4")]
|
|
24
|
+
[InlineData("src/A/B.cs:99999999999")]
|
|
25
|
+
[InlineData("src/A/B.cs:٣")]
|
|
26
|
+
[InlineData("src/A/B.cs:11\n")]
|
|
27
|
+
[InlineData(" src/A/B.cs:11")]
|
|
28
|
+
[InlineData("src/A/B.cs:11 ")]
|
|
29
|
+
[InlineData("src/A\tB.cs:11")]
|
|
30
|
+
public void Anything_else_does_not_parse(string cite)
|
|
31
|
+
{
|
|
32
|
+
Assert.Null(Citation.Parse(cite));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
[Theory]
|
|
36
|
+
[InlineData("src/A/B.cs:0007-007", "src/A/B.cs:7")]
|
|
37
|
+
[InlineData("src/A/B.cs:3-09", "src/A/B.cs:3-9")]
|
|
38
|
+
public void Prints_in_canonical_form(string cite, string canonical)
|
|
39
|
+
{
|
|
40
|
+
Assert.Equal(canonical, Citation.Parse(cite)!.ToString());
|
|
41
|
+
}
|
|
42
|
+
}
|