@phuc1403/musketeer 0.9.0 → 0.11.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 +53 -52
- package/README.md +1 -1
- package/bin/musketeer.js +168 -168
- package/manifest.json +19 -2
- package/package.json +48 -48
- package/src/dotnet-scaffold-copier.js +79 -79
- package/src/provisioner/detect.js +93 -93
- package/src/self-update.js +77 -77
- package/template/.claude/agents/git-manager.md +18 -18
- package/template/.claude/agents/hallmark-auditor.md +78 -78
- package/template/.claude/agents/researcher.md +33 -33
- package/template/.claude/hooks/block-unsafe-adr-title.cjs +85 -85
- package/template/.claude/hooks/init-adr-dir.cjs +173 -173
- package/template/.claude/hooks/inject-adr-flags.cjs +94 -94
- package/template/.claude/hooks/lib/adr/command-scan.cjs +115 -115
- package/template/.claude/hooks/lib/characteristics/checker.cjs +357 -357
- package/template/.claude/hooks/lib/git-info-cache.cjs +191 -191
- package/template/.claude/hooks/sync-adr-toc.cjs +146 -146
- package/template/.claude/hooks/usage-quota-cache-refresh.cjs +166 -166
- package/template/.claude/hooks/validate-characteristics-hook.cjs +66 -66
- package/template/.claude/hooks/validate-cml-hook.js +146 -145
- package/template/.claude/skills/adr-writer/SKILL.md +48 -48
- package/template/.claude/skills/adr-writer/references/adr-example.md +35 -35
- package/template/.claude/skills/architecture-characteristic-writer/SKILL.md +215 -215
- package/template/.claude/skills/architecture-characteristic-writer/assets/worksheet-template.md +29 -29
- package/template/.claude/skills/architecture-characteristic-writer/references/characteristics-catalog.md +40 -40
- package/template/.claude/skills/architecture-characteristic-writer/scripts/ranking-table.cjs +171 -171
- package/template/.claude/skills/context-map/SKILL.md +80 -80
- package/template/.claude/skills/context-map/example.cml +106 -106
- package/template/.claude/skills/context-map/reference/Bounded Context/Bounded Context.md +40 -40
- package/template/.claude/skills/context-map/reference/Bounded Context/businessModel.md +5 -5
- package/template/.claude/skills/context-map/reference/Bounded Context/domainVisionStatement.md +2 -2
- package/template/.claude/skills/context-map/reference/Bounded Context/evolution.md +5 -5
- package/template/.claude/skills/context-map/reference/Bounded Context/implementationTechnology.md +1 -1
- package/template/.claude/skills/context-map/reference/Bounded Context/implements.md +1 -1
- package/template/.claude/skills/context-map/reference/Bounded Context/knowledgeLevel.md +4 -4
- package/template/.claude/skills/context-map/reference/Bounded Context/realizes.md +9 -9
- package/template/.claude/skills/context-map/reference/Bounded Context/refines.md +10 -10
- package/template/.claude/skills/context-map/reference/Bounded Context/responsibilities.md +26 -26
- package/template/.claude/skills/context-map/reference/Bounded Context/type.md +23 -23
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Anticorruption Layer.md +5 -5
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Bounded Context Relationship.md +12 -12
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Conformist.md +5 -5
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Customer-Supplier (C-S).md +22 -22
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Open Host Service.md +4 -4
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Partnership (P).md +13 -13
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Published Language.md +4 -4
- package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Shared Kernel (SK).md +12 -12
- package/template/.claude/skills/context-map/reference/Context Map.md +62 -62
- package/template/.claude/skills/context-map/reference/Domain/Domain.md +30 -30
- package/template/.claude/skills/context-map/reference/Domain/supports.md +33 -33
- package/template/.claude/skills/context-map/reference/Domain/type.md +3 -3
- package/template/.claude/skills/context-map/reference/Semantic Rules.md +32 -32
- package/template/.claude/skills/hallmark/SKILL.md +552 -552
- package/template/.claude/skills/hallmark/references/anti-patterns.md +412 -412
- package/template/.claude/skills/hallmark/references/assets.md +406 -406
- package/template/.claude/skills/hallmark/references/color.md +95 -95
- package/template/.claude/skills/hallmark/references/component-cookbook.md +256 -256
- package/template/.claude/skills/hallmark/references/components/c1-outlined-chip.md +12 -12
- package/template/.claude/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -16
- package/template/.claude/skills/hallmark/references/components/c3-typographic-link.md +8 -8
- package/template/.claude/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -16
- package/template/.claude/skills/hallmark/references/components/f1-bento-grid.md +20 -20
- package/template/.claude/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -20
- package/template/.claude/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -11
- package/template/.claude/skills/hallmark/references/components/f4-step-sequence.md +11 -11
- package/template/.claude/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -11
- package/template/.claude/skills/hallmark/references/components/f6-product-card-grid.md +41 -41
- package/template/.claude/skills/hallmark/references/components/ft1-mast-headed.md +13 -13
- package/template/.claude/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -10
- package/template/.claude/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -12
- package/template/.claude/skills/hallmark/references/components/ft4-dense-typographic.md +10 -10
- package/template/.claude/skills/hallmark/references/components/ft5-statement.md +21 -21
- package/template/.claude/skills/hallmark/references/components/ft6-letter-close.md +19 -19
- package/template/.claude/skills/hallmark/references/components/ft7-newsletter-first.md +27 -27
- package/template/.claude/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -25
- package/template/.claude/skills/hallmark/references/components/h1-marquee.md +15 -15
- package/template/.claude/skills/hallmark/references/components/h2-split-diptych.md +15 -15
- package/template/.claude/skills/hallmark/references/components/h3-quote-led.md +11 -11
- package/template/.claude/skills/hallmark/references/components/h4-stat-led.md +14 -14
- package/template/.claude/skills/hallmark/references/components/h5-letter-hero.md +11 -11
- package/template/.claude/skills/hallmark/references/components/h6-photographic-fold.md +16 -16
- package/template/.claude/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -27
- package/template/.claude/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -23
- package/template/.claude/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -27
- package/template/.claude/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -12
- package/template/.claude/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -19
- package/template/.claude/skills/hallmark/references/components/n2-floating-chip.md +14 -14
- package/template/.claude/skills/hallmark/references/components/n3-side-rail.md +14 -14
- package/template/.claude/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -9
- package/template/.claude/skills/hallmark/references/components/n5-floating-pill.md +28 -28
- package/template/.claude/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -24
- package/template/.claude/skills/hallmark/references/components/n7-brutal-slab.md +22 -22
- package/template/.claude/skills/hallmark/references/components/n8-terminal-command.md +21 -21
- package/template/.claude/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -17
- package/template/.claude/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -15
- package/template/.claude/skills/hallmark/references/components/s2-hanging.md +13 -13
- package/template/.claude/skills/hallmark/references/components/s3-sticky-pinned.md +19 -19
- package/template/.claude/skills/hallmark/references/components/s4-inline-no-break.md +11 -11
- package/template/.claude/skills/hallmark/references/components/s5-bottom-anchored.md +13 -13
- package/template/.claude/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -12
- package/template/.claude/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -19
- package/template/.claude/skills/hallmark/references/components/t3-single-huge-quote.md +11 -11
- package/template/.claude/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -14
- package/template/.claude/skills/hallmark/references/contract.md +24 -24
- package/template/.claude/skills/hallmark/references/copy.md +182 -182
- package/template/.claude/skills/hallmark/references/custom-craft.md +626 -626
- package/template/.claude/skills/hallmark/references/custom-theme.md +329 -329
- package/template/.claude/skills/hallmark/references/design-md.md +116 -116
- package/template/.claude/skills/hallmark/references/export-formats.md +328 -328
- package/template/.claude/skills/hallmark/references/floating-nav.md +89 -89
- package/template/.claude/skills/hallmark/references/genres/atmospheric.md +65 -65
- package/template/.claude/skills/hallmark/references/genres/editorial.md +70 -70
- package/template/.claude/skills/hallmark/references/genres/modern-minimal.md +67 -67
- package/template/.claude/skills/hallmark/references/genres/playful.md +65 -65
- package/template/.claude/skills/hallmark/references/hero-enrichment.md +474 -474
- package/template/.claude/skills/hallmark/references/imagery-kit.md +170 -170
- package/template/.claude/skills/hallmark/references/interaction-and-states.md +207 -207
- package/template/.claude/skills/hallmark/references/layout-and-space.md +111 -111
- package/template/.claude/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -35
- package/template/.claude/skills/hallmark/references/macrostructures/02-long-document.md +34 -34
- package/template/.claude/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -31
- package/template/.claude/skills/hallmark/references/macrostructures/04-stat-led.md +32 -32
- package/template/.claude/skills/hallmark/references/macrostructures/05-workbench.md +32 -32
- package/template/.claude/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -33
- package/template/.claude/skills/hallmark/references/macrostructures/07-manifesto.md +32 -32
- package/template/.claude/skills/hallmark/references/macrostructures/08-photographic.md +34 -34
- package/template/.claude/skills/hallmark/references/macrostructures/09-quote-led.md +32 -32
- package/template/.claude/skills/hallmark/references/macrostructures/10-specimen.md +32 -32
- package/template/.claude/skills/hallmark/references/macrostructures/11-catalogue.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/12-letter.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/13-index-first.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/15-split-studio.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures/21-component-playground.md +23 -23
- package/template/.claude/skills/hallmark/references/macrostructures.md +89 -89
- package/template/.claude/skills/hallmark/references/microinteractions.md +260 -260
- package/template/.claude/skills/hallmark/references/motion.md +109 -109
- package/template/.claude/skills/hallmark/references/preview-examples.md +49 -49
- package/template/.claude/skills/hallmark/references/responsive.md +138 -138
- package/template/.claude/skills/hallmark/references/slop-test.md +205 -205
- package/template/.claude/skills/hallmark/references/structure.md +164 -164
- package/template/.claude/skills/hallmark/references/study.md +511 -511
- package/template/.claude/skills/hallmark/references/typography.md +243 -243
- package/template/.claude/skills/hallmark/references/verbs/audit.md +25 -25
- package/template/.claude/skills/hallmark/references/verbs/redesign.md +269 -269
- package/template/.claude/skills/hallmark-loop/SKILL.md +105 -105
- package/template/.claude/skills/hallmark-loop/references/auditor-call.md +60 -60
- package/template/.claude/skills/hallmark-loop/references/capture.md +78 -78
- package/template/.claude/skills/hallmark-loop/references/loop-control.md +79 -79
- package/template/.claude/skills/handoff/SKILL.md +15 -15
- package/template/.claude/skills/knowledge-crunching/SKILL.md +94 -94
- package/template/.claude/skills/logical-components/.gitattributes +2 -0
- package/template/.claude/skills/logical-components/SKILL.md +102 -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.Packages.props +8 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +69 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +39 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +70 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +39 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +37 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +65 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +187 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +166 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +87 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +83 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +196 -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 +72 -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/MarkdownRenderer.cs +73 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +203 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +155 -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 +76 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +132 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +173 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +84 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +178 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +59 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +53 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +75 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +63 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +227 -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 +82 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +34 -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 +176 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +190 -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 +32 -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 +50 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +65 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +119 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +162 -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 +49 -0
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +227 -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 +87 -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/MultiTargetApp/MultiTargetApp.slnx +6 -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/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/RenderGolden/components.json +63 -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/research/SKILL.md +69 -69
- package/template/.claude/skills/tdd/SKILL.md +142 -142
- package/template/.claude/skills/tdd/deep-modules.md +15 -15
- package/template/.claude/skills/tdd/interface-design.md +31 -31
- package/template/.claude/skills/tdd/mocking.md +59 -59
- package/template/.claude/skills/tdd/refactoring.md +10 -10
- package/template/.claude/skills/tdd/tests.md +61 -61
- package/template/.claude/statusline.cjs +100 -37
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>Which member runs in an implementation when a member of a seam (interface or abstract class) is used.</summary>
|
|
6
|
+
public static class SeamDispatch
|
|
7
|
+
{
|
|
8
|
+
/// <summary>
|
|
9
|
+
/// The member that runs in <paramref name="implementation"/> when <paramref name="member"/> of
|
|
10
|
+
/// <paramref name="seam"/> (as constructed at the use) is used: the implementation of that exact construction of the
|
|
11
|
+
/// interface member (a class implementing <c>IHandler<Ping></c> and <c>IHandler<Pong></c> runs its Pong
|
|
12
|
+
/// member for a Pong use), else the one of an open construction; for an abstract class, the most derived override,
|
|
13
|
+
/// possibly the seam's own body.
|
|
14
|
+
/// </summary>
|
|
15
|
+
public static ISymbol? ImplementationOf(INamedTypeSymbol implementation, INamedTypeSymbol seam, ISymbol member)
|
|
16
|
+
{
|
|
17
|
+
var wanted = SymbolNames.MemberId(member);
|
|
18
|
+
if (seam.TypeKind == TypeKind.Interface)
|
|
19
|
+
{
|
|
20
|
+
var definition = SymbolNames.FullName(seam.OriginalDefinition);
|
|
21
|
+
var constructions = implementation.AllInterfaces
|
|
22
|
+
.Where(i => SymbolNames.FullName(i.OriginalDefinition) == definition)
|
|
23
|
+
.ToList();
|
|
24
|
+
var exact = constructions.Where(i => Display(i) == Display(seam)).ToList();
|
|
25
|
+
return (exact.Count > 0 ? exact : constructions)
|
|
26
|
+
.SelectMany(i => i.GetMembers(member.Name))
|
|
27
|
+
.Where(candidate => SymbolNames.MemberId(candidate) == wanted)
|
|
28
|
+
.Select(implementation.FindImplementationForInterfaceMember)
|
|
29
|
+
.FirstOrDefault(found => found is not null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (var type = implementation; type is not null; type = type.BaseType)
|
|
33
|
+
{
|
|
34
|
+
foreach (var candidate in type.GetMembers(member.Name))
|
|
35
|
+
{
|
|
36
|
+
for (var overridden = candidate; overridden is not null; overridden = Overridden(overridden))
|
|
37
|
+
{
|
|
38
|
+
if (SymbolNames.MemberId(overridden) == wanted)
|
|
39
|
+
{
|
|
40
|
+
return candidate;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private static ISymbol? Overridden(ISymbol symbol) => symbol switch
|
|
50
|
+
{
|
|
51
|
+
IMethodSymbol method => method.OverriddenMethod,
|
|
52
|
+
IPropertySymbol property => property.OverriddenProperty,
|
|
53
|
+
IEventSymbol @event => @event.OverriddenEvent,
|
|
54
|
+
_ => null,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Compared as text: symbols of the same type seen from two projects' compilations are not the same instance.
|
|
58
|
+
private static string Display(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>Stable textual names for symbols, independent of nullable annotations and of where the repo is checked out.</summary>
|
|
6
|
+
public static class SymbolNames
|
|
7
|
+
{
|
|
8
|
+
// No miscellaneous options: in particular no nullable '?' suffix, so IServiceCollection? == IServiceCollection.
|
|
9
|
+
private static readonly SymbolDisplayFormat Qualified = new(
|
|
10
|
+
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
|
|
11
|
+
|
|
12
|
+
/// <summary><c>Microsoft.Extensions.DependencyInjection.IServiceCollection</c>, whatever its nullability.</summary>
|
|
13
|
+
public static string QualifiedName(ITypeSymbol type) => type.ToDisplayString(Qualified);
|
|
14
|
+
|
|
15
|
+
/// <summary>
|
|
16
|
+
/// Full metadata name: namespace plus metadata name (generic arity kept, e.g. <c>Ns.Cache`1</c>). A nested type
|
|
17
|
+
/// carries its containing types (<c>Ns.Receipts+Builder</c>), so it can never collide with a top-level namesake.
|
|
18
|
+
/// </summary>
|
|
19
|
+
public static string FullName(INamedTypeSymbol type) =>
|
|
20
|
+
type.ContainingType is { } outer
|
|
21
|
+
? $"{FullName(outer)}+{type.MetadataName}"
|
|
22
|
+
: type.ContainingNamespace.IsGlobalNamespace
|
|
23
|
+
? type.MetadataName
|
|
24
|
+
: $"{type.ContainingNamespace.ToDisplayString()}.{type.MetadataName}";
|
|
25
|
+
|
|
26
|
+
/// <summary>
|
|
27
|
+
/// A member's identity across compilations (symbols of one member seen from two projects are not the same instance):
|
|
28
|
+
/// the documentation-comment id of its definition, e.g. <c>M:Ns.Handler`1.Handle(`0)</c>.
|
|
29
|
+
/// </summary>
|
|
30
|
+
public static string MemberId(ISymbol symbol) =>
|
|
31
|
+
symbol.OriginalDefinition.GetDocumentationCommentId() ?? symbol.OriginalDefinition.ToDisplayString();
|
|
32
|
+
|
|
33
|
+
/// <summary>A name safe as a diagram id: <c>Ns.Receipts+Cache`1</c> → <c>Ns_Receipts_Cache_1</c>.</summary>
|
|
34
|
+
public static string ToIdentifier(string fullName) =>
|
|
35
|
+
fullName.Replace('.', '_').Replace('+', '_').Replace('`', '_');
|
|
36
|
+
|
|
37
|
+
/// <summary>The simple name without arity: <c>Ns.Cache`1</c> → <c>Cache</c>, <c>Ns.Outer+Inner</c> → <c>Inner</c>.</summary>
|
|
38
|
+
public static string SimpleName(string fullName)
|
|
39
|
+
{
|
|
40
|
+
var name = fullName[(fullName.LastIndexOfAny(['.', '+']) + 1)..];
|
|
41
|
+
var tick = name.IndexOf('`');
|
|
42
|
+
return tick < 0 ? name : name[..tick];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// <summary>The full name as a reader writes it: <c>Ns.Cache`1</c> → <c>Ns.Cache<></c>, <c>`2</c> → <c><,></c>.</summary>
|
|
46
|
+
public static string ReadableFullName(string fullName)
|
|
47
|
+
{
|
|
48
|
+
var tick = fullName.LastIndexOf('`');
|
|
49
|
+
return tick < 0 || !int.TryParse(fullName[(tick + 1)..], out var arity)
|
|
50
|
+
? fullName
|
|
51
|
+
: $"{fullName[..tick]}<{new string(',', arity - 1)}>";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
using System.Collections.Immutable;
|
|
2
|
+
using Microsoft.CodeAnalysis;
|
|
3
|
+
|
|
4
|
+
namespace LogicalComponents;
|
|
5
|
+
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// What a generic helper's type parameters stand for at the use being followed. Walking <c>Dispatch.Send<T></c>
|
|
8
|
+
/// for <c>Send(new Ping())</c> maps <c>T</c> to <c>Ping</c>, so the <c>ICommandHandler<T></c> it calls is
|
|
9
|
+
/// resolved as <c>ICommandHandler<Ping></c> and reaches only the Ping handlers, not every handler.
|
|
10
|
+
/// </summary>
|
|
11
|
+
public sealed class TypeMap
|
|
12
|
+
{
|
|
13
|
+
private readonly Dictionary<ITypeParameterSymbol, ITypeSymbol> _map;
|
|
14
|
+
|
|
15
|
+
private TypeMap(Dictionary<ITypeParameterSymbol, ITypeSymbol> map)
|
|
16
|
+
{
|
|
17
|
+
_map = map;
|
|
18
|
+
Key = string.Join(";", map
|
|
19
|
+
.Select(p => $"{p.Key.ContainingSymbol.ToDisplayString()}.{p.Key.Name}={p.Value.ToDisplayString()}")
|
|
20
|
+
.Order(StringComparer.Ordinal));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public static TypeMap Empty { get; } = new(new Dictionary<ITypeParameterSymbol, ITypeSymbol>(SymbolEqualityComparer.Default));
|
|
24
|
+
|
|
25
|
+
/// <summary>Stable text of the bindings: the same member walked for two different bindings is two walks.</summary>
|
|
26
|
+
public string Key { get; }
|
|
27
|
+
|
|
28
|
+
/// <summary>The type with every mapped type parameter replaced, inside generic arguments too.</summary>
|
|
29
|
+
public ITypeSymbol Substitute(ITypeSymbol type) => type switch
|
|
30
|
+
{
|
|
31
|
+
ITypeParameterSymbol parameter when _map.TryGetValue(parameter, out var bound) => bound,
|
|
32
|
+
INamedTypeSymbol { IsGenericType: true } named when named.TypeArguments.Any(Mentions) =>
|
|
33
|
+
named.OriginalDefinition.Construct([.. named.TypeArguments.Select(Substitute)]),
|
|
34
|
+
_ => type,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/// <summary>
|
|
38
|
+
/// The map for walking <paramref name="member"/>'s own code: the type parameters of its definition and of its
|
|
39
|
+
/// containing types, bound to the arguments this use supplies (read through this map, for nested generic helpers).
|
|
40
|
+
/// </summary>
|
|
41
|
+
public TypeMap For(ISymbol member)
|
|
42
|
+
{
|
|
43
|
+
var map = new Dictionary<ITypeParameterSymbol, ITypeSymbol>(SymbolEqualityComparer.Default);
|
|
44
|
+
void Bind(ImmutableArray<ITypeParameterSymbol> parameters, ImmutableArray<ITypeSymbol> arguments)
|
|
45
|
+
{
|
|
46
|
+
for (var i = 0; i < parameters.Length && i < arguments.Length; i++)
|
|
47
|
+
{
|
|
48
|
+
var argument = Substitute(arguments[i]);
|
|
49
|
+
if (!SymbolEqualityComparer.Default.Equals(parameters[i], argument))
|
|
50
|
+
{
|
|
51
|
+
map[parameters[i]] = argument;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (member is IMethodSymbol method)
|
|
57
|
+
{
|
|
58
|
+
Bind(method.OriginalDefinition.TypeParameters, method.TypeArguments);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (var type = member.ContainingType; type is not null; type = type.ContainingType)
|
|
62
|
+
{
|
|
63
|
+
Bind(type.OriginalDefinition.TypeParameters, type.TypeArguments);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return map.Count == 0 ? Empty : new TypeMap(map);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private bool Mentions(ITypeSymbol type) => type switch
|
|
70
|
+
{
|
|
71
|
+
ITypeParameterSymbol parameter => _map.ContainsKey(parameter),
|
|
72
|
+
INamedTypeSymbol { IsGenericType: true } named => named.TypeArguments.Any(Mentions),
|
|
73
|
+
_ => false,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
3
|
+
|
|
4
|
+
namespace LogicalComponents;
|
|
5
|
+
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// The syntax that runs when a member is used, for <see cref="MemberUseResolution"/> to walk. Always in (file, position)
|
|
8
|
+
/// order, so the walk, and the first path it records, is the same on every checkout.
|
|
9
|
+
/// </summary>
|
|
10
|
+
public static class WalkRoots
|
|
11
|
+
{
|
|
12
|
+
/// <summary>The declaration of a member: its body, accessor or initializer, from the implementing half if it is partial.</summary>
|
|
13
|
+
public static List<SyntaxNode> Body(ISymbol member) =>
|
|
14
|
+
Ordered(Implementation(member.OriginalDefinition).DeclaringSyntaxReferences.Select(r => r.GetSyntax()));
|
|
15
|
+
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// What <c>new</c> runs: the constructor (for a primary constructor, only its parameter defaults and base call, never
|
|
18
|
+
/// the whole type) and the type's instance field and property initializers. <c>RunsImplicitBase</c> is set when the
|
|
19
|
+
/// constructor calls no <c>base(...)</c>/<c>this(...)</c> itself, so the base class's parameterless constructor runs.
|
|
20
|
+
/// </summary>
|
|
21
|
+
public static (List<SyntaxNode> Roots, bool RunsImplicitBase) Construction(IMethodSymbol constructor)
|
|
22
|
+
{
|
|
23
|
+
var roots = new List<SyntaxNode>();
|
|
24
|
+
var explicitChain = false;
|
|
25
|
+
foreach (var syntax in Implementation(constructor.OriginalDefinition).DeclaringSyntaxReferences.Select(r => r.GetSyntax()))
|
|
26
|
+
{
|
|
27
|
+
if (syntax is TypeDeclarationSyntax type)
|
|
28
|
+
{
|
|
29
|
+
var baseCall = type.BaseList?.Types.OfType<PrimaryConstructorBaseTypeSyntax>().FirstOrDefault();
|
|
30
|
+
explicitChain |= baseCall is not null;
|
|
31
|
+
roots.AddRange(new SyntaxNode?[] { type.ParameterList, baseCall }.OfType<SyntaxNode>());
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
{
|
|
35
|
+
explicitChain |= syntax is ConstructorDeclarationSyntax { Initializer: not null };
|
|
36
|
+
roots.Add(syntax);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
foreach (var member in constructor.ContainingType.OriginalDefinition.GetMembers().Where(m => !m.IsStatic))
|
|
41
|
+
{
|
|
42
|
+
roots.AddRange(member.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).Select(syntax => syntax switch
|
|
43
|
+
{
|
|
44
|
+
VariableDeclaratorSyntax { Initializer: { } initializer } => initializer,
|
|
45
|
+
PropertyDeclarationSyntax { Initializer: { } initializer } => initializer,
|
|
46
|
+
_ => null,
|
|
47
|
+
}).OfType<SyntaxNode>());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (Ordered(roots), !explicitChain);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A partial method, property or constructor is walked where its body is written.
|
|
54
|
+
private static ISymbol Implementation(ISymbol member) => member switch
|
|
55
|
+
{
|
|
56
|
+
IMethodSymbol { PartialImplementationPart: { } implementation } => implementation,
|
|
57
|
+
IPropertySymbol { PartialImplementationPart: { } implementation } => implementation,
|
|
58
|
+
_ => member,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
private static List<SyntaxNode> Ordered(IEnumerable<SyntaxNode> roots) =>
|
|
62
|
+
roots.OrderBy(r => r.SyntaxTree.FilePath, StringComparer.Ordinal).ThenBy(r => r.SpanStart).ToList();
|
|
63
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
public static async Task<LoadedWorkspace> LoadAsync(string repoPath, bool restore = true, TextWriter? log = null)
|
|
41
|
+
{
|
|
42
|
+
var repoRoot = Path.GetFullPath(repoPath);
|
|
43
|
+
if (!Directory.Exists(repoRoot))
|
|
44
|
+
{
|
|
45
|
+
throw new DirectoryNotFoundException($"Repository path not found: {repoRoot}");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
log ??= TextWriter.Null;
|
|
49
|
+
|
|
50
|
+
var solutionFile = FindSolutionFile(repoRoot);
|
|
51
|
+
if (solutionFile is not null)
|
|
52
|
+
{
|
|
53
|
+
var name = Path.GetFileName(solutionFile);
|
|
54
|
+
var loaded = await LoadAsync(repoRoot, name, [solutionFile], restore, log,
|
|
55
|
+
(workspace, file) => workspace.OpenSolutionAsync(file));
|
|
56
|
+
if (loaded.Solution.Projects.Any())
|
|
57
|
+
{
|
|
58
|
+
return loaded;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
loaded.Dispose();
|
|
62
|
+
log.WriteLine($"{name} contains no C# source projects; falling back to project files.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var (projectFiles, glob) = FindProjectFiles(repoRoot);
|
|
66
|
+
if (projectFiles.Count == 0)
|
|
67
|
+
{
|
|
68
|
+
throw new WorkspaceLoadException($"No solution and no C# source project found under {repoRoot}");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return await LoadAsync(repoRoot, glob, projectFiles, restore, log, async (workspace, file) =>
|
|
72
|
+
{
|
|
73
|
+
// Opening a project also loads its references; skip ones already pulled in that way.
|
|
74
|
+
if (workspace.CurrentSolution.Projects.All(p => !PathsEqual(p.FilePath, file)))
|
|
75
|
+
{
|
|
76
|
+
await workspace.OpenProjectAsync(file);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// <summary>Restores and opens each entry file, then reduces the result to source projects.</summary>
|
|
82
|
+
private static async Task<LoadedWorkspace> LoadAsync(
|
|
83
|
+
string repoRoot, string source, IReadOnlyList<string> entryFiles, bool restore, TextWriter log,
|
|
84
|
+
Func<MSBuildWorkspace, string, Task> open)
|
|
85
|
+
{
|
|
86
|
+
var workspace = MSBuildWorkspace.Create();
|
|
87
|
+
try
|
|
88
|
+
{
|
|
89
|
+
var failures = new List<string>();
|
|
90
|
+
workspace.RegisterWorkspaceFailedHandler(e =>
|
|
91
|
+
{
|
|
92
|
+
if (e.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure)
|
|
93
|
+
{
|
|
94
|
+
failures.Add(e.Diagnostic.Message);
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
{
|
|
98
|
+
log.WriteLine($"warning: {e.Diagnostic.Message}");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
foreach (var file in entryFiles)
|
|
103
|
+
{
|
|
104
|
+
if (restore)
|
|
105
|
+
{
|
|
106
|
+
await RestoreAsync(file, repoRoot);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await open(workspace, file);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (failures.Count > 0)
|
|
113
|
+
{
|
|
114
|
+
throw new WorkspaceLoadException(
|
|
115
|
+
$"Loading {source} failed:{Environment.NewLine} " + string.Join(Environment.NewLine + " ", failures));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new LoadedWorkspace(workspace, KeepSourceProjects(workspace.CurrentSolution, repoRoot), repoRoot, source);
|
|
119
|
+
}
|
|
120
|
+
catch (Exception e)
|
|
121
|
+
{
|
|
122
|
+
workspace.Dispose();
|
|
123
|
+
throw e is WorkspaceLoadException ? e : new WorkspaceLoadException($"Loading {source} failed: {e.Message}");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// <summary>
|
|
128
|
+
/// Drops non-C# and test projects, and keeps one project per multi-targeted project file: the highest target
|
|
129
|
+
/// framework, renamed without its <c>(tfm)</c> suffix, so every type is seen exactly once.
|
|
130
|
+
/// </summary>
|
|
131
|
+
private static Solution KeepSourceProjects(Solution solution, string repoRoot)
|
|
132
|
+
{
|
|
133
|
+
foreach (var byFile in solution.Projects.GroupBy(p => p.FilePath ?? "", StringComparer.OrdinalIgnoreCase))
|
|
134
|
+
{
|
|
135
|
+
var projects = byFile.ToList();
|
|
136
|
+
var isSource = byFile.Key != ""
|
|
137
|
+
&& projects[0].Language == LanguageNames.CSharp
|
|
138
|
+
&& !ProjectFilters.IsTestProject(Path.GetRelativePath(repoRoot, byFile.Key), File.ReadAllText(byFile.Key));
|
|
139
|
+
var kept = isSource
|
|
140
|
+
? projects.MaxBy(p => ProjectFilters.TargetFrameworkRank(p.Name))
|
|
141
|
+
: null;
|
|
142
|
+
|
|
143
|
+
foreach (var project in projects.Where(p => p != kept))
|
|
144
|
+
{
|
|
145
|
+
solution = solution.RemoveProject(project.Id);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (kept is not null)
|
|
149
|
+
{
|
|
150
|
+
solution = solution.WithProjectName(kept.Id, ProjectFilters.StripTargetFramework(kept.Name));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return solution;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private static string? FindSolutionFile(string repoRoot)
|
|
158
|
+
{
|
|
159
|
+
// Top-level only: a nested solution usually belongs to a sample or a vendored dependency.
|
|
160
|
+
var candidates = Directory.EnumerateFiles(repoRoot, "*.slnx")
|
|
161
|
+
.Concat(Directory.EnumerateFiles(repoRoot, "*.sln"))
|
|
162
|
+
.Order(StringComparer.Ordinal)
|
|
163
|
+
.ToList();
|
|
164
|
+
|
|
165
|
+
return candidates.Count switch
|
|
166
|
+
{
|
|
167
|
+
0 => null,
|
|
168
|
+
1 => candidates[0],
|
|
169
|
+
_ => throw new WorkspaceLoadException(
|
|
170
|
+
$"More than one solution in {repoRoot}: {string.Join(", ", candidates.Select(Path.GetFileName))}"),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// <summary>C# project files under <c>src/</c>, or under the repo root minus build output and dot folders.</summary>
|
|
175
|
+
private static (List<string> Files, string Glob) FindProjectFiles(string repoRoot)
|
|
176
|
+
{
|
|
177
|
+
var src = Path.Combine(repoRoot, "src");
|
|
178
|
+
var (searchRoot, glob) = Directory.Exists(src) ? (src, "src/**/*.csproj") : (repoRoot, "**/*.csproj");
|
|
179
|
+
var files = Directory
|
|
180
|
+
.EnumerateFiles(searchRoot, "*.csproj", SearchOption.AllDirectories)
|
|
181
|
+
.Where(file => !Path.GetRelativePath(repoRoot, file)
|
|
182
|
+
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
|
183
|
+
.Any(segment => segment is "bin" or "obj" || segment.StartsWith('.')))
|
|
184
|
+
.Where(file => !ProjectFilters.IsTestProject(Path.GetRelativePath(repoRoot, file), File.ReadAllText(file)))
|
|
185
|
+
.Order(StringComparer.Ordinal)
|
|
186
|
+
.ToList();
|
|
187
|
+
return (files, glob);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// <summary>
|
|
191
|
+
/// MSBuildWorkspace needs restored projects (project.assets.json) to resolve references. Runs from the repo root
|
|
192
|
+
/// so the target's own global.json picks the SDK.
|
|
193
|
+
/// </summary>
|
|
194
|
+
private static async Task RestoreAsync(string path, string repoRoot)
|
|
195
|
+
{
|
|
196
|
+
var start = new ProcessStartInfo("dotnet", ["restore", path, "--nologo", "-v", "q"])
|
|
197
|
+
{
|
|
198
|
+
WorkingDirectory = repoRoot,
|
|
199
|
+
RedirectStandardOutput = true,
|
|
200
|
+
RedirectStandardError = true,
|
|
201
|
+
};
|
|
202
|
+
using var process = Process.Start(start)
|
|
203
|
+
?? throw new WorkspaceLoadException("Could not start `dotnet restore`.");
|
|
204
|
+
var stdout = process.StandardOutput.ReadToEndAsync();
|
|
205
|
+
var stderr = process.StandardError.ReadToEndAsync();
|
|
206
|
+
|
|
207
|
+
using var timeout = new CancellationTokenSource(RestoreTimeout);
|
|
208
|
+
try
|
|
209
|
+
{
|
|
210
|
+
await process.WaitForExitAsync(timeout.Token);
|
|
211
|
+
}
|
|
212
|
+
catch (OperationCanceledException)
|
|
213
|
+
{
|
|
214
|
+
process.Kill(entireProcessTree: true);
|
|
215
|
+
throw new WorkspaceLoadException($"`dotnet restore {path}` did not finish within {RestoreTimeout.TotalMinutes} minutes.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (process.ExitCode != 0)
|
|
219
|
+
{
|
|
220
|
+
throw new WorkspaceLoadException(
|
|
221
|
+
$"`dotnet restore {path}` failed ({process.ExitCode}):{Environment.NewLine}{await stdout}{await stderr}");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private static bool PathsEqual(string? a, string b) =>
|
|
226
|
+
a is not null && string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
|
|
227
|
+
}
|
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,82 @@
|
|
|
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 Batch_files_are_written_identically_every_time()
|
|
62
|
+
{
|
|
63
|
+
var first = Directory.CreateTempSubdirectory("lc-batches-");
|
|
64
|
+
var second = Directory.CreateTempSubdirectory("lc-batches-");
|
|
65
|
+
try
|
|
66
|
+
{
|
|
67
|
+
BatchPlanning.Write(BatchPlanning.Plan(WithComponents(20), 9), first.FullName);
|
|
68
|
+
BatchPlanning.Write(BatchPlanning.Plan(WithComponents(20), 9), second.FullName);
|
|
69
|
+
|
|
70
|
+
var names = Directory.GetFiles(first.FullName).Select(Path.GetFileName).Order().ToList();
|
|
71
|
+
Assert.Equal(Enumerable.Range(1, 7).Select(i => $"batch-{i:D2}.json"), names);
|
|
72
|
+
Assert.All(names, n => Assert.Equal(
|
|
73
|
+
File.ReadAllBytes(Path.Combine(first.FullName, n!)),
|
|
74
|
+
File.ReadAllBytes(Path.Combine(second.FullName, n!))));
|
|
75
|
+
}
|
|
76
|
+
finally
|
|
77
|
+
{
|
|
78
|
+
first.Delete(recursive: true);
|
|
79
|
+
second.Delete(recursive: true);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
}
|