@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,27 @@
1
+ using LogicalComponents;
2
+
3
+ // Entry point: dispatches to one command per pipeline step. Anything unknown prints usage and exits 1.
4
+ // candidates, extract and render report on stderr (their artifact is a file); the others report on stdout.
5
+ return args switch
6
+ {
7
+ ["candidates", .. var rest] => await CandidatesCommand.RunAsync(rest, Console.Error),
8
+ ["classify-batches", .. var rest] => await ClassifyBatchesCommand.RunAsync(rest, Console.Out),
9
+ ["classify-merge", .. var rest] => await ClassifyMergeCommand.RunAsync(rest, Console.Out),
10
+ ["extract", .. var rest] => await ExtractCommand.RunAsync(rest, Console.Error),
11
+ ["batches", .. var rest] => await BatchesCommand.RunAsync(rest, Console.Out),
12
+ ["prune", .. var rest] => await PruneCommand.RunAsync(rest, Console.Out),
13
+ ["render", .. var rest] => await RenderCommand.RunAsync(rest, Console.Error),
14
+ _ => PrintUsage(),
15
+ };
16
+
17
+ static int PrintUsage()
18
+ {
19
+ Console.Error.WriteLine(CandidatesCommand.Usage);
20
+ Console.Error.WriteLine(ClassifyBatchesCommand.Usage);
21
+ Console.Error.WriteLine(ClassifyMergeCommand.Usage);
22
+ Console.Error.WriteLine(ExtractCommand.Usage);
23
+ Console.Error.WriteLine(BatchesCommand.Usage);
24
+ Console.Error.WriteLine(PruneCommand.Usage);
25
+ Console.Error.WriteLine(RenderCommand.Usage);
26
+ return 1;
27
+ }
@@ -0,0 +1,76 @@
1
+ using System.Text.RegularExpressions;
2
+
3
+ namespace LogicalComponents;
4
+
5
+ /// <summary>
6
+ /// Pure rules for which loaded projects the extractor keeps: no test projects, and one project per
7
+ /// multi-targeted csproj. Kept free of Roslyn types so they are unit-testable from plain strings.
8
+ /// </summary>
9
+ public static partial class ProjectFilters
10
+ {
11
+ /// <summary>
12
+ /// A test project lives under a <c>tests</c>/<c>test</c> folder, is named like one
13
+ /// (<c>.Tests</c>, <c>.UnitTests</c>, <c>.IntegrationTests</c>, <c>.Test</c>, <c>.Specs</c>), or its project
14
+ /// file carries a test marker (test SDK, <c>IsTestProject</c>, or a test-framework package).
15
+ /// Test-support libraries such as <c>Foo.Testing</c> are deliberately kept: nothing here can tell them apart
16
+ /// from source, and their types can be excluded through config.
17
+ /// </summary>
18
+ public static bool IsTestProject(string relativePath, string projectFileText)
19
+ {
20
+ var segments = relativePath.Split('/', '\\');
21
+ if (segments.Any(s => s.Equals("tests", StringComparison.OrdinalIgnoreCase)
22
+ || s.Equals("test", StringComparison.OrdinalIgnoreCase)))
23
+ {
24
+ return true;
25
+ }
26
+
27
+ return TestProjectName().IsMatch(Path.GetFileNameWithoutExtension(relativePath))
28
+ || TestProjectMarker().IsMatch(projectFileText);
29
+ }
30
+
31
+ /// <summary>
32
+ /// Orders the target framework in a loaded multi-target project name such as <c>Foo(net10.0)</c>.
33
+ /// Families rank .NET Framework &lt; netstandard &lt; netcoreapp &lt; net5+; versions order within a family.
34
+ /// </summary>
35
+ public static (int Family, Version Version) TargetFrameworkRank(string loadedProjectName)
36
+ {
37
+ var match = TargetFrameworkSuffix().Match(loadedProjectName);
38
+ if (!match.Success)
39
+ {
40
+ return (0, new Version(0, 0));
41
+ }
42
+
43
+ // Drop an OS suffix: net9.0-windows ranks as net9.0.
44
+ var tfm = match.Groups["tfm"].Value.ToLowerInvariant().Split('-')[0];
45
+ return tfm switch
46
+ {
47
+ _ when tfm.StartsWith("netstandard") => (1, ParseVersion(tfm["netstandard".Length..])),
48
+ _ when tfm.StartsWith("netcoreapp") => (2, ParseVersion(tfm["netcoreapp".Length..])),
49
+ _ when tfm.Contains('.') => (3, ParseVersion(tfm["net".Length..])),
50
+ // .NET Framework monikers pack the version into digits: net472 is 4.7.2.
51
+ _ => (0, ParseVersion(string.Join('.', tfm["net".Length..].ToCharArray()))),
52
+ };
53
+ }
54
+
55
+ /// <summary><c>Foo(net10.0)</c> becomes <c>Foo</c>; a name without a framework suffix is returned as-is.</summary>
56
+ public static string StripTargetFramework(string loadedProjectName)
57
+ {
58
+ var match = TargetFrameworkSuffix().Match(loadedProjectName);
59
+ return match.Success ? match.Groups["name"].Value : loadedProjectName;
60
+ }
61
+
62
+ private static Version ParseVersion(string text) =>
63
+ Version.TryParse(text.Contains('.') ? text : text + ".0", out var version) ? version : new Version(0, 0);
64
+
65
+ // Case-sensitive on purpose: "Foo.Contest" and "Latest.Feeds" must not look like test projects.
66
+ [GeneratedRegex(@"\.\w*Tests?$|\.Specs?$")]
67
+ private static partial Regex TestProjectName();
68
+
69
+ [GeneratedRegex(
70
+ @"<IsTestProject>\s*true\s*</IsTestProject>|Sdk=""MSTest\.Sdk|Include=""(Microsoft\.NET\.Test\.Sdk|xunit|xunit\.v3|xunit\.core|xunit\.v3\.core|NUnit|MSTest|MSTest\.TestFramework|Microsoft\.Testing\.Platform)""",
71
+ RegexOptions.IgnoreCase)]
72
+ private static partial Regex TestProjectMarker();
73
+
74
+ [GeneratedRegex(@"^(?<name>.+)\((?<tfm>net[^)]*)\)$")]
75
+ private static partial Regex TargetFrameworkSuffix();
76
+ }
@@ -0,0 +1,132 @@
1
+ using System.Text.Json;
2
+
3
+ namespace LogicalComponents;
4
+
5
+ /// <summary>
6
+ /// <c>prune --resp &lt;dir&gt; --verify &lt;dir&gt; --out &lt;dir&gt;</c>: applies the verifier agents' verdicts. For every
7
+ /// <c>resp/batch-NN.json</c> it reads <c>verify/batch-NN.json</c> — <c>{ "&lt;id&gt;": [ { "item": 1, "supported": true,
8
+ /// "reason": "..." } ] }</c> — and writes the batch without its unsupported items to <c>--out</c>. The agents' files are
9
+ /// never edited, so pruning is repeatable. Every item needs exactly one verdict: an unjudged claim is not verified, so
10
+ /// any gap fails the step (exit 3) before anything is written. Exit codes: 0 written, 1 usage, 3 verdicts incomplete.
11
+ /// </summary>
12
+ public static class PruneCommand
13
+ {
14
+ public const string Usage = "usage: LogicalComponents prune --resp <dir> --verify <dir> --out <dir>";
15
+
16
+ public sealed record Verdict(int Item, bool Supported, string? Reason);
17
+
18
+ private static readonly JsonSerializerOptions ReadOptions = new()
19
+ {
20
+ PropertyNameCaseInsensitive = true,
21
+ ReadCommentHandling = JsonCommentHandling.Skip,
22
+ AllowTrailingCommas = true,
23
+ AllowDuplicateProperties = false,
24
+ };
25
+
26
+ public static async Task<int> RunAsync(string[] args, TextWriter output)
27
+ {
28
+ if (args is not ["--resp", var respDir, "--verify", var verifyDir, "--out", var outDir]
29
+ || !Directory.Exists(respDir) || !Directory.Exists(verifyDir))
30
+ {
31
+ await output.WriteLineAsync(Usage);
32
+ return 1;
33
+ }
34
+
35
+ var problems = new List<string>();
36
+ var pruned = new List<(string File, SortedDictionary<string, List<Responsibility>> Kept)>();
37
+ var removed = new List<string>();
38
+ var emptied = new List<string>();
39
+
40
+ foreach (var respFile in Directory.EnumerateFiles(respDir, "batch-*.json").Order(StringComparer.Ordinal))
41
+ {
42
+ var name = Path.GetFileName(respFile);
43
+ var batch = ResponsibilityBatches.Load([respFile], ReadOptions);
44
+ problems.AddRange(batch.Problems);
45
+ var verdicts = ReadVerdicts(Path.Combine(verifyDir, name), name, problems);
46
+ if (verdicts is null)
47
+ {
48
+ continue;
49
+ }
50
+
51
+ var kept = new SortedDictionary<string, List<Responsibility>>(StringComparer.Ordinal);
52
+ foreach (var (id, items) in batch.Responsibilities.OrderBy(r => r.Key, StringComparer.Ordinal))
53
+ {
54
+ var judged = verdicts.GetValueOrDefault(id) ?? [];
55
+ problems.AddRange(judged.Where(v => v.Item < 1 || v.Item > items.Count)
56
+ .Select(v => $"{id}[{v.Item}]: verdict for an item that does not exist ({name})"));
57
+
58
+ kept[id] = [];
59
+ for (var item = 1; item <= items.Count; item++)
60
+ {
61
+ var forItem = judged.Where(v => v.Item == item).ToList();
62
+ if (forItem.Count != 1)
63
+ {
64
+ problems.Add($"{id}[{item}]: {(forItem.Count == 0 ? "no verdict" : "more than one verdict")} ({name})");
65
+ }
66
+ else if (forItem[0].Supported)
67
+ {
68
+ kept[id].Add(items[item - 1]);
69
+ }
70
+ else
71
+ {
72
+ removed.Add($"removed {id}[{item}]: {forItem[0].Reason ?? "(no reason given)"}");
73
+ }
74
+ }
75
+
76
+ if (items.Count > 0 && kept[id].Count == 0)
77
+ {
78
+ emptied.Add($"empty {id}: every responsibility was unsupported");
79
+ }
80
+ }
81
+
82
+ problems.AddRange(verdicts.Keys.Where(id => !batch.Responsibilities.ContainsKey(id))
83
+ .Order(StringComparer.Ordinal)
84
+ .Select(id => $"{id}: verdicts for a component the batch does not answer ({name})"));
85
+ pruned.Add((name, kept));
86
+ }
87
+
88
+ if (problems.Count > 0)
89
+ {
90
+ foreach (var problem in problems)
91
+ {
92
+ await output.WriteLineAsync(problem);
93
+ }
94
+
95
+ return 3;
96
+ }
97
+
98
+ Directory.CreateDirectory(outDir);
99
+ foreach (var (file, kept) in pruned)
100
+ {
101
+ await File.WriteAllBytesAsync(Path.Combine(outDir, file), ComponentsFile.ToJson(kept));
102
+ }
103
+
104
+ foreach (var line in removed.Concat(emptied))
105
+ {
106
+ await output.WriteLineAsync(line);
107
+ }
108
+
109
+ await output.WriteLineAsync($"{removed.Count} removed, {emptied.Count} empty");
110
+ return 0;
111
+ }
112
+
113
+ private static Dictionary<string, List<Verdict>>? ReadVerdicts(string path, string name, List<string> problems)
114
+ {
115
+ if (!File.Exists(path))
116
+ {
117
+ problems.Add($"{name}: no verdict file");
118
+ return null;
119
+ }
120
+
121
+ try
122
+ {
123
+ var verdicts = JsonSerializer.Deserialize<Dictionary<string, List<Verdict>?>>(File.ReadAllText(path), ReadOptions) ?? [];
124
+ return verdicts.ToDictionary(v => v.Key, v => v.Value ?? [], StringComparer.Ordinal);
125
+ }
126
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
127
+ {
128
+ problems.Add($"{name}: not a valid verdict file ({e.Message})");
129
+ return null;
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,173 @@
1
+ using System.Text;
2
+ using System.Text.Json;
3
+ using Microsoft.CodeAnalysis.Text;
4
+
5
+ namespace LogicalComponents;
6
+
7
+ /// <summary>
8
+ /// <c>render --components &lt;components.json&gt; --resp &lt;dir|file&gt;... --repo &lt;repoPath&gt; --out &lt;md&gt;</c>.
9
+ /// Merges the agents' batch files (a directory means every <c>batch-*.json</c> in it, read in name order), validates them
10
+ /// against the extraction and the repo's files, and writes the markdown only when everything is valid.
11
+ /// Exit codes: 0 written; 1 usage; 2 inputs unusable (unreadable components.json, a missing path, a repo that does not
12
+ /// match components.json, an unwritable --out); 3 responsibilities rejected, every problem on stderr and no --out left.
13
+ /// </summary>
14
+ public static class RenderCommand
15
+ {
16
+ public const string Usage =
17
+ "usage: LogicalComponents render --components <components.json> --resp <dir|file>... --repo <repoPath> --out <md>";
18
+
19
+ private static readonly JsonSerializerOptions ReadOptions = new()
20
+ {
21
+ PropertyNameCaseInsensitive = true,
22
+ ReadCommentHandling = JsonCommentHandling.Skip,
23
+ AllowTrailingCommas = true,
24
+ // The same component twice in one file must not silently keep the last list.
25
+ AllowDuplicateProperties = false,
26
+ };
27
+
28
+ private sealed record Arguments(string Components, IReadOnlyList<string> Resp, string Repo, string Out);
29
+
30
+ public static async Task<int> RunAsync(string[] args, TextWriter errors)
31
+ {
32
+ var arguments = ParseArgs(args);
33
+ if (arguments is null)
34
+ {
35
+ await errors.WriteLineAsync(Usage);
36
+ return 1;
37
+ }
38
+
39
+ var extraction = await ReadExtractionAsync(arguments.Components, errors);
40
+ if (extraction is null)
41
+ {
42
+ return 2;
43
+ }
44
+
45
+ var missing = arguments.Resp.Where(p => !Directory.Exists(p) && !File.Exists(p))
46
+ .Concat(Directory.Exists(arguments.Repo) ? [] : [arguments.Repo])
47
+ .ToList();
48
+ if (missing.Count > 0)
49
+ {
50
+ await errors.WriteLineAsync($"Not found: {string.Join(", ", missing)}");
51
+ return 2;
52
+ }
53
+
54
+ // Checked before any citation: a repo that does not match components.json is not the agents' fault.
55
+ var sourceLines = ReadSourceLines(extraction, arguments.Repo, out var missingSources);
56
+ if (missingSources.Count > 0)
57
+ {
58
+ await errors.WriteLineAsync(
59
+ $"The repo does not match components.json (re-run extract); missing: {string.Join(", ", missingSources)}");
60
+ return 2;
61
+ }
62
+
63
+ var batches = ResponsibilityBatches.Load(arguments.Resp, ReadOptions);
64
+ var problems = batches.Problems
65
+ .Concat(ResponsibilityValidation.Validate(extraction, batches.Responsibilities, sourceLines, batches.FileOf))
66
+ .ToList();
67
+ if (problems.Count > 0)
68
+ {
69
+ foreach (var problem in problems)
70
+ {
71
+ await errors.WriteLineAsync(problem);
72
+ }
73
+
74
+ // Never leave a document from an earlier run looking like this run's result.
75
+ if (File.Exists(arguments.Out))
76
+ {
77
+ File.Delete(arguments.Out);
78
+ }
79
+
80
+ return 3;
81
+ }
82
+
83
+ try
84
+ {
85
+ Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(arguments.Out))!);
86
+ var markdown = MarkdownRenderer.Render(extraction, batches.Responsibilities);
87
+ await File.WriteAllBytesAsync(arguments.Out, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false).GetBytes(markdown));
88
+ return 0;
89
+ }
90
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException)
91
+ {
92
+ await errors.WriteLineAsync($"Cannot write {arguments.Out}: {e.Message}");
93
+ return 2;
94
+ }
95
+ }
96
+
97
+ private static async Task<ExtractResult?> ReadExtractionAsync(string path, TextWriter errors)
98
+ {
99
+ try
100
+ {
101
+ var extraction = ComponentsFile.Deserialize(await File.ReadAllBytesAsync(path));
102
+ // Deserialization leaves absent lists null despite the non-nullable types: `{}` is not a components.json.
103
+ if (extraction.Components is not null && extraction.ExternalNodes is not null
104
+ && extraction.Edges is not null && extraction.Warnings is not null)
105
+ {
106
+ return extraction;
107
+ }
108
+
109
+ await errors.WriteLineAsync($"{path} is not a components.json written by extract");
110
+ }
111
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
112
+ {
113
+ await errors.WriteLineAsync($"Cannot read {path}: {e.Message}");
114
+ }
115
+
116
+ return null;
117
+ }
118
+
119
+ /// <summary>
120
+ /// The lines of every file a component is declared in, split the way Roslyn splits them (so U+2028 and friends count
121
+ /// exactly as they did when extract measured the spans). Files missing from the repo are reported, not read.
122
+ /// </summary>
123
+ private static Dictionary<string, IReadOnlyList<string>> ReadSourceLines(
124
+ ExtractResult extraction, string repo, out List<string> missing)
125
+ {
126
+ var lines = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
127
+ missing = [];
128
+ var files = extraction.Components.SelectMany(c => c.Spans.Concat(c.Reaches ?? []).Select(s => s.File));
129
+ foreach (var file in files.Distinct().Order(StringComparer.Ordinal))
130
+ {
131
+ var path = Path.Combine(repo, file);
132
+ if (!File.Exists(path))
133
+ {
134
+ missing.Add(file);
135
+ continue;
136
+ }
137
+
138
+ var text = SourceText.From(File.ReadAllText(path));
139
+ lines[file] = text.Lines.Select(l => text.ToString(l.Span)).ToList();
140
+ }
141
+
142
+ return lines;
143
+ }
144
+
145
+ /// <summary>Each flag exactly once; <c>--resp</c> takes every following value up to the next flag.</summary>
146
+ private static Arguments? ParseArgs(string[] args)
147
+ {
148
+ var values = new Dictionary<string, List<string>>(StringComparer.Ordinal);
149
+ for (var i = 0; i < args.Length; i++)
150
+ {
151
+ var flag = args[i];
152
+ if (flag is not ("--components" or "--resp" or "--repo" or "--out") || values.ContainsKey(flag))
153
+ {
154
+ return null;
155
+ }
156
+
157
+ var taken = new List<string>();
158
+ while (i + 1 < args.Length && !args[i + 1].StartsWith("--", StringComparison.Ordinal))
159
+ {
160
+ taken.Add(args[++i]);
161
+ }
162
+
163
+ values[flag] = taken;
164
+ }
165
+
166
+ string? Single(string flag) => values.TryGetValue(flag, out var v) && v.Count == 1 ? v[0] : null;
167
+ var resp = values.GetValueOrDefault("--resp") ?? [];
168
+ return Single("--components") is { } components && Single("--repo") is { } repo && Single("--out") is { } output
169
+ && resp.Count > 0
170
+ ? new Arguments(components, resp, repo, output)
171
+ : null;
172
+ }
173
+ }
@@ -0,0 +1,84 @@
1
+ using System.Text.Json;
2
+
3
+ namespace LogicalComponents;
4
+
5
+ /// <summary>
6
+ /// The agents' batch files merged by component id, with every problem found while reading them. A component answered in
7
+ /// two files keeps the first file's answer (so validation can still report everything else) and is a problem; a null
8
+ /// list reads as no responsibilities; a null item is a problem naming its file.
9
+ /// </summary>
10
+ public sealed record ResponsibilityBatches(
11
+ Dictionary<string, IReadOnlyList<Responsibility>> Responsibilities,
12
+ Dictionary<string, string> FileOf,
13
+ List<string> Problems)
14
+ {
15
+ /// <summary>A directory contributes its <c>batch-*.json</c> files in name order; any other path is read as a file.</summary>
16
+ public static ResponsibilityBatches Load(IEnumerable<string> paths, JsonSerializerOptions options)
17
+ {
18
+ var files = paths
19
+ .SelectMany(p => Directory.Exists(p)
20
+ ? Directory.EnumerateFiles(p, "batch-*.json").Order(StringComparer.Ordinal)
21
+ : (IEnumerable<string>)[p])
22
+ .ToList();
23
+ var batches = new ResponsibilityBatches(new(StringComparer.Ordinal), new(StringComparer.Ordinal), []);
24
+ var sources = new Dictionary<string, List<string>>(StringComparer.Ordinal);
25
+
26
+ foreach (var file in files)
27
+ {
28
+ var name = Path.GetFileName(file);
29
+ Dictionary<string, List<Responsibility?>?>? content;
30
+ try
31
+ {
32
+ content = JsonSerializer.Deserialize<Dictionary<string, List<Responsibility?>?>>(File.ReadAllText(file), options);
33
+ }
34
+ catch (Exception e) when (e is IOException or UnauthorizedAccessException or JsonException)
35
+ {
36
+ batches.Problems.Add($"{name}: not a valid responsibilities file ({e.Message})");
37
+ continue;
38
+ }
39
+
40
+ foreach (var (id, items) in content ?? [])
41
+ {
42
+ if (!sources.TryGetValue(id, out var seenIn))
43
+ {
44
+ seenIn = [];
45
+ sources[id] = seenIn;
46
+ }
47
+
48
+ seenIn.Add(name);
49
+ if (seenIn.Count > 1)
50
+ {
51
+ continue;
52
+ }
53
+
54
+ batches.FileOf[id] = name;
55
+ batches.Responsibilities[id] = Clean(id, name, items ?? [], batches.Problems);
56
+ }
57
+ }
58
+
59
+ batches.Problems.AddRange(sources
60
+ .Where(s => s.Value.Count > 1)
61
+ .OrderBy(s => s.Key, StringComparer.Ordinal)
62
+ .Select(s => $"{s.Key}: responsibilities in more than one file ({string.Join(", ", s.Value)})"));
63
+ return batches;
64
+ }
65
+
66
+ // Null items become problems; null fields become empty strings so validation reports them as empty text or bad cites.
67
+ private static List<Responsibility> Clean(string id, string file, List<Responsibility?> items, List<string> problems)
68
+ {
69
+ var clean = new List<Responsibility>();
70
+ for (var index = 0; index < items.Count; index++)
71
+ {
72
+ if (items[index] is { } item)
73
+ {
74
+ clean.Add(new Responsibility(item.Text ?? "", item.Cite ?? ""));
75
+ }
76
+ else
77
+ {
78
+ problems.Add($"{id}[{index + 1}]: item is null ({file})");
79
+ }
80
+ }
81
+
82
+ return clean;
83
+ }
84
+ }
@@ -0,0 +1,178 @@
1
+ using System.Text.RegularExpressions;
2
+
3
+ namespace LogicalComponents;
4
+
5
+ /// <summary>
6
+ /// Checks the agents' responsibilities against the deterministic extraction before anything is rendered. Agents may
7
+ /// only describe the components extract found; every component needs at least one responsibility; each is one plain
8
+ /// line of text, said once; and its citation must cover real code inside that component's own declaration. All problems
9
+ /// are reported at once, sorted by component id then item, so a retry can fix everything in one pass.
10
+ /// </summary>
11
+ public static partial class ResponsibilityValidation
12
+ {
13
+ public const int MaxCitedLines = 60;
14
+ public const int MaxTextLength = 300;
15
+
16
+ /// <param name="sourceLines">The lines of every file a component is declared in, keyed by repo-relative path.</param>
17
+ /// <param name="fileOf">Which batch file each responsibility id came from, to route unknown-id errors.</param>
18
+ public static List<string> Validate(
19
+ ExtractResult extraction,
20
+ IReadOnlyDictionary<string, IReadOnlyList<Responsibility>> responsibilities,
21
+ IReadOnlyDictionary<string, IReadOnlyList<string>> sourceLines,
22
+ IReadOnlyDictionary<string, string>? fileOf = null)
23
+ {
24
+ var errors = new List<(string Id, int Item, string Message)>();
25
+ var components = extraction.Components.ToDictionary(c => c.Id, StringComparer.Ordinal);
26
+
27
+ foreach (var id in responsibilities.Keys.Where(id => !components.ContainsKey(id)))
28
+ {
29
+ errors.Add((id, 0, UnknownId(id, components.Keys, fileOf)));
30
+ }
31
+
32
+ foreach (var component in extraction.Components)
33
+ {
34
+ if (!responsibilities.TryGetValue(component.Id, out var items) || items.Count == 0)
35
+ {
36
+ errors.Add((component.Id, 0, $"{component.Id}: no responsibilities"));
37
+ continue;
38
+ }
39
+
40
+ var seen = new Dictionary<string, int>(StringComparer.Ordinal);
41
+ for (var index = 0; index < items.Count; index++)
42
+ {
43
+ var item = index + 1;
44
+ var prefix = $"{component.Id}[{item}]: ";
45
+ if (TextProblem(items[index].Text) is { } textProblem)
46
+ {
47
+ errors.Add((component.Id, item, prefix + textProblem));
48
+ }
49
+ else if (!seen.TryAdd(Normalize(items[index].Text), item))
50
+ {
51
+ errors.Add((component.Id, item, $"{prefix}repeats item {seen[Normalize(items[index].Text)]}"));
52
+ }
53
+
54
+ if (CiteProblem(items[index].Cite, component, sourceLines) is { } citeProblem)
55
+ {
56
+ errors.Add((component.Id, item, prefix + citeProblem));
57
+ }
58
+ }
59
+ }
60
+
61
+ return errors
62
+ .OrderBy(e => e.Id, StringComparer.Ordinal)
63
+ .ThenBy(e => e.Item)
64
+ .Select(e => e.Message)
65
+ .ToList();
66
+ }
67
+
68
+ private static string UnknownId(string id, IEnumerable<string> componentIds, IReadOnlyDictionary<string, string>? fileOf)
69
+ {
70
+ var message = $"{id}: not a component in components.json";
71
+ if (fileOf?.TryGetValue(id, out var file) == true)
72
+ {
73
+ message += $" ({file})";
74
+ }
75
+
76
+ var match = componentIds.FirstOrDefault(c => string.Equals(c, id, StringComparison.OrdinalIgnoreCase));
77
+ return match is null ? message : $"{message}; did you mean '{match}'?";
78
+ }
79
+
80
+ /// <summary>One plain markdown line: no HTML or links (they would render as something else), balanced code spans.</summary>
81
+ private static string? TextProblem(string text)
82
+ {
83
+ if (string.IsNullOrWhiteSpace(text))
84
+ {
85
+ return "text is empty";
86
+ }
87
+
88
+ if (text.Contains('\n') || text.Contains('\r'))
89
+ {
90
+ return "text must be a single line";
91
+ }
92
+
93
+ if (text.Trim().Length > MaxTextLength)
94
+ {
95
+ return $"text is {text.Trim().Length} characters; keep it under {MaxTextLength}";
96
+ }
97
+
98
+ if (text.Count(c => c == '`') % 2 != 0)
99
+ {
100
+ return "text has an unclosed ` code span";
101
+ }
102
+
103
+ var outsideCode = CodeSpan().Replace(text, "");
104
+ if (HtmlTag().IsMatch(outsideCode))
105
+ {
106
+ return "text contains HTML; put code in `backticks`";
107
+ }
108
+
109
+ return outsideCode.Contains("](") ? "text contains a link" : null;
110
+ }
111
+
112
+ private static string? CiteProblem(
113
+ string cite, Component component, IReadOnlyDictionary<string, IReadOnlyList<string>> sourceLines)
114
+ {
115
+ if (Citation.Parse(cite) is not { } citation)
116
+ {
117
+ return $"cite '{cite}' is not <file>:<line> or <file>:<start>-<end>";
118
+ }
119
+
120
+ var citable = component.Spans.Concat(component.Reaches ?? []).ToList();
121
+ var spans = citable.Where(s => s.File == citation.File).ToList();
122
+ if (spans.Count == 0)
123
+ {
124
+ return $"cite '{cite}' is not in the component's files: {string.Join(", ", citable.Select(s => s.File).Distinct())}";
125
+ }
126
+
127
+ // The render command checks every span file exists before validating; a missing one here is a caller bug.
128
+ var lines = sourceLines[citation.File];
129
+ if (citation.End > lines.Count)
130
+ {
131
+ return $"cite '{cite}' is past the end of the file ({lines.Count} lines)";
132
+ }
133
+
134
+ // Every code line must be citable, and the first and last lines must lie inside a citable span. Lines between
135
+ // two citable members (blank, comment, brace) are not code, so a citation may run across neighbouring helper
136
+ // members without claiming anything uncited, but it cannot start or end outside them.
137
+ bool Citable(int line) => spans.Any(s => s.Start <= line && line <= s.End);
138
+ if (Enumerable.Range(citation.Start, citation.LineCount).Any(n => !Citable(n) && (IsCode(lines[n - 1]) || n == citation.Start || n == citation.End)))
139
+ {
140
+ var where = component.Reaches is { Count: > 0 }
141
+ ? "the component's declaration and the helper code it runs"
142
+ : "the component's declaration";
143
+ return $"cite '{cite}' is outside {where} ({string.Join(", ", spans.Select(s => $"{s.Start}-{s.End}"))})";
144
+ }
145
+
146
+ if (citation.LineCount > MaxCitedLines)
147
+ {
148
+ return $"cite '{cite}' spans {citation.LineCount} lines; cite at most {MaxCitedLines}";
149
+ }
150
+
151
+ return Enumerable.Range(citation.Start, citation.LineCount).Any(n => IsCode(lines[n - 1]))
152
+ ? null
153
+ : $"cite '{cite}' covers no code (only blank, comment or brace lines)";
154
+ }
155
+
156
+ /// <summary>Not blank, not only a comment, not only braces, parentheses and semicolons.</summary>
157
+ private static bool IsCode(string line)
158
+ {
159
+ var trimmed = line.Trim();
160
+ return trimmed.Length > 0
161
+ && !trimmed.StartsWith("//", StringComparison.Ordinal)
162
+ && !trimmed.StartsWith("/*", StringComparison.Ordinal)
163
+ && !trimmed.StartsWith('*')
164
+ && trimmed.Trim('{', '}', '(', ')', ';', ' ', '\t').Length > 0;
165
+ }
166
+
167
+ // " Stores EACH order. " and "stores each order." are the same responsibility.
168
+ private static string Normalize(string text) => Whitespace().Replace(text.Trim(), " ").ToLowerInvariant();
169
+
170
+ [GeneratedRegex("`[^`]*`")]
171
+ private static partial Regex CodeSpan();
172
+
173
+ [GeneratedRegex("<[A-Za-z/!]")]
174
+ private static partial Regex HtmlTag();
175
+
176
+ [GeneratedRegex(@"\s+")]
177
+ private static partial Regex Whitespace();
178
+ }