@phuc1403/musketeer 0.12.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALLATION.md +1 -1
- package/README.md +2 -2
- package/manifest.json +4 -3
- package/package.json +1 -1
- package/template/.claude/hooks/inject-ubiquitous-language.cjs +2 -2
- package/template/.claude/skills/domain-expert/SKILL.md +76 -0
- package/template/.claude/skills/logical-components/SKILL.md +165 -94
- package/template/.claude/skills/logical-components/assets/component-diagram-template.html +104 -0
- package/template/.claude/skills/logical-components/references/component-analyst-prompt.md +88 -0
- package/template/.claude/skills/logical-components/references/component-level-detection.md +95 -0
- package/template/.claude/skills/logical-components/references/logical-component-concepts.md +92 -0
- package/template/.claude/skills/logical-components/scripts/build-component-model.cjs +120 -0
- package/template/.claude/skills/logical-components/scripts/index-declared-symbols.cjs +106 -0
- package/template/.claude/skills/logical-components/scripts/lib/coupling-metrics.cjs +48 -0
- package/template/.claude/skills/logical-components/scripts/lib/declared-symbol-patterns.cjs +57 -0
- package/template/.claude/skills/logical-components/scripts/lib/html-sections.cjs +91 -0
- package/template/.claude/skills/logical-components/scripts/lib/mermaid-graph.cjs +55 -0
- package/template/.claude/skills/logical-components/scripts/lib/read-json-input.cjs +109 -0
- package/template/.claude/skills/logical-components/scripts/lib/repo-walk.cjs +61 -0
- package/template/.claude/skills/logical-components/scripts/lib/verify-claims.cjs +159 -0
- package/template/.claude/skills/logical-components/scripts/render-component-diagram-html.cjs +112 -0
- package/template/.claude/skills/logical-components/scripts/scan-folder-tree.cjs +88 -0
- package/template/.claude/skills/logical-components/scripts/select-stale-components.cjs +129 -0
- package/template/.claude/skills/logical-components/.gitattributes +0 -2
- package/template/.claude/skills/logical-components/references/component-classifier-prompt.md +0 -53
- package/template/.claude/skills/logical-components/references/responsibility-agent-prompt.md +0 -67
- package/template/.claude/skills/logical-components/references/responsibility-verifier-prompt.md +0 -39
- package/template/.claude/skills/logical-components/scripts/Directory.Build.props +0 -7
- package/template/.claude/skills/logical-components/scripts/Directory.Build.rsp +0 -1
- package/template/.claude/skills/logical-components/scripts/Directory.Build.targets +0 -6
- package/template/.claude/skills/logical-components/scripts/Directory.Packages.props +0 -8
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchPlanning.cs +0 -70
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/BatchesCommand.cs +0 -47
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CandidatesCommand.cs +0 -98
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CitableSource.cs +0 -54
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Citation.cs +0 -37
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyBatchesCommand.cs +0 -86
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ClassifyMergeCommand.cs +0 -219
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentDiscovery.cs +0 -180
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentRule.cs +0 -130
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ComponentsFile.cs +0 -101
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/CouplingResolver.cs +0 -251
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/DisplayName.cs +0 -14
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/EdgeExtraction.cs +0 -111
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractCommand.cs +0 -87
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ExtractConfig.cs +0 -79
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/LogicalComponents.csproj +0 -15
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownCodeSpans.cs +0 -72
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MarkdownRenderer.cs +0 -78
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUseResolution.cs +0 -272
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/MemberUses.cs +0 -231
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PlainText.cs +0 -55
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PortResolution.cs +0 -75
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/Program.cs +0 -27
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ProjectFilters.cs +0 -77
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/PruneCommand.cs +0 -164
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/RenderCommand.cs +0 -234
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityBatches.cs +0 -84
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/ResponsibilityValidation.cs +0 -197
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SeamDispatch.cs +0 -115
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SourceCodeLines.cs +0 -79
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/SymbolNames.cs +0 -53
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/TypeMap.cs +0 -106
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WalkRoots.cs +0 -63
- package/template/.claude/skills/logical-components/scripts/LogicalComponents/WorkspaceLoader.cs +0 -297
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/AssignIdsTests.cs +0 -41
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchPlanningTests.cs +0 -90
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/BatchesCommandTests.cs +0 -37
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CandidatesCommandTests.cs +0 -59
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CitationTests.cs +0 -42
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ClassifyCommandsTests.cs +0 -249
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ComponentDiscoveryTests.cs +0 -253
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppExtractionTests.cs +0 -188
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingAppWorkspace.cs +0 -30
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/CouplingExtractionTests.cs +0 -174
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/DisplayNameTests.cs +0 -17
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ExtractConfigTests.cs +0 -72
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/FixturePaths.cs +0 -36
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/LogicalComponents.Tests.csproj +0 -23
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/MarkdownRendererTests.cs +0 -93
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ProjectFiltersTests.cs +0 -69
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/PruneCommandTests.cs +0 -149
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderCommandTests.cs +0 -225
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderGoldenTests.cs +0 -34
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/RenderTestData.cs +0 -55
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/ResponsibilityValidationTests.cs +0 -315
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/SampleAppWorkspace.cs +0 -27
- package/template/.claude/skills/logical-components/scripts/LogicalComponents.Tests/WorkspaceLoaderTests.cs +0 -266
- package/template/.claude/skills/logical-components/scripts/NuGet.config +0 -17
- package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.Application.csproj +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/BrokenApp/src/Broken.Application/Broken.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/CompositionRootApp.slnx +0 -5
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderExtensions.cs +0 -15
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/HostBuilderShims.cs +0 -25
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/NativeInterop.cs +0 -17
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/Root.Application.csproj +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootAutofacModule.cs +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/CompositionRootApp/src/Root.Application/RootGreeter.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/logical-components.json +0 -48
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AbstractMemberCases.cs +0 -27
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/AttributeCases.cs +0 -23
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/CouplingApp.Application.csproj +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericBaseCases.cs +0 -14
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/GenericSeamBindingCases.cs +0 -59
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ImplicitCallCases.cs +0 -230
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.First.cs +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/PartialInheritCases.Second.cs +0 -14
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/StackOverflowCases.cs +0 -14
- package/template/.claude/skills/logical-components/scripts/fixtures/CouplingApp/src/CouplingApp.Application/ViaDisambiguationCases.cs +0 -36
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/MultiTargetApp.slnx +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Legacy.FSharp.fsproj +0 -8
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Legacy.FSharp/Library.fs +0 -5
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Multi.Application.csproj +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Application/Scheduling.cs +0 -14
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/Multi.Infrastructure.csproj +0 -12
- package/template/.claude/skills/logical-components/scripts/fixtures/MultiTargetApp/src/Multi.Infrastructure/SchedulingClient.cs +0 -12
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/Solo.Application.csproj +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application/SoloGreeting.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/FakeGreeting.cs +0 -6
- package/template/.claude/skills/logical-components/scripts/fixtures/NoSolutionApp/src/Solo.Application.Tests/Solo.Application.Tests.csproj +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/ObjOutputApp.slnx +0 -5
- package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Greeting.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/ObjOutputApp/src/Obj.Application/Obj.Application.csproj +0 -14
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/components.json +0 -67
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/expected.md +0 -28
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/App/OrderAcceptance.cs +0 -16
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/repo/src/Infra/OrderQueue.cs +0 -10
- package/template/.claude/skills/logical-components/scripts/fixtures/RenderGolden/resp/batch-01.json +0 -18
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/SampleApp.slnx +0 -10
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/logical-components.json +0 -78
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CandidateCases.cs +0 -37
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/CouplingCases.cs +0 -33
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/DependencyInjection.cs +0 -18
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/EdgeCases.cs +0 -63
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.Base.cs +0 -5
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/LoudNotifying.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderAcceptance.cs +0 -17
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderOptions.cs +0 -9
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRejectedException.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRequest.cs +0 -4
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderRunning.cs +0 -18
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/OrderValidation.cs +0 -6
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Ports.cs +0 -19
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/PriceRules.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.Totals.cs +0 -6
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReceiptPrinting.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewCases.cs +0 -107
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/ReviewFixCases.cs +0 -143
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/RunClock.cs +0 -12
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/Sample.Application.csproj +0 -10
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Application/SeeThroughCases.cs +0 -89
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/OrderRunning.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Billing.Application/Sample.Billing.Application.csproj +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ForwardedNotifier.cs +0 -8
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/InfrastructureDependencyInjection.cs +0 -16
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Mailer.cs +0 -6
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/OrderQueue.cs +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueEntry.cs +0 -4
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/QueueStore.cs +0 -8
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewCases.cs +0 -35
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/ReviewFixCases.cs +0 -13
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/Sample.Infrastructure.csproj +0 -13
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/SeeThroughCases.cs +0 -18
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/src/Sample.Infrastructure/UnreachedPlumbing.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/FakeOrderQueue.cs +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/SampleApp/tests/Sample.Tests/Sample.Tests.csproj +0 -11
- package/template/.claude/skills/logical-components/scripts/fixtures/SharedOutsideRepo/SharedClock.cs +0 -7
- package/template/.claude/skills/logical-components/scripts/global.json +0 -5
- package/template/.claude/skills/logical-components/scripts/smoke-sample.sh +0 -26
|
@@ -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, '&')
|
|
11
|
+
.replace(/</g, '<')
|
|
12
|
+
.replace(/>/g, '>')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/'/g, ''');
|
|
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 };
|
|
@@ -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();
|