@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/ComponentDiscovery.cs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// Lists the candidates (every type in an Application or Infrastructure project that passes <see cref="ComponentRule"/>)
|
|
7
|
+
/// and keeps as components the ones the repo's frozen classification calls components. Output order is by full type
|
|
8
|
+
/// name, so the same code and config always yield the same list.
|
|
9
|
+
/// </summary>
|
|
10
|
+
public static class ComponentDiscovery
|
|
11
|
+
{
|
|
12
|
+
public const string ApplicationLayer = "Application";
|
|
13
|
+
public const string InfrastructureLayer = "Infrastructure";
|
|
14
|
+
|
|
15
|
+
/// <summary>The candidates, as components not yet named (empty id and name), plus out-of-repo warnings.</summary>
|
|
16
|
+
public sealed record CandidateList(IReadOnlyList<Component> Candidates, IReadOnlyList<string> Warnings);
|
|
17
|
+
|
|
18
|
+
public static async Task<CandidateList> CandidatesAsync(LoadedWorkspace loaded, CancellationToken cancellationToken = default)
|
|
19
|
+
{
|
|
20
|
+
var warnings = new List<string>();
|
|
21
|
+
var candidates = (await SourceTypesAsync(loaded.Solution, cancellationToken))
|
|
22
|
+
.Where(t => t.Layer is (ApplicationLayer or InfrastructureLayer) && ComponentRule.IsCandidate(t.Symbol))
|
|
23
|
+
.Select(t => ToComponent(t, loaded, warnings))
|
|
24
|
+
.OfType<Component>()
|
|
25
|
+
.ToList();
|
|
26
|
+
return new CandidateList(candidates, warnings);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public static async Task<DiscoveryResult> DiscoverAsync(
|
|
30
|
+
LoadedWorkspace loaded, ExtractConfig config, CancellationToken cancellationToken = default)
|
|
31
|
+
{
|
|
32
|
+
var (candidates, candidateWarnings) = await CandidatesAsync(loaded, cancellationToken);
|
|
33
|
+
var known = candidates.Select(c => c.Type).ToHashSet(StringComparer.Ordinal);
|
|
34
|
+
var stale = config.Components.Concat(config.Helpers.Keys)
|
|
35
|
+
.Where(name => !known.Contains(name))
|
|
36
|
+
.Distinct()
|
|
37
|
+
.Order(StringComparer.Ordinal)
|
|
38
|
+
.Select(name => $"config entry '{name}' names no candidate; ignored");
|
|
39
|
+
|
|
40
|
+
return new DiscoveryResult(
|
|
41
|
+
AssignIds(candidates.Where(c => config.Components.Contains(c.Type)).ToList()),
|
|
42
|
+
candidates.Where(c => !config.IsClassified(c.Type)).Select(c => c.Type).ToList(),
|
|
43
|
+
[.. candidateWarnings, .. stale]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// <summary>A top-level type declared in a loaded project, with the layer and module its project gives it.</summary>
|
|
47
|
+
public sealed record SourceType(INamedTypeSymbol Symbol, string Layer, string Module);
|
|
48
|
+
|
|
49
|
+
public static async Task<List<SourceType>> SourceTypesAsync(Solution solution, CancellationToken cancellationToken)
|
|
50
|
+
{
|
|
51
|
+
var types = new List<SourceType>();
|
|
52
|
+
foreach (var project in solution.Projects)
|
|
53
|
+
{
|
|
54
|
+
var compilation = await project.GetCompilationAsync(cancellationToken)
|
|
55
|
+
?? throw new InvalidOperationException($"No compilation for {project.Name}");
|
|
56
|
+
var (layer, module) = LayerAndModule(project.Name);
|
|
57
|
+
types.AddRange(TopLevelTypes(compilation.Assembly.GlobalNamespace)
|
|
58
|
+
.Where(t => t.Locations.Any(l => l.IsInSource))
|
|
59
|
+
.Select(t => new SourceType(t, layer, module)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return types.OrderBy(t => SymbolNames.FullName(t.Symbol), StringComparer.Ordinal).ToList();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// <summary>
|
|
66
|
+
/// <c>WorkerDwg.Export.Application</c> → (Application, Export); <c>Sample.Infrastructure</c> → (Infrastructure, Sample);
|
|
67
|
+
/// any other project → (Other, its last name segment).
|
|
68
|
+
/// </summary>
|
|
69
|
+
public static (string Layer, string Module) LayerAndModule(string projectName)
|
|
70
|
+
{
|
|
71
|
+
foreach (var layer in new[] { ApplicationLayer, InfrastructureLayer })
|
|
72
|
+
{
|
|
73
|
+
if (projectName.EndsWith("." + layer, StringComparison.Ordinal))
|
|
74
|
+
{
|
|
75
|
+
var rest = projectName[..^(layer.Length + 1)];
|
|
76
|
+
return (layer, rest[(rest.LastIndexOf('.') + 1)..]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return ("Other", projectName[(projectName.LastIndexOf('.') + 1)..]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// <summary>
|
|
84
|
+
/// Ids and names are the plain type name unless several components share it; then every one of them is prefixed
|
|
85
|
+
/// with its module (<c>Export_JobOutcomeReporting</c>, "Job Outcome Reporting (Export)"). If that still clashes
|
|
86
|
+
/// (same module, different namespace or generic arity), the full type name disambiguates. The same full type twice
|
|
87
|
+
/// (a file linked into two projects) has no honest id and is an error.
|
|
88
|
+
/// </summary>
|
|
89
|
+
public static List<Component> AssignIds(IReadOnlyList<Component> components)
|
|
90
|
+
{
|
|
91
|
+
var duplicateTypes = components.GroupBy(c => c.Type).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
|
92
|
+
if (duplicateTypes.Count > 0)
|
|
93
|
+
{
|
|
94
|
+
throw new InvalidOperationException(
|
|
95
|
+
$"Declared in more than one project, so no unique component id: {string.Join(", ", duplicateTypes.Order(StringComparer.Ordinal))}");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var bySimpleName = components.GroupBy(c => SymbolNames.SimpleName(c.Type)).ToDictionary(g => g.Key, g => g.Count());
|
|
99
|
+
var named = components.Select(c =>
|
|
100
|
+
{
|
|
101
|
+
var simple = SymbolNames.SimpleName(c.Type);
|
|
102
|
+
var words = DisplayName.FromTypeName(simple);
|
|
103
|
+
return bySimpleName[simple] == 1
|
|
104
|
+
? c with { Id = simple, Name = words }
|
|
105
|
+
: c with { Id = $"{c.Module}_{simple}", Name = $"{words} ({c.Module})" };
|
|
106
|
+
}).ToList();
|
|
107
|
+
|
|
108
|
+
var clashingIds = named.GroupBy(c => c.Id).Where(g => g.Count() > 1).Select(g => g.Key).ToHashSet();
|
|
109
|
+
return named
|
|
110
|
+
.Select(c => clashingIds.Contains(c.Id)
|
|
111
|
+
? c with
|
|
112
|
+
{
|
|
113
|
+
Id = SymbolNames.ToIdentifier(c.Type),
|
|
114
|
+
Name = $"{DisplayName.FromTypeName(SymbolNames.SimpleName(c.Type))} ({SymbolNames.ReadableFullName(c.Type)})",
|
|
115
|
+
}
|
|
116
|
+
: c)
|
|
117
|
+
.OrderBy(c => c.Type, StringComparer.Ordinal)
|
|
118
|
+
.ToList();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// <summary>
|
|
122
|
+
/// The component for a type, with spans from real source files inside the repo only. Generator output (under obj/)
|
|
123
|
+
/// is skipped silently; a declaration outside the repo cannot be cited, so it is skipped with a warning. No span
|
|
124
|
+
/// left means no component.
|
|
125
|
+
/// </summary>
|
|
126
|
+
public static Component? ToComponent(SourceType type, LoadedWorkspace loaded, List<string> warnings)
|
|
127
|
+
{
|
|
128
|
+
var spans = new List<SourceSpan>();
|
|
129
|
+
foreach (var reference in type.Symbol.DeclaringSyntaxReferences)
|
|
130
|
+
{
|
|
131
|
+
var (kind, relative) = CitableSource.Classify(loaded, reference.SyntaxTree);
|
|
132
|
+
if (kind == CitableSource.Kind.OutsideRepo)
|
|
133
|
+
{
|
|
134
|
+
var warning = $"declaration of {SymbolNames.FullName(type.Symbol)} outside the repo was ignored";
|
|
135
|
+
if (!warnings.Contains(warning))
|
|
136
|
+
{
|
|
137
|
+
warnings.Add(warning);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (kind != CitableSource.Kind.InRepo)
|
|
142
|
+
{
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
var lines = reference.GetSyntax().GetLocation().GetLineSpan();
|
|
147
|
+
spans.Add(new SourceSpan(relative, lines.StartLinePosition.Line + 1, lines.EndLinePosition.Line + 1));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (spans.Count == 0)
|
|
151
|
+
{
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return new Component(
|
|
156
|
+
Id: "",
|
|
157
|
+
Type: SymbolNames.FullName(type.Symbol),
|
|
158
|
+
Name: "",
|
|
159
|
+
Module: type.Module,
|
|
160
|
+
Layer: type.Layer,
|
|
161
|
+
Spans: spans.OrderBy(s => s.File, StringComparer.Ordinal).ThenBy(s => s.Start).ToList());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private static IEnumerable<INamedTypeSymbol> TopLevelTypes(INamespaceSymbol ns) =>
|
|
165
|
+
ns.GetTypeMembers().Concat(ns.GetNamespaceMembers().SelectMany(TopLevelTypes));
|
|
166
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// The deterministic test for "could this type be a logical component". Whether a candidate IS one is not decided here:
|
|
7
|
+
/// the repo's frozen classification (<see cref="ExtractConfig"/>) says component or helper. Whether a declaration comes
|
|
8
|
+
/// from a real file inside the repo is checked by discovery, which has the solution to hand.
|
|
9
|
+
/// </summary>
|
|
10
|
+
public static class ComponentRule
|
|
11
|
+
{
|
|
12
|
+
private const string ServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection";
|
|
13
|
+
|
|
14
|
+
/// <summary>
|
|
15
|
+
/// A candidate is a top-level, source-declared class (static classes included) or record (abstract records and
|
|
16
|
+
/// record structs included) that is not an abstract class (a seam), not file-local, not an exception, not an
|
|
17
|
+
/// <c>*Options</c> class, not a DI composition root, and declares at least one method with a body of its own.
|
|
18
|
+
/// </summary>
|
|
19
|
+
public static bool IsCandidate(INamedTypeSymbol type) =>
|
|
20
|
+
(type.TypeKind == TypeKind.Class || (type.TypeKind == TypeKind.Struct && type.IsRecord))
|
|
21
|
+
&& !EdgeExtraction.IsSeam(type)
|
|
22
|
+
&& !type.IsImplicitlyDeclared
|
|
23
|
+
// A file-local type's metadata name embeds a hash of its absolute path: different per checkout.
|
|
24
|
+
&& !type.IsFileLocal
|
|
25
|
+
&& type.ContainingType is null
|
|
26
|
+
&& type.Locations.Any(l => l.IsInSource)
|
|
27
|
+
&& !type.Name.EndsWith("Options", StringComparison.Ordinal)
|
|
28
|
+
&& !IsException(type)
|
|
29
|
+
&& !IsCompositionRoot(type)
|
|
30
|
+
&& DeclaresBehaviour(type);
|
|
31
|
+
|
|
32
|
+
/// <summary>
|
|
33
|
+
/// A static class that extends <c>IServiceCollection</c> (classic <c>this</c> parameter or a C# 14
|
|
34
|
+
/// <c>extension(IServiceCollection)</c> block) wires the app together and <c>new</c>s nearly every class; counting it
|
|
35
|
+
/// would make it look coupled to everything.
|
|
36
|
+
/// </summary>
|
|
37
|
+
public static bool IsCompositionRoot(INamedTypeSymbol type) =>
|
|
38
|
+
type.IsStatic
|
|
39
|
+
&& (type.GetMembers().OfType<IMethodSymbol>().Any(m =>
|
|
40
|
+
m.IsExtensionMethod && m.Parameters.Length > 0 && IsServiceCollection(m.Parameters[0].Type))
|
|
41
|
+
|| ExtensionBlocks(type).Any(block =>
|
|
42
|
+
block.ExtensionParameter is { } parameter && IsServiceCollection(parameter.Type)));
|
|
43
|
+
|
|
44
|
+
// An exception carries a failure; even with factory methods of its own it is data, not a unit of behaviour.
|
|
45
|
+
private static bool IsException(INamedTypeSymbol type)
|
|
46
|
+
{
|
|
47
|
+
for (var current = type.BaseType; current is not null; current = current.BaseType)
|
|
48
|
+
{
|
|
49
|
+
if (SymbolNames.QualifiedName(current) == "System.Exception")
|
|
50
|
+
{
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// <summary>
|
|
59
|
+
/// An ordinary method, explicit interface implementation, or C# 14 extension member written in the type, with a body.
|
|
60
|
+
/// Not behaviour: constructors, accessors, operators, abstract methods, compiler-synthesized record members, and
|
|
61
|
+
/// overrides of <c>object.ToString/Equals/GetHashCode</c> (formatting or comparing what the type holds).
|
|
62
|
+
/// </summary>
|
|
63
|
+
private static bool DeclaresBehaviour(INamedTypeSymbol type) =>
|
|
64
|
+
type.GetMembers().Concat(ExtensionBlocks(type).SelectMany(block => block.GetMembers()))
|
|
65
|
+
.OfType<IMethodSymbol>()
|
|
66
|
+
.Any(m => !m.IsImplicitlyDeclared
|
|
67
|
+
&& !m.IsAbstract
|
|
68
|
+
&& !(m.IsPartialDefinition && m.PartialImplementationPart is null)
|
|
69
|
+
&& m.MethodKind is MethodKind.Ordinary or MethodKind.ExplicitInterfaceImplementation
|
|
70
|
+
&& !OverridesObjectMember(m));
|
|
71
|
+
|
|
72
|
+
private static bool OverridesObjectMember(IMethodSymbol method)
|
|
73
|
+
{
|
|
74
|
+
var overridden = method.OverriddenMethod;
|
|
75
|
+
while (overridden?.OverriddenMethod is { } further)
|
|
76
|
+
{
|
|
77
|
+
overridden = further;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return overridden?.ContainingType.SpecialType == SpecialType.System_Object;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static IEnumerable<INamedTypeSymbol> ExtensionBlocks(INamedTypeSymbol type) =>
|
|
84
|
+
type.GetTypeMembers().Where(t => t.IsExtension);
|
|
85
|
+
|
|
86
|
+
private static bool IsServiceCollection(ITypeSymbol type) => SymbolNames.QualifiedName(type) == ServiceCollection;
|
|
87
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
using System.Text;
|
|
2
|
+
using System.Text.Encodings.Web;
|
|
3
|
+
using System.Text.Json;
|
|
4
|
+
|
|
5
|
+
namespace LogicalComponents;
|
|
6
|
+
|
|
7
|
+
/// <summary>Where a component's declaration lives: repo-relative path with forward slashes, 1-based inclusive lines.</summary>
|
|
8
|
+
public sealed record SourceSpan(string File, int Start, int End);
|
|
9
|
+
|
|
10
|
+
/// <summary>
|
|
11
|
+
/// One logical component: a class the repo config classifies as a unit of behaviour.
|
|
12
|
+
/// <see cref="Id"/> is what the diagram uses; <see cref="Type"/> is the full metadata name. <see cref="Reaches"/> is
|
|
13
|
+
/// the helper code (members of helpers, records and other non-components) that the component's code runs: a rule held
|
|
14
|
+
/// there is part of what the component does, so its responsibilities may cite it. Null until coupling is resolved.
|
|
15
|
+
/// </summary>
|
|
16
|
+
public sealed record Component(
|
|
17
|
+
string Id,
|
|
18
|
+
string Type,
|
|
19
|
+
string Name,
|
|
20
|
+
string Module,
|
|
21
|
+
string Layer,
|
|
22
|
+
IReadOnlyList<SourceSpan> Spans,
|
|
23
|
+
IReadOnlyList<SourceSpan>? Reaches = null);
|
|
24
|
+
|
|
25
|
+
/// <summary>
|
|
26
|
+
/// The classified components, the candidates the config does not classify yet (extract refuses to run with any), and
|
|
27
|
+
/// warnings.
|
|
28
|
+
/// </summary>
|
|
29
|
+
public sealed record DiscoveryResult(
|
|
30
|
+
IReadOnlyList<Component> Components, IReadOnlyList<string> Unclassified, IReadOnlyList<string> Warnings);
|
|
31
|
+
|
|
32
|
+
/// <summary>A diagram node that is not a component: an interface (<c>layer: "port"</c>) with no modelled implementation.</summary>
|
|
33
|
+
public sealed record ExternalNode(string Id, string Type, string Name, string Layer);
|
|
34
|
+
|
|
35
|
+
/// <summary>
|
|
36
|
+
/// One coupling edge between two node ids. <see cref="Kinds"/> is the sorted set of di/new/static, <see cref="Via"/> the
|
|
37
|
+
/// sorted interfaces it went through, <see cref="At"/> its first occurrence as <c>file:line</c>.
|
|
38
|
+
/// </summary>
|
|
39
|
+
public sealed record Edge(string From, string To, IReadOnlyList<string> Kinds, IReadOnlyList<string> Via, string At);
|
|
40
|
+
|
|
41
|
+
/// <summary>Everything <c>extract</c> writes: the components, the ports they reach, the edges, and the warnings.</summary>
|
|
42
|
+
public sealed record ExtractResult(
|
|
43
|
+
IReadOnlyList<Component> Components,
|
|
44
|
+
IReadOnlyList<ExternalNode> ExternalNodes,
|
|
45
|
+
IReadOnlyList<Edge> Edges,
|
|
46
|
+
IReadOnlyList<string> Warnings);
|
|
47
|
+
|
|
48
|
+
/// <summary>
|
|
49
|
+
/// Writes <c>components.json</c>. Byte-identical for identical input: fixed property order, LF line endings,
|
|
50
|
+
/// no BOM, no timestamps, and only repo-relative paths.
|
|
51
|
+
/// </summary>
|
|
52
|
+
public static class ComponentsFile
|
|
53
|
+
{
|
|
54
|
+
private static readonly JsonSerializerOptions Options = new()
|
|
55
|
+
{
|
|
56
|
+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
57
|
+
WriteIndented = true,
|
|
58
|
+
IndentSize = 2,
|
|
59
|
+
NewLine = "\n",
|
|
60
|
+
// Keep names and paths readable; the file is consumed by our own tools and by agents, not by browsers.
|
|
61
|
+
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
public static byte[] Serialize(ExtractResult result) =>
|
|
65
|
+
ToJson(new
|
|
66
|
+
{
|
|
67
|
+
components = result.Components,
|
|
68
|
+
externalNodes = result.ExternalNodes,
|
|
69
|
+
edges = result.Edges,
|
|
70
|
+
warnings = result.Warnings,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/// <summary>Any file the tool writes: indented camelCase JSON, LF line endings, no BOM, trailing newline.</summary>
|
|
74
|
+
public static byte[] ToJson<T>(T value) =>
|
|
75
|
+
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(JsonSerializer.Serialize(value, Options) + "\n");
|
|
76
|
+
|
|
77
|
+
/// <summary>Reads a components.json written by <see cref="Serialize(ExtractResult)"/>.</summary>
|
|
78
|
+
public static ExtractResult Deserialize(byte[] json) => FromJson<ExtractResult>(json);
|
|
79
|
+
|
|
80
|
+
/// <summary>Reads any file written by <see cref="ToJson{T}"/>.</summary>
|
|
81
|
+
public static T FromJson<T>(byte[] json) =>
|
|
82
|
+
JsonSerializer.Deserialize<T>(json, Options) ?? throw new JsonException("the file is empty");
|
|
83
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// The whole <c>extract</c> computation: the classified components, and an edge A → B for every way A depends on B:
|
|
7
|
+
/// A takes B (or a seam B implements) in its constructor (<c>di</c>), derives from B (<c>inherits</c>), or its code
|
|
8
|
+
/// runs B's code, directly or through seams and helpers (<c>new</c>, <c>static</c>, <c>call</c>; see
|
|
9
|
+
/// <see cref="MemberUseResolution"/>). Output order is fixed, so the same code and config yield the same bytes.
|
|
10
|
+
/// </summary>
|
|
11
|
+
public static class CouplingResolver
|
|
12
|
+
{
|
|
13
|
+
public static async Task<ExtractResult> ExtractAsync(
|
|
14
|
+
LoadedWorkspace loaded, ExtractConfig config, CancellationToken cancellationToken = default)
|
|
15
|
+
{
|
|
16
|
+
var discovery = await ComponentDiscovery.DiscoverAsync(loaded, config, cancellationToken);
|
|
17
|
+
if (discovery.Unclassified.Count > 0)
|
|
18
|
+
{
|
|
19
|
+
throw new UnclassifiedCandidatesException(discovery.Unclassified);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var sourceTypes = (await ComponentDiscovery.SourceTypesAsync(loaded.Solution, cancellationToken))
|
|
23
|
+
.Where(t => t.Layer is ComponentDiscovery.ApplicationLayer or ComponentDiscovery.InfrastructureLayer)
|
|
24
|
+
.GroupBy(t => SymbolNames.FullName(t.Symbol))
|
|
25
|
+
.ToDictionary(g => g.Key, g => g.First().Symbol, StringComparer.Ordinal);
|
|
26
|
+
var components = discovery.Components.Select(c => c.Type).ToHashSet(StringComparer.Ordinal);
|
|
27
|
+
var warnings = await CompileErrorWarningsAsync(loaded.Solution, cancellationToken);
|
|
28
|
+
var ports = new SortedDictionary<string, INamedTypeSymbol>(StringComparer.Ordinal);
|
|
29
|
+
var raw = new List<MemberUseResolution.RawEdge>();
|
|
30
|
+
var portResolution = new PortResolution(loaded.Solution);
|
|
31
|
+
var resolution = new MemberUseResolution(loaded, components, portResolution);
|
|
32
|
+
var reaches = new Dictionary<string, List<SourceSpan>>(StringComparer.Ordinal);
|
|
33
|
+
|
|
34
|
+
foreach (var component in discovery.Components)
|
|
35
|
+
{
|
|
36
|
+
var symbol = sourceTypes[component.Type];
|
|
37
|
+
var self = new HashSet<string>(StringComparer.Ordinal) { component.Type };
|
|
38
|
+
var declarations = symbol.DeclaringSyntaxReferences.Select(r => r.GetSyntax()).ToList();
|
|
39
|
+
var roots = new List<SyntaxNode>(declarations);
|
|
40
|
+
|
|
41
|
+
// Up the base chain: the first component ancestor is an 'inherits' edge; the code of non-component source
|
|
42
|
+
// bases in between (typically abstract) is this component's own behaviour, so it is walked as such.
|
|
43
|
+
for (var baseType = symbol.BaseType?.OriginalDefinition;
|
|
44
|
+
baseType is not null && CitableSource.IsRepoSource(baseType, loaded);
|
|
45
|
+
baseType = baseType.BaseType?.OriginalDefinition)
|
|
46
|
+
{
|
|
47
|
+
var baseName = SymbolNames.FullName(baseType);
|
|
48
|
+
if (components.Contains(baseName))
|
|
49
|
+
{
|
|
50
|
+
var (file, line) = BaseListLocation(declarations, loaded) ?? (component.Spans[0].File, component.Spans[0].Start);
|
|
51
|
+
raw.Add(new(component.Type, baseName, EdgeExtraction.Inherits, [], file, line));
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
self.Add(baseName);
|
|
56
|
+
roots.AddRange(baseType.DeclaringSyntaxReferences.Select(r => r.GetSyntax()));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
foreach (var reference in EdgeExtraction.DiReferences(symbol, loaded))
|
|
60
|
+
{
|
|
61
|
+
raw.AddRange(await DiEdgesAsync(component.Type, reference, components, ports, portResolution, loaded, cancellationToken));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// In (file, position) order, so the first path the walk records is the same on every checkout.
|
|
65
|
+
var ordered = roots.OrderBy(r => r.SyntaxTree.FilePath, StringComparer.Ordinal).ThenBy(r => r.SpanStart);
|
|
66
|
+
var uses = await MemberUses.ReadAsync(ordered, loaded, cancellationToken);
|
|
67
|
+
var resolved = await resolution.ResolveAsync(component.Type, self, uses, cancellationToken);
|
|
68
|
+
raw.AddRange(resolved.Edges);
|
|
69
|
+
reaches[component.Type] = resolved.Reaches;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var ids = discovery.Components.ToDictionary(c => c.Type, c => c.Id, StringComparer.Ordinal);
|
|
73
|
+
var externalNodes = PortNodes(ports, ids);
|
|
74
|
+
foreach (var node in externalNodes)
|
|
75
|
+
{
|
|
76
|
+
ids[node.Type] = node.Id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
warnings.AddRange(ports.Keys.Select(p => $"port {p} has no implementation"));
|
|
80
|
+
return new ExtractResult(
|
|
81
|
+
discovery.Components.Select(c => c with { Reaches = reaches[c.Type] }).ToList(),
|
|
82
|
+
externalNodes,
|
|
83
|
+
MergeEdges(raw, ids),
|
|
84
|
+
[.. discovery.Warnings, .. warnings.Where(w => !discovery.Warnings.Contains(w)).Distinct().Order(StringComparer.Ordinal)]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// <summary>
|
|
88
|
+
/// A constructor parameter as edges: to the component it is, to every component implementing the seam it is, or to
|
|
89
|
+
/// a port node when nothing in the repo implements that seam. A parameter of a helper type is no edge; what the
|
|
90
|
+
/// component calls on it is found by <see cref="MemberUseResolution"/>.
|
|
91
|
+
/// </summary>
|
|
92
|
+
private static async Task<List<MemberUseResolution.RawEdge>> DiEdgesAsync(
|
|
93
|
+
string from,
|
|
94
|
+
EdgeExtraction.Reference reference,
|
|
95
|
+
IReadOnlySet<string> components,
|
|
96
|
+
SortedDictionary<string, INamedTypeSymbol> ports,
|
|
97
|
+
PortResolution portResolution,
|
|
98
|
+
LoadedWorkspace loaded,
|
|
99
|
+
CancellationToken cancellationToken)
|
|
100
|
+
{
|
|
101
|
+
var target = reference.Target;
|
|
102
|
+
if (!EdgeExtraction.IsSeam(target))
|
|
103
|
+
{
|
|
104
|
+
var name = SymbolNames.FullName(target);
|
|
105
|
+
return components.Contains(name) ? [new(from, name, EdgeExtraction.Di, [], reference.File, reference.Line)] : [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Seams from the framework (ILogger<T>, IReadOnlyList<T>...) are plumbing, not design.
|
|
109
|
+
if (!CitableSource.IsRepoSource(target.OriginalDefinition, loaded))
|
|
110
|
+
{
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var implementations = await portResolution.ImplementationsAsync(target, cancellationToken);
|
|
115
|
+
if (implementations.Count == 0)
|
|
116
|
+
{
|
|
117
|
+
var portName = SymbolNames.FullName(target.OriginalDefinition);
|
|
118
|
+
ports[portName] = target.OriginalDefinition;
|
|
119
|
+
return [new(from, portName, EdgeExtraction.Di, [], reference.File, reference.Line)];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return implementations
|
|
123
|
+
.Select(SymbolNames.FullName)
|
|
124
|
+
.Where(components.Contains)
|
|
125
|
+
.Select(to => new MemberUseResolution.RawEdge(from, to, EdgeExtraction.Di, [target.Name], reference.File, reference.Line))
|
|
126
|
+
.ToList();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// <summary>Where a partial class names its base class: the base list of the declaration that has one.</summary>
|
|
130
|
+
private static (string File, int Line)? BaseListLocation(IEnumerable<SyntaxNode> declarations, LoadedWorkspace loaded)
|
|
131
|
+
{
|
|
132
|
+
foreach (var baseList in declarations.OfType<Microsoft.CodeAnalysis.CSharp.Syntax.TypeDeclarationSyntax>()
|
|
133
|
+
.Select(d => d.BaseList).OfType<Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax>())
|
|
134
|
+
{
|
|
135
|
+
var (kind, file) = CitableSource.Classify(loaded, baseList.SyntaxTree);
|
|
136
|
+
if (kind == CitableSource.Kind.InRepo)
|
|
137
|
+
{
|
|
138
|
+
return (file, baseList.GetLocation().GetLineSpan().StartLinePosition.Line + 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// <summary>Code that does not compile binds partially, so edges can be missing: say so instead of staying silent.</summary>
|
|
146
|
+
private static async Task<List<string>> CompileErrorWarningsAsync(Solution solution, CancellationToken cancellationToken)
|
|
147
|
+
{
|
|
148
|
+
var warnings = new List<string>();
|
|
149
|
+
foreach (var project in solution.Projects.OrderBy(p => p.Name, StringComparer.Ordinal))
|
|
150
|
+
{
|
|
151
|
+
var compilation = await project.GetCompilationAsync(cancellationToken);
|
|
152
|
+
var errors = compilation?.GetDiagnostics(cancellationToken).Count(d => d.Severity == DiagnosticSeverity.Error) ?? 0;
|
|
153
|
+
if (errors > 0)
|
|
154
|
+
{
|
|
155
|
+
warnings.Add($"project {project.Name} has {errors} compile error(s); coupling may be incomplete");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return warnings;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// <summary>Port nodes use the seam's simple name as id, unless that clashes with a component or another port.</summary>
|
|
163
|
+
private static List<ExternalNode> PortNodes(
|
|
164
|
+
SortedDictionary<string, INamedTypeSymbol> ports, IReadOnlyDictionary<string, string> componentIds)
|
|
165
|
+
{
|
|
166
|
+
var taken = componentIds.Values.ToHashSet(StringComparer.Ordinal);
|
|
167
|
+
var bySimpleName = ports.Values.GroupBy(p => p.Name).ToDictionary(g => g.Key, g => g.Count());
|
|
168
|
+
return ports
|
|
169
|
+
.Select(p =>
|
|
170
|
+
{
|
|
171
|
+
var simple = p.Value.Name;
|
|
172
|
+
var id = bySimpleName[simple] == 1 && !taken.Contains(simple) ? simple : SymbolNames.ToIdentifier(p.Key);
|
|
173
|
+
return new ExternalNode(id, p.Key, DisplayName.FromTypeName(simple), "port");
|
|
174
|
+
})
|
|
175
|
+
.OrderBy(n => n.Id, StringComparer.Ordinal)
|
|
176
|
+
.ToList();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// <summary>One edge per (from, to): kinds and via as sorted sets, cited at the earliest (file, line) recorded use.</summary>
|
|
180
|
+
private static List<Edge> MergeEdges(List<MemberUseResolution.RawEdge> raw, IReadOnlyDictionary<string, string> ids) =>
|
|
181
|
+
raw.Where(e => e.From != e.To)
|
|
182
|
+
.GroupBy(e => (From: ids[e.From], To: ids[e.To]))
|
|
183
|
+
.Select(g =>
|
|
184
|
+
{
|
|
185
|
+
var first = g.OrderBy(e => e.File, StringComparer.Ordinal).ThenBy(e => e.Line).First();
|
|
186
|
+
return new Edge(
|
|
187
|
+
g.Key.From,
|
|
188
|
+
g.Key.To,
|
|
189
|
+
g.Select(e => e.Kind).Distinct().Order(StringComparer.Ordinal).ToList(),
|
|
190
|
+
g.SelectMany(e => e.Via).Distinct().Order(StringComparer.Ordinal).ToList(),
|
|
191
|
+
$"{first.File}:{first.Line}");
|
|
192
|
+
})
|
|
193
|
+
.OrderBy(e => e.From, StringComparer.Ordinal)
|
|
194
|
+
.ThenBy(e => e.To, StringComparer.Ordinal)
|
|
195
|
+
.ToList();
|
|
196
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
using System.Text.RegularExpressions;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>Turns a PascalCase type name into the words shown in the doc: ExportJobAcceptance → Export Job Acceptance.</summary>
|
|
6
|
+
public static partial class DisplayName
|
|
7
|
+
{
|
|
8
|
+
public static string FromTypeName(string typeName) => WordBoundary().Replace(typeName, " ");
|
|
9
|
+
|
|
10
|
+
// A boundary sits before an uppercase letter that follows a lowercase letter or digit (JobAcceptance, Utf8Decoding),
|
|
11
|
+
// or before the last capital of an acronym run that starts a new word (IOStream → IO Stream).
|
|
12
|
+
[GeneratedRegex("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")]
|
|
13
|
+
private static partial Regex WordBoundary();
|
|
14
|
+
}
|
package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
using Microsoft.CodeAnalysis;
|
|
2
|
+
|
|
3
|
+
namespace LogicalComponents;
|
|
4
|
+
|
|
5
|
+
/// <summary>
|
|
6
|
+
/// The edge kinds, and what a component's constructors take (<c>di</c>: each parameter's type, primary constructors
|
|
7
|
+
/// included, looked through wrappers). The uses inside code are read by <see cref="MemberUses"/>.
|
|
8
|
+
/// </summary>
|
|
9
|
+
public static class EdgeExtraction
|
|
10
|
+
{
|
|
11
|
+
public const string Call = "call";
|
|
12
|
+
public const string Di = "di";
|
|
13
|
+
public const string Inherits = "inherits";
|
|
14
|
+
public const string New = "new";
|
|
15
|
+
public const string Static = "static";
|
|
16
|
+
|
|
17
|
+
/// <summary>A constructor parameter's type, at a repo-relative file and 1-based line.</summary>
|
|
18
|
+
public sealed record Reference(INamedTypeSymbol Target, string Kind, string File, int Line);
|
|
19
|
+
|
|
20
|
+
/// <summary>What the type's constructors take: each parameter's type, looked through wrappers.</summary>
|
|
21
|
+
public static List<Reference> DiReferences(INamedTypeSymbol type, LoadedWorkspace loaded)
|
|
22
|
+
{
|
|
23
|
+
var references = new List<Reference>();
|
|
24
|
+
foreach (var constructor in type.InstanceConstructors.Where(c => !c.IsImplicitlyDeclared))
|
|
25
|
+
{
|
|
26
|
+
foreach (var parameter in constructor.Parameters)
|
|
27
|
+
{
|
|
28
|
+
var location = parameter.Locations.FirstOrDefault(l => l.IsInSource);
|
|
29
|
+
var (kind, file) = CitableSource.Classify(loaded, location?.SourceTree);
|
|
30
|
+
if (kind == CitableSource.Kind.InRepo)
|
|
31
|
+
{
|
|
32
|
+
var line = location!.GetLineSpan().StartLinePosition.Line + 1;
|
|
33
|
+
references.AddRange(DiTargets(parameter.Type).Select(t => new Reference(t, Di, file, line)));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return references;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Containers that deliver a dependency without being one: the dependency is what they wrap.
|
|
42
|
+
private static readonly HashSet<string> Wrappers =
|
|
43
|
+
[
|
|
44
|
+
"System.Collections.Generic.IEnumerable`1",
|
|
45
|
+
"System.Collections.Generic.IReadOnlyCollection`1",
|
|
46
|
+
"System.Collections.Generic.IReadOnlyList`1",
|
|
47
|
+
"System.Collections.Generic.ICollection`1",
|
|
48
|
+
"System.Collections.Generic.IList`1",
|
|
49
|
+
"System.Lazy`1",
|
|
50
|
+
"System.Func`1",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/// <summary>
|
|
54
|
+
/// What a constructor parameter depends on: the type itself, or what a wrapper delivers (<c>IEnumerable<IX></c>,
|
|
55
|
+
/// <c>IReadOnlyList<IX></c>, <c>Lazy<X></c>, <c>Func<X></c>, <c>X[]</c>), looked through recursively.
|
|
56
|
+
/// </summary>
|
|
57
|
+
private static IEnumerable<INamedTypeSymbol> DiTargets(ITypeSymbol type)
|
|
58
|
+
{
|
|
59
|
+
switch (type)
|
|
60
|
+
{
|
|
61
|
+
case IArrayTypeSymbol array:
|
|
62
|
+
foreach (var element in DiTargets(array.ElementType))
|
|
63
|
+
{
|
|
64
|
+
yield return element;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
break;
|
|
68
|
+
case INamedTypeSymbol { IsGenericType: true } named
|
|
69
|
+
when Wrappers.Contains(SymbolNames.FullName(named.OriginalDefinition)):
|
|
70
|
+
foreach (var element in DiTargets(named.TypeArguments[0]))
|
|
71
|
+
{
|
|
72
|
+
yield return element;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
break;
|
|
76
|
+
case INamedTypeSymbol named:
|
|
77
|
+
yield return Normalize(named);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// <summary>
|
|
83
|
+
/// The type a reference couples to. Interfaces and abstract classes keep their construction (<c>IHandler<Order></c>)
|
|
84
|
+
/// so ports resolve to implementations of exactly that construction. Any other type is taken as its generic
|
|
85
|
+
/// definition, and a nested type as its outermost containing type: using <c>Receipts.Builder</c> couples to
|
|
86
|
+
/// <c>Receipts</c>, never to an unrelated top-level <c>Builder</c>.
|
|
87
|
+
/// </summary>
|
|
88
|
+
public static INamedTypeSymbol Normalize(INamedTypeSymbol type)
|
|
89
|
+
{
|
|
90
|
+
if (IsSeam(type))
|
|
91
|
+
{
|
|
92
|
+
return type;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
var outermost = type.OriginalDefinition;
|
|
96
|
+
while (outermost.ContainingType is { } outer)
|
|
97
|
+
{
|
|
98
|
+
outermost = outer.OriginalDefinition;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return outermost;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// <summary>
|
|
105
|
+
/// An interface or abstract class: a dependency on it resolves to the concrete classes behind it. An abstract
|
|
106
|
+
/// record is a data hierarchy (LabelSegment → LiteralSegment, PlaceholderSegment), not a seam.
|
|
107
|
+
/// </summary>
|
|
108
|
+
public static bool IsSeam(INamedTypeSymbol type) =>
|
|
109
|
+
type.TypeKind == TypeKind.Interface
|
|
110
|
+
|| (type.TypeKind == TypeKind.Class && type.IsAbstract && !type.IsStatic && !type.IsRecord);
|
|
111
|
+
}
|