@phuc1403/musketeer 0.12.1 → 0.15.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 (169) hide show
  1. package/INSTALLATION.md +1 -1
  2. package/README.md +2 -2
  3. package/manifest.json +4 -3
  4. package/package.json +1 -1
  5. package/template/.claude/hooks/inject-ubiquitous-language.cjs +2 -2
  6. package/template/.claude/skills/domain-expert/SKILL.md +76 -0
  7. package/template/.claude/skills/logical-components/SKILL.md +165 -94
  8. package/template/.claude/skills/logical-components/assets/component-diagram-template.html +104 -0
  9. package/template/.claude/skills/logical-components/references/component-analyst-prompt.md +88 -0
  10. package/template/.claude/skills/logical-components/references/component-level-detection.md +95 -0
  11. package/template/.claude/skills/logical-components/references/logical-component-concepts.md +92 -0
  12. package/template/.claude/skills/logical-components/scripts/build-component-model.cjs +120 -0
  13. package/template/.claude/skills/logical-components/scripts/index-declared-symbols.cjs +106 -0
  14. package/template/.claude/skills/logical-components/scripts/lib/coupling-metrics.cjs +48 -0
  15. package/template/.claude/skills/logical-components/scripts/lib/declared-symbol-patterns.cjs +57 -0
  16. package/template/.claude/skills/logical-components/scripts/lib/html-sections.cjs +91 -0
  17. package/template/.claude/skills/logical-components/scripts/lib/mermaid-graph.cjs +55 -0
  18. package/template/.claude/skills/logical-components/scripts/lib/read-json-input.cjs +109 -0
  19. package/template/.claude/skills/logical-components/scripts/lib/repo-walk.cjs +61 -0
  20. package/template/.claude/skills/logical-components/scripts/lib/verify-claims.cjs +159 -0
  21. package/template/.claude/skills/logical-components/scripts/render-component-diagram-html.cjs +112 -0
  22. package/template/.claude/skills/logical-components/scripts/scan-folder-tree.cjs +88 -0
  23. package/template/.claude/skills/logical-components/scripts/select-stale-components.cjs +129 -0
  24. package/template/.claude/skills/logical-components/.gitattributes +0 -2
  25. package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +0 -53
  26. package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +0 -67
  27. package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +0 -39
  28. package/template/.claude/skills/logical-components/scripts/Directory.Build.props +0 -7
  29. package/template/.claude/skills/logical-components/scripts/Directory.Build.rsp +0 -1
  30. package/template/.claude/skills/logical-components/scripts/Directory.Build.targets +0 -6
  31. package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +0 -8
  32. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +0 -70
  33. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +0 -47
  34. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +0 -98
  35. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +0 -54
  36. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +0 -37
  37. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +0 -86
  38. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +0 -219
  39. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +0 -180
  40. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +0 -130
  41. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +0 -101
  42. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +0 -251
  43. package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +0 -14
  44. package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +0 -111
  45. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +0 -87
  46. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +0 -79
  47. package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +0 -15
  48. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs +0 -72
  49. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +0 -78
  50. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +0 -272
  51. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +0 -231
  52. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PlainText.cs +0 -55
  53. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +0 -75
  54. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +0 -27
  55. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +0 -77
  56. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +0 -164
  57. package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +0 -234
  58. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +0 -84
  59. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +0 -197
  60. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +0 -115
  61. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SourceCodeLines.cs +0 -79
  62. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +0 -53
  63. package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +0 -106
  64. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +0 -63
  65. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +0 -297
  66. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +0 -41
  67. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +0 -90
  68. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchesCommandTests.cs +0 -37
  69. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +0 -59
  70. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +0 -42
  71. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +0 -249
  72. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +0 -253
  73. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppExtractionTests.cs +0 -188
  74. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppWorkspace.cs +0 -30
  75. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +0 -174
  76. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +0 -17
  77. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +0 -72
  78. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +0 -36
  79. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +0 -23
  80. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +0 -93
  81. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +0 -69
  82. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +0 -149
  83. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +0 -225
  84. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +0 -34
  85. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +0 -55
  86. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +0 -315
  87. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +0 -27
  88. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +0 -266
  89. package/template/.claude/skills/logical-components/scripts/NuGet.config +0 -17
  90. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +0 -7
  91. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +0 -7
  92. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/CompositionRootApp.slnx +0 -5
  93. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderExtensions.cs +0 -15
  94. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderShims.cs +0 -25
  95. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/NativeInterop.cs +0 -17
  96. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/Root.Application.csproj +0 -7
  97. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootAutofacModule.cs +0 -11
  98. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootGreeter.cs +0 -7
  99. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/logical-components.json +0 -48
  100. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AbstractMemberCases.cs +0 -27
  101. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AttributeCases.cs +0 -23
  102. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/CouplingApp.Application.csproj +0 -7
  103. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericBaseCases.cs +0 -14
  104. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericSeamBindingCases.cs +0 -59
  105. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ImplicitCallCases.cs +0 -230
  106. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.First.cs +0 -11
  107. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.Second.cs +0 -14
  108. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/StackOverflowCases.cs +0 -14
  109. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ViaDisambiguationCases.cs +0 -36
  110. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +0 -7
  111. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +0 -8
  112. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +0 -5
  113. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +0 -7
  114. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +0 -14
  115. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/Multi.Infrastructure.csproj +0 -12
  116. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/SchedulingClient.cs +0 -12
  117. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +0 -7
  118. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +0 -7
  119. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +0 -6
  120. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +0 -11
  121. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/ObjOutputApp.slnx +0 -5
  122. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Greeting.cs +0 -7
  123. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Obj.Application.csproj +0 -14
  124. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +0 -67
  125. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +0 -28
  126. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +0 -16
  127. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +0 -10
  128. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +0 -18
  129. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +0 -10
  130. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +0 -78
  131. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +0 -37
  132. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +0 -33
  133. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +0 -18
  134. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +0 -63
  135. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +0 -5
  136. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +0 -7
  137. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +0 -17
  138. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +0 -9
  139. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +0 -7
  140. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +0 -4
  141. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +0 -18
  142. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +0 -6
  143. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +0 -19
  144. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +0 -7
  145. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +0 -6
  146. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +0 -7
  147. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +0 -107
  148. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +0 -143
  149. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +0 -12
  150. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +0 -10
  151. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +0 -89
  152. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +0 -7
  153. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +0 -11
  154. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +0 -8
  155. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +0 -16
  156. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +0 -6
  157. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +0 -11
  158. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +0 -4
  159. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +0 -8
  160. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +0 -35
  161. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +0 -13
  162. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +0 -13
  163. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +0 -18
  164. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +0 -7
  165. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +0 -11
  166. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +0 -11
  167. package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +0 -7
  168. package/template/.claude/skills/logical-components/scripts/global.json +0 -5
  169. package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +0 -26
@@ -1,54 +0,0 @@
1
- using Microsoft.CodeAnalysis;
2
-
3
- namespace LogicalComponents;
4
-
5
- /// <summary>
6
- /// Decides whether a syntax tree is source a reader can open and cite: a real project document inside the repo.
7
- /// Spans and edge locations only ever come from such trees, which is what keeps the output checkout-independent.
8
- /// </summary>
9
- public static class CitableSource
10
- {
11
- public enum Kind
12
- {
13
- /// <summary>
14
- /// Not a project document, a source generator's virtual output, or a real file under a bin/ or obj/ folder
15
- /// (gRPC/NSwag output, a stray build copy): never cited, silently skipped.
16
- /// </summary>
17
- NotSource,
18
-
19
- /// <summary>A real file outside the repo root (linked file, other drive): cannot be cited portably.</summary>
20
- OutsideRepo,
21
-
22
- /// <summary>A real file inside the repo: <c>RelativePath</c> is repo-relative with forward slashes.</summary>
23
- InRepo,
24
- }
25
-
26
- public static (Kind Kind, string RelativePath) Classify(LoadedWorkspace loaded, SyntaxTree? tree)
27
- {
28
- if (tree is null || loaded.Solution.GetDocument(tree) is null or SourceGeneratedDocument)
29
- {
30
- return (Kind.NotSource, "");
31
- }
32
-
33
- var relative = Path.GetRelativePath(loaded.RepoRoot, tree.FilePath);
34
- if (Path.IsPathRooted(relative) || relative.StartsWith(".."))
35
- {
36
- return (Kind.OutsideRepo, "");
37
- }
38
-
39
- return IsBuildOutputPath(relative) ? (Kind.NotSource, "") : (Kind.InRepo, relative.Replace('\\', '/'));
40
- }
41
-
42
- /// <summary>
43
- /// True when any path segment is <c>bin</c> or <c>obj</c>, case-insensitively: build output is never real source,
44
- /// whether it is design-time generator output (gRPC, NSwag) or a build step's stray copy of a project file.
45
- /// Shared with <see cref="WorkspaceLoader"/>'s project-file discovery so the two never disagree.
46
- /// </summary>
47
- public static bool IsBuildOutputPath(string relativePath) =>
48
- relativePath.Split('/', '\\').Any(segment =>
49
- segment.Equals("bin", StringComparison.OrdinalIgnoreCase) || segment.Equals("obj", StringComparison.OrdinalIgnoreCase));
50
-
51
- /// <summary>Declared in a real document of the loaded solution (not metadata, not generator output).</summary>
52
- public static bool IsRepoSource(Microsoft.CodeAnalysis.INamedTypeSymbol type, LoadedWorkspace loaded) =>
53
- type.Locations.Any(l => l.IsInSource && Classify(loaded, l.SourceTree).Kind != Kind.NotSource);
54
- }
@@ -1,37 +0,0 @@
1
- using System.Text.RegularExpressions;
2
-
3
- namespace LogicalComponents;
4
-
5
- /// <summary>One responsibility an agent wrote for a component, and the source lines it claims show it.</summary>
6
- public sealed record Responsibility(string Text, string Cite);
7
-
8
- /// <summary>
9
- /// A parsed <c>file:line</c> or <c>file:start-end</c> citation: a repo-relative path with forward slashes and a 1-based,
10
- /// inclusive line range. Anything looser (surrounding whitespace, control characters, backslashes, non-ASCII digits,
11
- /// absurd line numbers) does not parse, so it can never reach the rendered document.
12
- /// </summary>
13
- public sealed partial record Citation(string File, int Start, int End)
14
- {
15
- public int LineCount => End - Start + 1;
16
-
17
- public static Citation? Parse(string cite)
18
- {
19
- var match = Pattern().Match(cite);
20
- if (!match.Success)
21
- {
22
- return null;
23
- }
24
-
25
- var start = int.Parse(match.Groups["start"].Value);
26
- var end = match.Groups["end"].Success ? int.Parse(match.Groups["end"].Value) : start;
27
- return start >= 1 && end >= start ? new Citation(match.Groups["file"].Value, start, end) : null;
28
- }
29
-
30
- /// <summary>The canonical form rendered in the document: <c>file:7</c> or <c>file:7-12</c>, never <c>file:0007-007</c>.</summary>
31
- public override string ToString() => Start == End ? $"{File}:{Start}" : $"{File}:{Start}-{End}";
32
-
33
- // At most 7 ASCII digits per number, so int.Parse cannot overflow; \z (not $) so a trailing newline cannot slip
34
- // through; the path has no colon, backslash or control character and neither starts nor ends with whitespace.
35
- [GeneratedRegex(@"\A(?<file>[^:\\\s\p{C}](?:[^:\\\p{C}]*[^:\\\s\p{C}])?):(?<start>[0-9]{1,7})(?:-(?<end>[0-9]{1,7}))?\z")]
36
- private static partial Regex Pattern();
37
- }
@@ -1,86 +0,0 @@
1
- using System.Security.Cryptography;
2
- using System.Text.Json;
3
-
4
- namespace LogicalComponents;
5
-
6
- /// <summary>
7
- /// <c>classify-batches --candidates &lt;candidates.json&gt; --agents &lt;n&gt; --repo &lt;repoPath&gt; --out &lt;dir&gt;</c>: splits
8
- /// the candidates the config does not classify yet into at most n equal batches, in candidates.json order, one
9
- /// <c>batch-NN.json</c> per classifier agent. Already classified candidates are never sent again: the repo config is
10
- /// read fresh here (not trusted from candidates.json's own status), so a stale candidates.json - written before an
11
- /// earlier classify-merge - cannot resend a type that is already reviewed. It also records the repo config's hash, so
12
- /// classify-merge can refuse to merge if anything edited the config in between.
13
- /// Exit codes: 0 written, 1 usage, 2 unreadable candidates.json or repo config.
14
- /// </summary>
15
- public static class ClassifyBatchesCommand
16
- {
17
- public const string Usage =
18
- "usage: LogicalComponents classify-batches --candidates <candidates.json> --agents <n> --repo <repoPath> --out <dir>";
19
-
20
- /// <summary>The file in the batches directory holding the config hash at batching time.</summary>
21
- public const string ConfigHashFile = "config.sha256";
22
-
23
- public sealed record ClassifyCandidate(string Type, string Layer, string Module, IReadOnlyList<SourceSpan> Spans);
24
-
25
- public sealed record ClassifyBatch(int Batch, IReadOnlyList<ClassifyCandidate> Candidates);
26
-
27
- public static async Task<int> RunAsync(string[] args, TextWriter output)
28
- {
29
- if (args is not ["--candidates", var candidatesPath, "--agents", var agentsText, "--repo", var repo, "--out", var outDir]
30
- || !int.TryParse(agentsText, out var agents) || agents < 1 || !Directory.Exists(repo))
31
- {
32
- await output.WriteLineAsync(Usage);
33
- return 1;
34
- }
35
-
36
- CandidatesCommand.CandidatesFile file;
37
- try
38
- {
39
- file = ComponentsFile.FromJson<CandidatesCommand.CandidatesFile>(await File.ReadAllBytesAsync(candidatesPath));
40
- }
41
- catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
42
- {
43
- await output.WriteLineAsync($"Cannot read {candidatesPath}: {e.Message}");
44
- return 2;
45
- }
46
-
47
- if (file.Candidates is null)
48
- {
49
- await output.WriteLineAsync($"{candidatesPath} is not a candidates.json written by the candidates command");
50
- return 2;
51
- }
52
-
53
- ExtractConfig config;
54
- try
55
- {
56
- config = ExtractConfig.Load(repo);
57
- }
58
- catch (ExtractConfigException e)
59
- {
60
- await output.WriteLineAsync(e.Message);
61
- return 2;
62
- }
63
-
64
- // c.Status reflects the config at candidates time; re-check against the config now in case it changed since
65
- // (classify-merge could have classified some of these candidates in an earlier round).
66
- var unclassified = file.Candidates
67
- .Where(c => c.Status == CandidatesCommand.Unclassified && !config.IsClassified(c.Type))
68
- .Select(c => new ClassifyCandidate(c.Type, c.Layer, c.Module, c.Spans))
69
- .ToList();
70
- var size = Math.Max(1, (unclassified.Count + agents - 1) / agents);
71
- var batches = unclassified.Chunk(size).Select((chunk, index) => new ClassifyBatch(index + 1, chunk)).ToList();
72
-
73
- BatchPlanning.Write(batches, b => b.Batch, outDir);
74
- await File.WriteAllTextAsync(Path.Combine(outDir, ConfigHashFile), ConfigHash(repo));
75
- await output.WriteLineAsync(
76
- $"{unclassified.Count} unclassified candidates in {batches.Count} batches -> {Path.GetFullPath(outDir)}");
77
- return 0;
78
- }
79
-
80
- /// <summary>SHA-256 of the repo's config file as it is now, or <c>absent</c>.</summary>
81
- public static string ConfigHash(string repo)
82
- {
83
- var path = Path.Combine(repo, ExtractConfig.FileName);
84
- return File.Exists(path) ? Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))) : "absent";
85
- }
86
- }
@@ -1,219 +0,0 @@
1
- using System.Text.Json;
2
-
3
- namespace LogicalComponents;
4
-
5
- /// <summary>
6
- /// <c>classify-merge --batches &lt;dir&gt; --verdicts &lt;dir&gt; --repo &lt;repoPath&gt;</c>: adds the classifier agents' verdicts
7
- /// (<c>verdicts/batch-NN.json</c>: <c>[{ type, verdict: component|helper, reason, cite }]</c>) to the repo's
8
- /// <c>logical-components.json</c>. The agents' output is checked against the batches they were given before anything is
9
- /// written: one verdict file per batch and none left over, exactly one verdict per candidate of the batch and no other
10
- /// type, a known verdict, a one-line reason under 200 characters, a citation inside the candidate's declaration, and a
11
- /// type the config does not already classify (a stale candidates.json could otherwise resend an already-reviewed type).
12
- /// The config must be unchanged since classify-batches, so nothing can slip an entry past the review.
13
- /// Prints each added entry (helpers first, for review) and a summary on <c>output</c>.
14
- /// Exit codes: 0 written, 1 usage, 2 the existing config or a batch file is unreadable, 3 the verdicts break the
15
- /// contract or the config changed (every problem listed, nothing written).
16
- /// </summary>
17
- public static class ClassifyMergeCommand
18
- {
19
- public const string Usage = "usage: LogicalComponents classify-merge --batches <dir> --verdicts <dir> --repo <repoPath>";
20
-
21
- private const int MaxReasonLength = 200;
22
- private const int MaxCitedLines = 30;
23
-
24
- private sealed record VerdictJson(string? Type, string? Verdict, string? Reason, string? Cite);
25
-
26
- /// <summary>A batch file from classify-batches could not be read: an environment/tooling problem, not the agents' fault.</summary>
27
- private sealed class MalformedBatchException(string message) : Exception(message);
28
-
29
- public static async Task<int> RunAsync(string[] args, TextWriter output)
30
- {
31
- if (args is not ["--batches", var batchDir, "--verdicts", var verdictDir, "--repo", var repo]
32
- || !Directory.Exists(batchDir) || !Directory.Exists(verdictDir) || !Directory.Exists(repo))
33
- {
34
- await output.WriteLineAsync(Usage);
35
- return 1;
36
- }
37
-
38
- ExtractConfig existing;
39
- try
40
- {
41
- existing = ExtractConfig.Load(repo);
42
- }
43
- catch (ExtractConfigException e)
44
- {
45
- await output.WriteLineAsync(e.Message);
46
- return 2;
47
- }
48
-
49
- var problems = new List<string>();
50
- var hashFile = Path.Combine(batchDir, ClassifyBatchesCommand.ConfigHashFile);
51
- if (!File.Exists(hashFile) || File.ReadAllText(hashFile) != ClassifyBatchesCommand.ConfigHash(repo))
52
- {
53
- problems.Add($"{ExtractConfig.FileName} changed since classify-batches; review that change, then batch again");
54
- }
55
-
56
- List<VerdictJson> verdicts;
57
- try
58
- {
59
- verdicts = Check(batchDir, verdictDir, existing, problems);
60
- }
61
- catch (MalformedBatchException e)
62
- {
63
- await output.WriteLineAsync(e.Message);
64
- return 2;
65
- }
66
-
67
- if (problems.Count > 0)
68
- {
69
- foreach (var problem in problems)
70
- {
71
- await output.WriteLineAsync(problem);
72
- }
73
-
74
- return 3;
75
- }
76
-
77
- var components = existing.Components.ToHashSet(StringComparer.Ordinal);
78
- var helpers = new SortedDictionary<string, string>(existing.Helpers.ToDictionary(), StringComparer.Ordinal);
79
- var added = verdicts.OrderBy(v => v.Type, StringComparer.Ordinal).ToList();
80
- foreach (var verdict in added)
81
- {
82
- if (verdict.Verdict == CandidatesCommand.Helper)
83
- {
84
- helpers[verdict.Type!] = verdict.Reason!;
85
- }
86
- else
87
- {
88
- components.Add(verdict.Type!);
89
- }
90
- }
91
-
92
- if (added.Count > 0)
93
- {
94
- await File.WriteAllBytesAsync(
95
- Path.Combine(repo, ExtractConfig.FileName),
96
- ComponentsFile.ToJson(new { components = components.Order(StringComparer.Ordinal), helpers }));
97
- }
98
-
99
- foreach (var verdict in added.OrderBy(v => v.Verdict == CandidatesCommand.Helper ? 0 : 1))
100
- {
101
- await output.WriteLineAsync($"{verdict.Verdict} {verdict.Type}: {verdict.Reason}");
102
- }
103
-
104
- await output.WriteLineAsync(
105
- $"{added.Count(v => v.Verdict == CandidatesCommand.Component)} components, "
106
- + $"{added.Count(v => v.Verdict == CandidatesCommand.Helper)} helpers added");
107
- return 0;
108
- }
109
-
110
- /// <summary>Every verdict of every batch, or problems (in batch file order) when the agents' output breaks the contract.</summary>
111
- /// <exception cref="MalformedBatchException">A batch file itself (written by classify-batches, not an agent) is not valid JSON.</exception>
112
- private static List<VerdictJson> Check(string batchDir, string verdictDir, ExtractConfig existing, List<string> problems)
113
- {
114
- var batchFiles = Directory.EnumerateFiles(batchDir, "batch-*.json").Select(Path.GetFileName).OfType<string>()
115
- .Order(StringComparer.Ordinal).ToList();
116
- problems.AddRange(Directory.EnumerateFiles(verdictDir, "batch-*.json").Select(Path.GetFileName).OfType<string>()
117
- .Where(name => !batchFiles.Contains(name))
118
- .Order(StringComparer.Ordinal)
119
- .Select(name => $"{name}: verdicts for a batch that does not exist (left from an earlier round?)"));
120
-
121
- var all = new List<VerdictJson>();
122
- foreach (var name in batchFiles)
123
- {
124
- ClassifyBatchesCommand.ClassifyBatch batch;
125
- try
126
- {
127
- batch = ComponentsFile.FromJson<ClassifyBatchesCommand.ClassifyBatch>(File.ReadAllBytes(Path.Combine(batchDir, name)));
128
- }
129
- catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
130
- {
131
- throw new MalformedBatchException($"{name}: not a valid batch file ({e.Message})");
132
- }
133
-
134
- var verdictPath = Path.Combine(verdictDir, name);
135
- if (!File.Exists(verdictPath))
136
- {
137
- problems.Add($"{name}: no verdict file");
138
- continue;
139
- }
140
-
141
- List<VerdictJson?> read;
142
- try
143
- {
144
- read = ComponentsFile.FromJson<List<VerdictJson?>>(File.ReadAllBytes(verdictPath));
145
- }
146
- catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
147
- {
148
- problems.Add($"{name}: not a verdict array ({e.Message})");
149
- continue;
150
- }
151
-
152
- var verdicts = read.OfType<VerdictJson>().ToList();
153
- var candidates = batch.Candidates.ToDictionary(c => c.Type, StringComparer.Ordinal);
154
- foreach (var group in verdicts.GroupBy(v => v.Type ?? "", StringComparer.Ordinal))
155
- {
156
- if (!candidates.TryGetValue(group.Key, out var candidate))
157
- {
158
- problems.Add($"{name}: {Printable(group.Key)}: not a candidate of this batch");
159
- }
160
- else if (group.Count() > 1)
161
- {
162
- problems.Add($"{name}: {group.Key}: more than one verdict");
163
- }
164
- else if (Problem(group.Single(), candidate, existing) is { } problem)
165
- {
166
- problems.Add($"{name}: {group.Key}: {problem}");
167
- }
168
- }
169
-
170
- problems.AddRange(candidates.Keys.Where(type => verdicts.All(v => v.Type != type))
171
- .Select(type => $"{name}: {type}: no verdict"));
172
- all.AddRange(verdicts);
173
- }
174
-
175
- return all;
176
- }
177
-
178
- private static string? Problem(VerdictJson verdict, ClassifyBatchesCommand.ClassifyCandidate candidate, ExtractConfig existing)
179
- {
180
- // Never overwrite a reviewed entry: a stale candidates.json could otherwise resend an already-classified type
181
- // and have its verdict silently replace the reviewed reason, or put the type in both lists.
182
- if (existing.IsClassified(candidate.Type))
183
- {
184
- return $"already classified in {ExtractConfig.FileName}; re-run candidates";
185
- }
186
-
187
- if (verdict.Verdict is not (CandidatesCommand.Component or CandidatesCommand.Helper))
188
- {
189
- return "verdict must be component or helper";
190
- }
191
-
192
- if (string.IsNullOrWhiteSpace(verdict.Reason))
193
- {
194
- return "reason is empty";
195
- }
196
-
197
- // The reason is printed at the review gate and stored in the config: one plain line, so it cannot fake other
198
- // lines or hide a bidi/zero-width character in what looks like a normal sentence.
199
- if (PlainText.HasDisallowedUnicode(verdict.Reason))
200
- {
201
- return "reason has control characters";
202
- }
203
-
204
- if (verdict.Reason.Length > MaxReasonLength)
205
- {
206
- return $"reason is {verdict.Reason.Length} characters; keep it under {MaxReasonLength}";
207
- }
208
-
209
- var cite = Citation.Parse(verdict.Cite ?? "");
210
- return cite is not null
211
- && cite.LineCount <= MaxCitedLines
212
- && candidate.Spans.Any(s => s.File == cite.File && s.Start <= cite.Start && cite.End <= s.End)
213
- ? null
214
- : $"cite '{verdict.Cite}' is not inside its declaration";
215
- }
216
-
217
- // An unknown type is agent text: print it without anything that could start a new line at the review gate.
218
- private static string Printable(string text) => new(text.Select(c => PlainText.IsDisallowed(c) ? '?' : c).ToArray());
219
- }
@@ -1,180 +0,0 @@
1
- using Microsoft.CodeAnalysis;
2
-
3
- namespace LogicalComponents;
4
-
5
- /// <summary>
6
- /// Lists the candidates (every type in an Application or Infrastructure project that passes <see cref="ComponentRule"/>)
7
- /// and keeps as components the ones the repo's frozen classification calls components. Output order is by full type
8
- /// name, so the same code and config always yield the same list.
9
- /// </summary>
10
- public static class ComponentDiscovery
11
- {
12
- public const string ApplicationLayer = "Application";
13
- public const string InfrastructureLayer = "Infrastructure";
14
-
15
- /// <summary>The candidates, as components not yet named (empty id and name), plus out-of-repo warnings.</summary>
16
- public sealed record CandidateList(IReadOnlyList<Component> Candidates, IReadOnlyList<string> Warnings);
17
-
18
- public static async Task<CandidateList> CandidatesAsync(LoadedWorkspace loaded, CancellationToken cancellationToken = default)
19
- {
20
- var warnings = new List<string>();
21
- var types = await SourceTypesAsync(loaded.Solution, cancellationToken);
22
- if (types.All(t => t.Layer is not (ApplicationLayer or InfrastructureLayer)))
23
- {
24
- // Nothing to classify is easy to miss: an empty document with no explanation looks like success.
25
- warnings.Add(
26
- $"no {ApplicationLayer} or {InfrastructureLayer} project found among: {string.Join(", ", loaded.ProjectNames)}");
27
- }
28
-
29
- var candidates = types
30
- .Where(t => t.Layer is (ApplicationLayer or InfrastructureLayer) && ComponentRule.IsCandidate(t.Symbol))
31
- .Select(t => ToComponent(t, loaded, warnings))
32
- .OfType<Component>()
33
- .ToList();
34
- return new CandidateList(candidates, warnings);
35
- }
36
-
37
- public static async Task<DiscoveryResult> DiscoverAsync(
38
- LoadedWorkspace loaded, ExtractConfig config, CancellationToken cancellationToken = default)
39
- {
40
- var (candidates, candidateWarnings) = await CandidatesAsync(loaded, cancellationToken);
41
- var known = candidates.Select(c => c.Type).ToHashSet(StringComparer.Ordinal);
42
- var stale = config.Components.Concat(config.Helpers.Keys)
43
- .Where(name => !known.Contains(name))
44
- .Distinct()
45
- .Order(StringComparer.Ordinal)
46
- .Select(name => $"config entry '{name}' names no candidate; ignored");
47
-
48
- return new DiscoveryResult(
49
- AssignIds(candidates.Where(c => config.Components.Contains(c.Type)).ToList()),
50
- candidates.Where(c => !config.IsClassified(c.Type)).Select(c => c.Type).ToList(),
51
- [.. candidateWarnings, .. stale]);
52
- }
53
-
54
- /// <summary>A top-level type declared in a loaded project, with the layer and module its project gives it.</summary>
55
- public sealed record SourceType(INamedTypeSymbol Symbol, string Layer, string Module);
56
-
57
- public static async Task<List<SourceType>> SourceTypesAsync(Solution solution, CancellationToken cancellationToken)
58
- {
59
- var types = new List<SourceType>();
60
- foreach (var project in solution.Projects)
61
- {
62
- var compilation = await project.GetCompilationAsync(cancellationToken)
63
- ?? throw new InvalidOperationException($"No compilation for {project.Name}");
64
- var (layer, module) = LayerAndModule(project.Name);
65
- types.AddRange(TopLevelTypes(compilation.Assembly.GlobalNamespace)
66
- .Where(t => t.Locations.Any(l => l.IsInSource))
67
- .Select(t => new SourceType(t, layer, module)));
68
- }
69
-
70
- return types.OrderBy(t => SymbolNames.FullName(t.Symbol), StringComparer.Ordinal).ToList();
71
- }
72
-
73
- /// <summary>
74
- /// <c>WorkerDwg.Export.Application</c> → (Application, Export); <c>Sample.Infrastructure</c> → (Infrastructure, Sample);
75
- /// a bare <c>Application</c>/<c>Infrastructure</c> project → that layer, itself as its module; matched
76
- /// case-insensitively, so <c>Foo.application</c> counts too. Any other project → (Other, its last name segment).
77
- /// </summary>
78
- public static (string Layer, string Module) LayerAndModule(string projectName)
79
- {
80
- foreach (var layer in new[] { ApplicationLayer, InfrastructureLayer })
81
- {
82
- if (string.Equals(projectName, layer, StringComparison.OrdinalIgnoreCase))
83
- {
84
- return (layer, projectName);
85
- }
86
-
87
- if (projectName.EndsWith("." + layer, StringComparison.OrdinalIgnoreCase))
88
- {
89
- var rest = projectName[..^(layer.Length + 1)];
90
- return (layer, rest[(rest.LastIndexOf('.') + 1)..]);
91
- }
92
- }
93
-
94
- return ("Other", projectName[(projectName.LastIndexOf('.') + 1)..]);
95
- }
96
-
97
- /// <summary>
98
- /// Ids and names are the plain type name unless several components share it; then every one of them is prefixed
99
- /// with its module (<c>Export_JobOutcomeReporting</c>, "Job Outcome Reporting (Export)"). If that still clashes
100
- /// (same module, different namespace or generic arity), the full type name disambiguates. The same full type twice
101
- /// (a file linked into two projects) has no honest id and is an error.
102
- /// </summary>
103
- public static List<Component> AssignIds(IReadOnlyList<Component> components)
104
- {
105
- var duplicateTypes = components.GroupBy(c => c.Type).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
106
- if (duplicateTypes.Count > 0)
107
- {
108
- throw new InvalidOperationException(
109
- $"Declared in more than one project, so no unique component id: {string.Join(", ", duplicateTypes.Order(StringComparer.Ordinal))}");
110
- }
111
-
112
- var bySimpleName = components.GroupBy(c => SymbolNames.SimpleName(c.Type)).ToDictionary(g => g.Key, g => g.Count());
113
- var named = components.Select(c =>
114
- {
115
- var simple = SymbolNames.SimpleName(c.Type);
116
- var words = DisplayName.FromTypeName(simple);
117
- return bySimpleName[simple] == 1
118
- ? c with { Id = simple, Name = words }
119
- : c with { Id = $"{c.Module}_{simple}", Name = $"{words} ({c.Module})" };
120
- }).ToList();
121
-
122
- var clashingIds = named.GroupBy(c => c.Id).Where(g => g.Count() > 1).Select(g => g.Key).ToHashSet();
123
- return named
124
- .Select(c => clashingIds.Contains(c.Id)
125
- ? c with
126
- {
127
- Id = SymbolNames.ToIdentifier(c.Type),
128
- Name = $"{DisplayName.FromTypeName(SymbolNames.SimpleName(c.Type))} ({SymbolNames.ReadableFullName(c.Type)})",
129
- }
130
- : c)
131
- .OrderBy(c => c.Type, StringComparer.Ordinal)
132
- .ToList();
133
- }
134
-
135
- /// <summary>
136
- /// The component for a type, with spans from real source files inside the repo only. Generator output — a source
137
- /// generator's virtual document, or a real file under bin/ or obj/ — is skipped silently; a declaration outside the
138
- /// repo cannot be cited, so it is skipped with a warning. No span left means no component.
139
- /// </summary>
140
- public static Component? ToComponent(SourceType type, LoadedWorkspace loaded, List<string> warnings)
141
- {
142
- var spans = new List<SourceSpan>();
143
- foreach (var reference in type.Symbol.DeclaringSyntaxReferences)
144
- {
145
- var (kind, relative) = CitableSource.Classify(loaded, reference.SyntaxTree);
146
- if (kind == CitableSource.Kind.OutsideRepo)
147
- {
148
- var warning = $"declaration of {SymbolNames.FullName(type.Symbol)} outside the repo was ignored";
149
- if (!warnings.Contains(warning))
150
- {
151
- warnings.Add(warning);
152
- }
153
- }
154
-
155
- if (kind != CitableSource.Kind.InRepo)
156
- {
157
- continue;
158
- }
159
-
160
- var lines = reference.GetSyntax().GetLocation().GetLineSpan();
161
- spans.Add(new SourceSpan(relative, lines.StartLinePosition.Line + 1, lines.EndLinePosition.Line + 1));
162
- }
163
-
164
- if (spans.Count == 0)
165
- {
166
- return null;
167
- }
168
-
169
- return new Component(
170
- Id: "",
171
- Type: SymbolNames.FullName(type.Symbol),
172
- Name: "",
173
- Module: type.Module,
174
- Layer: type.Layer,
175
- Spans: spans.OrderBy(s => s.File, StringComparer.Ordinal).ThenBy(s => s.Start).ToList());
176
- }
177
-
178
- private static IEnumerable<INamedTypeSymbol> TopLevelTypes(INamespaceSymbol ns) =>
179
- ns.GetTypeMembers().Concat(ns.GetNamespaceMembers().SelectMany(TopLevelTypes));
180
- }