@phuc1403/musketeer 0.12.0 → 0.14.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 (168) hide show
  1. package/INSTALLATION.md +1 -1
  2. package/README.md +2 -2
  3. package/manifest.json +3 -3
  4. package/package.json +1 -1
  5. package/template/.claude/skills/logical-components/SKILL.md +165 -94
  6. package/template/.claude/skills/logical-components/assets/component-diagram-template.html +104 -0
  7. package/template/.claude/skills/logical-components/references/component-analyst-prompt.md +88 -0
  8. package/template/.claude/skills/logical-components/references/component-level-detection.md +95 -0
  9. package/template/.claude/skills/logical-components/references/logical-component-concepts.md +92 -0
  10. package/template/.claude/skills/logical-components/scripts/build-component-model.cjs +120 -0
  11. package/template/.claude/skills/logical-components/scripts/index-declared-symbols.cjs +106 -0
  12. package/template/.claude/skills/logical-components/scripts/lib/coupling-metrics.cjs +48 -0
  13. package/template/.claude/skills/logical-components/scripts/lib/declared-symbol-patterns.cjs +57 -0
  14. package/template/.claude/skills/logical-components/scripts/lib/html-sections.cjs +91 -0
  15. package/template/.claude/skills/logical-components/scripts/lib/mermaid-graph.cjs +55 -0
  16. package/template/.claude/skills/logical-components/scripts/lib/read-json-input.cjs +109 -0
  17. package/template/.claude/skills/logical-components/scripts/lib/repo-walk.cjs +61 -0
  18. package/template/.claude/skills/logical-components/scripts/lib/verify-claims.cjs +159 -0
  19. package/template/.claude/skills/logical-components/scripts/render-component-diagram-html.cjs +112 -0
  20. package/template/.claude/skills/logical-components/scripts/scan-folder-tree.cjs +88 -0
  21. package/template/.claude/skills/logical-components/scripts/select-stale-components.cjs +129 -0
  22. package/template/.claude/statusline.cjs +26 -22
  23. package/template/.claude/skills/logical-components/.gitattributes +0 -2
  24. package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +0 -53
  25. package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +0 -67
  26. package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +0 -39
  27. package/template/.claude/skills/logical-components/scripts/Directory.Build.props +0 -7
  28. package/template/.claude/skills/logical-components/scripts/Directory.Build.rsp +0 -1
  29. package/template/.claude/skills/logical-components/scripts/Directory.Build.targets +0 -6
  30. package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +0 -8
  31. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +0 -70
  32. package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +0 -47
  33. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +0 -98
  34. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +0 -54
  35. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +0 -37
  36. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +0 -86
  37. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +0 -219
  38. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +0 -180
  39. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +0 -130
  40. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +0 -101
  41. package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +0 -251
  42. package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +0 -14
  43. package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +0 -111
  44. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +0 -87
  45. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +0 -79
  46. package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +0 -15
  47. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs +0 -72
  48. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +0 -78
  49. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +0 -272
  50. package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +0 -231
  51. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PlainText.cs +0 -55
  52. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +0 -75
  53. package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +0 -27
  54. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +0 -77
  55. package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +0 -164
  56. package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +0 -234
  57. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +0 -84
  58. package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +0 -197
  59. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +0 -115
  60. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SourceCodeLines.cs +0 -79
  61. package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +0 -53
  62. package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +0 -106
  63. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +0 -63
  64. package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +0 -297
  65. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +0 -41
  66. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +0 -90
  67. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchesCommandTests.cs +0 -37
  68. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +0 -59
  69. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +0 -42
  70. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +0 -249
  71. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +0 -253
  72. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppExtractionTests.cs +0 -188
  73. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppWorkspace.cs +0 -30
  74. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +0 -174
  75. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +0 -17
  76. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +0 -72
  77. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +0 -36
  78. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +0 -23
  79. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +0 -93
  80. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +0 -69
  81. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +0 -149
  82. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +0 -225
  83. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +0 -34
  84. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +0 -55
  85. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +0 -315
  86. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +0 -27
  87. package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +0 -266
  88. package/template/.claude/skills/logical-components/scripts/NuGet.config +0 -17
  89. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +0 -7
  90. package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +0 -7
  91. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/CompositionRootApp.slnx +0 -5
  92. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderExtensions.cs +0 -15
  93. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderShims.cs +0 -25
  94. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/NativeInterop.cs +0 -17
  95. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/Root.Application.csproj +0 -7
  96. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootAutofacModule.cs +0 -11
  97. package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootGreeter.cs +0 -7
  98. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/logical-components.json +0 -48
  99. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AbstractMemberCases.cs +0 -27
  100. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AttributeCases.cs +0 -23
  101. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/CouplingApp.Application.csproj +0 -7
  102. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericBaseCases.cs +0 -14
  103. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericSeamBindingCases.cs +0 -59
  104. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ImplicitCallCases.cs +0 -230
  105. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.First.cs +0 -11
  106. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.Second.cs +0 -14
  107. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/StackOverflowCases.cs +0 -14
  108. package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ViaDisambiguationCases.cs +0 -36
  109. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +0 -7
  110. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +0 -8
  111. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +0 -5
  112. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +0 -7
  113. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +0 -14
  114. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/Multi.Infrastructure.csproj +0 -12
  115. package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/SchedulingClient.cs +0 -12
  116. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +0 -7
  117. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +0 -7
  118. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +0 -6
  119. package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +0 -11
  120. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/ObjOutputApp.slnx +0 -5
  121. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Greeting.cs +0 -7
  122. package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Obj.Application.csproj +0 -14
  123. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +0 -67
  124. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +0 -28
  125. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +0 -16
  126. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +0 -10
  127. package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +0 -18
  128. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +0 -10
  129. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +0 -78
  130. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +0 -37
  131. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +0 -33
  132. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +0 -18
  133. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +0 -63
  134. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +0 -5
  135. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +0 -7
  136. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +0 -17
  137. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +0 -9
  138. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +0 -7
  139. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +0 -4
  140. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +0 -18
  141. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +0 -6
  142. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +0 -19
  143. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +0 -7
  144. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +0 -6
  145. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +0 -7
  146. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +0 -107
  147. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +0 -143
  148. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +0 -12
  149. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +0 -10
  150. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +0 -89
  151. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +0 -7
  152. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +0 -11
  153. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +0 -8
  154. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +0 -16
  155. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +0 -6
  156. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +0 -11
  157. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +0 -4
  158. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +0 -8
  159. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +0 -35
  160. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +0 -13
  161. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +0 -13
  162. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +0 -18
  163. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +0 -7
  164. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +0 -11
  165. package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +0 -11
  166. package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +0 -7
  167. package/template/.claude/skills/logical-components/scripts/global.json +0 -5
  168. package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +0 -26
@@ -0,0 +1,95 @@
1
+ # Component level detection
2
+
3
+ How to turn a folder tree into modules and components. The main agent decides; the user confirms before any
4
+ analysis runs. Works for any language: judge folder names and contents, not syntax.
5
+
6
+ ## Classify every folder
7
+
8
+ | Kind | Recognise it by | What to do |
9
+ |---|---|---|
10
+ | **Layer** | A technical name: Controllers, Handlers, Services, Repositories, Models, Entities, Dtos, Api, Web, Application, Domain, Infrastructure, Persistence, Data, Core, Adapters, Ports, UseCases, `App.Application`-style project names | Look inside it. Its children are the candidates. |
11
+ | **Module** | A folder whose children are components or layers, named after a business area: `Modules/*`, bounded contexts, `Auctions`, `Billing` | Becomes a module (a subgraph). Components inside get its id. |
12
+ | **Component** | A feature folder whose files together serve one functional purpose: `BidCapture`, `order-placement`, `pricing` | One component = the whole subtree. Do not look deeper. |
13
+ | **Container** | A folder that only groups others: source roots (`src`, `lib`, `app`, `packages`), `Modules`, `Features`, `Services` holding service folders | Look through it; never a component or module. Not listed at the gate. |
14
+ | **Excluded** | Tests, build output, generated or tooling code, composition roots | Never a component. |
15
+
16
+ Rules:
17
+
18
+ - **Stop at the first component level.** Subfolders of a component (`BidCapture/Validation`) belong to it.
19
+ - **Merge across layers by folder name.** Same feature folder under several layers → one component, several paths.
20
+ Compare names case- and separator-insensitively: `BidCapture`, `bid-capture`, `bid_capture` match.
21
+ - **A layer with files but no feature subfolders** (a `Controllers` folder of mixed controllers, an `Api`
22
+ project) becomes one component for the whole layer, named after its role: `Http Api`, `Cli`, `Message Consumers`.
23
+ Its path is the outermost folder of that layer (`src/App.Api`, not `src/App.Api/Controllers`), so files added
24
+ beside `Controllers` later still belong to it.
25
+ - **Composition roots** (`Program.cs`, `main.go`, `Host`, `Startup`, DI wiring only) are not components.
26
+ - **Shared / Common** folders with real code are components like any other.
27
+ - **Single-folder repos** (all code in `src/`) → group by file name prefix only if obvious; otherwise one
28
+ component per top-level folder and say so at the gate.
29
+ - **Names:** the folder's words in Title Case when they already say what the code does (`BidderSignOn` →
30
+ `Bidder Sign-on`); id is kebab-case and unique (`bidder-sign-on`). Rename only technical or vague names
31
+ (`Core`, `Common`, entity-trap names like `OrderManager`) when the code clearly does something narrower, and
32
+ mention a vague name you kept at the gate.
33
+ - **Low cohesion:** when a component's files do unrelated jobs, keep the folder as one component and say so in
34
+ its gate line; the analysis will list both jobs.
35
+ - `module` is `null` when the repo has no module level, and `modules` is then `[]`.
36
+ - **Excluded line at the gate:** list the excluded folders the scan reports on its `excluded:` line and any
37
+ composition roots you left out, each with its reason; write `Excluded: none` when there are none.
38
+
39
+ Excluded folders (the scripts skip them too): `bin`, `obj`, `node_modules`, `dist`, `build`, `out`, `target`,
40
+ `.git`, `test`, `tests`, `__tests__`, `spec`, `*.Tests`, `*.Test`, `coverage`, `vendor`, `generated`,
41
+ `migrations`, and test files (`*.test.*`, `*.spec.*`, `*_test.go`, `test_*.py`).
42
+
43
+ ## structure.json
44
+
45
+ ```json
46
+ { "modules": [{ "id": "auctions", "name": "Auctions" }],
47
+ "components": [{ "id": "bid-capture", "name": "Bid Capture", "module": "auctions",
48
+ "paths": ["src/Modules/Auctions/BidCapture"] }] }
49
+ ```
50
+
51
+ Paths are repo-relative with forward slashes. Every path must exist.
52
+
53
+ ## Worked examples
54
+
55
+ ### Flat feature folders (TypeScript)
56
+
57
+ ```
58
+ src/order-placement src/inventory-management src/item-pricing src/supplier-ordering src/email-notification
59
+ tests/
60
+ ```
61
+
62
+ No layers, no modules: five components, `module: null`, one path each. `tests/` excluded.
63
+
64
+ ### Modular monolith (C#)
65
+
66
+ ```
67
+ src/Modules/Auctions/{BidCapture, BidTracker, LiveAuctionSession}
68
+ src/Modules/Bidders/{BidderRegistration, BidderSignOn}
69
+ src/Host/Program.cs
70
+ ```
71
+
72
+ `Modules` is a container; `Auctions` and `Bidders` are modules; their children are components.
73
+ `Host` is a composition root, excluded.
74
+
75
+ ### Clean Architecture (C#)
76
+
77
+ ```
78
+ src/App.Application/{BidCapture, TripViewer}
79
+ src/App.Infrastructure/{BidCapture, VideoStreamer}
80
+ src/App.Api/Controllers/{BidsController.cs, TripsController.cs}
81
+ tests/App.Tests/
82
+ ```
83
+
84
+ `App.Application`, `App.Infrastructure`, `App.Api` are layers. `BidCapture` exists in two layers → one
85
+ `bid-capture` component with both paths. `App.Api` has no feature folders → one `http-api` component
86
+ (`Http Api`). Result: `bid-capture`, `http-api`, `trip-viewer`, `video-streamer`, no modules.
87
+
88
+ ## Counter-examples
89
+
90
+ - `src/Services/OrderService.cs, PaymentService.cs` — `Services` is a layer of mixed files. If the other layers
91
+ are organised per feature, merge each file's feature into it; otherwise make `Services` one component and say
92
+ the code is organised by layer, not by feature.
93
+ - `src/Domain/Order/`, `src/Domain/Customer/` — entity folders. Still components (folder = component), but
94
+ mention the entity trap at the gate if their code mixes unrelated jobs.
95
+ - A module with a single folder inside → that folder is the component; keep the module.
@@ -0,0 +1,92 @@
1
+ # Logical component concepts
2
+
3
+ What the skill measures, and the words it uses. Read before deciding components or writing an analysis.
4
+
5
+ ## Logical vs physical architecture
6
+
7
+ | | Logical architecture | Physical architecture |
8
+ |---|---|---|
9
+ | Focus | Functional building blocks and how they interact | Tangible implementation |
10
+ | Answers | What the system does | How the system is built |
11
+ | Includes | Logical components (`Bidder Registration`, `Auction Search`) | Services, databases, protocols, UI, API gateways |
12
+ | Ignores | Services, databases, protocols, hardware | — (it groups and implements logical components) |
13
+
14
+ This skill recovers the **logical** architecture of an existing codebase. It does not draw deployment units,
15
+ databases or classes.
16
+
17
+ ## Component
18
+
19
+ A **logical component** is a building block that does one functional job of the system. In code it is a
20
+ **folder subtree**: every source file under the folder (recursively) belongs to it. When layers split one feature
21
+ across several folders (`Application/BidCapture`, `Infrastructure/BidCapture`), those folders are **one component
22
+ with several paths**. Classes are never nodes; a component's responsibilities are the combined responsibilities of
23
+ its code.
24
+
25
+ A good component name says exactly what it does: `Bid Capture`, `Item Pricing`, `Video Streamer`. A third-party
26
+ wrapper (a video streaming client) is still a component.
27
+
28
+ ## Responsibility
29
+
30
+ A **responsibility** is a short, verb-first statement of something the component does for the system, written for
31
+ the folder as a whole, not per class:
32
+
33
+ - Good: `Receive bids from online bidders`, `Raise an item price when its stock runs low`.
34
+ - Bad: `BidReceiver class`, `Has a Receive method`, `Handles bids` (vague), one line per file.
35
+
36
+ A component usually has 3–7 responsibilities. Each one cites evidence: the file and line range that implements it.
37
+
38
+ ## Cohesion and the entity trap
39
+
40
+ **Cohesion** is how closely related a component's responsibilities are. High cohesion: they all serve one purpose.
41
+ Low cohesion: a dumping ground of unrelated jobs, often a `Utility` component.
42
+
43
+ The **entity trap** is grouping every action on one entity into one component (`Bid Manager` accepts bids, picks
44
+ winners, audits, notifies, reports). The name is vague and the component bloats. Red-flag words in a name:
45
+ Manager, Supervisor, Handler, Controller, Agent, Service, Engine, Mediator, Coordinator, Orchestrator, Processor,
46
+ Utility, Worker. Not a hard rule: `Reference Data Manager` is specific enough. Name components after what they do.
47
+
48
+ ## Coupling
49
+
50
+ **Coupling** is how much components know about and rely on each other. Arrow `A → B` means **A knows about B**:
51
+ A's code references a symbol B declares. Any reference counts:
52
+
53
+ - calling a function or method of B;
54
+ - constructing B's type;
55
+ - using B's type as a field, parameter, return or generic argument (DTOs included);
56
+ - inheriting from or implementing B's class or interface.
57
+
58
+ The arrow label is what A asks B to do, as a verb phrase: `Decrement inventory`, `Notify the customer`.
59
+ A component referencing its own symbols is not coupling.
60
+
61
+ - **Afferent coupling (CA)**: number of distinct components that point **to** this one (incoming).
62
+ `Bidder Profile`, used by `Auction Registration` and `Automatic Payment`, has CA = 2.
63
+ - **Efferent coupling (CE)**: number of distinct components this one points **to** (outgoing).
64
+ `Bid Capture` sends to `Bid Streamer` and `Bid Tracker`: CE = 2.
65
+ - **Total coupling (CT)** = CA + CE per component. **Total system coupling** = Σ CT = 2 × number of edges.
66
+
67
+ Several references from A to B are one edge with several labels; CA and CE count components, not references.
68
+
69
+ ## Law of Demeter
70
+
71
+ A component should know as little as possible about what other components do. The more it knows about what must
72
+ happen elsewhere, the more coupled it is.
73
+
74
+ Order Placement example, external "place an order" actor left out:
75
+
76
+ - **Before:** Order Placement → Inventory Management, Item Pricing, Supplier Ordering, Email Notification.
77
+ Order Placement CE = 4; total system coupling 8.
78
+ - **After:** Order Placement → Inventory Management (`Decrement inventory`), Email Notification
79
+ (`Notify the customer`); Inventory Management → Item Pricing (`Stock low? Raise the price`), Supplier Ordering
80
+ (`Stock low? Order more`). Order Placement CE = 2, Inventory Management CA = 1, CE = 2, CT = 3; total still 8.
81
+
82
+ The knowledge was not removed, it moved to the component most responsible for it.
83
+
84
+ ## Trade-off
85
+
86
+ | | Tightly coupled | Loosely coupled |
87
+ |---|---|---|
88
+ | Workflow | One hub knows every step | Each component knows only the next step |
89
+ | Benefit | Easy to read the whole flow in one place | A change rarely breaks other components |
90
+ | Drawback | A change anywhere can break the hub | The full flow is spread across components |
91
+
92
+ The skill reports numbers; it does not judge a design good or bad.
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Merges the confirmed structure, the symbol index and every component
5
+ // analysis into docs/logical-components.json. This is the trust boundary:
6
+ // claims become facts only after verify-claims accepts them.
7
+ // Usage: build-component-model.cjs --repo <repo> --structure <structure.json> --symbols <symbols.json>
8
+ // --analysis <dir> --out <docs/logical-components.json>
9
+ // Exit: 0 ok, 1 usage, 2 unreadable input, 3 missing analysis.
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { parseArgs } = require('util');
14
+ const { InputError, isInputFailure, loadStructure, loadSymbols, writeJson, isObject } = require('./lib/read-json-input.cjs');
15
+ const { createVerifier, oneLine } = require('./lib/verify-claims.cjs');
16
+ const { aggregateEdges, computeMetrics } = require('./lib/coupling-metrics.cjs');
17
+
18
+ const USAGE = 'usage: build-component-model.cjs --repo <repo> --structure <structure.json> --symbols <symbols.json> '
19
+ + '--analysis <dir> --out <docs/logical-components.json>';
20
+
21
+ function parse() {
22
+ const names = ['repo', 'structure', 'symbols', 'analysis', 'out'];
23
+ try {
24
+ const { values } = parseArgs({ options: Object.fromEntries(names.map((n) => [n, { type: 'string' }])) });
25
+ if (names.every((n) => values[n])) return values;
26
+ } catch (error) {
27
+ process.stderr.write(`${error.message}\n`);
28
+ }
29
+ process.stderr.write(`${USAGE}\n`);
30
+ return process.exit(1);
31
+ }
32
+
33
+ // Parsed analysis, or null with a rejection when the agent wrote something unusable.
34
+ function readAnalysis(file, id, rejected) {
35
+ let analysis;
36
+ try {
37
+ analysis = JSON.parse(fs.readFileSync(file, 'utf8'));
38
+ } catch (error) {
39
+ rejected.push({ component: id, kind: 'analysis', claim: `${id}.json`, reason: oneLine(`invalid JSON: ${error.message}`) });
40
+ return null;
41
+ }
42
+ if (!isObject(analysis)) {
43
+ rejected.push({ component: id, kind: 'analysis', claim: `${id}.json`, reason: 'analysis must be a JSON object' });
44
+ return null;
45
+ }
46
+ return analysis;
47
+ }
48
+
49
+ const listOf = (value) => (Array.isArray(value) ? value : []);
50
+
51
+ function buildModel(repo, structure, symbols, analysisDir) {
52
+ const verifier = createVerifier(repo, structure, symbols);
53
+ const rejected = [];
54
+ const usages = [];
55
+ const components = structure.components.map((component) => {
56
+ const id = component.id;
57
+ const analysis = readAnalysis(path.join(analysisDir, `${id}.json`), id, rejected);
58
+ const responsibilities = [];
59
+ for (const claim of listOf(analysis && analysis.responsibilities)) {
60
+ const checked = verifier.verifyResponsibility(component, claim);
61
+ if (checked.value) responsibilities.push(checked.value);
62
+ else rejected.push({ component: id, kind: 'responsibility', claim: checked.claim, reason: checked.reason });
63
+ }
64
+ for (const usage of listOf(analysis && analysis.usages)) {
65
+ const checked = verifier.verifyUsage(component, usage);
66
+ if (checked.value) usages.push(checked.value);
67
+ else rejected.push({ component: id, kind: 'usage', claim: checked.claim, reason: checked.reason });
68
+ }
69
+ return { component, responsibilities, analysis };
70
+ });
71
+
72
+ const edges = aggregateEdges(usages);
73
+ const { metrics, totalCoupling } = computeMetrics(structure.components.map((c) => c.id), edges);
74
+ return {
75
+ version: 1,
76
+ modules: structure.modules,
77
+ components: components.map(({ component, responsibilities, analysis }) => ({
78
+ id: component.id,
79
+ name: component.name,
80
+ module: component.module,
81
+ paths: component.paths,
82
+ responsibilities,
83
+ ...metrics[component.id],
84
+ fingerprint: symbols.fingerprints[component.id],
85
+ declaredSymbols: symbols.byComponent[component.id],
86
+ analysis,
87
+ })),
88
+ edges,
89
+ totalCoupling,
90
+ rejected,
91
+ };
92
+ }
93
+
94
+ function main() {
95
+ const args = parse();
96
+ const repo = path.resolve(args.repo);
97
+ let model;
98
+ try {
99
+ const structure = loadStructure(args.structure);
100
+ const symbols = loadSymbols(args.symbols, structure);
101
+ const missing = structure.components.map((c) => c.id).filter((id) => !fs.existsSync(path.join(args.analysis, `${id}.json`)));
102
+ if (missing.length) {
103
+ process.stderr.write(`missing analysis for: ${missing.join(', ')}\n`);
104
+ process.exit(3);
105
+ }
106
+ model = buildModel(repo, structure, symbols, args.analysis);
107
+ } catch (error) {
108
+ if (!isInputFailure(error)) throw error;
109
+ process.stderr.write(`${error.message}\n`);
110
+ process.exit(2);
111
+ }
112
+ writeJson(args.out, model);
113
+
114
+ const lines = [`components ${model.components.length}, edges ${model.edges.length}, total coupling ${model.totalCoupling}, rejected ${model.rejected.length}`];
115
+ for (const r of model.rejected) lines.push(`rejected ${r.component} ${r.kind}: ${r.reason} [${r.claim}]`);
116
+ for (const c of model.components) if (!c.responsibilities.length) lines.push(`empty ${c.id}`);
117
+ process.stdout.write(`${lines.join('\n')}\n`);
118
+ }
119
+
120
+ main();
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Builds symbols.json from a confirmed structure.json: which component declares
5
+ // each top-level symbol, and a content fingerprint per component for reruns.
6
+ // Usage: index-declared-symbols.cjs --repo <repo> --structure <structure.json> --out <symbols.json>
7
+ // Exit: 0 ok, 1 usage, 2 unreadable structure or missing component path.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const crypto = require('crypto');
12
+ const { parseArgs } = require('util');
13
+ const { listComponentFiles, readSource } = require('./lib/repo-walk.cjs');
14
+ const { languageOf, extractDeclaredSymbols } = require('./lib/declared-symbol-patterns.cjs');
15
+ const { InputError, isInputFailure, loadStructure, writeJson } = require('./lib/read-json-input.cjs');
16
+
17
+ const USAGE = 'usage: index-declared-symbols.cjs --repo <repo> --structure <structure.json> --out <symbols.json>';
18
+
19
+ function parse() {
20
+ try {
21
+ const { values } = parseArgs({ options: { repo: { type: 'string' }, structure: { type: 'string' }, out: { type: 'string' } } });
22
+ if (values.repo && values.structure && values.out) return values;
23
+ } catch (error) {
24
+ process.stderr.write(`${error.message}\n`);
25
+ }
26
+ process.stderr.write(`${USAGE}\n`);
27
+ return process.exit(1);
28
+ }
29
+
30
+ // sha256 over every file's path and LF-normalised content, in sorted order.
31
+ function fingerprint(repo, files) {
32
+ const hash = crypto.createHash('sha256');
33
+ for (const file of files) hash.update(`${file}\0${readSource(repo, file)}\0`);
34
+ return `sha256:${hash.digest('hex')}`;
35
+ }
36
+
37
+ // A structure path must name an existing folder with the exact letter case on disk:
38
+ // Windows and macOS would open `src/bids` for `src/Bids`, but every file path the
39
+ // scripts compare afterwards would then disagree with it.
40
+ function pathProblem(repo, relPath) {
41
+ let current = repo;
42
+ const actual = [];
43
+ for (const segment of relPath.split('/')) {
44
+ const match = fs.existsSync(current) && fs.statSync(current).isDirectory()
45
+ ? fs.readdirSync(current).find((name) => name.toLowerCase() === segment.toLowerCase())
46
+ : undefined;
47
+ if (match === undefined) return `missing path ${relPath}`;
48
+ actual.push(match);
49
+ current = path.join(current, match);
50
+ }
51
+ if (!fs.statSync(current).isDirectory()) return `path ${relPath} is a file, not a folder`;
52
+ const onDisk = actual.join('/');
53
+ return onDisk === relPath ? null : `path ${relPath} differs in letter case from the folder on disk: use ${onDisk}`;
54
+ }
55
+
56
+ function buildIndex(repo, structure) {
57
+ // A Map, so symbols named like Object.prototype members (`toString`, `constructor`) are plain keys.
58
+ const symbols = new Map();
59
+ const byComponent = {};
60
+ const fingerprints = {};
61
+ for (const component of structure.components) {
62
+ const files = listComponentFiles(repo, component.paths);
63
+ const declared = new Set();
64
+ for (const file of files) {
65
+ for (const name of extractDeclaredSymbols(readSource(repo, file), languageOf(file))) declared.add(name);
66
+ }
67
+ byComponent[component.id] = [...declared].sort();
68
+ fingerprints[component.id] = fingerprint(repo, files);
69
+ for (const name of declared) {
70
+ if (!symbols.has(name)) symbols.set(name, []);
71
+ symbols.get(name).push(component.id);
72
+ }
73
+ }
74
+ // Built with defineProperty so a `__proto__` symbol becomes an own key, not a prototype change.
75
+ const sortedSymbols = {};
76
+ for (const name of [...symbols.keys()].sort()) {
77
+ Object.defineProperty(sortedSymbols, name, { value: symbols.get(name).sort(), enumerable: true, writable: true, configurable: true });
78
+ }
79
+ return { symbols: sortedSymbols, byComponent, fingerprints };
80
+ }
81
+
82
+ function main() {
83
+ const args = parse();
84
+ const repo = path.resolve(args.repo);
85
+ try {
86
+ const structure = loadStructure(args.structure);
87
+ const missing = [];
88
+ for (const c of structure.components) {
89
+ for (const p of c.paths) {
90
+ const problem = pathProblem(repo, p);
91
+ if (problem) missing.push(`${problem} (component ${c.id})`);
92
+ }
93
+ }
94
+ if (missing.length) throw new InputError(missing.join('\n'));
95
+ const index = buildIndex(repo, structure);
96
+ writeJson(args.out, index);
97
+ const ambiguous = Object.values(index.symbols).filter((owners) => owners.length > 1).length;
98
+ process.stdout.write(`components ${structure.components.length}, symbols ${Object.keys(index.symbols).length}, ambiguous ${ambiguous}\n`);
99
+ } catch (error) {
100
+ if (!isInputFailure(error)) throw error;
101
+ process.stderr.write(`${error.message}\n`);
102
+ process.exit(2);
103
+ }
104
+ }
105
+
106
+ main();
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ // Pure coupling maths. An edge A -> B means A knows about B. CE counts the
4
+ // distinct components a component points to, CA the distinct components that
5
+ // point to it, CT = CA + CE, and total system coupling is the sum of CT.
6
+
7
+ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
8
+
9
+ // Verified usages -> one edge per (from, to) with distinct sorted labels and evidence.
10
+ function aggregateEdges(usages) {
11
+ const edges = new Map();
12
+ for (const u of usages) {
13
+ const key = `${u.from}\0${u.to}`;
14
+ if (!edges.has(key)) edges.set(key, { from: u.from, to: u.to, labels: new Set(), evidence: new Map() });
15
+ const edge = edges.get(key);
16
+ edge.labels.add(u.action);
17
+ edge.evidence.set(`${u.file}\0${u.line}\0${u.symbol}`, { file: u.file, line: u.line, symbol: u.symbol });
18
+ }
19
+ return [...edges.values()]
20
+ .sort((a, b) => compare(a.from, b.from) || compare(a.to, b.to))
21
+ .map((e) => ({
22
+ from: e.from,
23
+ to: e.to,
24
+ labels: [...e.labels].sort(compare),
25
+ evidence: [...e.evidence.values()].sort((a, b) => compare(a.file, b.file) || a.line - b.line || compare(a.symbol, b.symbol)),
26
+ }));
27
+ }
28
+
29
+ function computeMetrics(componentIds, edges) {
30
+ const metrics = {};
31
+ for (const id of componentIds) metrics[id] = { ca: 0, ce: 0, ct: 0 };
32
+ const seen = new Set();
33
+ for (const { from, to } of edges) {
34
+ const key = `${from}\0${to}`;
35
+ if (from === to || seen.has(key)) continue;
36
+ seen.add(key);
37
+ metrics[from].ce += 1;
38
+ metrics[to].ca += 1;
39
+ }
40
+ let totalCoupling = 0;
41
+ for (const id of componentIds) {
42
+ metrics[id].ct = metrics[id].ca + metrics[id].ce;
43
+ totalCoupling += metrics[id].ct;
44
+ }
45
+ return { metrics, totalCoupling };
46
+ }
47
+
48
+ module.exports = { aggregateEdges, computeMetrics, compare };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ // Language detection and top-level declaration patterns. Regexes, not parsers:
4
+ // they find the names other components can reference (types, exported
5
+ // functions). A missed declaration only means a missing edge, never a wrong one.
6
+
7
+ const LANGUAGES = {
8
+ '.cs': 'csharp',
9
+ '.java': 'java',
10
+ '.kt': 'kotlin', '.kts': 'kotlin',
11
+ '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
12
+ '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
13
+ '.py': 'python',
14
+ '.go': 'go',
15
+ };
16
+
17
+ const LABELS = { csharp: 'C#', java: 'Java', kotlin: 'Kotlin', typescript: 'TS', javascript: 'JS', python: 'Python', go: 'Go' };
18
+
19
+ const CSHARP_MODIFIERS = 'public|internal|private|protected|static|sealed|abstract|partial|readonly|ref|unsafe|file|new';
20
+ const JVM_MODIFIERS = 'public|private|protected|internal|static|final|abstract|sealed|open|data|inner|enum|annotation|value|inline';
21
+ const ES_EXPORT = /^\s*export\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(?:function\s*\*?\s*|(?:const\s+enum|class|const|let|var|interface|type|enum)\s+)([A-Za-z_$][\w$]*)/gm;
22
+
23
+ const PATTERNS = {
24
+ csharp: [new RegExp(`^\\s*(?:(?:${CSHARP_MODIFIERS})\\s+)*(?:class|record(?:\\s+(?:class|struct))?|struct|interface|enum)\\s+([A-Za-z_]\\w*)`, 'gm')],
25
+ java: [new RegExp(`^\\s*(?:(?:${JVM_MODIFIERS})\\s+)*(?:class|interface|enum|record)\\s+([A-Za-z_]\\w*)`, 'gm')],
26
+ kotlin: [new RegExp(`^\\s*(?:(?:${JVM_MODIFIERS})\\s+)*(?:class|interface|object|enum)\\s+([A-Za-z_]\\w*)`, 'gm')],
27
+ typescript: [ES_EXPORT],
28
+ javascript: [ES_EXPORT],
29
+ // Top level only (column 0); a leading underscore marks a private name.
30
+ python: [/^(?:async\s+)?(?:class|def)\s+([A-Za-z]\w*)/gm],
31
+ // Exported (capitalised) types and functions; methods start with a receiver `(`.
32
+ go: [/^type\s+([A-Z]\w*)/gm, /^func\s+([A-Z]\w*)\s*[[(]/gm],
33
+ };
34
+
35
+ function languageOf(fileName) {
36
+ const dot = fileName.lastIndexOf('.');
37
+ return dot < 0 ? null : LANGUAGES[fileName.slice(dot).toLowerCase()] || null;
38
+ }
39
+
40
+ // Sorted, de-duplicated names declared at the top level of `text`.
41
+ function extractDeclaredSymbols(text, language) {
42
+ const names = new Set();
43
+ for (const pattern of PATTERNS[language] || []) {
44
+ pattern.lastIndex = 0;
45
+ for (const match of text.matchAll(pattern)) names.add(match[1]);
46
+ }
47
+ return [...names].sort();
48
+ }
49
+
50
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+
52
+ // Matches any of `names` as a whole identifier (`$` counts as a word char for JS).
53
+ function wholeWordPattern(names, flags = '') {
54
+ return new RegExp(`(?<![\\w$])(?:${names.map(escapeRegExp).join('|')})(?![\\w$])`, flags);
55
+ }
56
+
57
+ module.exports = { languageOf, extractDeclaredSymbols, wholeWordPattern, LABELS };
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ // HTML fragments of the logical-components page: component cards, the
4
+ // coupling and edge tables, and the rejected-claims list. Every value from the
5
+ // model is escaped; evidence links point at repo files relative to the page.
6
+
7
+ const path = require('path');
8
+
9
+ const escapeHtml = (value) => String(value)
10
+ .replace(/&/g, '&amp;')
11
+ .replace(/</g, '&lt;')
12
+ .replace(/>/g, '&gt;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/'/g, '&#39;');
15
+
16
+ const anchor = (id) => `component-${id}`;
17
+
18
+ // fileBase: posix path from the page's folder to the repo root ('..' for docs/).
19
+ function evidenceLink(fileBase, file, start, end) {
20
+ const fragment = end === undefined || end === start ? `#L${start}` : `#L${start}-L${end}`;
21
+ const text = `${path.posix.basename(file)}:${end === undefined || end === start ? start : `${start}-${end}`}`;
22
+ const encoded = file.split('/').map(encodeURIComponent).join('/');
23
+ const href = `${fileBase ? `${fileBase}/` : ''}${encoded}${fragment}`;
24
+ return `<a href="${escapeHtml(href)}" title="${escapeHtml(file)}">${escapeHtml(text)}</a>`;
25
+ }
26
+
27
+ const componentLink = (byId, id) => `<a href="#${anchor(id)}">${escapeHtml(byId.get(id).name)}</a>`;
28
+
29
+ function edgeItems(edges, byId, fileBase, other) {
30
+ if (!edges.length) return '<p class="none">None</p>';
31
+ const items = edges.map((e) => {
32
+ const evidence = e.evidence.map((ev) => evidenceLink(fileBase, ev.file, ev.line)).join(', ');
33
+ return `<li>${componentLink(byId, e[other])}: ${escapeHtml(e.labels.join('; '))} <span class="evidence">${evidence}</span></li>`;
34
+ });
35
+ return `<ul>${items.join('')}</ul>`;
36
+ }
37
+
38
+ function renderCards(model, fileBase) {
39
+ const byId = new Map(model.components.map((c) => [c.id, c]));
40
+ const moduleNames = new Map(model.modules.map((m) => [m.id, m.name]));
41
+ return model.components.map((c) => {
42
+ const responsibilities = c.responsibilities.length
43
+ ? `<ol>${c.responsibilities.map((r) => {
44
+ const links = r.evidence.map((ev) => evidenceLink(fileBase, ev.file, ev.lines[0], ev.lines[1])).join(', ');
45
+ return `<li>${escapeHtml(r.text)} <span class="evidence">${links}</span></li>`;
46
+ }).join('')}</ol>`
47
+ : '<p class="none">No verified responsibilities</p>';
48
+ const module = c.module === null ? '' : `${escapeHtml(moduleNames.get(c.module))} · `;
49
+ return [
50
+ `<section class="card" id="${anchor(c.id)}">`,
51
+ `<h3>${escapeHtml(c.name)}</h3>`,
52
+ `<p class="meta">${module}${c.paths.map((p) => `<code>${escapeHtml(p)}</code>`).join(' ')}</p>`,
53
+ `<p class="metrics">CA ${c.ca} · CE ${c.ce} · CT ${c.ct}</p>`,
54
+ `<h4>Responsibilities</h4>${responsibilities}`,
55
+ `<h4>Knows about (CE ${c.ce})</h4>${edgeItems(model.edges.filter((e) => e.from === c.id), byId, fileBase, 'to')}`,
56
+ `<h4>Known by (CA ${c.ca})</h4>${edgeItems(model.edges.filter((e) => e.to === c.id), byId, fileBase, 'from')}`,
57
+ '</section>',
58
+ ].join('\n');
59
+ }).join('\n');
60
+ }
61
+
62
+ function renderCouplingTable(model) {
63
+ const moduleNames = new Map(model.modules.map((m) => [m.id, m.name]));
64
+ const rows = [...model.components]
65
+ .sort((a, b) => b.ct - a.ct || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
66
+ .map((c) => `<tr><td><a href="#${anchor(c.id)}">${escapeHtml(c.name)}</a></td>`
67
+ + `<td>${c.module === null ? '' : escapeHtml(moduleNames.get(c.module))}</td>`
68
+ + `<td>${c.ca}</td><td>${c.ce}</td><td>${c.ct}</td></tr>`);
69
+ return `<table><thead><tr><th>Component</th><th>Module</th><th>CA</th><th>CE</th><th>CT</th></tr></thead>\n`
70
+ + `<tbody>\n${rows.join('\n')}\n</tbody></table>`;
71
+ }
72
+
73
+ // Plain edge list: readable even when the Mermaid CDN is unreachable.
74
+ function renderEdgeTable(model, fileBase) {
75
+ if (!model.edges.length) return '<p class="none">No verified edges</p>';
76
+ const byId = new Map(model.components.map((c) => [c.id, c]));
77
+ const rows = model.edges.map((e) => `<tr><td>${componentLink(byId, e.from)}</td><td>${componentLink(byId, e.to)}</td>`
78
+ + `<td>${escapeHtml(e.labels.join('; '))}</td>`
79
+ + `<td>${e.evidence.map((ev) => evidenceLink(fileBase, ev.file, ev.line)).join(', ')}</td></tr>`);
80
+ return `<table><thead><tr><th>From</th><th>To</th><th>Asks to</th><th>Evidence</th></tr></thead>\n`
81
+ + `<tbody>\n${rows.join('\n')}\n</tbody></table>`;
82
+ }
83
+
84
+ function renderRejected(model) {
85
+ if (!model.rejected.length) return '<p class="none">No rejected claims</p>';
86
+ const items = model.rejected.map((r) => `<li><code>${escapeHtml(r.component)}</code> ${escapeHtml(r.kind)}: `
87
+ + `${escapeHtml(r.claim)} <span class="reason">${escapeHtml(r.reason)}</span></li>`);
88
+ return `<details class="rejected">\n<summary>Rejected claims (${model.rejected.length})</summary>\n<ul>${items.join('\n')}</ul>\n</details>`;
89
+ }
90
+
91
+ module.exports = { escapeHtml, renderCards, renderCouplingTable, renderEdgeTable, renderRejected };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ // Mermaid flowchart source for the component model. Node ids are index-based
4
+ // (c0, c1, ...) so any component name, in any script, is safe; every piece of
5
+ // text goes through mermaidText before it lands inside a quoted label.
6
+
7
+ const MAX_LABELS = 2;
8
+
9
+ // Mermaid entity codes for characters that could end a label or start markup.
10
+ // `#` goes first so the codes added afterwards are not escaped twice.
11
+ function mermaidText(value) {
12
+ return String(value)
13
+ .replace(/\s+/g, ' ')
14
+ .trim()
15
+ .replace(/#/g, '#35;')
16
+ .replace(/"/g, '#quot;')
17
+ .replace(/&/g, '#amp;')
18
+ .replace(/</g, '#lt;')
19
+ .replace(/>/g, '#gt;')
20
+ .replace(/`/g, '#96;')
21
+ // `%%{init}%%` directives and quoted keys must not survive as Mermaid syntax.
22
+ .replace(/%/g, '#37;')
23
+ .replace(/\{/g, '#123;')
24
+ .replace(/\}/g, '#125;')
25
+ .replace(/'/g, '#39;');
26
+ }
27
+
28
+ function edgeLabel(labels) {
29
+ const shown = labels.slice(0, MAX_LABELS).map(mermaidText).join('; ');
30
+ return labels.length > MAX_LABELS ? `${shown} +${labels.length - MAX_LABELS}` : shown;
31
+ }
32
+
33
+ function buildMermaid(model) {
34
+ const nodeIds = new Map(model.components.map((c, i) => [c.id, `c${i}`]));
35
+ const node = (c, indent) =>
36
+ `${indent}${nodeIds.get(c.id)}["${mermaidText(c.name)}<br/>CA ${c.ca} · CE ${c.ce} · CT ${c.ct}"]`;
37
+
38
+ const lines = ['flowchart LR'];
39
+ model.modules.forEach((module, i) => {
40
+ const members = model.components.filter((c) => c.module === module.id);
41
+ if (!members.length) return;
42
+ lines.push(` subgraph m${i}["${mermaidText(module.name)}"]`);
43
+ for (const c of members) lines.push(node(c, ' '));
44
+ lines.push(' end');
45
+ });
46
+ for (const c of model.components.filter((x) => x.module === null)) lines.push(node(c, ' '));
47
+ for (const e of model.edges) {
48
+ lines.push(` ${nodeIds.get(e.from)} -->|"${edgeLabel(e.labels)}"| ${nodeIds.get(e.to)}`);
49
+ }
50
+ // Ids are validated kebab-case, so they are safe inside the call string.
51
+ for (const c of model.components) lines.push(` click ${nodeIds.get(c.id)} call focusCard("${c.id}")`);
52
+ return lines.join('\n');
53
+ }
54
+
55
+ module.exports = { buildMermaid, mermaidText };