@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
package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
namespace LogicalComponents;
|
|
2
|
+
|
|
3
|
+
/// <summary>
|
|
4
|
+
/// <c>extract <repoPath> [--out <components.json>]</c>: loads the repo and writes the deterministic extraction.
|
|
5
|
+
/// Progress and warnings go to <c>errors</c> (stderr); the artifact goes to the --out file.
|
|
6
|
+
/// Exit codes: 0 written, 1 usage, 2 the target repo or its config could not be loaded, 4 candidates not classified
|
|
7
|
+
/// in the repo config yet (one <c>unclassified <type></c> line each; nothing is written).
|
|
8
|
+
/// </summary>
|
|
9
|
+
public static class ExtractCommand
|
|
10
|
+
{
|
|
11
|
+
public const string Usage = "usage: LogicalComponents extract <repoPath> [--out <components.json>]";
|
|
12
|
+
|
|
13
|
+
public static async Task<int> RunAsync(string[] args, TextWriter errors)
|
|
14
|
+
{
|
|
15
|
+
if (args is not [var repoPath, ..] || !TryParseOut(args[1..], out var outPath))
|
|
16
|
+
{
|
|
17
|
+
await errors.WriteLineAsync(Usage);
|
|
18
|
+
return 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try
|
|
22
|
+
{
|
|
23
|
+
using var loaded = await WorkspaceLoader.LoadAsync(repoPath, log: errors);
|
|
24
|
+
await errors.WriteLineAsync($"Loaded {loaded.ProjectNames.Count} projects from {loaded.Source}.");
|
|
25
|
+
|
|
26
|
+
var result = await CouplingResolver.ExtractAsync(loaded, ExtractConfig.Load(loaded.RepoRoot));
|
|
27
|
+
foreach (var warning in result.Warnings)
|
|
28
|
+
{
|
|
29
|
+
await errors.WriteLineAsync($"warning: {warning}");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outPath))!);
|
|
33
|
+
await File.WriteAllBytesAsync(outPath, ComponentsFile.Serialize(result));
|
|
34
|
+
await errors.WriteLineAsync(
|
|
35
|
+
$"{result.Components.Count} components, {result.ExternalNodes.Count} ports, {result.Edges.Count} edges, "
|
|
36
|
+
+ $"{result.Warnings.Count} warnings -> {Path.GetFullPath(outPath)}");
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
catch (UnclassifiedCandidatesException e)
|
|
40
|
+
{
|
|
41
|
+
foreach (var type in e.Types)
|
|
42
|
+
{
|
|
43
|
+
await errors.WriteLineAsync($"unclassified {type}");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await errors.WriteLineAsync($"{e.Message}: run the classify step, review it, then extract again.");
|
|
47
|
+
return 4;
|
|
48
|
+
}
|
|
49
|
+
catch (Exception e) when (e is DirectoryNotFoundException or WorkspaceLoadException or ExtractConfigException)
|
|
50
|
+
{
|
|
51
|
+
await errors.WriteLineAsync(e.Message);
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
catch (Exception e)
|
|
55
|
+
{
|
|
56
|
+
// Anything else (I/O, access, build host) is still a load failure: report it, don't crash with a stack trace.
|
|
57
|
+
await errors.WriteLineAsync($"Unexpected error while extracting {repoPath}: {e.Message}");
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private static bool TryParseOut(string[] options, out string outPath)
|
|
63
|
+
{
|
|
64
|
+
outPath = "components.json";
|
|
65
|
+
return options switch
|
|
66
|
+
{
|
|
67
|
+
[] => true,
|
|
68
|
+
["--out", var path] => (outPath = path) is not null,
|
|
69
|
+
_ => false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
using System.Text.Json;
|
|
2
|
+
using System.Text.Json.Serialization;
|
|
3
|
+
|
|
4
|
+
namespace LogicalComponents;
|
|
5
|
+
|
|
6
|
+
/// <summary>A config file in the target repo that could not be read, or contradicts itself.</summary>
|
|
7
|
+
public sealed class ExtractConfigException(string message) : Exception(message);
|
|
8
|
+
|
|
9
|
+
/// <summary>Candidates the repo config does not classify yet: extract refuses to guess.</summary>
|
|
10
|
+
public sealed class UnclassifiedCandidatesException(IReadOnlyList<string> types)
|
|
11
|
+
: Exception($"{types.Count} candidate(s) not classified in {ExtractConfig.FileName}")
|
|
12
|
+
{
|
|
13
|
+
public IReadOnlyList<string> Types { get; } = types;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// <summary>
|
|
17
|
+
/// The frozen classification in <c>logical-components.json</c> at the repo root: every candidate class is listed, by
|
|
18
|
+
/// full metadata name, either as a component or as a helper (with the reason it is not a component). An LLM proposes
|
|
19
|
+
/// the entries once and a person reviews them; extract only ever reads this file, so the same code and the same file
|
|
20
|
+
/// always give the same components.
|
|
21
|
+
/// </summary>
|
|
22
|
+
public sealed record ExtractConfig(IReadOnlyList<string> Components, IReadOnlyDictionary<string, string> Helpers)
|
|
23
|
+
{
|
|
24
|
+
public const string FileName = "logical-components.json";
|
|
25
|
+
|
|
26
|
+
public static ExtractConfig Empty { get; } = new([], new Dictionary<string, string>());
|
|
27
|
+
|
|
28
|
+
private static readonly JsonSerializerOptions Options = new()
|
|
29
|
+
{
|
|
30
|
+
PropertyNameCaseInsensitive = true,
|
|
31
|
+
ReadCommentHandling = JsonCommentHandling.Skip,
|
|
32
|
+
AllowTrailingCommas = true,
|
|
33
|
+
// A misspelt key ("helper") must fail rather than silently classify nothing.
|
|
34
|
+
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
public bool IsClassified(string fullName) => Helpers.ContainsKey(fullName) || Components.Contains(fullName);
|
|
38
|
+
|
|
39
|
+
public static ExtractConfig Load(string repoRoot)
|
|
40
|
+
{
|
|
41
|
+
var path = Path.Combine(repoRoot, FileName);
|
|
42
|
+
if (!File.Exists(path))
|
|
43
|
+
{
|
|
44
|
+
return Empty;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ConfigFile? file;
|
|
48
|
+
try
|
|
49
|
+
{
|
|
50
|
+
file = JsonSerializer.Deserialize<ConfigFile>(File.ReadAllText(path), Options);
|
|
51
|
+
}
|
|
52
|
+
catch (JsonException e)
|
|
53
|
+
{
|
|
54
|
+
throw new ExtractConfigException($"Invalid {FileName}: {e.Message}");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (file?.Exclude is not null || file?.Include is not null)
|
|
58
|
+
{
|
|
59
|
+
throw new ExtractConfigException(
|
|
60
|
+
$"Invalid {FileName}: 'exclude'/'include' were replaced by 'components' and 'helpers' "
|
|
61
|
+
+ "(full type names); re-run the classify step to regenerate it.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var components = file?.Components ?? [];
|
|
65
|
+
var helpers = file?.Helpers ?? [];
|
|
66
|
+
var both = components.Where(helpers.ContainsKey).Distinct().Order(StringComparer.Ordinal).ToList();
|
|
67
|
+
if (both.Count > 0)
|
|
68
|
+
{
|
|
69
|
+
throw new ExtractConfigException(
|
|
70
|
+
$"Invalid {FileName}: {string.Join(", ", both.Select(n => $"'{n}'"))} is in both components and helpers.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return new ExtractConfig(components, helpers);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Exclude and Include are read only to reject a file written for the old format with a useful message.
|
|
77
|
+
private sealed record ConfigFile(
|
|
78
|
+
string[]? Components, Dictionary<string, string>? Helpers, string[]? Exclude, string[]? Include);
|
|
79
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
|
+
|
|
3
|
+
<PropertyGroup>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
|
+
<OutputType>Exe</OutputType>
|
|
6
|
+
<Nullable>enable</Nullable>
|
|
7
|
+
<ImplicitUsings>enable</ImplicitUsings>
|
|
8
|
+
</PropertyGroup>
|
|
9
|
+
|
|
10
|
+
<ItemGroup>
|
|
11
|
+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.9.0" />
|
|
12
|
+
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.9.0" />
|
|
13
|
+
</ItemGroup>
|
|
14
|
+
|
|
15
|
+
</Project>
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
using System.Text;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// Writes the final document: <c># Logical Components</c> (one section per component, sorted by display name, each
|
|
7
|
+
/// responsibility with its citation) and <c># Coupling</c> (a legend, then a Mermaid flowchart of the extracted edges).
|
|
8
|
+
/// Everything but the responsibility text comes from the deterministic extraction; the output has LF line endings and no
|
|
9
|
+
/// timestamp.
|
|
10
|
+
/// Call only with responsibilities that passed <see cref="ResponsibilityValidation"/>.
|
|
11
|
+
/// </summary>
|
|
12
|
+
public static class MarkdownRenderer
|
|
13
|
+
{
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// How to read the diagram, so incoming and outgoing arrows are not read backwards, and what it leaves out, so an
|
|
16
|
+
/// entry point with no incoming arrow is not mistaken for dead code.
|
|
17
|
+
/// </summary>
|
|
18
|
+
private const string Legend =
|
|
19
|
+
"`A --> B` means A depends on B: it calls, creates, holds or inherits from B. A component's incoming arrows are its "
|
|
20
|
+
+ "afferent coupling, its outgoing arrows its efferent coupling. Only components in `*.Application` and "
|
|
21
|
+
+ "`*.Infrastructure` projects are drawn; callers in other projects, such as the host, are not shown.";
|
|
22
|
+
|
|
23
|
+
public static string Render(
|
|
24
|
+
ExtractResult extraction, IReadOnlyDictionary<string, IReadOnlyList<Responsibility>> responsibilities)
|
|
25
|
+
{
|
|
26
|
+
var md = new StringBuilder();
|
|
27
|
+
md.Append("# Logical Components\n\n");
|
|
28
|
+
foreach (var component in extraction.Components
|
|
29
|
+
.OrderBy(c => c.Name, StringComparer.Ordinal)
|
|
30
|
+
.ThenBy(c => c.Id, StringComparer.Ordinal))
|
|
31
|
+
{
|
|
32
|
+
md.Append($"## {component.Name}\n\n");
|
|
33
|
+
md.Append($"`{SymbolNames.ReadableFullName(component.Type)}`\n\n");
|
|
34
|
+
foreach (var responsibility in responsibilities[component.Id])
|
|
35
|
+
{
|
|
36
|
+
md.Append($"- {responsibility.Text.Trim()} (`{Citation.Parse(responsibility.Cite)}`)\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
md.Append('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ELK lays out a large flowchart far more readably than Mermaid's default dagre.
|
|
43
|
+
md.Append("# Coupling\n\n").Append(Legend).Append("\n\n```mermaid\n%%{init: {\"layout\": \"elk\"}}%%\nflowchart LR\n");
|
|
44
|
+
var nodes = extraction.Components
|
|
45
|
+
.Select(c => (c.Id, c.Name))
|
|
46
|
+
.Concat(extraction.ExternalNodes.Select(n => (n.Id, n.Name)))
|
|
47
|
+
.OrderBy(n => n.Id, StringComparer.Ordinal);
|
|
48
|
+
foreach (var (id, name) in nodes)
|
|
49
|
+
{
|
|
50
|
+
md.Append($" {NodeId(id)}[\"{Escape(name)}\"]\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// A --> B: A depends on B (efferent for A, afferent for B). How A reaches B (di, new, static, inherits) is not
|
|
54
|
+
// drawn; it stays in components.json.
|
|
55
|
+
foreach (var edge in extraction.Edges)
|
|
56
|
+
{
|
|
57
|
+
md.Append($" {NodeId(edge.From)} --> {NodeId(edge.To)}\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
md.Append("```\n");
|
|
61
|
+
return md.ToString();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Mermaid node labels are double-quoted; a literal quote is written as an entity.
|
|
65
|
+
private static string Escape(string label) => label.Replace("\"", "#quot;");
|
|
66
|
+
|
|
67
|
+
/// <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 a
|
|
69
|
+
/// keyword, and reduced to letters, digits and underscores so a module name with spaces cannot break a line.
|
|
70
|
+
/// </summary>
|
|
71
|
+
private static string NodeId(string id) =>
|
|
72
|
+
"n_" + new string(id.Select(c => char.IsAsciiLetterOrDigit(c) ? c : '_').ToArray());
|
|
73
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
using System.Collections.Immutable;
|
|
2
|
+
using Microsoft.CodeAnalysis;
|
|
3
|
+
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
4
|
+
|
|
5
|
+
namespace LogicalComponents;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Follows what a component's code uses until it lands on another component. A use of a member of:
|
|
9
|
+
/// <list type="bullet">
|
|
10
|
+
/// <item>a component is an edge to it;</item>
|
|
11
|
+
/// <item>an interface or abstract class (a seam) goes on to the member that runs in each implementation of the
|
|
12
|
+
/// construction used (see <see cref="SeamDispatch"/>);</item>
|
|
13
|
+
/// <item>any other type of the repo (a helper, a record, a non-candidate) is seen through: the code that runs for that
|
|
14
|
+
/// use is walked the same way, with the helper's type parameters bound to the use's type arguments (see
|
|
15
|
+
/// <see cref="TypeMap"/>). It is walked per member and per accessor, so reading one property never couples to what
|
|
16
|
+
/// another member or the setter calls, and compiler-written record members run nothing.</item>
|
|
17
|
+
/// </list>
|
|
18
|
+
/// Framework and library members end the walk. The kind of an edge is the kind of the last use; its citation is the
|
|
19
|
+
/// line in the component where the chain starts; <c>via</c> names the seams and seen-through types on the first path
|
|
20
|
+
/// found (members already walked for a component are not walked again). The helper code walked is the component's
|
|
21
|
+
/// <c>reaches</c>.
|
|
22
|
+
/// </summary>
|
|
23
|
+
public sealed class MemberUseResolution(LoadedWorkspace loaded, IReadOnlySet<string> components, PortResolution ports)
|
|
24
|
+
{
|
|
25
|
+
public sealed record RawEdge(string From, string To, string Kind, IReadOnlyList<string> Via, string File, int Line);
|
|
26
|
+
|
|
27
|
+
/// <summary>The edges a component's code leads to, and the helper code it runs on the way (sorted, distinct).</summary>
|
|
28
|
+
public sealed record Resolution(List<RawEdge> Edges, List<SourceSpan> Reaches);
|
|
29
|
+
|
|
30
|
+
// Shared by every component's walk: what a piece of syntax uses never changes within one loaded solution.
|
|
31
|
+
private readonly Dictionary<SyntaxNode, List<MemberUses.MemberUse>> _uses = [];
|
|
32
|
+
private readonly Dictionary<INamedTypeSymbol, bool> _repoSource = new(SymbolEqualityComparer.Default);
|
|
33
|
+
|
|
34
|
+
/// <summary>
|
|
35
|
+
/// 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.
|
|
37
|
+
/// </summary>
|
|
38
|
+
public async Task<Resolution> ResolveAsync(
|
|
39
|
+
string from, IReadOnlySet<string> self, IEnumerable<MemberUses.MemberUse> uses, CancellationToken cancellationToken)
|
|
40
|
+
{
|
|
41
|
+
var walk = new Walk(from, self);
|
|
42
|
+
foreach (var use in uses)
|
|
43
|
+
{
|
|
44
|
+
await ResolveUseAsync(use.Member, use.Kind, [], use.File, use.Line, TypeMap.Empty, walk, cancellationToken);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Resolution(
|
|
48
|
+
walk.Edges,
|
|
49
|
+
walk.Reaches.OrderBy(s => s.File, StringComparer.Ordinal).ThenBy(s => s.Start).ThenBy(s => s.End).ToList());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// One component's walk: members already seen through (for the same type bindings) are not walked again, which also
|
|
53
|
+
// ends cycles.
|
|
54
|
+
private sealed class Walk(string from, IReadOnlySet<string> self)
|
|
55
|
+
{
|
|
56
|
+
public string From { get; } = from;
|
|
57
|
+
public IReadOnlySet<string> Self { get; } = self;
|
|
58
|
+
public HashSet<string> Visited { get; } = new(StringComparer.Ordinal);
|
|
59
|
+
public List<RawEdge> Edges { get; } = [];
|
|
60
|
+
public HashSet<SourceSpan> Reaches { get; } = [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async Task ResolveUseAsync(
|
|
64
|
+
ISymbol member,
|
|
65
|
+
string kind,
|
|
66
|
+
ImmutableList<string> via,
|
|
67
|
+
string file,
|
|
68
|
+
int line,
|
|
69
|
+
TypeMap map,
|
|
70
|
+
Walk walk,
|
|
71
|
+
CancellationToken cancellationToken)
|
|
72
|
+
{
|
|
73
|
+
// A C# 14 extension member belongs to the static class around its extension block.
|
|
74
|
+
var declaring = member.ContainingType is { IsExtension: true } block ? block.ContainingType : member.ContainingType;
|
|
75
|
+
if (declaring is null)
|
|
76
|
+
{
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (map.Substitute(declaring) is INamedTypeSymbol bound)
|
|
81
|
+
{
|
|
82
|
+
declaring = bound;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var owner = EdgeExtraction.Normalize(declaring.OriginalDefinition);
|
|
86
|
+
if (!IsRepo(owner) || walk.Self.Contains(SymbolNames.FullName(owner)))
|
|
87
|
+
{
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (components.Contains(SymbolNames.FullName(owner)))
|
|
92
|
+
{
|
|
93
|
+
walk.Edges.Add(new RawEdge(walk.From, SymbolNames.FullName(owner), kind, via, file, line));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (EdgeExtraction.IsSeam(declaring) && !member.IsStatic && kind != EdgeExtraction.New)
|
|
98
|
+
{
|
|
99
|
+
foreach (var implementation in await ports.ImplementationsAsync(declaring, cancellationToken))
|
|
100
|
+
{
|
|
101
|
+
if (SeamDispatch.ImplementationOf(implementation, declaring, member) is not { } runs)
|
|
102
|
+
{
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// An inherited default body lives on the seam itself: walk it rather than resolve the seam again.
|
|
107
|
+
if (runs.ContainingType is { } runsIn && EdgeExtraction.IsSeam(runsIn))
|
|
108
|
+
{
|
|
109
|
+
await SeeThroughAsync(runs, kind, via.Add(declaring.Name), file, line, map, walk, cancellationToken);
|
|
110
|
+
}
|
|
111
|
+
else
|
|
112
|
+
{
|
|
113
|
+
await ResolveUseAsync(runs, kind, via.Add(declaring.Name), file, line, TypeMap.Empty, walk, cancellationToken);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await SeeThroughAsync(member, kind, via.Add(owner.Name), file, line, map, walk, cancellationToken);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async Task SeeThroughAsync(
|
|
124
|
+
ISymbol member,
|
|
125
|
+
string kind,
|
|
126
|
+
ImmutableList<string> via,
|
|
127
|
+
string file,
|
|
128
|
+
int line,
|
|
129
|
+
TypeMap map,
|
|
130
|
+
Walk walk,
|
|
131
|
+
CancellationToken cancellationToken)
|
|
132
|
+
{
|
|
133
|
+
// Compiler-written members (a record's ToString/Equals/Deconstruct, an auto-property accessor) run nothing of the
|
|
134
|
+
// type's own code; their declaring syntax is the whole type, which must not be walked. Constructors still run
|
|
135
|
+
// initializers. Using an instance field runs no code: its initializer ran when the object was constructed.
|
|
136
|
+
if ((member.IsImplicitlyDeclared && kind != EdgeExtraction.New) || member is IFieldSymbol { IsStatic: false })
|
|
137
|
+
{
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
var inner = map.For(member);
|
|
142
|
+
var key = (kind == EdgeExtraction.New ? "new " : "") + SymbolNames.MemberId(member) + "|" + inner.Key;
|
|
143
|
+
if (!walk.Visited.Add(key))
|
|
144
|
+
{
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var (roots, runsImplicitBase) = kind == EdgeExtraction.New && member is IMethodSymbol constructor
|
|
149
|
+
? WalkRoots.Construction(constructor)
|
|
150
|
+
: (WalkRoots.Body(member), false);
|
|
151
|
+
Record(roots, walk);
|
|
152
|
+
|
|
153
|
+
foreach (var root in roots)
|
|
154
|
+
{
|
|
155
|
+
foreach (var use in await UsesAsync(root, cancellationToken))
|
|
156
|
+
{
|
|
157
|
+
await ResolveUseAsync(use.Member, use.Kind, via, file, line, inner, walk, cancellationToken);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (runsImplicitBase
|
|
162
|
+
&& member.ContainingType.BaseType?.InstanceConstructors.FirstOrDefault(c => c.Parameters.Length == 0) is { } baseConstructor)
|
|
163
|
+
{
|
|
164
|
+
await ResolveUseAsync(baseConstructor, EdgeExtraction.New, via, file, line, inner, walk, cancellationToken);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The walked helper code becomes citable for the component; a parameter declares a value and runs nothing.
|
|
169
|
+
private void Record(IEnumerable<SyntaxNode> roots, Walk walk)
|
|
170
|
+
{
|
|
171
|
+
foreach (var root in roots.Where(r => r is not (ParameterSyntax or ParameterListSyntax)))
|
|
172
|
+
{
|
|
173
|
+
var (sourceKind, relative) = CitableSource.Classify(loaded, root.SyntaxTree);
|
|
174
|
+
if (sourceKind == CitableSource.Kind.InRepo)
|
|
175
|
+
{
|
|
176
|
+
var lines = root.GetLocation().GetLineSpan();
|
|
177
|
+
walk.Reaches.Add(new SourceSpan(relative, lines.StartLinePosition.Line + 1, lines.EndLinePosition.Line + 1));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async Task<List<MemberUses.MemberUse>> UsesAsync(SyntaxNode root, CancellationToken cancellationToken)
|
|
183
|
+
{
|
|
184
|
+
if (!_uses.TryGetValue(root, out var uses))
|
|
185
|
+
{
|
|
186
|
+
uses = await MemberUses.ReadAsync([root], loaded, cancellationToken);
|
|
187
|
+
_uses[root] = uses;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return uses;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private bool IsRepo(INamedTypeSymbol type)
|
|
194
|
+
{
|
|
195
|
+
if (!_repoSource.TryGetValue(type, out var isRepo))
|
|
196
|
+
{
|
|
197
|
+
isRepo = CitableSource.IsRepoSource(type, loaded);
|
|
198
|
+
_repoSource[type] = isRepo;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return isRepo;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
using Microsoft.CodeAnalysis.CSharp;
|
|
3
|
+
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
4
|
+
|
|
5
|
+
namespace LogicalComponents;
|
|
6
|
+
|
|
7
|
+
/// <summary>
|
|
8
|
+
/// Reads what a piece of code uses, as members: <c>new</c> (the constructor an object creation, a <c>: this(...)</c> /
|
|
9
|
+
/// <c>: base(...)</c> initializer or a primary-constructor base call runs), <c>static</c> (a static member, an extension
|
|
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 a <c>foreach</c> runs). A property or indexer use is the accessor that
|
|
12
|
+
/// actually runs: a read is the getter, an assignment the setter, a compound assignment or increment both. Only
|
|
13
|
+
/// citable source is read, so generator output never contributes. What a use couples to is decided by
|
|
14
|
+
/// <see cref="MemberUseResolution"/>.
|
|
15
|
+
/// </summary>
|
|
16
|
+
public static class MemberUses
|
|
17
|
+
{
|
|
18
|
+
/// <summary>One use of <see cref="Member"/>, at a repo-relative file and 1-based line.</summary>
|
|
19
|
+
public sealed record MemberUse(ISymbol Member, string Kind, string File, int Line);
|
|
20
|
+
|
|
21
|
+
/// <summary>Every member use inside the given syntax nodes, in source order. Nodes outside the repo are skipped.</summary>
|
|
22
|
+
public static async Task<List<MemberUse>> ReadAsync(
|
|
23
|
+
IEnumerable<SyntaxNode> roots, LoadedWorkspace loaded, CancellationToken cancellationToken)
|
|
24
|
+
{
|
|
25
|
+
var uses = new List<MemberUse>();
|
|
26
|
+
foreach (var root in roots)
|
|
27
|
+
{
|
|
28
|
+
var (kind, file) = CitableSource.Classify(loaded, root.SyntaxTree);
|
|
29
|
+
if (kind != CitableSource.Kind.InRepo || loaded.Solution.GetDocument(root.SyntaxTree) is not { } document)
|
|
30
|
+
{
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var model = await document.GetSemanticModelAsync(cancellationToken)
|
|
35
|
+
?? throw new InvalidOperationException($"No semantic model for {file}");
|
|
36
|
+
foreach (var node in root.DescendantNodesAndSelf())
|
|
37
|
+
{
|
|
38
|
+
var line = node.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
|
|
39
|
+
uses.AddRange(Uses(node, model, cancellationToken).Select(u => new MemberUse(u.Member, u.Kind, file, line)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return uses;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private static IEnumerable<(ISymbol Member, string Kind)> Uses(SyntaxNode node, SemanticModel model, CancellationToken token)
|
|
47
|
+
{
|
|
48
|
+
ISymbol? Symbol() => model.GetSymbolInfo(node, token).Symbol;
|
|
49
|
+
|
|
50
|
+
switch (node)
|
|
51
|
+
{
|
|
52
|
+
case BaseObjectCreationExpressionSyntax or ConstructorInitializerSyntax or PrimaryConstructorBaseTypeSyntax:
|
|
53
|
+
if (Symbol() is IMethodSymbol constructor)
|
|
54
|
+
{
|
|
55
|
+
yield return (constructor, EdgeExtraction.New);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
break;
|
|
59
|
+
case SimpleNameSyntax name when !IsInsideNameOf(name, model):
|
|
60
|
+
foreach (var use in Classify(Symbol(), name))
|
|
61
|
+
{
|
|
62
|
+
yield return use;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
break;
|
|
66
|
+
case ElementAccessExpressionSyntax when Symbol() is IPropertySymbol indexer:
|
|
67
|
+
foreach (var accessor in AccessorsThatRun(indexer, node))
|
|
68
|
+
{
|
|
69
|
+
yield return (accessor, EdgeExtraction.Call);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
break;
|
|
73
|
+
case BinaryExpressionSyntax or PrefixUnaryExpressionSyntax or PostfixUnaryExpressionSyntax
|
|
74
|
+
or CastExpressionSyntax or AssignmentExpressionSyntax
|
|
75
|
+
when Symbol() is IMethodSymbol { MethodKind: MethodKind.UserDefinedOperator or MethodKind.Conversion } op:
|
|
76
|
+
yield return (op, EdgeExtraction.Static);
|
|
77
|
+
break;
|
|
78
|
+
case CommonForEachStatementSyntax loop when model.GetForEachStatementInfo(loop).GetEnumeratorMethod is { } enumerator:
|
|
79
|
+
yield return (enumerator, enumerator.IsStatic ? EdgeExtraction.Static : EdgeExtraction.Call);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// <summary>
|
|
85
|
+
/// A symbol a name refers to, as uses: static or extension members are <c>static</c> (a classic extension method is
|
|
86
|
+
/// seen as its unreduced static form), other members are <c>call</c>, a property is the accessors that run. Locals,
|
|
87
|
+
/// parameters, types, namespaces and consts (copied into the caller at compile time) are not uses.
|
|
88
|
+
/// </summary>
|
|
89
|
+
private static IEnumerable<(ISymbol, string)> Classify(ISymbol? symbol, SyntaxNode usage)
|
|
90
|
+
{
|
|
91
|
+
var kind = symbol is not null && (symbol.IsStatic || symbol.ContainingType is { IsExtension: true })
|
|
92
|
+
? EdgeExtraction.Static
|
|
93
|
+
: EdgeExtraction.Call;
|
|
94
|
+
switch (symbol)
|
|
95
|
+
{
|
|
96
|
+
case IFieldSymbol { IsConst: true }:
|
|
97
|
+
case IMethodSymbol { MethodKind: MethodKind.LocalFunction or MethodKind.AnonymousFunction }:
|
|
98
|
+
break;
|
|
99
|
+
case IMethodSymbol { ReducedFrom: { } extension }:
|
|
100
|
+
yield return (extension, EdgeExtraction.Static);
|
|
101
|
+
break;
|
|
102
|
+
case IPropertySymbol property:
|
|
103
|
+
foreach (var accessor in AccessorsThatRun(property, usage))
|
|
104
|
+
{
|
|
105
|
+
yield return (accessor, kind);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
break;
|
|
109
|
+
case IMethodSymbol or IFieldSymbol or IEventSymbol:
|
|
110
|
+
yield return (symbol, kind);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// <summary>
|
|
116
|
+
/// The accessors a property or indexer use runs, from where it stands: the target of <c>=</c> (including an object
|
|
117
|
+
/// initializer) runs the setter; a compound assignment, <c>++</c>/<c>--</c>, or a <c>ref</c>/<c>out</c> argument runs
|
|
118
|
+
/// both; anything else is a read and runs the getter. An accessor the property does not have runs nothing.
|
|
119
|
+
/// </summary>
|
|
120
|
+
private static IEnumerable<IMethodSymbol> AccessorsThatRun(IPropertySymbol property, SyntaxNode usage)
|
|
121
|
+
{
|
|
122
|
+
// The whole access expression: `a.B` for the name B, `a?.B` for a conditional access, the name itself otherwise.
|
|
123
|
+
var access = usage.Parent switch
|
|
124
|
+
{
|
|
125
|
+
MemberAccessExpressionSyntax member when member.Name == usage => member,
|
|
126
|
+
MemberBindingExpressionSyntax binding => (SyntaxNode)binding,
|
|
127
|
+
_ => usage,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
var (reads, writes) = access.Parent switch
|
|
131
|
+
{
|
|
132
|
+
AssignmentExpressionSyntax assignment when assignment.Left == access =>
|
|
133
|
+
(!assignment.IsKind(SyntaxKind.SimpleAssignmentExpression), true),
|
|
134
|
+
PrefixUnaryExpressionSyntax or PostfixUnaryExpressionSyntax => (true, true),
|
|
135
|
+
ArgumentSyntax { RefKindKeyword.RawKind: not 0 } => (true, true),
|
|
136
|
+
_ => (true, false),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (reads && property.GetMethod is { } getter)
|
|
140
|
+
{
|
|
141
|
+
yield return getter;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (writes && property.SetMethod is { } setter)
|
|
145
|
+
{
|
|
146
|
+
yield return setter;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// nameof(Foo.Bar) mentions a member without using it.
|
|
151
|
+
private static bool IsInsideNameOf(SyntaxNode node, SemanticModel model) =>
|
|
152
|
+
node.Ancestors().OfType<InvocationExpressionSyntax>().Any(invocation =>
|
|
153
|
+
invocation.Expression is IdentifierNameSyntax { Identifier.Text: "nameof" }
|
|
154
|
+
&& model.GetSymbolInfo(invocation).Symbol is null);
|
|
155
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
using Microsoft.CodeAnalysis.FindSymbols;
|
|
3
|
+
|
|
4
|
+
namespace LogicalComponents;
|
|
5
|
+
|
|
6
|
+
/// <summary>
|
|
7
|
+
/// Maps a seam (an interface or abstract class) to the concrete classes behind it in the loaded solution. Test
|
|
8
|
+
/// projects were removed at load time, so test doubles never appear. Asking the type system rather than parsing DI
|
|
9
|
+
/// registrations covers every registration style (instance, factory, forwarding) at once.
|
|
10
|
+
/// A constructed generic seam (<c>IHandler<Order></c>) resolves only to classes implementing exactly that construction,
|
|
11
|
+
/// or an open generic one (<c>LoggingHandler<T> : IHandler<T></c>). Results are cached per construction and sorted.
|
|
12
|
+
/// </summary>
|
|
13
|
+
public sealed class PortResolution(Solution solution)
|
|
14
|
+
{
|
|
15
|
+
private readonly Dictionary<string, IReadOnlyList<INamedTypeSymbol>> _cache = new(StringComparer.Ordinal);
|
|
16
|
+
|
|
17
|
+
public async Task<IReadOnlyList<INamedTypeSymbol>> ImplementationsAsync(
|
|
18
|
+
INamedTypeSymbol seam, CancellationToken cancellationToken)
|
|
19
|
+
{
|
|
20
|
+
var key = Display(seam);
|
|
21
|
+
if (_cache.TryGetValue(key, out var cached))
|
|
22
|
+
{
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var definition = seam.OriginalDefinition;
|
|
27
|
+
IEnumerable<INamedTypeSymbol> found = definition.TypeKind == TypeKind.Interface
|
|
28
|
+
? (await SymbolFinder.FindImplementationsAsync(definition, solution, cancellationToken: cancellationToken))
|
|
29
|
+
.OfType<INamedTypeSymbol>()
|
|
30
|
+
: await SymbolFinder.FindDerivedClassesAsync(definition, solution, transitive: true, cancellationToken: cancellationToken);
|
|
31
|
+
|
|
32
|
+
var implementations = found
|
|
33
|
+
.Where(t => t.TypeKind == TypeKind.Class && !t.IsAbstract)
|
|
34
|
+
.Select(t => t.OriginalDefinition)
|
|
35
|
+
.Where(t => ImplementsConstruction(t, seam))
|
|
36
|
+
.DistinctBy(SymbolNames.FullName)
|
|
37
|
+
.OrderBy(SymbolNames.FullName, StringComparer.Ordinal)
|
|
38
|
+
.ToList();
|
|
39
|
+
_cache[key] = implementations;
|
|
40
|
+
return implementations;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private static bool ImplementsConstruction(INamedTypeSymbol implementation, INamedTypeSymbol seam)
|
|
44
|
+
{
|
|
45
|
+
if (!seam.IsGenericType || seam.TypeArguments.All(a => a is ITypeParameterSymbol))
|
|
46
|
+
{
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var definition = SymbolNames.FullName(seam.OriginalDefinition);
|
|
51
|
+
var wanted = Display(seam);
|
|
52
|
+
return Supertypes(implementation, seam.TypeKind).Any(s =>
|
|
53
|
+
SymbolNames.FullName(s.OriginalDefinition) == definition
|
|
54
|
+
&& (Display(s) == wanted || s.TypeArguments.Any(a => a is ITypeParameterSymbol)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static IEnumerable<INamedTypeSymbol> Supertypes(INamedTypeSymbol type, TypeKind seamKind)
|
|
58
|
+
{
|
|
59
|
+
if (seamKind == TypeKind.Interface)
|
|
60
|
+
{
|
|
61
|
+
return type.AllInterfaces;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var bases = new List<INamedTypeSymbol>();
|
|
65
|
+
for (var current = type.BaseType; current is not null; current = current.BaseType)
|
|
66
|
+
{
|
|
67
|
+
bases.Add(current);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return bases;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Compared as text: symbols of the same type seen from two projects' compilations are not always the same instance.
|
|
74
|
+
private static string Display(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
|
75
|
+
}
|