@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.
Files changed (280) hide show
  1. package/INSTALLATION.md +53 -52
  2. package/README.md +1 -1
  3. package/bin/musketeer.js +168 -168
  4. package/manifest.json +19 -2
  5. package/package.json +48 -48
  6. package/src/dotnet-scaffold-copier.js +79 -79
  7. package/src/provisioner/detect.js +93 -93
  8. package/src/self-update.js +77 -77
  9. package/template/.claude/agents/git-manager.md +18 -18
  10. package/template/.claude/agents/hallmark-auditor.md +78 -78
  11. package/template/.claude/agents/researcher.md +33 -33
  12. package/template/.claude/hooks/block-unsafe-adr-title.cjs +85 -85
  13. package/template/.claude/hooks/init-adr-dir.cjs +173 -173
  14. package/template/.claude/hooks/inject-adr-flags.cjs +94 -94
  15. package/template/.claude/hooks/lib/adr/command-scan.cjs +115 -115
  16. package/template/.claude/hooks/lib/characteristics/checker.cjs +357 -357
  17. package/template/.claude/hooks/lib/git-info-cache.cjs +191 -191
  18. package/template/.claude/hooks/sync-adr-toc.cjs +146 -146
  19. package/template/.claude/hooks/usage-quota-cache-refresh.cjs +166 -166
  20. package/template/.claude/hooks/validate-characteristics-hook.cjs +66 -66
  21. package/template/.claude/hooks/validate-cml-hook.js +146 -145
  22. package/template/.claude/skills/adr-writer/SKILL.md +48 -48
  23. package/template/.claude/skills/adr-writer/references/adr-example.md +35 -35
  24. package/template/.claude/skills/architecture-characteristic-writer/SKILL.md +215 -215
  25. package/template/.claude/skills/architecture-characteristic-writer/assets/worksheet-template.md +29 -29
  26. package/template/.claude/skills/architecture-characteristic-writer/references/characteristics-catalog.md +40 -40
  27. package/template/.claude/skills/architecture-characteristic-writer/scripts/ranking-table.cjs +171 -171
  28. package/template/.claude/skills/context-map/SKILL.md +80 -80
  29. package/template/.claude/skills/context-map/example.cml +106 -106
  30. package/template/.claude/skills/context-map/reference/Bounded Context/Bounded Context.md +40 -40
  31. package/template/.claude/skills/context-map/reference/Bounded Context/businessModel.md +5 -5
  32. package/template/.claude/skills/context-map/reference/Bounded Context/domainVisionStatement.md +2 -2
  33. package/template/.claude/skills/context-map/reference/Bounded Context/evolution.md +5 -5
  34. package/template/.claude/skills/context-map/reference/Bounded Context/implementationTechnology.md +1 -1
  35. package/template/.claude/skills/context-map/reference/Bounded Context/implements.md +1 -1
  36. package/template/.claude/skills/context-map/reference/Bounded Context/knowledgeLevel.md +4 -4
  37. package/template/.claude/skills/context-map/reference/Bounded Context/realizes.md +9 -9
  38. package/template/.claude/skills/context-map/reference/Bounded Context/refines.md +10 -10
  39. package/template/.claude/skills/context-map/reference/Bounded Context/responsibilities.md +26 -26
  40. package/template/.claude/skills/context-map/reference/Bounded Context/type.md +23 -23
  41. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Anticorruption Layer.md +5 -5
  42. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Bounded Context Relationship.md +12 -12
  43. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Conformist.md +5 -5
  44. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Customer-Supplier (C-S).md +22 -22
  45. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Open Host Service.md +4 -4
  46. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Partnership (P).md +13 -13
  47. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Published Language.md +4 -4
  48. package/template/.claude/skills/context-map/reference/Bounded Context Relationship/Shared Kernel (SK).md +12 -12
  49. package/template/.claude/skills/context-map/reference/Context Map.md +62 -62
  50. package/template/.claude/skills/context-map/reference/Domain/Domain.md +30 -30
  51. package/template/.claude/skills/context-map/reference/Domain/supports.md +33 -33
  52. package/template/.claude/skills/context-map/reference/Domain/type.md +3 -3
  53. package/template/.claude/skills/context-map/reference/Semantic Rules.md +32 -32
  54. package/template/.claude/skills/hallmark/SKILL.md +552 -552
  55. package/template/.claude/skills/hallmark/references/anti-patterns.md +412 -412
  56. package/template/.claude/skills/hallmark/references/assets.md +406 -406
  57. package/template/.claude/skills/hallmark/references/color.md +95 -95
  58. package/template/.claude/skills/hallmark/references/component-cookbook.md +256 -256
  59. package/template/.claude/skills/hallmark/references/components/c1-outlined-chip.md +12 -12
  60. package/template/.claude/skills/hallmark/references/components/c2-inline-form-as-cta.md +16 -16
  61. package/template/.claude/skills/hallmark/references/components/c3-typographic-link.md +8 -8
  62. package/template/.claude/skills/hallmark/references/components/c4-sticky-bottom-bar.md +16 -16
  63. package/template/.claude/skills/hallmark/references/components/f1-bento-grid.md +20 -20
  64. package/template/.claude/skills/hallmark/references/components/f2-sticky-scroll-stack.md +20 -20
  65. package/template/.claude/skills/hallmark/references/components/f3-tabular-spec-sheet.md +11 -11
  66. package/template/.claude/skills/hallmark/references/components/f4-step-sequence.md +11 -11
  67. package/template/.claude/skills/hallmark/references/components/f5-annotated-screenshot.md +11 -11
  68. package/template/.claude/skills/hallmark/references/components/f6-product-card-grid.md +41 -41
  69. package/template/.claude/skills/hallmark/references/components/ft1-mast-headed.md +13 -13
  70. package/template/.claude/skills/hallmark/references/components/ft2-inline-rule-single-line.md +10 -10
  71. package/template/.claude/skills/hallmark/references/components/ft3-index-style-category-list.md +12 -12
  72. package/template/.claude/skills/hallmark/references/components/ft4-dense-typographic.md +10 -10
  73. package/template/.claude/skills/hallmark/references/components/ft5-statement.md +21 -21
  74. package/template/.claude/skills/hallmark/references/components/ft6-letter-close.md +19 -19
  75. package/template/.claude/skills/hallmark/references/components/ft7-newsletter-first.md +27 -27
  76. package/template/.claude/skills/hallmark/references/components/ft8-marquee-scroll.md +25 -25
  77. package/template/.claude/skills/hallmark/references/components/h1-marquee.md +15 -15
  78. package/template/.claude/skills/hallmark/references/components/h2-split-diptych.md +15 -15
  79. package/template/.claude/skills/hallmark/references/components/h3-quote-led.md +11 -11
  80. package/template/.claude/skills/hallmark/references/components/h4-stat-led.md +14 -14
  81. package/template/.claude/skills/hallmark/references/components/h5-letter-hero.md +11 -11
  82. package/template/.claude/skills/hallmark/references/components/h6-photographic-fold.md +16 -16
  83. package/template/.claude/skills/hallmark/references/components/h7-demo-video-clipped-by-viewport-edge.md +27 -27
  84. package/template/.claude/skills/hallmark/references/components/h8-mockup-split-browser-framed.md +23 -23
  85. package/template/.claude/skills/hallmark/references/components/h9-custom-illustration-centerpiece.md +27 -27
  86. package/template/.claude/skills/hallmark/references/components/n1-wordmark-2-links.md +12 -12
  87. package/template/.claude/skills/hallmark/references/components/n10-floating-on-scroll-morph.md +19 -19
  88. package/template/.claude/skills/hallmark/references/components/n2-floating-chip.md +14 -14
  89. package/template/.claude/skills/hallmark/references/components/n3-side-rail.md +14 -14
  90. package/template/.claude/skills/hallmark/references/components/n4-hidden-behind-k.md +9 -9
  91. package/template/.claude/skills/hallmark/references/components/n5-floating-pill.md +28 -28
  92. package/template/.claude/skills/hallmark/references/components/n6-newspaper-masthead.md +24 -24
  93. package/template/.claude/skills/hallmark/references/components/n7-brutal-slab.md +22 -22
  94. package/template/.claude/skills/hallmark/references/components/n8-terminal-command.md +21 -21
  95. package/template/.claude/skills/hallmark/references/components/n9-edge-aligned-minimal.md +17 -17
  96. package/template/.claude/skills/hallmark/references/components/s1-left-margin-numbered.md +15 -15
  97. package/template/.claude/skills/hallmark/references/components/s2-hanging.md +13 -13
  98. package/template/.claude/skills/hallmark/references/components/s3-sticky-pinned.md +19 -19
  99. package/template/.claude/skills/hallmark/references/components/s4-inline-no-break.md +11 -11
  100. package/template/.claude/skills/hallmark/references/components/s5-bottom-anchored.md +13 -13
  101. package/template/.claude/skills/hallmark/references/components/t1-pull-quote-with-marginalia.md +12 -12
  102. package/template/.claude/skills/hallmark/references/components/t2-logo-wall-hairline.md +19 -19
  103. package/template/.claude/skills/hallmark/references/components/t3-single-huge-quote.md +11 -11
  104. package/template/.claude/skills/hallmark/references/components/t4-numbered-stat-strip.md +14 -14
  105. package/template/.claude/skills/hallmark/references/contract.md +24 -24
  106. package/template/.claude/skills/hallmark/references/copy.md +182 -182
  107. package/template/.claude/skills/hallmark/references/custom-craft.md +626 -626
  108. package/template/.claude/skills/hallmark/references/custom-theme.md +329 -329
  109. package/template/.claude/skills/hallmark/references/design-md.md +116 -116
  110. package/template/.claude/skills/hallmark/references/export-formats.md +328 -328
  111. package/template/.claude/skills/hallmark/references/floating-nav.md +89 -89
  112. package/template/.claude/skills/hallmark/references/genres/atmospheric.md +65 -65
  113. package/template/.claude/skills/hallmark/references/genres/editorial.md +70 -70
  114. package/template/.claude/skills/hallmark/references/genres/modern-minimal.md +67 -67
  115. package/template/.claude/skills/hallmark/references/genres/playful.md +65 -65
  116. package/template/.claude/skills/hallmark/references/hero-enrichment.md +474 -474
  117. package/template/.claude/skills/hallmark/references/imagery-kit.md +170 -170
  118. package/template/.claude/skills/hallmark/references/interaction-and-states.md +207 -207
  119. package/template/.claude/skills/hallmark/references/layout-and-space.md +111 -111
  120. package/template/.claude/skills/hallmark/references/macrostructures/01-bento-grid.md +35 -35
  121. package/template/.claude/skills/hallmark/references/macrostructures/02-long-document.md +34 -34
  122. package/template/.claude/skills/hallmark/references/macrostructures/03-marquee-hero.md +31 -31
  123. package/template/.claude/skills/hallmark/references/macrostructures/04-stat-led.md +32 -32
  124. package/template/.claude/skills/hallmark/references/macrostructures/05-workbench.md +32 -32
  125. package/template/.claude/skills/hallmark/references/macrostructures/06-conversational-faq.md +33 -33
  126. package/template/.claude/skills/hallmark/references/macrostructures/07-manifesto.md +32 -32
  127. package/template/.claude/skills/hallmark/references/macrostructures/08-photographic.md +34 -34
  128. package/template/.claude/skills/hallmark/references/macrostructures/09-quote-led.md +32 -32
  129. package/template/.claude/skills/hallmark/references/macrostructures/10-specimen.md +32 -32
  130. package/template/.claude/skills/hallmark/references/macrostructures/11-catalogue.md +23 -23
  131. package/template/.claude/skills/hallmark/references/macrostructures/12-letter.md +23 -23
  132. package/template/.claude/skills/hallmark/references/macrostructures/13-index-first.md +23 -23
  133. package/template/.claude/skills/hallmark/references/macrostructures/14-narrative-workflow.md +23 -23
  134. package/template/.claude/skills/hallmark/references/macrostructures/15-split-studio.md +23 -23
  135. package/template/.claude/skills/hallmark/references/macrostructures/16-feature-stack.md +23 -23
  136. package/template/.claude/skills/hallmark/references/macrostructures/17-type-specimen.md +23 -23
  137. package/template/.claude/skills/hallmark/references/macrostructures/18-portfolio-grid.md +23 -23
  138. package/template/.claude/skills/hallmark/references/macrostructures/19-map-diagram.md +23 -23
  139. package/template/.claude/skills/hallmark/references/macrostructures/20-ecosystem-index.md +23 -23
  140. package/template/.claude/skills/hallmark/references/macrostructures/21-component-playground.md +23 -23
  141. package/template/.claude/skills/hallmark/references/macrostructures.md +89 -89
  142. package/template/.claude/skills/hallmark/references/microinteractions.md +260 -260
  143. package/template/.claude/skills/hallmark/references/motion.md +109 -109
  144. package/template/.claude/skills/hallmark/references/preview-examples.md +49 -49
  145. package/template/.claude/skills/hallmark/references/responsive.md +138 -138
  146. package/template/.claude/skills/hallmark/references/slop-test.md +205 -205
  147. package/template/.claude/skills/hallmark/references/structure.md +164 -164
  148. package/template/.claude/skills/hallmark/references/study.md +511 -511
  149. package/template/.claude/skills/hallmark/references/typography.md +243 -243
  150. package/template/.claude/skills/hallmark/references/verbs/audit.md +25 -25
  151. package/template/.claude/skills/hallmark/references/verbs/redesign.md +269 -269
  152. package/template/.claude/skills/hallmark-loop/SKILL.md +105 -105
  153. package/template/.claude/skills/hallmark-loop/references/auditor-call.md +60 -60
  154. package/template/.claude/skills/hallmark-loop/references/capture.md +78 -78
  155. package/template/.claude/skills/hallmark-loop/references/loop-control.md +79 -79
  156. package/template/.claude/skills/handoff/SKILL.md +15 -15
  157. package/template/.claude/skills/knowledge-crunching/SKILL.md +94 -94
  158. package/template/.claude/skills/logical-components/.gitattributes +2 -0
  159. package/template/.claude/skills/logical-components/SKILL.md +102 -0
  160. package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +53 -0
  161. package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +67 -0
  162. package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +39 -0
  163. package/template/.claude/skills/logical-components/scripts/Directory.Build.props +7 -0
  164. package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +8 -0
  165. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +69 -0
  166. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +39 -0
  167. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +70 -0
  168. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +39 -0
  169. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +37 -0
  170. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +65 -0
  171. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +187 -0
  172. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +166 -0
  173. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +87 -0
  174. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +83 -0
  175. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +196 -0
  176. package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +14 -0
  177. package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +111 -0
  178. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +72 -0
  179. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +79 -0
  180. package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +15 -0
  181. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +73 -0
  182. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +203 -0
  183. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +155 -0
  184. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +75 -0
  185. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +27 -0
  186. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +76 -0
  187. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +132 -0
  188. package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +173 -0
  189. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +84 -0
  190. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +178 -0
  191. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +59 -0
  192. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +53 -0
  193. package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +75 -0
  194. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +63 -0
  195. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +227 -0
  196. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +41 -0
  197. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +82 -0
  198. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +34 -0
  199. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +42 -0
  200. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +176 -0
  201. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +190 -0
  202. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +174 -0
  203. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +17 -0
  204. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +72 -0
  205. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +32 -0
  206. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +23 -0
  207. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +50 -0
  208. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +65 -0
  209. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +119 -0
  210. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +162 -0
  211. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +34 -0
  212. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +49 -0
  213. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +227 -0
  214. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +27 -0
  215. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +87 -0
  216. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +7 -0
  217. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +7 -0
  218. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +6 -0
  219. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +8 -0
  220. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +5 -0
  221. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +7 -0
  222. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +14 -0
  223. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +7 -0
  224. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +7 -0
  225. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +6 -0
  226. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +11 -0
  227. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +63 -0
  228. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +28 -0
  229. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +16 -0
  230. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +10 -0
  231. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +18 -0
  232. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +10 -0
  233. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +78 -0
  234. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +37 -0
  235. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +33 -0
  236. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +18 -0
  237. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +63 -0
  238. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +5 -0
  239. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +7 -0
  240. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +17 -0
  241. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +9 -0
  242. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +7 -0
  243. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +4 -0
  244. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +18 -0
  245. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +6 -0
  246. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +19 -0
  247. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +7 -0
  248. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +6 -0
  249. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +7 -0
  250. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +107 -0
  251. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +143 -0
  252. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +12 -0
  253. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +10 -0
  254. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +89 -0
  255. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +7 -0
  256. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +11 -0
  257. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +8 -0
  258. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +16 -0
  259. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +6 -0
  260. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +11 -0
  261. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +4 -0
  262. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +8 -0
  263. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +35 -0
  264. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +13 -0
  265. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +13 -0
  266. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +18 -0
  267. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +7 -0
  268. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +11 -0
  269. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +11 -0
  270. package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +7 -0
  271. package/template/.claude/skills/logical-components/scripts/global.json +5 -0
  272. package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +26 -0
  273. package/template/.claude/skills/research/SKILL.md +69 -69
  274. package/template/.claude/skills/tdd/SKILL.md +142 -142
  275. package/template/.claude/skills/tdd/deep-modules.md +15 -15
  276. package/template/.claude/skills/tdd/interface-design.md +31 -31
  277. package/template/.claude/skills/tdd/mocking.md +59 -59
  278. package/template/.claude/skills/tdd/refactoring.md +10 -10
  279. package/template/.claude/skills/tdd/tests.md +61 -61
  280. package/template/.claude/statusline.cjs +100 -37
@@ -0,0 +1,42 @@
1
+ namespace LogicalComponents.Tests;
2
+
3
+ public class CitationTests
4
+ {
5
+ [Theory]
6
+ [InlineData("src/A/B.cs:12-30", "src/A/B.cs", 12, 30)]
7
+ [InlineData("src/A/B.cs:7", "src/A/B.cs", 7, 7)]
8
+ [InlineData("src/My Folder/C.cs:1-2", "src/My Folder/C.cs", 1, 2)]
9
+ [InlineData("src/A/B.cs:0007-007", "src/A/B.cs", 7, 7)]
10
+ public void Parses_a_file_with_a_line_or_a_line_range(string cite, string file, int start, int end)
11
+ {
12
+ Assert.Equal(new Citation(file, start, end), Citation.Parse(cite));
13
+ }
14
+
15
+ [Theory]
16
+ [InlineData("")]
17
+ [InlineData("src/A/B.cs")]
18
+ [InlineData("src/A/B.cs:")]
19
+ [InlineData("src/A/B.cs:0-3")]
20
+ [InlineData("src/A/B.cs:9-3")]
21
+ [InlineData("src/A/B.cs:a-b")]
22
+ [InlineData(":3-4")]
23
+ [InlineData("src\\A\\B.cs:3-4")]
24
+ [InlineData("src/A/B.cs:99999999999")]
25
+ [InlineData("src/A/B.cs:٣")]
26
+ [InlineData("src/A/B.cs:11\n")]
27
+ [InlineData(" src/A/B.cs:11")]
28
+ [InlineData("src/A/B.cs:11 ")]
29
+ [InlineData("src/A\tB.cs:11")]
30
+ public void Anything_else_does_not_parse(string cite)
31
+ {
32
+ Assert.Null(Citation.Parse(cite));
33
+ }
34
+
35
+ [Theory]
36
+ [InlineData("src/A/B.cs:0007-007", "src/A/B.cs:7")]
37
+ [InlineData("src/A/B.cs:3-09", "src/A/B.cs:3-9")]
38
+ public void Prints_in_canonical_form(string cite, string canonical)
39
+ {
40
+ Assert.Equal(canonical, Citation.Parse(cite)!.ToString());
41
+ }
42
+ }
@@ -0,0 +1,176 @@
1
+ using System.Text;
2
+
3
+ namespace LogicalComponents.Tests;
4
+
5
+ public class ClassifyCommandsTests : IDisposable
6
+ {
7
+ private readonly DirectoryInfo _work = Directory.CreateTempSubdirectory("lc-classify-");
8
+
9
+ public ClassifyCommandsTests() => Directory.CreateDirectory(PathOf("repo"));
10
+
11
+ public void Dispose() => _work.Delete(recursive: true);
12
+
13
+ private string PathOf(string relative) => Path.Combine(_work.FullName, relative);
14
+
15
+ private static CandidatesCommand.Candidate Candidate(string type, string status) =>
16
+ new(type, "Application", "Sample", [new SourceSpan($"src/{type}.cs", 1, 9)], status);
17
+
18
+ private static string Verdict(string type, string verdict, string reason = "Reason.", string? cite = null) =>
19
+ $$"""{ "type": "{{type}}", "verdict": "{{verdict}}", "reason": "{{reason}}", "cite": "{{cite ?? $"src/{type}.cs:2-4"}}" }""";
20
+
21
+ private async Task<int> BatchAsync(int agents, params CandidatesCommand.Candidate[] candidates)
22
+ {
23
+ File.WriteAllBytes(PathOf("candidates.json"), ComponentsFile.ToJson(new CandidatesCommand.CandidatesFile(candidates, [])));
24
+ return await ClassifyBatchesCommand.RunAsync(
25
+ ["--candidates", PathOf("candidates.json"), "--agents", agents.ToString(), "--repo", PathOf("repo"), "--out", PathOf("batches")],
26
+ new StringWriter());
27
+ }
28
+
29
+ private void WriteVerdicts(string batchFile, params string[] verdicts)
30
+ {
31
+ Directory.CreateDirectory(PathOf("verdicts"));
32
+ File.WriteAllText(PathOf($"verdicts/{batchFile}"), "[" + string.Join(",", verdicts) + "]");
33
+ }
34
+
35
+ private Task<int> MergeAsync(TextWriter output) => ClassifyMergeCommand.RunAsync(
36
+ ["--batches", PathOf("batches"), "--verdicts", PathOf("verdicts"), "--repo", PathOf("repo")], output);
37
+
38
+ [Fact]
39
+ public async Task Batches_hold_only_the_unclassified_candidates_split_evenly()
40
+ {
41
+ File.WriteAllBytes(PathOf("candidates.json"), ComponentsFile.ToJson(new CandidatesCommand.CandidatesFile(
42
+ [
43
+ Candidate("A.One", "component"), Candidate("A.Two", "unclassified"), Candidate("A.Three", "helper"),
44
+ Candidate("A.Four", "unclassified"), Candidate("A.Five", "unclassified"),
45
+ ],
46
+ [])));
47
+ var output = new StringWriter();
48
+
49
+ var exit = await ClassifyBatchesCommand.RunAsync(
50
+ ["--candidates", PathOf("candidates.json"), "--agents", "2", "--repo", PathOf("repo"), "--out", PathOf("batches")], output);
51
+
52
+ Assert.Equal(0, exit);
53
+ Assert.Equal(["batch-01.json", "batch-02.json"], Directory.GetFiles(PathOf("batches"), "batch-*.json").Select(Path.GetFileName).Order());
54
+ var first = File.ReadAllText(PathOf("batches/batch-01.json"));
55
+ Assert.Contains("\"A.Two\"", first);
56
+ Assert.Contains("\"A.Four\"", first);
57
+ Assert.DoesNotContain("A.One", first);
58
+ Assert.Contains("\"A.Five\"", File.ReadAllText(PathOf("batches/batch-02.json")));
59
+ Assert.StartsWith("3 unclassified candidates in 2 batches", output.ToString());
60
+ }
61
+
62
+ [Fact]
63
+ public async Task Nothing_unclassified_writes_no_batches()
64
+ {
65
+ Directory.CreateDirectory(PathOf("batches"));
66
+ File.WriteAllText(PathOf("batches/batch-07.json"), "stale");
67
+
68
+ Assert.Equal(0, await BatchAsync(9, Candidate("A.One", "component")));
69
+
70
+ Assert.Empty(Directory.GetFiles(PathOf("batches"), "batch-*.json"));
71
+ }
72
+
73
+ [Fact]
74
+ public async Task Merge_adds_every_verdict_of_every_batch_and_writes_sorted_deterministic_json()
75
+ {
76
+ File.WriteAllText(PathOf("repo/logical-components.json"), """
77
+ { "components": ["A.Kept"], "helpers": { "A.Old": "Reviewed by a person." } }
78
+ """);
79
+ await BatchAsync(1, Candidate("A.Zeta", "unclassified"), Candidate("A.Beta", "unclassified"));
80
+ WriteVerdicts("batch-01.json", Verdict("A.Zeta", "component", "Rejects late orders."), Verdict("A.Beta", "helper", "Formats a name."));
81
+ var output = new StringWriter();
82
+
83
+ var exit = await MergeAsync(output);
84
+
85
+ Assert.Equal(0, exit);
86
+ Assert.Equal(
87
+ "{\n \"components\": [\n \"A.Kept\",\n \"A.Zeta\"\n ],\n \"helpers\": {\n"
88
+ + " \"A.Beta\": \"Formats a name.\",\n \"A.Old\": \"Reviewed by a person.\"\n }\n}\n",
89
+ Encoding.UTF8.GetString(File.ReadAllBytes(PathOf("repo/logical-components.json"))));
90
+ Assert.Equal(
91
+ "helper A.Beta: Formats a name.\ncomponent A.Zeta: Rejects late orders.\n1 components, 1 helpers added\n",
92
+ output.ToString().Replace("\r\n", "\n"));
93
+ }
94
+
95
+ [Fact]
96
+ public async Task Merge_creates_the_config_when_the_repo_has_none()
97
+ {
98
+ await BatchAsync(1, Candidate("A.One", "unclassified"));
99
+ WriteVerdicts("batch-01.json", Verdict("A.One", "helper", "Value object."));
100
+
101
+ Assert.Equal(0, await MergeAsync(new StringWriter()));
102
+
103
+ var config = ExtractConfig.Load(PathOf("repo"));
104
+ Assert.Empty(config.Components);
105
+ Assert.Equal("Value object.", config.Helpers["A.One"]);
106
+ }
107
+
108
+ public static TheoryData<string, string> BadVerdicts => new()
109
+ {
110
+ { Verdict("A.One", "maybe"), "batch-01.json: A.One: verdict must be component or helper" },
111
+ { Verdict("A.One", "helper", ""), "batch-01.json: A.One: reason is empty" },
112
+ { Verdict("A.One", "helper", "x\\nkept Evil: already classified"), "batch-01.json: A.One: reason has control characters" },
113
+ { Verdict("A.One", "helper", new string('x', 201)), "batch-01.json: A.One: reason is 201 characters; keep it under 200" },
114
+ { Verdict("A.One", "helper", cite: "src/A.One.cs:12"), "batch-01.json: A.One: cite 'src/A.One.cs:12' is not inside its declaration" },
115
+ { Verdict("A.One", "helper", cite: "src/Other.cs:2"), "batch-01.json: A.One: cite 'src/Other.cs:2' is not inside its declaration" },
116
+ { Verdict("A.One", "helper") + "," + Verdict("A.Ghost", "helper"), "batch-01.json: A.Ghost: not a candidate of this batch" },
117
+ { Verdict("A.One", "helper") + "," + Verdict("A.One", "component"), "batch-01.json: A.One: more than one verdict" },
118
+ { "null", "batch-01.json: A.One: no verdict" },
119
+ };
120
+
121
+ [Theory]
122
+ [MemberData(nameof(BadVerdicts))]
123
+ public async Task A_verdict_breaking_the_contract_fails_the_merge_and_writes_nothing(string verdicts, string error)
124
+ {
125
+ await BatchAsync(1, Candidate("A.One", "unclassified"));
126
+ Directory.CreateDirectory(PathOf("verdicts"));
127
+ File.WriteAllText(PathOf("verdicts/batch-01.json"), $"[{verdicts}]");
128
+ var output = new StringWriter();
129
+
130
+ var exit = await MergeAsync(output);
131
+
132
+ Assert.Equal(3, exit);
133
+ Assert.Contains(error, output.ToString());
134
+ Assert.False(File.Exists(PathOf("repo/logical-components.json")));
135
+ }
136
+
137
+ [Fact]
138
+ public async Task A_missing_verdict_file_and_a_stale_one_from_an_earlier_round_both_fail()
139
+ {
140
+ await BatchAsync(1, Candidate("A.One", "unclassified"));
141
+ WriteVerdicts("batch-02.json", Verdict("A.Old", "helper"));
142
+ var output = new StringWriter();
143
+
144
+ var exit = await MergeAsync(output);
145
+
146
+ Assert.Equal(3, exit);
147
+ Assert.Contains("batch-01.json: no verdict file", output.ToString());
148
+ Assert.Contains("batch-02.json: verdicts for a batch that does not exist (left from an earlier round?)", output.ToString());
149
+ }
150
+
151
+ [Fact]
152
+ public async Task A_config_changed_after_batching_fails_the_merge_so_no_edit_bypasses_the_review()
153
+ {
154
+ await BatchAsync(1, Candidate("A.One", "unclassified"));
155
+ WriteVerdicts("batch-01.json", Verdict("A.One", "helper"));
156
+ File.WriteAllText(PathOf("repo/logical-components.json"), """{ "components": ["A.Sneaky"] }""");
157
+ var output = new StringWriter();
158
+
159
+ var exit = await MergeAsync(output);
160
+
161
+ Assert.Equal(3, exit);
162
+ Assert.Contains("logical-components.json changed since classify-batches", output.ToString());
163
+ }
164
+
165
+ [Fact]
166
+ public async Task A_missing_repo_directory_is_a_usage_error_not_a_crash()
167
+ {
168
+ await BatchAsync(1, Candidate("A.One", "unclassified"));
169
+ WriteVerdicts("batch-01.json", Verdict("A.One", "helper"));
170
+
171
+ var exit = await ClassifyMergeCommand.RunAsync(
172
+ ["--batches", PathOf("batches"), "--verdicts", PathOf("verdicts"), "--repo", PathOf("no-such-repo")], new StringWriter());
173
+
174
+ Assert.Equal(1, exit);
175
+ }
176
+ }
@@ -0,0 +1,190 @@
1
+ using System.Text;
2
+
3
+ namespace LogicalComponents.Tests;
4
+
5
+ [Collection(FixtureSolutions.Name)]
6
+ public class ComponentDiscoveryTests(SampleAppWorkspace sample)
7
+ {
8
+ private const string OutOfRepoWarning =
9
+ "declaration of Sample.Billing.Application.SharedClock outside the repo was ignored";
10
+
11
+ private static readonly ExtractConfig FixtureConfig = ExtractConfig.Load(FixturePaths.SampleApp);
12
+
13
+ private Task<DiscoveryResult> DiscoverAsync(ExtractConfig? config = null) =>
14
+ ComponentDiscovery.DiscoverAsync(sample.Loaded, config ?? FixtureConfig, TestContext.Current.CancellationToken);
15
+
16
+ [Fact]
17
+ public async Task Candidates_are_the_classes_and_records_with_methods_of_their_own_in_application_and_infrastructure()
18
+ {
19
+ var result = await ComponentDiscovery.CandidatesAsync(sample.Loaded, TestContext.Current.CancellationToken);
20
+
21
+ // The fixture config classifies every candidate, so the candidates are exactly its entries. Out: records with no
22
+ // method (OrderRequest, Shape, Circle, QueueEntry, Job) or only a ToString override (Money), OrderSnapshot
23
+ // (properties and a constructor), OrderOptions (*Options), OrderRejectedException (exception), the composition
24
+ // roots, FileHelper (file-local), interfaces and abstract classes (seams), Receipts.Builder (nested), SharedClock
25
+ // (declared outside the repo). In: records with methods, and Infrastructure classes whether reached or not.
26
+ var types = result.Candidates.Select(c => c.Type).ToList();
27
+ Assert.Equal(FixtureConfig.Components.Concat(FixtureConfig.Helpers.Keys).Order(StringComparer.Ordinal), types);
28
+ Assert.Contains("Sample.Application.PaymentMethod", types);
29
+ Assert.Contains("Sample.Application.Quantity", types);
30
+ Assert.Contains("Sample.Infrastructure.UnreachedPlumbing", types);
31
+ Assert.DoesNotContain(types, t => t is "Sample.Application.Money" or "Sample.Application.OrderSnapshot"
32
+ or "Sample.Application.OrderRequest" or "Sample.Application.Job" or "Sample.Application.Shape"
33
+ or "Sample.Application.OrderOptions" or "Sample.Application.StageBase" or "Sample.Infrastructure.QueueEntry"
34
+ or "Sample.Application.Hooks" or "Sample.Application.Registry" or "Sample.Application.Tracked");
35
+ Assert.Equal([OutOfRepoWarning], result.Warnings);
36
+ }
37
+
38
+ [Fact]
39
+ public async Task Components_are_exactly_the_candidates_the_config_classifies_as_components()
40
+ {
41
+ var result = await DiscoverAsync();
42
+
43
+ Assert.Equal(FixtureConfig.Components.Order(StringComparer.Ordinal), result.Components.Select(c => c.Type));
44
+ Assert.DoesNotContain(result.Components, c => FixtureConfig.Helpers.ContainsKey(c.Type));
45
+ Assert.Empty(result.Unclassified);
46
+ Assert.Equal([OutOfRepoWarning], result.Warnings);
47
+ }
48
+
49
+ [Fact]
50
+ public async Task Candidates_in_neither_list_are_unclassified_and_never_components()
51
+ {
52
+ var config = FixtureConfig with
53
+ {
54
+ Components = FixtureConfig.Components.Where(c => c != "Sample.Infrastructure.Mailer").ToList(),
55
+ Helpers = FixtureConfig.Helpers.Where(h => h.Key != "Sample.Application.Quantity").ToDictionary(),
56
+ };
57
+
58
+ var result = await DiscoverAsync(config);
59
+
60
+ Assert.Equal(["Sample.Application.Quantity", "Sample.Infrastructure.Mailer"], result.Unclassified);
61
+ Assert.DoesNotContain(result.Components, c => c.Type == "Sample.Infrastructure.Mailer");
62
+ }
63
+
64
+ [Fact]
65
+ public async Task Config_entries_naming_no_candidate_are_warnings_not_failures()
66
+ {
67
+ var config = FixtureConfig with
68
+ {
69
+ Components = [.. FixtureConfig.Components, "Sample.Application.NoSuchType", "Sample.Application.OrderRequest"],
70
+ Helpers = new Dictionary<string, string>(FixtureConfig.Helpers) { ["RunClock"] = "simple names do not match" },
71
+ };
72
+
73
+ var result = await DiscoverAsync(config);
74
+
75
+ Assert.Equal(
76
+ [
77
+ OutOfRepoWarning,
78
+ "config entry 'RunClock' names no candidate; ignored",
79
+ "config entry 'Sample.Application.NoSuchType' names no candidate; ignored",
80
+ "config entry 'Sample.Application.OrderRequest' names no candidate; ignored",
81
+ ],
82
+ result.Warnings);
83
+ Assert.Equal(FixtureConfig.Components.Count, result.Components.Count);
84
+ }
85
+
86
+ [Fact]
87
+ public async Task Clashing_simple_names_get_module_ids_and_names_while_others_stay_plain()
88
+ {
89
+ var result = await DiscoverAsync();
90
+ var byType = result.Components.ToDictionary(c => c.Type);
91
+
92
+ var sampleRunning = byType["Sample.Application.OrderRunning"];
93
+ Assert.Equal(("Sample_OrderRunning", "Order Running (Sample)", "Sample"),
94
+ (sampleRunning.Id, sampleRunning.Name, sampleRunning.Module));
95
+
96
+ var billingRunning = byType["Sample.Billing.Application.OrderRunning"];
97
+ Assert.Equal(("Billing_OrderRunning", "Order Running (Billing)", "Billing"),
98
+ (billingRunning.Id, billingRunning.Name, billingRunning.Module));
99
+
100
+ var acceptance = byType["Sample.Application.OrderAcceptance"];
101
+ Assert.Equal(("OrderAcceptance", "Order Acceptance"), (acceptance.Id, acceptance.Name));
102
+ }
103
+
104
+ [Fact]
105
+ public async Task Same_module_clashes_fall_back_to_the_full_type_name_without_generic_arity_in_the_display_name()
106
+ {
107
+ var result = await DiscoverAsync();
108
+ var byType = result.Components.ToDictionary(c => c.Type);
109
+
110
+ Assert.Equal(("Sample_Application_Cache", "Cache (Sample.Application.Cache)"),
111
+ (byType["Sample.Application.Cache"].Id, byType["Sample.Application.Cache"].Name));
112
+ Assert.Equal(("Sample_Application_Cache_1", "Cache (Sample.Application.Cache<>)"),
113
+ (byType["Sample.Application.Cache`1"].Id, byType["Sample.Application.Cache`1"].Name));
114
+ }
115
+
116
+ [Fact]
117
+ public async Task Spans_are_repo_relative_class_declaration_lines_from_real_source_files_only()
118
+ {
119
+ var result = await DiscoverAsync();
120
+ var byId = result.Components.ToDictionary(c => c.Id);
121
+
122
+ Assert.Equal([new SourceSpan("src/Sample.Application/OrderAcceptance.cs", 4, 17)], byId["OrderAcceptance"].Spans);
123
+ Assert.Equal(
124
+ [
125
+ new SourceSpan("src/Sample.Application/ReceiptPrinting.Totals.cs", 3, 6),
126
+ new SourceSpan("src/Sample.Application/ReceiptPrinting.cs", 4, 7),
127
+ ],
128
+ byId["ReceiptPrinting"].Spans);
129
+ // The [GeneratedRegex] half lives in generator output under obj/; it must not be a span.
130
+ Assert.Equal([new SourceSpan("src/Sample.Application/EdgeCases.cs", 13, 19)], byId["OrderNumberFormat"].Spans);
131
+ Assert.All(result.Components.SelectMany(c => c.Spans), s => Assert.StartsWith("src/", s.File));
132
+ }
133
+
134
+ [Fact]
135
+ public async Task Serialized_output_is_identical_across_runs_lf_only_bom_free_and_repo_relative()
136
+ {
137
+ var first = Serialize(await DiscoverAsync());
138
+ var second = Serialize(await DiscoverAsync());
139
+
140
+ Assert.Equal(first, second);
141
+ var text = Encoding.UTF8.GetString(first);
142
+ Assert.False(first.AsSpan().StartsWith(Encoding.UTF8.Preamble));
143
+ Assert.DoesNotContain("\r", text);
144
+ Assert.EndsWith("\n", text);
145
+ Assert.DoesNotContain(sample.Loaded.RepoRoot.Replace('\\', '/'), text, StringComparison.OrdinalIgnoreCase);
146
+ Assert.DoesNotContain(sample.Loaded.RepoRoot, text, StringComparison.OrdinalIgnoreCase);
147
+ }
148
+
149
+ [Fact]
150
+ public async Task Output_is_identical_for_the_same_code_checked_out_in_another_folder()
151
+ {
152
+ var copyRoot = Directory.CreateTempSubdirectory("lc-checkout-");
153
+ try
154
+ {
155
+ CopySource(FixturePaths.SampleApp, Path.Combine(copyRoot.FullName, "SampleApp"));
156
+ CopySource(Path.Combine(FixturePaths.Root, "SharedOutsideRepo"), Path.Combine(copyRoot.FullName, "SharedOutsideRepo"));
157
+ using var copy = await WorkspaceLoader.LoadAsync(Path.Combine(copyRoot.FullName, "SampleApp"));
158
+
159
+ var fromCopy = await ComponentDiscovery.DiscoverAsync(copy, FixtureConfig, TestContext.Current.CancellationToken);
160
+
161
+ Assert.Equal(
162
+ Encoding.UTF8.GetString(Serialize(await DiscoverAsync())),
163
+ Encoding.UTF8.GetString(Serialize(fromCopy)));
164
+ }
165
+ finally
166
+ {
167
+ copyRoot.Delete(recursive: true);
168
+ }
169
+ }
170
+
171
+ private static byte[] Serialize(DiscoveryResult result) =>
172
+ ComponentsFile.Serialize(new ExtractResult(result.Components, [], [], result.Warnings));
173
+
174
+ // Copies source files only: bin/obj hold absolute paths of the original checkout.
175
+ private static void CopySource(string from, string to)
176
+ {
177
+ foreach (var file in Directory.EnumerateFiles(from, "*", SearchOption.AllDirectories))
178
+ {
179
+ var relative = Path.GetRelativePath(from, file);
180
+ if (relative.Split(Path.DirectorySeparatorChar).Any(s => s is "bin" or "obj"))
181
+ {
182
+ continue;
183
+ }
184
+
185
+ var target = Path.Combine(to, relative);
186
+ Directory.CreateDirectory(Path.GetDirectoryName(target)!);
187
+ File.Copy(file, target);
188
+ }
189
+ }
190
+ }
@@ -0,0 +1,174 @@
1
+ namespace LogicalComponents.Tests;
2
+
3
+ [Collection(FixtureSolutions.Name)]
4
+ public class CouplingExtractionTests(SampleAppWorkspace sample)
5
+ {
6
+ private const string AuditTrailPortWarning = "port Sample.Application.IAuditTrail has no implementation";
7
+
8
+ private static readonly ExtractConfig FixtureConfig = ExtractConfig.Load(FixturePaths.SampleApp);
9
+
10
+ private Task<ExtractResult> ExtractAsync(ExtractConfig? config = null) =>
11
+ CouplingResolver.ExtractAsync(sample.Loaded, config ?? FixtureConfig, TestContext.Current.CancellationToken);
12
+
13
+ private static string Describe(Edge e) =>
14
+ $"{e.From} -> {e.To} [{string.Join(",", e.Kinds)}]" + (e.Via.Count > 0 ? $" via {string.Join(",", e.Via)}" : "");
15
+
16
+ [Fact]
17
+ public async Task Edges_are_every_way_a_component_runs_or_holds_another_seen_through_helpers_and_seams()
18
+ {
19
+ var result = await ExtractAsync();
20
+
21
+ // Not edges: anything from DependencyInjection (composition root), references to data (OrderRequest,
22
+ // QueueEntry), BCL/framework types (DateTime, Console, Enumerable, IReadOnlyList<string>, Lazy, Func), self calls
23
+ // (OrderNumberFormat.Digits, ShippingStage -> its base's Stamp), a call on a port nothing implements
24
+ // (IAuditTrail.Record), FakeOrderQueue (test project, never loaded), other constructions of a generic port
25
+ // (TextHandler), a static member on an interface (FallbackPricing -> DefaultPricing), a const read
26
+ // (LineCounting -> Limits), the top-level namesake of a nested type (Builder), a record property that runs
27
+ // nothing (JobCounting -> Job.Id), compiler-written record members (JobShowing -> Job.ToString/Equals), a
28
+ // property's setter when only its getter runs (TrackedReading), other constructions of a generic seam reached
29
+ // through a generic helper (PingSending -> PongHandling), and helpers themselves (LabelFormat, Dispatch, ...).
30
+ Assert.Equal(
31
+ [
32
+ "Archiving -> DiskStorage [call,di] via StorageBase",
33
+ "BatchRunning -> PriceFormatting [static]",
34
+ "BatchRunning -> RunClock [call,di,new]",
35
+ "CrashSettling -> ExportRecovery [call] via IRecovery",
36
+ "CrashSettling -> ImportRecovery [call] via IRecovery",
37
+ "CrashSettling -> RecoveryRouting [call,di]",
38
+ "Echoing -> PriceFormatting [static] via PingText",
39
+ "ForwardedNotifier -> Mailer [call,di]",
40
+ "JobReporting -> CallbackNormalizing [static] via Job",
41
+ "LabelMaking -> RunClock [call,new] via LabelBase,ShortLabel",
42
+ "Labelling -> RunClock [call] via LabelFormat",
43
+ "LazyWork -> ForwardedNotifier [di] via INotifier",
44
+ "LazyWork -> OrderQueue [di] via IOrderQueue",
45
+ "LazyWork -> OrderValidation [di]",
46
+ "LazyWork -> RunClock [di]",
47
+ "LoudNotifying -> Notifying [inherits]",
48
+ "Measuring -> PriceRules [static] via Metres",
49
+ "NotifyAll -> ForwardedNotifier [call,di] via INotifier",
50
+ "NotifyAll -> OrderTextExtensions [static]",
51
+ "OrderAcceptance -> ForwardedNotifier [call,di] via INotifier",
52
+ "OrderAcceptance -> OrderQueue [call,di] via IOrderQueue",
53
+ "OrderAcceptance -> OrderValidation [call,di]",
54
+ "OrderHandling -> LoggingHandler [call,di] via IHandler",
55
+ "OrderHandling -> OrderRequestHandler [call,di] via IHandler",
56
+ "OrderQueue -> QueueStore [call,new]",
57
+ "OrderValidation -> PriceRules [static]",
58
+ "PingSending -> PingHandling [call] via Dispatch,ICommandHandler",
59
+ "PongViaSeam -> PongHandling [call,di] via ICommandHandler",
60
+ "PongViaSeam -> RunClock [call] via ICommandHandler,TwoWay",
61
+ "ReceiptIssuing -> Receipts [call,new]",
62
+ "RecoveryRouting -> ExportRecovery [di] via IRecovery",
63
+ "RecoveryRouting -> ImportRecovery [di] via IRecovery",
64
+ "RegistryReading -> RunClock [call] via Registry",
65
+ "Sample_OrderRunning -> IAuditTrail [di]",
66
+ "Sample_OrderRunning -> RunClock [call,new]",
67
+ "ShippingStage -> PriceRules [static]",
68
+ "Stamping -> PriceRules [static] via StampedText",
69
+ "TickReading -> PriceRules [static] via Ticks",
70
+ "TitleReading -> DwgDrawing [call] via IDrawing",
71
+ "TrackedWriting -> CallbackNormalizing [static] via Tracked",
72
+ "UrgentNotifying -> Notifying [inherits]",
73
+ ],
74
+ result.Edges.Select(Describe));
75
+ }
76
+
77
+ [Fact]
78
+ public async Task An_edge_seen_through_a_helper_cites_the_line_in_the_component_where_the_chain_starts()
79
+ {
80
+ var result = await ExtractAsync();
81
+ var at = result.Edges.ToDictionary(e => (e.From, e.To), e => e.At);
82
+
83
+ Assert.Equal("src/Sample.Application/SeeThroughCases.cs:43", at[("JobReporting", "CallbackNormalizing")]);
84
+ Assert.Equal("src/Sample.Application/SeeThroughCases.cs:59", at[("Labelling", "RunClock")]);
85
+ Assert.Equal("src/Sample.Application/SeeThroughCases.cs:72", at[("Stamping", "PriceRules")]);
86
+ }
87
+
88
+ [Fact]
89
+ public async Task Each_component_reaches_exactly_the_helper_members_its_code_runs()
90
+ {
91
+ var result = await ExtractAsync();
92
+ var reaches = result.Components.ToDictionary(c => c.Id, c => c.Reaches ?? []);
93
+ const string file = "src/Sample.Application/SeeThroughCases.cs";
94
+
95
+ // The property of the record it reads, not the record's other members.
96
+ Assert.Equal([new SourceSpan(file, 33, 33)], reaches["JobReporting"]);
97
+ Assert.Equal([new SourceSpan(file, 54, 54)], reaches["Labelling"]);
98
+ // Constructing a helper runs its field initializers.
99
+ Assert.Equal([new SourceSpan(file, 65, 65)], reaches["Stamping"]);
100
+ Assert.Empty(reaches["PriceRules"]);
101
+ }
102
+
103
+ [Fact]
104
+ public async Task Reaches_hold_only_code_that_runs_for_the_use()
105
+ {
106
+ var result = await ExtractAsync();
107
+ var reaches = result.Components.ToDictionary(c => c.Id, c => c.Reaches ?? []);
108
+ const string file = "src/Sample.Application/ReviewFixCases.cs";
109
+
110
+ // The Pong handler of a helper implementing two constructions, not the Ping one listed first.
111
+ Assert.Equal([new SourceSpan(file, 36, 39)], reaches["PongViaSeam"]);
112
+ // Compiler-written record members and a getter-only read reach nothing.
113
+ Assert.Empty(reaches["JobShowing"]);
114
+ Assert.Single(reaches["TrackedReading"]);
115
+ Assert.DoesNotContain(reaches["TrackedReading"], s => s.Start == 63);
116
+ }
117
+
118
+ [Fact]
119
+ public async Task An_inherits_edge_cites_the_base_list_of_the_partial_that_declares_it()
120
+ {
121
+ var result = await ExtractAsync();
122
+
123
+ Assert.Equal(
124
+ "src/Sample.Application/LoudNotifying.Base.cs:3",
125
+ result.Edges.Single(e => (e.From, e.To) == ("LoudNotifying", "Notifying")).At);
126
+ }
127
+
128
+ [Fact]
129
+ public async Task Every_classified_component_is_a_node_whether_anything_reaches_it_or_not()
130
+ {
131
+ var result = await ExtractAsync();
132
+
133
+ Assert.Equal(FixtureConfig.Components.Order(StringComparer.Ordinal), result.Components.Select(c => c.Type));
134
+ Assert.Contains(result.Components, c => c.Type == "Sample.Infrastructure.UnreachedPlumbing");
135
+ }
136
+
137
+ [Fact]
138
+ public async Task A_port_with_no_implementation_is_an_external_node_with_a_warning()
139
+ {
140
+ var result = await ExtractAsync();
141
+
142
+ Assert.Equal([new ExternalNode("IAuditTrail", "Sample.Application.IAuditTrail", "I Audit Trail", "port")], result.ExternalNodes);
143
+ Assert.Contains(AuditTrailPortWarning, result.Warnings);
144
+ }
145
+
146
+ [Fact]
147
+ public async Task Each_edge_cites_its_first_occurrence()
148
+ {
149
+ var result = await ExtractAsync();
150
+ var at = result.Edges.ToDictionary(e => (e.From, e.To), e => e.At);
151
+
152
+ Assert.Equal("src/Sample.Application/OrderAcceptance.cs:4", at[("OrderAcceptance", "OrderQueue")]);
153
+ Assert.Equal("src/Sample.Application/OrderRunning.cs:15", at[("Sample_OrderRunning", "RunClock")]);
154
+ Assert.Equal("src/Sample.Application/CouplingCases.cs:5", at[("BatchRunning", "RunClock")]);
155
+ }
156
+
157
+ [Fact]
158
+ public async Task Serialized_extraction_is_identical_across_runs()
159
+ {
160
+ Assert.Equal(ComponentsFile.Serialize(await ExtractAsync()), ComponentsFile.Serialize(await ExtractAsync()));
161
+ }
162
+
163
+ [Fact]
164
+ public async Task A_repo_that_does_not_compile_is_extracted_with_a_warning_per_broken_project()
165
+ {
166
+ using var broken = await WorkspaceLoader.LoadAsync(FixturePaths.BrokenApp);
167
+
168
+ var config = ExtractConfig.Empty with { Components = ["Broken.Application.Broken"] };
169
+ var result = await CouplingResolver.ExtractAsync(broken, config, TestContext.Current.CancellationToken);
170
+
171
+ Assert.Equal(["project Broken.Application has 1 compile error(s); coupling may be incomplete"], result.Warnings);
172
+ Assert.Equal(["Broken.Application.Broken"], result.Components.Select(c => c.Type));
173
+ }
174
+ }
@@ -0,0 +1,17 @@
1
+ namespace LogicalComponents.Tests;
2
+
3
+ public class DisplayNameTests
4
+ {
5
+ [Theory]
6
+ [InlineData("ExportJobAcceptance", "Export Job Acceptance")]
7
+ [InlineData("OgcServer", "Ogc Server")]
8
+ [InlineData("IOStream", "IO Stream")]
9
+ [InlineData("GeoServer", "Geo Server")]
10
+ [InlineData("Utf8Decoding", "Utf8 Decoding")]
11
+ [InlineData("HTTPClientFactory", "HTTP Client Factory")]
12
+ [InlineData("Run", "Run")]
13
+ public void Pascal_case_splits_into_words_keeping_acronyms_together(string typeName, string expected)
14
+ {
15
+ Assert.Equal(expected, DisplayName.FromTypeName(typeName));
16
+ }
17
+ }