@phuc1403/musketeer 0.11.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/README.md +2 -2
- package/manifest.json +12 -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/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/SKILL.md +23 -18
- package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +1 -1
- package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +1 -1
- 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/LogicalComponents/BatchPlanning.cs +2 -1
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +9 -1
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +39 -11
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +19 -4
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +25 -4
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +43 -11
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +20 -6
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +57 -14
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +20 -2
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +70 -15
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +28 -13
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs +72 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +11 -6
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +91 -22
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +85 -9
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PlainText.cs +55 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +3 -2
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +36 -4
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +75 -14
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +45 -26
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +56 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SourceCodeLines.cs +79 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +31 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +86 -16
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +8 -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 +25 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +73 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +63 -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 +1 -1
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +4 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +45 -2
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +4 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +30 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +66 -3
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +19 -13
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +89 -1
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +181 -2
- package/template/.claude/skills/logical-components/scripts/NuGet.config +17 -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 +1 -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/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 +5 -1
package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
using System.Text;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// CommonMark code-span parsing for <see cref="ResponsibilityValidation"/>'s plain-text check: a backtick run of
|
|
7
|
+
/// length n opens a code span, which closes at the next run of exactly length n (a run of a different length inside is
|
|
8
|
+
/// literal content; a run with no matching close is an unclosed delimiter, not a span). A naive `` `[^`]*` `` match
|
|
9
|
+
/// gets this wrong for an escaped or mismatched backtick, which could let HTML or a link hide inside what looks like,
|
|
10
|
+
/// but is not, a real code span.
|
|
11
|
+
/// </summary>
|
|
12
|
+
public static class MarkdownCodeSpans
|
|
13
|
+
{
|
|
14
|
+
/// <summary>The text with every real code span removed, and whether any backtick run was left unmatched.</summary>
|
|
15
|
+
public static (string Outside, bool Unclosed) Strip(string text)
|
|
16
|
+
{
|
|
17
|
+
var outside = new StringBuilder();
|
|
18
|
+
var unclosed = false;
|
|
19
|
+
var i = 0;
|
|
20
|
+
while (i < text.Length)
|
|
21
|
+
{
|
|
22
|
+
if (text[i] != '`')
|
|
23
|
+
{
|
|
24
|
+
outside.Append(text[i]);
|
|
25
|
+
i++;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var openStart = i;
|
|
30
|
+
while (i < text.Length && text[i] == '`')
|
|
31
|
+
{
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var openLength = i - openStart;
|
|
36
|
+
var closeStart = -1;
|
|
37
|
+
var j = i;
|
|
38
|
+
while (j < text.Length)
|
|
39
|
+
{
|
|
40
|
+
if (text[j] != '`')
|
|
41
|
+
{
|
|
42
|
+
j++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var runStart = j;
|
|
47
|
+
while (j < text.Length && text[j] == '`')
|
|
48
|
+
{
|
|
49
|
+
j++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (j - runStart == openLength)
|
|
53
|
+
{
|
|
54
|
+
closeStart = runStart;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (closeStart >= 0)
|
|
60
|
+
{
|
|
61
|
+
i = closeStart + openLength;
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
{
|
|
65
|
+
unclosed = true;
|
|
66
|
+
outside.Append('`', openLength);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (outside.ToString(), unclosed);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs
CHANGED
|
@@ -61,13 +61,18 @@ public static class MarkdownRenderer
|
|
|
61
61
|
return md.ToString();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// Mermaid node labels are double-quoted; a literal quote is written as an entity.
|
|
65
|
-
|
|
64
|
+
// Mermaid node labels are double-quoted; a literal quote is written as an entity. Angle brackets are escaped the
|
|
65
|
+
// same way: some renderers treat a label as HTML, and an unescaped `<` in a type or module name could otherwise
|
|
66
|
+
// inject markup into the diagram.
|
|
67
|
+
private static string Escape(string label) => label.Replace("\"", "#quot;").Replace("<", "#lt;").Replace(">", "#gt;");
|
|
66
68
|
|
|
67
69
|
/// <summary>
|
|
68
|
-
/// A Mermaid-safe node id: prefixed so a component called <c>end</c>, <c>graph</c> or <c>class</c> is never read as
|
|
69
|
-
/// keyword, and
|
|
70
|
+
/// A Mermaid-safe node id: prefixed so a component called <c>end</c>, <c>graph</c> or <c>class</c> is never read as
|
|
71
|
+
/// a keyword, and every character that is not an ASCII letter or digit - including a literal underscore, so the
|
|
72
|
+
/// escape below cannot collide with one already in the id - is replaced by <c>_x{hex}_</c>. That makes the mapping
|
|
73
|
+
/// injective (two different ids always get two different node ids), unlike collapsing every such character to a
|
|
74
|
+
/// single <c>_</c>, which let ids as different as "My Module_Queue" and "My-Module.Queue" collide.
|
|
70
75
|
/// </summary>
|
|
71
|
-
private static string NodeId(string id) =>
|
|
72
|
-
|
|
76
|
+
private static string NodeId(string id) => "n_" + string.Concat(
|
|
77
|
+
id.Select(c => char.IsAsciiLetterOrDigit(c) ? c.ToString() : $"_x{(int)c:x}_"));
|
|
73
78
|
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs
CHANGED
|
@@ -22,10 +22,19 @@ namespace LogicalComponents;
|
|
|
22
22
|
/// </summary>
|
|
23
23
|
public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<string> components, PortResolution ports)
|
|
24
24
|
{
|
|
25
|
+
// How many seen-through hops one chain may follow before it is cut off with a warning: real helper chains never
|
|
26
|
+
// run this deep, but a generic helper that instantiates itself one level deeper at every hop (Nest<T> calling
|
|
27
|
+
// Nest<List<T>>) never revisits the same (member, bindings) pair, so the cycle guard below never catches it and the
|
|
28
|
+
// walk would otherwise recurse until the process stack overflows.
|
|
29
|
+
private const int MaxWalkDepth = 200;
|
|
30
|
+
|
|
25
31
|
public sealed record RawEdge(string From, string To, string Kind, IReadOnlyList<string> Via, string File, int Line);
|
|
26
32
|
|
|
27
|
-
/// <summary>
|
|
28
|
-
|
|
33
|
+
/// <summary>
|
|
34
|
+
/// The edges a component's code leads to, the helper code it runs on the way (sorted, distinct), and warnings about
|
|
35
|
+
/// coupling the walk could not fully resolve.
|
|
36
|
+
/// </summary>
|
|
37
|
+
public sealed record Resolution(List<RawEdge> Edges, List<SourceSpan> Reaches, List<string> Warnings);
|
|
29
38
|
|
|
30
39
|
// Shared by every component's walk: what a piece of syntax uses never changes within one loaded solution.
|
|
31
40
|
private readonly Dictionary<SyntaxNode, List<MemberUses.MemberUse>> _uses = [];
|
|
@@ -33,24 +42,28 @@ public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<str
|
|
|
33
42
|
|
|
34
43
|
/// <summary>
|
|
35
44
|
/// The edges from <paramref name="from"/> reached by <paramref name="uses"/>. <paramref name="self"/> holds the
|
|
36
|
-
/// component and the base classes whose code counts as its own: uses of those are not coupling.
|
|
45
|
+
/// component and the base classes whose code counts as its own: uses of those are not coupling. <paramref name="map"/>
|
|
46
|
+
/// binds the type parameters of any base class among <paramref name="uses"/>' declaring types to what the component
|
|
47
|
+
/// supplies as its base class construction, so a generic base's own seam calls resolve to the right construction.
|
|
37
48
|
/// </summary>
|
|
38
49
|
public async Task<Resolution> ResolveAsync(
|
|
39
|
-
string from, IReadOnlySet<string> self, IEnumerable<MemberUses.MemberUse> uses,
|
|
50
|
+
string from, IReadOnlySet<string> self, IEnumerable<MemberUses.MemberUse> uses, TypeMap map,
|
|
51
|
+
CancellationToken cancellationToken)
|
|
40
52
|
{
|
|
41
53
|
var walk = new Walk(from, self);
|
|
42
54
|
foreach (var use in uses)
|
|
43
55
|
{
|
|
44
|
-
await ResolveUseAsync(use.Member, use.Kind, [], use.File, use.Line,
|
|
56
|
+
await ResolveUseAsync(use.Member, use.Kind, [], use.File, use.Line, map, walk, cancellationToken);
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
return new Resolution(
|
|
48
60
|
walk.Edges,
|
|
49
|
-
walk.Reaches.OrderBy(s => s.File, StringComparer.Ordinal).ThenBy(s => s.Start).ThenBy(s => s.End).ToList()
|
|
61
|
+
walk.Reaches.OrderBy(s => s.File, StringComparer.Ordinal).ThenBy(s => s.Start).ThenBy(s => s.End).ToList(),
|
|
62
|
+
walk.Warnings.Order(StringComparer.Ordinal).ToList());
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
// One component's walk: members already seen through (for the same type bindings) are not walked again, which also
|
|
53
|
-
// ends cycles.
|
|
66
|
+
// ends cycles. Depth guards the chains a cycle can't catch (see MaxWalkDepth).
|
|
54
67
|
private sealed class Walk(string from, IReadOnlySet<string> self)
|
|
55
68
|
{
|
|
56
69
|
public string From { get; } = from;
|
|
@@ -58,6 +71,10 @@ public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<str
|
|
|
58
71
|
public HashSet<string> Visited { get; } = new(StringComparer.Ordinal);
|
|
59
72
|
public List<RawEdge> Edges { get; } = [];
|
|
60
73
|
public HashSet<SourceSpan> Reaches { get; } = [];
|
|
74
|
+
public HashSet<string> Warnings { get; } = new(StringComparer.Ordinal);
|
|
75
|
+
public int Depth { get; set; }
|
|
76
|
+
|
|
77
|
+
public void Warn(string message) => Warnings.Add(message);
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
private async Task ResolveUseAsync(
|
|
@@ -96,7 +113,21 @@ public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<str
|
|
|
96
113
|
|
|
97
114
|
if (EdgeExtraction.IsSeam(declaring) && !member.IsStatic && kind != EdgeExtraction.New)
|
|
98
115
|
{
|
|
99
|
-
|
|
116
|
+
// A concrete, non-virtual member of an abstract class cannot be overridden: it runs as written for every
|
|
117
|
+
// subclass (or none), so it is this walk's own behaviour rather than a per-implementation dispatch.
|
|
118
|
+
if (declaring.TypeKind == TypeKind.Class && !IsOverridable(member))
|
|
119
|
+
{
|
|
120
|
+
await SeeThroughAsync(member, kind, via.Add(SymbolNames.FullName(declaring)), file, line, map, walk, cancellationToken);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var implementations = await ports.ImplementationsAsync(declaring, cancellationToken);
|
|
125
|
+
if (implementations.Count == 0 && declaring.TypeKind == TypeKind.Class)
|
|
126
|
+
{
|
|
127
|
+
walk.Warn($"{SymbolNames.FullName(declaring.OriginalDefinition)}.{member.Name} has no concrete subclass in the repo");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
foreach (var implementation in implementations)
|
|
100
131
|
{
|
|
101
132
|
if (SeamDispatch.ImplementationOf(implementation, declaring, member) is not { } runs)
|
|
102
133
|
{
|
|
@@ -106,18 +137,22 @@ public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<str
|
|
|
106
137
|
// An inherited default body lives on the seam itself: walk it rather than resolve the seam again.
|
|
107
138
|
if (runs.ContainingType is { } runsIn && EdgeExtraction.IsSeam(runsIn))
|
|
108
139
|
{
|
|
109
|
-
await SeeThroughAsync(runs, kind, via.Add(declaring
|
|
140
|
+
await SeeThroughAsync(runs, kind, via.Add(SymbolNames.FullName(declaring)), file, line, map, walk, cancellationToken);
|
|
110
141
|
}
|
|
111
142
|
else
|
|
112
143
|
{
|
|
113
|
-
|
|
144
|
+
// Bind the implementation's own type parameters (an open-generic decorator) and the resolved
|
|
145
|
+
// method's own type parameters (a generic method declared on the seam) to what this use actually
|
|
146
|
+
// supplies, so a seam the implementation calls in turn resolves to the right construction too.
|
|
147
|
+
var binding = SeamDispatch.BindingFor(implementation, declaring, member, runs, map);
|
|
148
|
+
await ResolveUseAsync(runs, kind, via.Add(SymbolNames.FullName(declaring)), file, line, binding, walk, cancellationToken);
|
|
114
149
|
}
|
|
115
150
|
}
|
|
116
151
|
|
|
117
152
|
return;
|
|
118
153
|
}
|
|
119
154
|
|
|
120
|
-
await SeeThroughAsync(member, kind, via.Add(owner
|
|
155
|
+
await SeeThroughAsync(member, kind, via.Add(SymbolNames.FullName(owner)), file, line, map, walk, cancellationToken);
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
private async Task SeeThroughAsync(
|
|
@@ -145,26 +180,60 @@ public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<str
|
|
|
145
180
|
return;
|
|
146
181
|
}
|
|
147
182
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
183
|
+
// A cycle that never repeats a (member, bindings) pair (a generic helper instantiating itself one level
|
|
184
|
+
// deeper at every hop) would otherwise recurse until the process stack overflows: cut it off instead.
|
|
185
|
+
if (walk.Depth >= MaxWalkDepth)
|
|
186
|
+
{
|
|
187
|
+
walk.Warn($"{walk.From}: coupling walk stopped at depth {MaxWalkDepth} following {member.Name}, possible unbounded generic recursion");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
152
190
|
|
|
153
|
-
|
|
191
|
+
walk.Depth++;
|
|
192
|
+
try
|
|
154
193
|
{
|
|
155
|
-
|
|
194
|
+
var (roots, runsImplicitBase) = kind == EdgeExtraction.New && member is IMethodSymbol constructor
|
|
195
|
+
? WalkRoots.Construction(constructor)
|
|
196
|
+
: (WalkRoots.Body(member), false);
|
|
197
|
+
Record(roots, walk);
|
|
198
|
+
|
|
199
|
+
foreach (var root in roots)
|
|
156
200
|
{
|
|
157
|
-
|
|
201
|
+
foreach (var use in await UsesAsync(root, cancellationToken))
|
|
202
|
+
{
|
|
203
|
+
await ResolveUseAsync(use.Member, use.Kind, via, file, line, inner, walk, cancellationToken);
|
|
204
|
+
}
|
|
158
205
|
}
|
|
159
|
-
}
|
|
160
206
|
|
|
161
|
-
|
|
162
|
-
|
|
207
|
+
if (runsImplicitBase
|
|
208
|
+
&& member.ContainingType.BaseType?.InstanceConstructors.FirstOrDefault(c => c.Parameters.Length == 0) is { } baseConstructor)
|
|
209
|
+
{
|
|
210
|
+
await ResolveUseAsync(baseConstructor, EdgeExtraction.New, via, file, line, inner, walk, cancellationToken);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// The static constructor runs once, lazily, before the type's first static member use or instantiation:
|
|
214
|
+
// consistent with a static field's own initializer, which is already walked wherever that field is read.
|
|
215
|
+
if ((kind == EdgeExtraction.Static || kind == EdgeExtraction.New)
|
|
216
|
+
&& member.ContainingType?.OriginalDefinition.StaticConstructors.FirstOrDefault(c => !c.IsImplicitlyDeclared)
|
|
217
|
+
is { } staticConstructor)
|
|
218
|
+
{
|
|
219
|
+
await ResolveUseAsync(staticConstructor, EdgeExtraction.Call, via, file, line, inner, walk, cancellationToken);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally
|
|
163
223
|
{
|
|
164
|
-
|
|
224
|
+
walk.Depth--;
|
|
165
225
|
}
|
|
166
226
|
}
|
|
167
227
|
|
|
228
|
+
/// <summary>A method, property or event that a subclass could give a different body: dispatch, not fixed code.</summary>
|
|
229
|
+
private static bool IsOverridable(ISymbol member) => member switch
|
|
230
|
+
{
|
|
231
|
+
IMethodSymbol method => method.IsVirtual || method.IsAbstract || method.IsOverride,
|
|
232
|
+
IPropertySymbol property => property.IsVirtual || property.IsAbstract || property.IsOverride,
|
|
233
|
+
IEventSymbol @event => @event.IsVirtual || @event.IsAbstract || @event.IsOverride,
|
|
234
|
+
_ => false,
|
|
235
|
+
};
|
|
236
|
+
|
|
168
237
|
// The walked helper code becomes citable for the component; a parameter declares a value and runs nothing.
|
|
169
238
|
private void Record(IEnumerable<SyntaxNode> roots, Walk walk)
|
|
170
239
|
{
|
|
@@ -8,17 +8,23 @@ namespace LogicalComponents;
|
|
|
8
8
|
/// Reads what a piece of code uses, as members: <c>new</c> (the constructor an object creation, a <c>: this(...)</c> /
|
|
9
9
|
/// <c>: base(...)</c> initializer or a primary-constructor base call runs), <c>static</c> (a static member, an extension
|
|
10
10
|
/// method or C# 14 extension member, a user-defined operator or conversion) and <c>call</c> (an instance member used on
|
|
11
|
-
/// any receiver, an indexer, the enumerator
|
|
12
|
-
///
|
|
13
|
-
///
|
|
14
|
-
///
|
|
11
|
+
/// any receiver, an indexer or conditional-access indexer, the enumerator, deconstruction and <c>MoveNext</c>/<c>Current</c>
|
|
12
|
+
/// a <c>foreach</c> runs, the <c>Add</c> a collection initializer element runs, a deconstruction assignment's
|
|
13
|
+
/// <c>Deconstruct</c>, and the <c>GetAwaiter</c>/<c>IsCompleted</c>/<c>GetResult</c> an <c>await</c> runs). A property or
|
|
14
|
+
/// indexer use is the accessor that actually runs: a read is the getter, an assignment the setter, a compound
|
|
15
|
+
/// assignment or increment both. Attribute names are not uses: an attribute is metadata the compiler reads, not code the
|
|
16
|
+
/// annotated member runs. Only citable source is read, so generator output never contributes. What a use couples to is
|
|
17
|
+
/// decided by <see cref="MemberUseResolution"/>.
|
|
15
18
|
/// </summary>
|
|
16
19
|
public static class MemberUses
|
|
17
20
|
{
|
|
18
21
|
/// <summary>One use of <see cref="Member"/>, at a repo-relative file and 1-based line.</summary>
|
|
19
22
|
public sealed record MemberUse(ISymbol Member, string Kind, string File, int Line);
|
|
20
23
|
|
|
21
|
-
/// <summary>
|
|
24
|
+
/// <summary>
|
|
25
|
+
/// Every member use inside the given syntax nodes, in source order. Nodes outside the repo, and nodes inside an
|
|
26
|
+
/// attribute list, are skipped.
|
|
27
|
+
/// </summary>
|
|
22
28
|
public static async Task<List<MemberUse>> ReadAsync(
|
|
23
29
|
IEnumerable<SyntaxNode> roots, LoadedWorkspace loaded, CancellationToken cancellationToken)
|
|
24
30
|
{
|
|
@@ -33,7 +39,10 @@ public static class MemberUses
|
|
|
33
39
|
|
|
34
40
|
var model = await document.GetSemanticModelAsync(cancellationToken)
|
|
35
41
|
?? throw new InvalidOperationException($"No semantic model for {file}");
|
|
36
|
-
|
|
42
|
+
|
|
43
|
+
// An attribute name resolves to its constructor, but applying an attribute is not running code: never
|
|
44
|
+
// descend into an attribute list at all, so nothing inside one is ever read as a use.
|
|
45
|
+
foreach (var node in root.DescendantNodesAndSelf(descendant => descendant is not AttributeListSyntax))
|
|
37
46
|
{
|
|
38
47
|
var line = node.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
|
|
39
48
|
uses.AddRange(Uses(node, model, cancellationToken).Select(u => new MemberUse(u.Member, u.Kind, file, line)));
|
|
@@ -46,6 +55,7 @@ public static class MemberUses
|
|
|
46
55
|
private static IEnumerable<(ISymbol Member, string Kind)> Uses(SyntaxNode node, SemanticModel model, CancellationToken token)
|
|
47
56
|
{
|
|
48
57
|
ISymbol? Symbol() => model.GetSymbolInfo(node, token).Symbol;
|
|
58
|
+
(ISymbol, string) AsUse(ISymbol member) => (member, member.IsStatic ? EdgeExtraction.Static : EdgeExtraction.Call);
|
|
49
59
|
|
|
50
60
|
switch (node)
|
|
51
61
|
{
|
|
@@ -63,7 +73,7 @@ public static class MemberUses
|
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
break;
|
|
66
|
-
case ElementAccessExpressionSyntax when Symbol() is IPropertySymbol indexer:
|
|
76
|
+
case ElementAccessExpressionSyntax or ElementBindingExpressionSyntax when Symbol() is IPropertySymbol indexer:
|
|
67
77
|
foreach (var accessor in AccessorsThatRun(indexer, node))
|
|
68
78
|
{
|
|
69
79
|
yield return (accessor, EdgeExtraction.Call);
|
|
@@ -75,9 +85,75 @@ public static class MemberUses
|
|
|
75
85
|
when Symbol() is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator or MethodKind.Conversion } op:
|
|
76
86
|
yield return (op, EdgeExtraction.Static);
|
|
77
87
|
break;
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
// A deconstruction assignment, `(a, b) = point;`: tried before the operator-assignment case above, but only
|
|
89
|
+
// claims the node when it is actually a deconstruction, so a plain or compound assignment still falls
|
|
90
|
+
// through to it.
|
|
91
|
+
case AssignmentExpressionSyntax assignment when DeconstructMethods(model.GetDeconstructionInfo(assignment)).ToList()
|
|
92
|
+
is { Count: > 0 } assignmentDeconstructs:
|
|
93
|
+
foreach (var method in assignmentDeconstructs)
|
|
94
|
+
{
|
|
95
|
+
yield return AsUse(method);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
break;
|
|
99
|
+
case CommonForEachStatementSyntax loop:
|
|
100
|
+
var info = model.GetForEachStatementInfo(loop);
|
|
101
|
+
foreach (var member in new ISymbol?[] { info.GetEnumeratorMethod, info.MoveNextMethod, info.CurrentProperty?.GetMethod })
|
|
102
|
+
{
|
|
103
|
+
if (member is not null)
|
|
104
|
+
{
|
|
105
|
+
yield return AsUse(member);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// foreach (var (a, b) in pairs): the per-element deconstruction, same as an explicit assignment's.
|
|
110
|
+
if (loop is ForEachVariableStatementSyntax deconstructingLoop)
|
|
111
|
+
{
|
|
112
|
+
foreach (var method in DeconstructMethods(model.GetDeconstructionInfo(deconstructingLoop)))
|
|
113
|
+
{
|
|
114
|
+
yield return AsUse(method);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
80
118
|
break;
|
|
119
|
+
case InitializerExpressionSyntax initializer when initializer.IsKind(SyntaxKind.CollectionInitializerExpression):
|
|
120
|
+
foreach (var element in initializer.Expressions)
|
|
121
|
+
{
|
|
122
|
+
if (model.GetCollectionInitializerSymbolInfo(element).Symbol is IMethodSymbol add)
|
|
123
|
+
{
|
|
124
|
+
yield return AsUse(add);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
break;
|
|
129
|
+
case AwaitExpressionSyntax awaitExpression:
|
|
130
|
+
var awaited = model.GetAwaitExpressionInfo(awaitExpression);
|
|
131
|
+
foreach (var member in new ISymbol?[] { awaited.GetAwaiterMethod, awaited.IsCompletedProperty?.GetMethod, awaited.GetResultMethod })
|
|
132
|
+
{
|
|
133
|
+
if (member is not null)
|
|
134
|
+
{
|
|
135
|
+
yield return AsUse(member);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// <summary>The Deconstruct methods a deconstruction runs, including any nested tuple/pattern's own.</summary>
|
|
144
|
+
private static IEnumerable<IMethodSymbol> DeconstructMethods(DeconstructionInfo info)
|
|
145
|
+
{
|
|
146
|
+
if (info.Method is { } method)
|
|
147
|
+
{
|
|
148
|
+
yield return method;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
foreach (var nested in info.Nested)
|
|
152
|
+
{
|
|
153
|
+
foreach (var method2 in DeconstructMethods(nested))
|
|
154
|
+
{
|
|
155
|
+
yield return method2;
|
|
156
|
+
}
|
|
81
157
|
}
|
|
82
158
|
}
|
|
83
159
|
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs
CHANGED
|
@@ -62,8 +62,9 @@ public static partial class ProjectFilters
|
|
|
62
62
|
private static Version ParseVersion(string text) =>
|
|
63
63
|
Version.TryParse(text.Contains('.') ? text : text + ".0", out var version) ? version : new Version(0, 0);
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
|
|
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)$")]
|
|
67
68
|
private static partial Regex TestProjectName();
|
|
68
69
|
|
|
69
70
|
[GeneratedRegex(
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs
CHANGED
|
@@ -6,8 +6,10 @@ namespace LogicalComponents;
|
|
|
6
6
|
/// <c>prune --resp <dir> --verify <dir> --out <dir></c>: applies the verifier agents' verdicts. For every
|
|
7
7
|
/// <c>resp/batch-NN.json</c> it reads <c>verify/batch-NN.json</c> — <c>{ "<id>": [ { "item": 1, "supported": true,
|
|
8
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,
|
|
10
|
-
///
|
|
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.
|
|
11
13
|
/// </summary>
|
|
12
14
|
public static class PruneCommand
|
|
13
15
|
{
|
|
@@ -15,6 +17,11 @@ public static class PruneCommand
|
|
|
15
17
|
|
|
16
18
|
public sealed record Verdict(int Item, bool Supported, string? Reason);
|
|
17
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
|
+
|
|
18
25
|
private static readonly JsonSerializerOptions ReadOptions = new()
|
|
19
26
|
{
|
|
20
27
|
PropertyNameCaseInsensitive = true,
|
|
@@ -118,15 +125,40 @@ public static class PruneCommand
|
|
|
118
125
|
return null;
|
|
119
126
|
}
|
|
120
127
|
|
|
128
|
+
Dictionary<string, List<RawVerdict?>?>? raw;
|
|
121
129
|
try
|
|
122
130
|
{
|
|
123
|
-
|
|
124
|
-
return verdicts.ToDictionary(v => v.Key, v => v.Value ?? [], StringComparer.Ordinal);
|
|
131
|
+
raw = JsonSerializer.Deserialize<Dictionary<string, List<RawVerdict?>?>>(File.ReadAllText(path), ReadOptions);
|
|
125
132
|
}
|
|
126
133
|
catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
|
|
127
134
|
{
|
|
128
135
|
problems.Add($"{name}: not a valid verdict file ({e.Message})");
|
|
129
136
|
return null;
|
|
130
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;
|
|
131
163
|
}
|
|
132
164
|
}
|