@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,109 @@
1
+ 'use strict';
2
+
3
+ // Reads and validates the JSON files passed between the skill's steps.
4
+ // Anything malformed raises InputError, which every CLI maps to exit code 2.
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ class InputError extends Error {}
10
+
11
+ const ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
+
13
+ function readJson(file, label) {
14
+ try {
15
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
16
+ } catch (error) {
17
+ throw new InputError(`cannot read ${label} ${file}: ${error.message}`);
18
+ }
19
+ }
20
+
21
+ function writeJson(file, value) {
22
+ fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
23
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
24
+ }
25
+
26
+ const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
27
+
28
+ // Repo-relative folder path in posix form; rejects anything escaping the repo.
29
+ function normalizeRepoPath(raw) {
30
+ const p = path.posix.normalize(String(raw).replace(/\\/g, '/')).replace(/\/+$/, '');
31
+ if (!p || p === '.' || p.startsWith('../') || p === '..' || p.startsWith('/') || /^[A-Za-z]:/.test(p)) return null;
32
+ return p;
33
+ }
34
+
35
+ function fail(label, message) {
36
+ throw new InputError(`invalid ${label}: ${message}`);
37
+ }
38
+
39
+ // Validates structure.json and returns it normalised: posix paths, sorted.
40
+ function loadStructure(file) {
41
+ const raw = readJson(file, 'structure');
42
+ if (!raw || !Array.isArray(raw.modules) || !Array.isArray(raw.components)) fail('structure', 'needs modules and components arrays');
43
+ const modules = raw.modules.map((m) => {
44
+ if (!m || !ID.test(m.id) || typeof m.name !== 'string' || !m.name.trim()) fail('structure', `bad module ${JSON.stringify(m)}`);
45
+ return { id: m.id, name: m.name };
46
+ }).sort(byId);
47
+ const moduleIds = new Set(modules.map((m) => m.id));
48
+ const ids = new Set();
49
+ const owners = [];
50
+ const components = raw.components.map((c) => {
51
+ if (!c || !ID.test(c.id)) fail('structure', `component id must be kebab-case: ${JSON.stringify(c && c.id)}`);
52
+ if (ids.has(c.id)) fail('structure', `duplicate component id ${c.id}`);
53
+ ids.add(c.id);
54
+ if (typeof c.name !== 'string' || !c.name.trim()) fail('structure', `component ${c.id} needs a name`);
55
+ const module = c.module === undefined ? null : c.module;
56
+ if (module !== null && !moduleIds.has(module)) fail('structure', `component ${c.id} names unknown module ${module}`);
57
+ if (!Array.isArray(c.paths) || c.paths.length === 0) fail('structure', `component ${c.id} needs paths`);
58
+ const paths = [...new Set(c.paths.map((p) => {
59
+ const normalized = normalizeRepoPath(p);
60
+ if (!normalized) fail('structure', `component ${c.id} has a path outside the repo: ${p}`);
61
+ return normalized;
62
+ }))].sort();
63
+ for (const p of paths) owners.push({ id: c.id, path: p });
64
+ return { id: c.id, name: c.name, module, paths };
65
+ }).sort(byId);
66
+ // Compared case-insensitively: on Windows and macOS `src/Bids` and `src/bids` are one folder.
67
+ for (const a of owners) {
68
+ for (const b of owners) {
69
+ const pa = a.path.toLowerCase();
70
+ const pb = b.path.toLowerCase();
71
+ if (a !== b && (pa === pb || pa.startsWith(`${pb}/`))) {
72
+ fail('structure', `path ${a.path} (${a.id}) overlaps ${b.path} (${b.id})`);
73
+ }
74
+ }
75
+ }
76
+ return { modules, components };
77
+ }
78
+
79
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
80
+
81
+ // symbols.json must come from the same structure: every component indexed.
82
+ function loadSymbols(file, structure) {
83
+ const raw = readJson(file, 'symbols');
84
+ if (!isObject(raw) || !isObject(raw.symbols) || !isObject(raw.byComponent) || !isObject(raw.fingerprints)) {
85
+ fail('symbols', 'needs symbols, byComponent and fingerprints objects');
86
+ }
87
+ for (const c of structure.components) {
88
+ if (!Array.isArray(raw.byComponent[c.id]) || typeof raw.fingerprints[c.id] !== 'string') {
89
+ fail('symbols', `no entry for component ${c.id}; re-run index-declared-symbols.cjs with this structure`);
90
+ }
91
+ }
92
+ return raw;
93
+ }
94
+
95
+ // The previously written docs/logical-components.json, checked enough to reuse.
96
+ function loadPreviousModel(file) {
97
+ const raw = readJson(file, 'previous model');
98
+ const valid = isObject(raw) && raw.version === 1 && Array.isArray(raw.components) && raw.components.every((c) =>
99
+ isObject(c) && typeof c.id === 'string' && Array.isArray(c.paths) && Array.isArray(c.declaredSymbols));
100
+ if (!valid) fail('previous model', `${file} is not a version 1 logical-components model`);
101
+ return raw;
102
+ }
103
+
104
+ // Bad input or a file system that changed under the run (a folder deleted or
105
+ // unreadable mid-walk): reported on stderr with exit 2, never as a stack trace.
106
+ const FS_CODES = new Set(['ENOENT', 'EACCES', 'EPERM', 'ENOTDIR', 'EISDIR', 'EBUSY']);
107
+ const isInputFailure = (error) => error instanceof InputError || (error && FS_CODES.has(error.code));
108
+
109
+ module.exports = { InputError, isInputFailure, readJson, writeJson, loadStructure, loadSymbols, loadPreviousModel, normalizeRepoPath, byId, isObject, ID };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ // Walks source files under repo folders with the exclusions every
4
+ // logical-components script shares, so the scan, the index, the fingerprints
5
+ // and the claim checks all see exactly the same set of files.
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { languageOf } = require('./declared-symbol-patterns.cjs');
10
+
11
+ // Folder names that never hold component code: tests, build output, tooling.
12
+ const EXCLUDED_DIRS = new Set([
13
+ 'bin', 'obj', 'node_modules', 'dist', 'build', 'out', 'target', 'test', 'tests', '__tests__', 'spec',
14
+ 'coverage', 'vendor', 'generated', 'migrations',
15
+ ]);
16
+ const TEST_FILE = [/\.(test|spec)\.[^.]+$/i, /_test\.go$/, /^test_.*\.py$/];
17
+
18
+ function isExcludedDir(name) {
19
+ return name.startsWith('.') || EXCLUDED_DIRS.has(name.toLowerCase()) || /\.tests?$/i.test(name);
20
+ }
21
+
22
+ function isSourceFile(name) {
23
+ return languageOf(name) !== null && !TEST_FILE.some((re) => re.test(name));
24
+ }
25
+
26
+ const toPosix = (p) => p.split(path.sep).join('/');
27
+
28
+ function sortedEntries(absDir) {
29
+ return fs.readdirSync(absDir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
30
+ }
31
+
32
+ // Repo-relative posix paths of every source file under relDir, sorted.
33
+ function walkSourceFiles(repo, relDir) {
34
+ const found = [];
35
+ const visit = (rel) => {
36
+ for (const entry of sortedEntries(path.join(repo, rel))) {
37
+ const child = rel ? `${rel}/${entry.name}` : entry.name;
38
+ if (entry.isDirectory()) {
39
+ if (!isExcludedDir(entry.name)) visit(child);
40
+ } else if (entry.isFile() && isSourceFile(entry.name)) {
41
+ found.push(child);
42
+ }
43
+ }
44
+ };
45
+ visit(relDir);
46
+ return found;
47
+ }
48
+
49
+ // Every source file of a component (all its paths), sorted and de-duplicated.
50
+ function listComponentFiles(repo, paths) {
51
+ const files = new Set();
52
+ for (const p of paths) for (const f of walkSourceFiles(repo, p)) files.add(f);
53
+ return [...files].sort();
54
+ }
55
+
56
+ // File text with line endings normalised, so Windows checkouts read like LF ones.
57
+ function readSource(repo, relFile) {
58
+ return fs.readFileSync(path.join(repo, relFile), 'utf8').replace(/\r\n?/g, '\n');
59
+ }
60
+
61
+ module.exports = { isExcludedDir, isSourceFile, toPosix, sortedEntries, walkSourceFiles, listComponentFiles, readSource };
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ // Checks one component analyst's claims against the code. A responsibility
4
+ // needs evidence inside the component's own files; a usage needs the target's
5
+ // symbol to really appear near the cited line. Everything that fails is
6
+ // returned with a precise reason, so the repair round can fix it.
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { listComponentFiles, readSource } = require('./repo-walk.cjs');
11
+ const { wholeWordPattern } = require('./declared-symbol-patterns.cjs');
12
+ const { normalizeRepoPath } = require('./read-json-input.cjs');
13
+
14
+ const MAX_TEXT = 160;
15
+ const MAX_ACTION = 60;
16
+ const MAX_SPAN = 60;
17
+ const LINE_TOLERANCE = 3;
18
+ // Markup starters, control characters, and invisible or bidi-reordering characters.
19
+ const FORBIDDEN = /[<`[\u0000-\u0009\u000B-\u001F\u007F​-‏‪-‮⁠-⁤⁦-⁩]/;
20
+ const UNPRINTABLE = /[\u0000-\u001F\u007F​-‏‪-‮⁠-⁤⁦-⁩]/g;
21
+ const IMPORT_LINE = /^\s*(?:import|using|from|package|require|#include)\b|\brequire\s*\(|\bfrom\s+['"]/;
22
+ // Inside a Go `import ( ... )` block an import is a bare quoted path, optionally aliased.
23
+ const GO_IMPORT_ENTRY = /^\s*(?:[\w.]+\s+)?"[^"]*"\s*$/;
24
+
25
+ // Single printable line for stdout and the rejected list.
26
+ const oneLine = (value, max = 120) => String(value).replace(UNPRINTABLE, ' ').slice(0, max);
27
+ const words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
28
+
29
+ // True when a run of consecutive identifier segments of the import line spells
30
+ // the folder name: `BidCapture` matches `App.Application.BidCapture` and
31
+ // `../bid-capture/x`, but `Order` does not match `App.OrderPlacement`.
32
+ function importNamesFolder(line, folder) {
33
+ const target = words(folder).join('');
34
+ const segments = words(line);
35
+ for (let start = 0; start < segments.length; start += 1) {
36
+ let joined = '';
37
+ for (let end = start; end < segments.length && joined.length < target.length; end += 1) {
38
+ joined += segments[end];
39
+ if (joined === target) return true;
40
+ }
41
+ }
42
+ return false;
43
+ }
44
+
45
+ function importLines(rel, lines) {
46
+ const isGo = rel.endsWith('.go');
47
+ return lines.filter((l) => IMPORT_LINE.test(l) || (isGo && GO_IMPORT_ENTRY.test(l)));
48
+ }
49
+
50
+ function plainTextProblem(value, label, max) {
51
+ if (typeof value !== 'string' || !value.trim()) return `${label} is empty`;
52
+ if (/[\r\n]/.test(value)) return `${label} contains a line break`;
53
+ if (value.length > max) return `${label} is longer than ${max} characters`;
54
+ if (FORBIDDEN.test(value)) return `${label} contains a forbidden character (<, backtick, [, control or invisible)`;
55
+ return null;
56
+ }
57
+
58
+ const isLine = (n) => Number.isInteger(n) && n >= 1;
59
+
60
+ function createVerifier(repo, structure, symbols) {
61
+ const components = new Map(structure.components.map((c) => [c.id, c]));
62
+ const fileSets = new Map();
63
+ const fileLines = new Map();
64
+
65
+ const filesOf = (component) => {
66
+ if (!fileSets.has(component.id)) fileSets.set(component.id, new Set(listComponentFiles(repo, component.paths)));
67
+ return fileSets.get(component.id);
68
+ };
69
+ const linesOf = (rel) => {
70
+ if (!fileLines.has(rel)) {
71
+ const lines = readSource(repo, rel).split('\n');
72
+ if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
73
+ fileLines.set(rel, lines);
74
+ }
75
+ return fileLines.get(rel);
76
+ };
77
+
78
+ // Returns { rel } for a scanned source file of the component, else { problem }.
79
+ function ownFile(component, file) {
80
+ const rel = typeof file === 'string' ? normalizeRepoPath(file) : null;
81
+ if (!rel || !component.paths.some((p) => rel === p || rel.startsWith(`${p}/`))) {
82
+ return { problem: `${oneLine(file)} is outside the component paths` };
83
+ }
84
+ if (!fs.existsSync(path.join(repo, rel))) return { problem: `${rel} does not exist` };
85
+ if (!filesOf(component).has(rel)) return { problem: `${rel} is not a scanned source file` };
86
+ return { rel };
87
+ }
88
+
89
+ function evidenceProblem(component, evidence) {
90
+ const lines = evidence && evidence.lines;
91
+ if (!evidence || !Array.isArray(lines) || lines.length !== 2 || !isLine(lines[0]) || !isLine(lines[1]) || lines[1] < lines[0]) {
92
+ return { problem: 'evidence must be { file, lines: [start, end] }' };
93
+ }
94
+ const { rel, problem } = ownFile(component, evidence.file);
95
+ if (problem) return { problem };
96
+ const [start, end] = lines;
97
+ if (end - start + 1 > MAX_SPAN) return { problem: `lines ${start}-${end} span more than ${MAX_SPAN} lines` };
98
+ const count = linesOf(rel).length;
99
+ if (end > count) return { problem: `lines ${start}-${end} are outside ${rel} (${count} lines)` };
100
+ return { value: { file: rel, lines: [start, end] } };
101
+ }
102
+
103
+ function verifyResponsibility(component, claim) {
104
+ const text = claim && claim.text;
105
+ const label = oneLine(text === undefined ? '(no text)' : text);
106
+ const textProblem = plainTextProblem(text, 'text', MAX_TEXT);
107
+ if (textProblem) return { claim: label, reason: textProblem };
108
+ const evidence = [];
109
+ const problems = [];
110
+ for (const item of Array.isArray(claim.evidence) ? claim.evidence : []) {
111
+ const checked = evidenceProblem(component, item);
112
+ if (checked.problem) problems.push(checked.problem);
113
+ else evidence.push(checked.value);
114
+ }
115
+ if (!evidence.length) return { claim: label, reason: `no valid evidence: ${problems.join('; ') || 'none given'}` };
116
+ return { value: { text: text.trim(), evidence } };
117
+ }
118
+
119
+ // Nearest line (earlier wins a tie) within the tolerance that names the symbol.
120
+ function findSymbolLine(rel, symbol, cited) {
121
+ const lines = linesOf(rel);
122
+ const pattern = wholeWordPattern([symbol]);
123
+ for (let distance = 0; distance <= LINE_TOLERANCE; distance += 1) {
124
+ for (const n of distance === 0 ? [cited] : [cited - distance, cited + distance]) {
125
+ if (n >= 1 && n <= lines.length && pattern.test(lines[n - 1])) return n;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+
131
+ function verifyUsage(component, usage) {
132
+ const { target, symbol, file, line, action } = usage || {};
133
+ const label = oneLine(`${symbol} -> ${target}`);
134
+ const reject = (reason) => ({ claim: label, reason });
135
+ if (target === component.id) return reject(`target ${target} is the component itself`);
136
+ if (typeof target !== 'string' || !components.has(target)) return reject(`unknown target ${oneLine(target)}`);
137
+ const owners = typeof symbol === 'string' && Object.hasOwn(symbols.symbols, symbol) ? symbols.symbols[symbol] : [];
138
+ if (!owners.includes(target)) return reject(`symbol ${oneLine(symbol)} is not declared by ${target}`);
139
+ const { rel, problem } = ownFile(component, file);
140
+ if (problem) return reject(problem);
141
+ if (!isLine(line)) return reject('line must be a positive integer');
142
+ const actionProblem = plainTextProblem(action, 'action', MAX_ACTION);
143
+ if (actionProblem) return reject(actionProblem);
144
+ const found = findSymbolLine(rel, symbol, line);
145
+ if (found === null) return reject(`symbol ${symbol} not found within ${LINE_TOLERANCE} lines of ${rel}:${line}`);
146
+ if (owners.length > 1) {
147
+ const folders = [...new Set(components.get(target).paths.map((p) => p.split('/').pop()))];
148
+ const imports = importLines(rel, linesOf(rel));
149
+ if (!folders.some((f) => imports.some((l) => importNamesFolder(l, f)))) {
150
+ return reject(`ambiguous symbol ${symbol} (declared by ${owners.join(', ')}) and ${rel} imports nothing named ${folders.join(' or ')}`);
151
+ }
152
+ }
153
+ return { value: { from: component.id, to: target, symbol, file: rel, line: found, action: action.trim() } };
154
+ }
155
+
156
+ return { verifyResponsibility, verifyUsage };
157
+ }
158
+
159
+ module.exports = { createVerifier, oneLine };
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Renders docs/logical-components.json as one self-contained HTML page:
5
+ // Mermaid component diagram, coupling and edge tables, component cards and
6
+ // rejected claims. Same model in, byte-identical page out.
7
+ // Usage: render-component-diagram-html.cjs --model <json> --out <html> [--repo <repo root, default cwd>]
8
+ // Exit: 0 ok, 1 usage, 2 unreadable or invalid model (the existing page is left untouched).
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { parseArgs } = require('util');
13
+ const { InputError, isInputFailure, readJson, isObject, normalizeRepoPath, ID } = require('./lib/read-json-input.cjs');
14
+ const { buildMermaid } = require('./lib/mermaid-graph.cjs');
15
+ const { escapeHtml, renderCards, renderCouplingTable, renderEdgeTable, renderRejected } = require('./lib/html-sections.cjs');
16
+
17
+ const TEMPLATE = path.join(__dirname, '..', 'assets', 'component-diagram-template.html');
18
+ const USAGE = 'usage: render-component-diagram-html.cjs --model <logical-components.json> --out <page.html> [--repo <repo>]';
19
+
20
+ function parse() {
21
+ try {
22
+ const { values } = parseArgs({ options: { model: { type: 'string' }, out: { type: 'string' }, repo: { type: 'string' } } });
23
+ if (values.model && values.out) return values;
24
+ } catch (error) {
25
+ process.stderr.write(`${error.message}\n`);
26
+ }
27
+ process.stderr.write(`${USAGE}\n`);
28
+ return process.exit(1);
29
+ }
30
+
31
+ const isCount = (n) => Number.isInteger(n) && n >= 0;
32
+ const isLine = (n) => Number.isInteger(n) && n >= 1;
33
+ const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
34
+ // Evidence links point into the repo only: a plain relative path, no scheme, no `..`.
35
+ const isRepoFile = (f) => typeof f === 'string' && normalizeRepoPath(f) === f && !/^[a-z][a-z0-9+.-]*:/i.test(f);
36
+
37
+ // Checks every field the renderer reads, so a hand-edited model fails loudly.
38
+ function validateModel(model) {
39
+ const problem = (message) => { throw new InputError(`invalid model: ${message}`); };
40
+ if (!isObject(model) || model.version !== 1) problem('version must be 1');
41
+ if (!Array.isArray(model.modules) || !Array.isArray(model.components) || !Array.isArray(model.edges) || !Array.isArray(model.rejected)) {
42
+ problem('needs modules, components, edges and rejected arrays');
43
+ }
44
+ const modules = new Set();
45
+ for (const m of model.modules) {
46
+ if (!isObject(m) || !ID.test(m.id) || typeof m.name !== 'string') problem(`bad module ${JSON.stringify(m)}`);
47
+ modules.add(m.id);
48
+ }
49
+ const ids = new Set();
50
+ for (const c of model.components) {
51
+ if (!isObject(c) || !ID.test(c.id) || ids.has(c.id)) problem(`bad or duplicate component id ${JSON.stringify(c && c.id)}`);
52
+ ids.add(c.id);
53
+ if (typeof c.name !== 'string' || !strings(c.paths) || !(c.module === null || modules.has(c.module))) problem(`bad component ${c.id}`);
54
+ if (![c.ca, c.ce, c.ct].every(isCount) || !Array.isArray(c.responsibilities)) problem(`bad metrics or responsibilities in ${c.id}`);
55
+ for (const r of c.responsibilities) {
56
+ if (!isObject(r) || typeof r.text !== 'string' || !Array.isArray(r.evidence)
57
+ || !r.evidence.every((e) => isObject(e) && isRepoFile(e.file) && Array.isArray(e.lines) && e.lines.every(isLine))) {
58
+ problem(`bad responsibility in ${c.id}`);
59
+ }
60
+ }
61
+ }
62
+ for (const e of model.edges) {
63
+ if (!isObject(e) || !ids.has(e.from) || !ids.has(e.to) || !strings(e.labels) || !Array.isArray(e.evidence)
64
+ || !e.evidence.every((ev) => isObject(ev) && isRepoFile(ev.file) && isLine(ev.line))) {
65
+ problem(`bad edge ${JSON.stringify(e && [e.from, e.to])}`);
66
+ }
67
+ }
68
+ if (!isCount(model.totalCoupling)) problem('totalCoupling must be a count');
69
+ if (!model.rejected.every((r) => isObject(r) && ['component', 'kind', 'claim', 'reason'].every((k) => typeof r[k] === 'string'))) {
70
+ problem('bad rejected entry');
71
+ }
72
+ }
73
+
74
+ function renderPage(model, fileBase) {
75
+ const parts = {
76
+ SUMMARY: `<strong>${model.components.length} components · ${model.edges.length} edges</strong> · `
77
+ + `<strong>Total system coupling = ${model.totalCoupling}</strong>`,
78
+ MERMAID: escapeHtml(buildMermaid(model)),
79
+ COUPLING: renderCouplingTable(model),
80
+ EDGES: renderEdgeTable(model, fileBase),
81
+ CARDS: renderCards(model, fileBase),
82
+ REJECTED: renderRejected(model),
83
+ };
84
+ const template = fs.readFileSync(TEMPLATE, 'utf8').replace(/\r\n/g, '\n');
85
+ return template.replace(/\{\{([A-Z]+)\}\}/g, (match, key) => {
86
+ if (!Object.hasOwn(parts, key)) throw new Error(`template has unknown placeholder ${match}`);
87
+ return parts[key];
88
+ });
89
+ }
90
+
91
+ function main() {
92
+ const args = parse();
93
+ let html;
94
+ try {
95
+ const model = readJson(args.model, 'model');
96
+ validateModel(model);
97
+ const outDir = path.dirname(path.resolve(args.out));
98
+ const fileBase = path.relative(outDir, path.resolve(args.repo || process.cwd())).split(path.sep).join('/');
99
+ html = renderPage(model, fileBase);
100
+ } catch (error) {
101
+ if (!isInputFailure(error)) throw error;
102
+ process.stderr.write(`${error.message}\n`);
103
+ process.exit(2);
104
+ }
105
+ fs.mkdirSync(path.dirname(path.resolve(args.out)), { recursive: true });
106
+ const temp = `${args.out}.tmp-${process.pid}`;
107
+ fs.writeFileSync(temp, html);
108
+ fs.renameSync(temp, args.out);
109
+ process.stdout.write(`wrote ${args.out}\n`);
110
+ }
111
+
112
+ main();
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Prints the repo's folder tree with source-file counts and languages, so the
5
+ // main agent can decide which folder level holds the logical components.
6
+ // Usage: scan-folder-tree.cjs <repo> [--depth 4]
7
+ // Exit: 0 ok, 1 usage.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { parseArgs } = require('util');
12
+ const { isExcludedDir, isSourceFile, sortedEntries } = require('./lib/repo-walk.cjs');
13
+ const { languageOf, LABELS } = require('./lib/declared-symbol-patterns.cjs');
14
+
15
+ // Build output and dependencies are skipped silently; any other skipped folder
16
+ // (tests, generated code, migrations) is named so the gate can list it.
17
+ const SILENT = new Set(['bin', 'obj', 'node_modules', 'dist', 'build', 'out', 'target', 'coverage', 'vendor']);
18
+
19
+ function hasSourceFile(absDir) {
20
+ return sortedEntries(absDir).some((e) => (e.isDirectory() ? hasSourceFile(path.join(absDir, e.name)) : languageOf(e.name) !== null));
21
+ }
22
+
23
+ function usage(message) {
24
+ process.stderr.write(`${message}\nusage: scan-folder-tree.cjs <repo> [--depth 4]\n`);
25
+ process.exit(1);
26
+ }
27
+
28
+ // Folder node with recursive source-file count and per-language counts.
29
+ // `excluded` collects repo-relative paths of skipped folders worth naming.
30
+ function scan(absDir, name, rel, excluded) {
31
+ const node = { name, files: 0, languages: {}, children: [] };
32
+ for (const entry of sortedEntries(absDir)) {
33
+ if (entry.isDirectory()) {
34
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
35
+ if (isExcludedDir(entry.name)) {
36
+ const silent = entry.name.startsWith('.') || SILENT.has(entry.name.toLowerCase());
37
+ if (!silent && hasSourceFile(path.join(absDir, entry.name))) excluded.push(`${childRel}/`);
38
+ continue;
39
+ }
40
+ const child = scan(path.join(absDir, entry.name), entry.name, childRel, excluded);
41
+ if (child.files === 0) continue;
42
+ node.children.push(child);
43
+ node.files += child.files;
44
+ for (const [lang, n] of Object.entries(child.languages)) node.languages[lang] = (node.languages[lang] || 0) + n;
45
+ } else if (entry.isFile() && isSourceFile(entry.name)) {
46
+ const lang = languageOf(entry.name);
47
+ node.files += 1;
48
+ node.languages[lang] = (node.languages[lang] || 0) + 1;
49
+ }
50
+ }
51
+ return node;
52
+ }
53
+
54
+ function describe(node) {
55
+ const langs = Object.keys(node.languages).sort().map((l) => `${LABELS[l]} ${node.languages[l]}`).join(', ');
56
+ return `${node.name}/ ${node.files} file${node.files === 1 ? '' : 's'} (${langs})`;
57
+ }
58
+
59
+ function render(node, depth, maxDepth, lines) {
60
+ lines.push(`${' '.repeat(depth)}${describe(node)}`);
61
+ if (depth >= maxDepth) return;
62
+ for (const child of node.children) render(child, depth + 1, maxDepth, lines);
63
+ }
64
+
65
+ function main() {
66
+ let parsed;
67
+ try {
68
+ parsed = parseArgs({ allowPositionals: true, options: { depth: { type: 'string', default: '4' } } });
69
+ } catch (error) {
70
+ usage(error.message);
71
+ }
72
+ const [repo] = parsed.positionals;
73
+ if (!repo) usage('missing <repo>');
74
+ const depth = Number(parsed.values.depth);
75
+ if (!Number.isInteger(depth) || depth < 1) usage(`--depth must be a positive integer: ${parsed.values.depth}`);
76
+ const abs = path.resolve(repo);
77
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) usage(`not a directory: ${repo}`);
78
+
79
+ const excluded = [];
80
+ const root = scan(abs, path.basename(abs), '', excluded);
81
+ const lines = [];
82
+ if (root.files === 0) lines.push(`${root.name}/ no source files found`);
83
+ else render(root, 0, depth, lines);
84
+ lines.push(`excluded: ${excluded.length ? excluded.sort().join(', ') : 'none'}`);
85
+ process.stdout.write(`${lines.join('\n')}\n`);
86
+ }
87
+
88
+ main();
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Decides which components need a fresh analyst agent on this run and copies
5
+ // the stored analysis of every other component out of the previous model.
6
+ // Usage: select-stale-components.cjs --repo <repo> --structure <structure.json> --symbols <symbols.json>
7
+ // --previous <docs/logical-components.json> --analysis <dir> [--full]
8
+ // Exit: 0 ok, 1 usage, 2 unreadable input or previous model.
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { parseArgs } = require('util');
13
+ const { listComponentFiles, readSource } = require('./lib/repo-walk.cjs');
14
+ const { wholeWordPattern } = require('./lib/declared-symbol-patterns.cjs');
15
+ const { InputError, isInputFailure, loadStructure, loadSymbols, loadPreviousModel, writeJson, isObject } = require('./lib/read-json-input.cjs');
16
+
17
+ const USAGE = 'usage: select-stale-components.cjs --repo <repo> --structure <structure.json> --symbols <symbols.json> '
18
+ + '--previous <docs/logical-components.json> --analysis <dir> [--full]';
19
+
20
+ function parse() {
21
+ const options = {
22
+ repo: { type: 'string' }, structure: { type: 'string' }, symbols: { type: 'string' },
23
+ previous: { type: 'string' }, analysis: { type: 'string' }, full: { type: 'boolean', default: false },
24
+ };
25
+ try {
26
+ const { values } = parseArgs({ options });
27
+ if (values.repo && values.structure && values.symbols && values.previous && values.analysis) return values;
28
+ } catch (error) {
29
+ process.stderr.write(`${error.message}\n`);
30
+ }
31
+ process.stderr.write(`${USAGE}\n`);
32
+ return process.exit(1);
33
+ }
34
+
35
+ const samePaths = (a, b) => JSON.stringify([...a].sort()) === JSON.stringify([...b].sort());
36
+
37
+ // Symbols whose owners differ from the previous run: `new` when nobody declared
38
+ // them before, `moved` when other components did. A stored usage of either may
39
+ // now point at the wrong component or miss an edge, so components naming one re-run.
40
+ function changedSymbols(previous, current) {
41
+ const ownersBefore = new Map();
42
+ for (const c of previous.components) {
43
+ for (const name of c.declaredSymbols) {
44
+ if (!ownersBefore.has(name)) ownersBefore.set(name, []);
45
+ ownersBefore.get(name).push(c.id);
46
+ }
47
+ }
48
+ const changed = new Map();
49
+ for (const name of Object.keys(current)) {
50
+ const before = ownersBefore.get(name);
51
+ if (!before) changed.set(name, 'new');
52
+ else if (before.sort().join(',') !== [...current[name]].sort().join(',')) changed.set(name, 'moved');
53
+ }
54
+ return changed;
55
+ }
56
+
57
+ // First changed symbol (alphabetically) that the component's code names, not counting its own.
58
+ function changedSymbolMentioned(repo, component, changed, ownSymbols) {
59
+ const names = [...changed.keys()].filter((name) => !ownSymbols.includes(name));
60
+ if (!names.length) return null;
61
+ const pattern = wholeWordPattern(names, 'g');
62
+ const found = new Set();
63
+ for (const file of listComponentFiles(repo, component.paths)) {
64
+ for (const match of readSource(repo, file).matchAll(pattern)) found.add(match[0]);
65
+ }
66
+ if (!found.size) return null;
67
+ const name = [...found].sort()[0];
68
+ return `mentions ${changed.get(name)} symbol ${name}`;
69
+ }
70
+
71
+ function staleReason(repo, component, context) {
72
+ const { full, previous, symbols, changed, componentIds } = context;
73
+ if (full) return '--full';
74
+ if (!previous) return 'no previous model';
75
+ const before = previous.components.find((c) => c.id === component.id);
76
+ if (!before) return 'new component';
77
+ if (!samePaths(before.paths, component.paths)) return 'paths changed';
78
+ if (before.fingerprint !== symbols.fingerprints[component.id]) return 'content changed';
79
+ if (!isObject(before.analysis)) return 'no stored analysis';
80
+ const usages = Array.isArray(before.analysis.usages) ? before.analysis.usages : [];
81
+ const gone = usages.map((u) => u && u.target).find((t) => typeof t === 'string' && t !== component.id && !componentIds.has(t));
82
+ if (gone) return `uses removed component ${gone}`;
83
+ return changedSymbolMentioned(repo, component, changed, symbols.byComponent[component.id]);
84
+ }
85
+
86
+ function main() {
87
+ const args = parse();
88
+ const repo = path.resolve(args.repo);
89
+ try {
90
+ const structure = loadStructure(args.structure);
91
+ const symbols = loadSymbols(args.symbols, structure);
92
+ let previous = null;
93
+ if (!args.full && fs.existsSync(args.previous)) {
94
+ try {
95
+ previous = loadPreviousModel(args.previous);
96
+ } catch (error) {
97
+ throw new InputError(`${error.message}\nre-run with --full to analyse every component from scratch`);
98
+ }
99
+ }
100
+ const context = {
101
+ full: args.full,
102
+ previous,
103
+ symbols,
104
+ changed: previous ? changedSymbols(previous, symbols.symbols) : new Map(),
105
+ componentIds: new Set(structure.components.map((c) => c.id)),
106
+ };
107
+
108
+ fs.mkdirSync(args.analysis, { recursive: true });
109
+ const lines = [];
110
+ for (const component of structure.components) {
111
+ const out = path.join(args.analysis, `${component.id}.json`);
112
+ const reason = staleReason(repo, component, context);
113
+ if (reason) {
114
+ fs.rmSync(out, { force: true });
115
+ lines.push(`stale ${component.id} ${reason}`);
116
+ } else {
117
+ writeJson(out, previous.components.find((c) => c.id === component.id).analysis);
118
+ }
119
+ }
120
+ lines.push(`${lines.length} of ${structure.components.length} components stale`);
121
+ process.stdout.write(`${lines.join('\n')}\n`);
122
+ } catch (error) {
123
+ if (!isInputFailure(error)) throw error;
124
+ process.stderr.write(`${error.message}\n`);
125
+ process.exit(2);
126
+ }
127
+ }
128
+
129
+ main();