@brainwav/diagram 1.0.7 → 1.1.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 (91) hide show
  1. package/.diagram/contracts/machine-command-coverage.json +73 -0
  2. package/.diagram/migration/finalization-policy.json +20 -0
  3. package/LICENSE +202 -21
  4. package/README.md +132 -339
  5. package/package.json +46 -13
  6. package/scripts/refresh-diagram-context.sh +274 -182
  7. package/src/analyzers/default-analyzer.js +11 -0
  8. package/src/analyzers/index.js +34 -0
  9. package/src/artifacts/agent-context.js +105 -0
  10. package/src/artifacts/artifact-budget.js +224 -0
  11. package/src/artifacts/brief.js +153 -0
  12. package/src/artifacts/evidence-manifest.js +206 -0
  13. package/src/artifacts/evidence-summary.js +29 -0
  14. package/src/commands/analyze.js +125 -0
  15. package/src/commands/changed.js +185 -0
  16. package/src/commands/context.js +110 -0
  17. package/src/commands/diff.js +142 -0
  18. package/src/commands/doctor.js +335 -0
  19. package/src/commands/explain.js +273 -0
  20. package/src/commands/generate-all.js +170 -0
  21. package/src/commands/generate-animated.js +50 -0
  22. package/src/commands/generate-video.js +65 -0
  23. package/src/commands/generate.js +522 -0
  24. package/src/commands/init.js +123 -0
  25. package/src/commands/output.js +76 -0
  26. package/src/commands/scan.js +624 -0
  27. package/src/commands/shared.js +396 -0
  28. package/src/commands/validate.js +328 -0
  29. package/src/commands/video-shared.js +105 -0
  30. package/src/commands/workflow-pr.js +26 -0
  31. package/src/confidence/pipeline.js +186 -0
  32. package/src/config/diagramrc.js +79 -0
  33. package/src/context/build-context-pack.js +291 -0
  34. package/src/context/normalize-diagram-manifest.js +282 -0
  35. package/src/core/analysis-generation-analyze-components.js +102 -0
  36. package/src/core/analysis-generation-analyze-dependencies.js +33 -0
  37. package/src/core/analysis-generation-analyze-files.js +48 -0
  38. package/src/core/analysis-generation-analyze-options.js +73 -0
  39. package/src/core/analysis-generation-analyze.js +63 -0
  40. package/src/core/analysis-generation-constants.js +53 -0
  41. package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
  42. package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
  43. package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
  44. package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
  45. package/src/core/analysis-generation-diagrams-core.js +12 -0
  46. package/src/core/analysis-generation-diagrams-empty.js +68 -0
  47. package/src/core/analysis-generation-diagrams-erd.js +59 -0
  48. package/src/core/analysis-generation-diagrams-limit.js +27 -0
  49. package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
  50. package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
  51. package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
  52. package/src/core/analysis-generation-diagrams-role-data.js +182 -0
  53. package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
  54. package/src/core/analysis-generation-diagrams-role-security.js +129 -0
  55. package/src/core/analysis-generation-diagrams-role.js +25 -0
  56. package/src/core/analysis-generation-diagrams.js +182 -0
  57. package/src/core/analysis-generation-role-tags-constants.js +55 -0
  58. package/src/core/analysis-generation-role-tags-imports.js +32 -0
  59. package/src/core/analysis-generation-role-tags-infer.js +49 -0
  60. package/src/core/analysis-generation-role-tags-match.js +19 -0
  61. package/src/core/analysis-generation-role-tags.js +7 -0
  62. package/src/core/analysis-generation-utils-core.js +308 -0
  63. package/src/core/analysis-generation-utils-graph.js +321 -0
  64. package/src/core/analysis-generation-utils-resolution.js +76 -0
  65. package/src/core/analysis-generation-utils.js +9 -0
  66. package/src/core/analysis-generation.js +44 -0
  67. package/src/diagram.js +180 -1760
  68. package/src/formatters/console.js +198 -0
  69. package/src/formatters/index.js +41 -0
  70. package/src/formatters/json.js +113 -0
  71. package/src/formatters/junit.js +123 -0
  72. package/src/graph.js +159 -0
  73. package/src/incremental/cache.js +210 -0
  74. package/src/ir/architecture-ir.js +48 -0
  75. package/src/migration/evidence.js +262 -0
  76. package/src/migration/finalization-policy.js +35 -0
  77. package/src/renderers/report-html.js +265 -0
  78. package/src/rules/factory.js +108 -0
  79. package/src/rules/types/base.js +54 -0
  80. package/src/rules/types/import-rule.js +286 -0
  81. package/src/rules.js +380 -0
  82. package/src/schema/erd-confidence.js +56 -0
  83. package/src/schema/erd-extractor.js +504 -0
  84. package/src/schema/erd-model.js +176 -0
  85. package/src/schema/rules-schema.js +170 -0
  86. package/src/utils/suggestions.js +67 -0
  87. package/src/video.js +4 -5
  88. package/src/workflow/git-helpers.js +576 -0
  89. package/src/workflow/pr-command.js +694 -0
  90. package/src/workflow/pr-impact.js +848 -0
  91. package/src/workflow/sort-utils.js +16 -0
@@ -0,0 +1,224 @@
1
+ const AGENT_DIAGRAM_PRIORITY = Object.freeze([
2
+ 'architecture',
3
+ 'dependency',
4
+ 'erd',
5
+ 'database',
6
+ 'security',
7
+ 'auth',
8
+ 'events',
9
+ 'user',
10
+ 'flow',
11
+ 'class',
12
+ 'sequence',
13
+ 'agent',
14
+ 'c4context',
15
+ 'rag',
16
+ ]);
17
+ const BYTES_PER_TOKEN_ESTIMATE = 4;
18
+
19
+ const BASE_ARTIFACT_PROFILES = Object.freeze({
20
+ full: Object.freeze({
21
+ name: 'full',
22
+ maxBytesTotal: null,
23
+ maxBytesPerDiagram: null,
24
+ maxDiagrams: null,
25
+ priorityOrder: AGENT_DIAGRAM_PRIORITY,
26
+ }),
27
+ agent: Object.freeze({
28
+ name: 'agent',
29
+ maxBytesTotal: 12000,
30
+ maxBytesPerDiagram: 4000,
31
+ maxDiagrams: 4,
32
+ priorityOrder: AGENT_DIAGRAM_PRIORITY,
33
+ }),
34
+ 'ultra-compact': Object.freeze({
35
+ name: 'ultra-compact',
36
+ maxBytesTotal: 9000,
37
+ maxBytesPerDiagram: 3000,
38
+ maxDiagrams: 3,
39
+ priorityOrder: AGENT_DIAGRAM_PRIORITY,
40
+ }),
41
+ });
42
+
43
+ function toPositiveInt(value) {
44
+ if (value === null || value === undefined) return null;
45
+ if (value === '') return null;
46
+ const parsed = Number.parseInt(String(value), 10);
47
+ if (!Number.isFinite(parsed) || parsed <= 0) {
48
+ return null;
49
+ }
50
+ return parsed;
51
+ }
52
+
53
+ function byteLength(text) {
54
+ return Buffer.byteLength(String(text || ''), 'utf8');
55
+ }
56
+
57
+ function estimateTokensFromBytes(byteCount) {
58
+ const normalized = Number.isFinite(byteCount) ? byteCount : 0;
59
+ return Math.ceil(normalized / BYTES_PER_TOKEN_ESTIMATE);
60
+ }
61
+
62
+ function resolveArtifactProfile(profileName = 'full', overrides = {}) {
63
+ const normalizedName = String(profileName || 'full').toLowerCase();
64
+ const base = BASE_ARTIFACT_PROFILES[normalizedName];
65
+ if (!base) {
66
+ const supported = Object.keys(BASE_ARTIFACT_PROFILES).join(', ');
67
+ throw new Error(`Invalid artifact profile "${profileName}". Supported profiles: ${supported}`);
68
+ }
69
+
70
+ return {
71
+ ...base,
72
+ maxBytesTotal: toPositiveInt(overrides.maxBytesTotal) ?? base.maxBytesTotal,
73
+ maxBytesPerDiagram: toPositiveInt(overrides.maxBytesPerDiagram) ?? base.maxBytesPerDiagram,
74
+ maxDiagrams: toPositiveInt(overrides.maxDiagrams) ?? base.maxDiagrams,
75
+ priorityOrder: Array.isArray(base.priorityOrder) ? [...base.priorityOrder] : [],
76
+ };
77
+ }
78
+
79
+ function sortByPriority(diagrams, priorityOrder) {
80
+ const rank = new Map((priorityOrder || []).map((type, index) => [type, index]));
81
+ return [...(Array.isArray(diagrams) ? diagrams : [])].sort((left, right) => {
82
+ const leftRank = rank.has(left.type) ? rank.get(left.type) : Number.MAX_SAFE_INTEGER;
83
+ const rightRank = rank.has(right.type) ? rank.get(right.type) : Number.MAX_SAFE_INTEGER;
84
+ if (leftRank !== rightRank) {
85
+ return leftRank - rightRank;
86
+ }
87
+ return String(left.type || '').localeCompare(String(right.type || ''));
88
+ });
89
+ }
90
+
91
+ function truncateMermaidByBytes(mermaid, maxBytes) {
92
+ const content = String(mermaid || '');
93
+ const originalBytes = byteLength(content);
94
+ if (!maxBytes || originalBytes <= maxBytes) {
95
+ return {
96
+ content,
97
+ bytes: originalBytes,
98
+ originalBytes,
99
+ truncated: false,
100
+ omittedLines: 0,
101
+ };
102
+ }
103
+
104
+ const sourceLines = content.split(/\r?\n/);
105
+ const marker = `%% compacted: truncated to ${maxBytes} bytes from ${originalBytes} bytes`;
106
+ const suffix = '%% compacted: tail omitted';
107
+ const header = `${marker}\n`;
108
+ const footer = `\n${suffix}\n`;
109
+ const maxBodyBytes = maxBytes - byteLength(header) - byteLength(footer);
110
+ if (maxBodyBytes <= 0) {
111
+ const minimal = `${marker}\n${suffix}\n`;
112
+ return {
113
+ content: minimal,
114
+ bytes: byteLength(minimal),
115
+ originalBytes,
116
+ truncated: true,
117
+ omittedLines: sourceLines.length,
118
+ };
119
+ }
120
+
121
+ const keptLines = [];
122
+ let bodyBytes = 0;
123
+ for (const line of sourceLines) {
124
+ const candidate = keptLines.length > 0 ? `\n${line}` : line;
125
+ const candidateBytes = byteLength(candidate);
126
+ if (bodyBytes + candidateBytes > maxBodyBytes) {
127
+ break;
128
+ }
129
+ keptLines.push(line);
130
+ bodyBytes += candidateBytes;
131
+ }
132
+
133
+ const compacted = `${header}${keptLines.join('\n')}${footer}`;
134
+ return {
135
+ content: compacted,
136
+ bytes: byteLength(compacted),
137
+ originalBytes,
138
+ truncated: true,
139
+ omittedLines: Math.max(sourceLines.length - keptLines.length, 0),
140
+ };
141
+ }
142
+
143
+ function applyArtifactBudget(diagrams, profile) {
144
+ const sorted = sortByPriority(diagrams, profile.priorityOrder);
145
+ const included = [];
146
+ const omitted = [];
147
+ let writtenBytes = 0;
148
+ let originalBytes = 0;
149
+
150
+ for (const diagram of sorted) {
151
+ const source = String(diagram.mermaid || '');
152
+ const sourceBytes = byteLength(source);
153
+ originalBytes += sourceBytes;
154
+
155
+ if (profile.name !== 'full' && diagram.metadata?.compactEligible === false) {
156
+ omitted.push({
157
+ type: diagram.type,
158
+ reason: 'low_confidence',
159
+ originalBytes: sourceBytes,
160
+ });
161
+ continue;
162
+ }
163
+
164
+ if (profile.maxDiagrams && included.length >= profile.maxDiagrams) {
165
+ omitted.push({
166
+ type: diagram.type,
167
+ reason: 'max_diagrams',
168
+ originalBytes: sourceBytes,
169
+ });
170
+ continue;
171
+ }
172
+
173
+ const compacted = truncateMermaidByBytes(source, profile.maxBytesPerDiagram);
174
+
175
+ if (profile.maxBytesTotal && writtenBytes + compacted.bytes > profile.maxBytesTotal) {
176
+ omitted.push({
177
+ type: diagram.type,
178
+ reason: 'max_total_bytes',
179
+ originalBytes: sourceBytes,
180
+ });
181
+ continue;
182
+ }
183
+
184
+ included.push({
185
+ type: diagram.type,
186
+ mermaid: compacted.content,
187
+ metadata: diagram.metadata,
188
+ bytes: compacted.bytes,
189
+ originalBytes: compacted.originalBytes,
190
+ truncated: compacted.truncated,
191
+ omittedLines: compacted.omittedLines,
192
+ bytesSaved: Math.max(compacted.originalBytes - compacted.bytes, 0),
193
+ });
194
+ writtenBytes += compacted.bytes;
195
+ }
196
+
197
+ const truncatedTypes = included
198
+ .filter((entry) => entry.truncated)
199
+ .map((entry) => entry.type);
200
+ const bytesSaved = Math.max(originalBytes - writtenBytes, 0);
201
+
202
+ return {
203
+ included,
204
+ omitted,
205
+ truncatedTypes,
206
+ applied: omitted.length > 0 || truncatedTypes.length > 0,
207
+ summary: {
208
+ generatedCount: sorted.length,
209
+ includedCount: included.length,
210
+ omittedCount: omitted.length,
211
+ originalBytes,
212
+ writtenBytes,
213
+ bytesSaved,
214
+ },
215
+ };
216
+ }
217
+
218
+ module.exports = {
219
+ AGENT_DIAGRAM_PRIORITY,
220
+ estimateTokensFromBytes,
221
+ resolveArtifactProfile,
222
+ applyArtifactBudget,
223
+ sortByPriority,
224
+ };
@@ -0,0 +1,153 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { summarizeAnalysis } = require('./evidence-summary');
4
+
5
+ const BRIEF_HEADINGS = Object.freeze([
6
+ '# Archscope Evidence Brief',
7
+ '## Summary',
8
+ '## Artifact Read Order',
9
+ '## Risk And Validation',
10
+ '## Warnings',
11
+ '## Agent Handoff',
12
+ '## Next Action',
13
+ ]);
14
+
15
+ function formatList(items, emptyText) {
16
+ if (!Array.isArray(items) || items.length === 0) {
17
+ return [`- ${emptyText}`];
18
+ }
19
+ return items.map((item) => `- ${item}`);
20
+ }
21
+
22
+ /**
23
+ * Build a markdown "Archscope Evidence Brief" describing analysis and manifest outcomes.
24
+ *
25
+ * Produces a structured markdown string containing headings for summary, artifact read order,
26
+ * risk and validation, warnings, agent handoff and next actions, plus PR-specific details when
27
+ * PR impact information is provided or PR evidence generation failed.
28
+ *
29
+ * @param {Object} params - Input parameters.
30
+ * @param {Object} params.manifest - Manifest describing artifacts, read order and validation status.
31
+ * @param {Object} params.analysis - Analysis result used to summarise components, files, languages and areas.
32
+ * @param {Object|null} [params.prImpact=null] - Optional PR impact data; when present the brief includes PR base/head, changed components, blast radius, risk reasons, suggested reviewer checks, validation evidence and confidence.
33
+ * @param {string[]} [params.warnings=[]] - Array of warning messages to include in the brief.
34
+ * @param {Array<{category:string,message:string,artifact?:string}>} [params.errors=[]] - Array of error objects to include; PR evidence generation errors are surfaced in the PR section.
35
+ * @returns {string} A markdown-formatted evidence brief.
36
+ */
37
+ function buildArchitectureBrief({
38
+ manifest,
39
+ analysis,
40
+ prImpact = null,
41
+ warnings = [],
42
+ errors = [],
43
+ }) {
44
+ const [
45
+ titleHeading,
46
+ summaryHeading,
47
+ readOrderHeading,
48
+ riskHeading,
49
+ warningsHeading,
50
+ handoffHeading,
51
+ nextActionHeading,
52
+ ] = BRIEF_HEADINGS;
53
+ const summary = summarizeAnalysis(analysis);
54
+ const languageText = summary.languages.length > 0
55
+ ? summary.languages.map(([name, count]) => `${name} (${count})`).join(', ')
56
+ : 'unknown';
57
+ const areaText = summary.areas.length > 0
58
+ ? summary.areas.map(([name, count]) => `${name} (${count})`).join(', ')
59
+ : 'unknown';
60
+ const prError = errors.find((error) => error.artifact === 'pr-impact');
61
+ const modeText = prImpact || prError ? 'pr scan' : 'repository scan';
62
+ const warningLines = formatList(warnings, 'No warnings recorded.');
63
+ const errorLines = formatList(
64
+ errors.map((error) => `${error.category}: ${error.message}`),
65
+ 'No errors recorded.'
66
+ );
67
+ const prImpactPath = prImpact?.prImpactPath
68
+ || manifest.artifacts.find((entry) => entry.id === 'pr-impact' && entry.status === 'written')?.path
69
+ || null;
70
+ const decisionLine = prImpact
71
+ ? `- Review decision: inspect ${prImpactPath || 'the PR evidence outcome'} before approving architecture-sensitive changes.`
72
+ : prError
73
+ ? '- Review decision: resolve PR evidence failure before approving architecture-sensitive changes.'
74
+ : '- Review decision: read the manifest and brief before handing this repo to a reviewer or coding agent.';
75
+ let prLines;
76
+ if (prImpact) {
77
+ prLines = [
78
+ `- PR base: ${prImpact.base}`,
79
+ `- PR head: ${prImpact.head}`,
80
+ `- Changed components: ${prImpact.agentSummary?.changedComponents ?? prImpact.changedComponents?.length ?? 0}`,
81
+ `- Blast radius: ${prImpact.blastRadius?.impactedComponents?.length ?? 0}`,
82
+ `- Risk reasons: ${(prImpact.agentSummary?.riskReasons || []).join(', ') || 'none'}`,
83
+ `- Reviewer checks: ${(prImpact.agentSummary?.suggestedReviewerChecks || []).join('; ') || 'none'}`,
84
+ prImpactPath
85
+ ? `- Validation evidence: workflow pr contract reused via ${prImpactPath}`
86
+ : `- Validation evidence: PR impact artifact not written (${prImpact._meta?.status || 'not_written'}).`,
87
+ `- Confidence: ${prImpact.confidence?.level || 'unknown'}`,
88
+ ];
89
+ } else if (prError) {
90
+ prLines = [`- PR evidence generation failed: ${prError.category}: ${prError.message}`];
91
+ } else {
92
+ prLines = ['- PR refs not supplied.'];
93
+ }
94
+
95
+ const lines = [
96
+ titleHeading,
97
+ '',
98
+ summaryHeading,
99
+ '',
100
+ `- Mode: ${modeText}`,
101
+ `- Components detected: ${summary.componentCount}`,
102
+ `- Files considered: ${summary.totalFilesFound}`,
103
+ `- Entry points detected: ${summary.entryPointCount}`,
104
+ `- Languages: ${languageText}`,
105
+ `- Architecture areas: ${areaText}`,
106
+ decisionLine,
107
+ '',
108
+ readOrderHeading,
109
+ '',
110
+ ...manifest.artifactReadOrder.map((artifactPath, index) => `${index + 1}. ${artifactPath}`),
111
+ '',
112
+ riskHeading,
113
+ '',
114
+ `- Validation: ${manifest.validation.status}`,
115
+ `- Risk: ${prImpact?.risk?.level || 'unknown until PR refs or policy validation are supplied'}`,
116
+ `- Evidence status: ${manifest.artifacts.some((entry) => entry.status === 'failed') ? 'failed' : 'written'}`,
117
+ ...prLines,
118
+ '',
119
+ warningsHeading,
120
+ '',
121
+ ...warningLines,
122
+ '',
123
+ handoffHeading,
124
+ '',
125
+ `- Read ${manifest.artifactReadOrder[0]} first for artifact status.`,
126
+ `- Use ${manifest.primaryAgentArtifact} as the parser-safe agent contract.`,
127
+ `- Open ${manifest.primaryHumanArtifact} for the concise human summary.`,
128
+ '',
129
+ nextActionHeading,
130
+ '',
131
+ ...errorLines,
132
+ ];
133
+
134
+ return `${lines.join('\n')}\n`;
135
+ }
136
+
137
+ function writeArchitectureBrief(filePath, input) {
138
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
139
+ const content = buildArchitectureBrief(input);
140
+ fs.writeFileSync(filePath, content);
141
+ return {
142
+ path: filePath,
143
+ lines: content.trimEnd().split(/\r?\n/).length,
144
+ bytes: Buffer.byteLength(content, 'utf8'),
145
+ };
146
+ }
147
+
148
+ module.exports = {
149
+ BRIEF_HEADINGS,
150
+ buildArchitectureBrief,
151
+ summarizeAnalysis,
152
+ writeArchitectureBrief,
153
+ };
@@ -0,0 +1,206 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { estimateTokensFromBytes } = require('./artifact-budget');
4
+
5
+ const FIXED_DETERMINISTIC_TIMESTAMP = '1970-01-01T00:00:00.000Z';
6
+ const ARTIFACT_STATUSES = new Set(['written', 'deferred', 'partial', 'failed']);
7
+
8
+ function unixPath(value) {
9
+ return String(value || '').split(path.sep).join('/');
10
+ }
11
+
12
+ function sortStrings(values) {
13
+ return [...(values || [])].sort((a, b) => String(a).localeCompare(String(b)));
14
+ }
15
+
16
+ function getGeneratedAt(deterministic) {
17
+ return deterministic ? FIXED_DETERMINISTIC_TIMESTAMP : new Date().toISOString();
18
+ }
19
+
20
+ function realpathIfPresent(value) {
21
+ try {
22
+ return fs.realpathSync(value);
23
+ } catch (_error) {
24
+ return value;
25
+ }
26
+ }
27
+
28
+ function writeJsonFile(filePath, payload) {
29
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
30
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
31
+ }
32
+
33
+ function createGenerateAllManifest({
34
+ root,
35
+ outDir,
36
+ artifactProfile,
37
+ budgeted,
38
+ deterministic = false,
39
+ }) {
40
+ const realRoot = realpathIfPresent(root);
41
+ return {
42
+ generatedAt: getGeneratedAt(deterministic),
43
+ schemaVersion: '1.0',
44
+ rootPath: root,
45
+ diagramDir: path.relative(realRoot, outDir) || '.',
46
+ compaction: {
47
+ applied: budgeted.applied,
48
+ profile: artifactProfile.name,
49
+ maxTotalBytes: artifactProfile.maxBytesTotal,
50
+ maxPerDiagramBytes: artifactProfile.maxBytesPerDiagram,
51
+ maxDiagrams: artifactProfile.maxDiagrams,
52
+ generatedDiagrams: budgeted.summary.generatedCount,
53
+ writtenDiagrams: budgeted.summary.includedCount,
54
+ omittedTypes: budgeted.omitted.map((entry) => entry.type),
55
+ truncatedTypes: budgeted.truncatedTypes,
56
+ bytesSaved: budgeted.summary.bytesSaved,
57
+ estimatedTokensSaved: estimateTokensFromBytes(budgeted.summary.bytesSaved),
58
+ reason: artifactProfile.name === 'full'
59
+ ? 'full_profile'
60
+ : (budgeted.applied ? 'budget_constraints' : 'within_budget'),
61
+ },
62
+ diagrams: [],
63
+ };
64
+ }
65
+
66
+ function artifactEntry({
67
+ id,
68
+ path: artifactPath,
69
+ status,
70
+ role,
71
+ optional = false,
72
+ reason = null,
73
+ errorCategory = null,
74
+ }) {
75
+ if (!ARTIFACT_STATUSES.has(status)) {
76
+ throw new Error(`Invalid artifact status for ${id}: ${status}`);
77
+ }
78
+ return {
79
+ id,
80
+ path: artifactPath,
81
+ status,
82
+ role,
83
+ optional,
84
+ ...(reason ? { reason } : {}),
85
+ ...(errorCategory ? { errorCategory } : {}),
86
+ };
87
+ }
88
+
89
+ function createScanEvidenceManifest({
90
+ root,
91
+ outDir,
92
+ deterministic = false,
93
+ warnings = [],
94
+ artifactStatuses = {},
95
+ artifactReasons = {},
96
+ artifactErrorCategories = {},
97
+ }) {
98
+ const realRoot = realpathIfPresent(root);
99
+ const diagramDir = unixPath(path.relative(realRoot, outDir) || '.');
100
+ const artifactPath = (fileName) => unixPath(path.join(diagramDir, fileName));
101
+ const statusFor = (id, fallback) => artifactStatuses[id] || fallback;
102
+ const reportStatus = statusFor('report', 'deferred');
103
+
104
+ const artifacts = [
105
+ artifactEntry({
106
+ id: 'manifest',
107
+ path: artifactPath('manifest.json'),
108
+ status: statusFor('manifest', 'written'),
109
+ role: 'artifact-index',
110
+ reason: artifactReasons.manifest || null,
111
+ errorCategory: artifactErrorCategories.manifest || null,
112
+ }),
113
+ artifactEntry({
114
+ id: 'brief',
115
+ path: artifactPath('brief.md'),
116
+ status: statusFor('brief', 'deferred'),
117
+ role: 'primary-human-summary',
118
+ reason: artifactReasons.brief
119
+ || (statusFor('brief', 'deferred') === 'deferred' ? 'p1_not_implemented' : null),
120
+ errorCategory: artifactErrorCategories.brief || null,
121
+ }),
122
+ artifactEntry({
123
+ id: 'agent-context',
124
+ path: artifactPath('agent-context.json'),
125
+ status: statusFor('agent-context', 'deferred'),
126
+ role: 'primary-agent-context',
127
+ reason: artifactReasons['agent-context']
128
+ || (statusFor('agent-context', 'deferred') === 'deferred' ? 'p1_not_implemented' : null),
129
+ errorCategory: artifactErrorCategories['agent-context'] || null,
130
+ }),
131
+ artifactEntry({
132
+ id: 'architecture',
133
+ path: artifactPath('architecture.mmd'),
134
+ status: statusFor('architecture', 'deferred'),
135
+ role: 'supporting-diagram',
136
+ reason: artifactReasons.architecture
137
+ || (statusFor('architecture', 'deferred') === 'deferred' ? 'p1_not_implemented' : null),
138
+ errorCategory: artifactErrorCategories.architecture || null,
139
+ }),
140
+ artifactEntry({
141
+ id: 'report',
142
+ path: artifactPath('report.html'),
143
+ status: reportStatus,
144
+ role: 'human-report',
145
+ optional: true,
146
+ reason: artifactReasons.report || (reportStatus === 'deferred' ? 'ui_spec_required' : null),
147
+ errorCategory: artifactErrorCategories.report || null,
148
+ }),
149
+ artifactEntry({
150
+ id: 'pr-impact',
151
+ path: artifactPath('pr-impact/pr-impact.json'),
152
+ status: statusFor('pr-impact', 'deferred'),
153
+ role: 'pr-impact-json',
154
+ optional: true,
155
+ reason: artifactReasons['pr-impact']
156
+ || (statusFor('pr-impact', 'deferred') === 'deferred' ? 'pr_refs_not_supplied' : null),
157
+ errorCategory: artifactErrorCategories['pr-impact'] || null,
158
+ }),
159
+ ].sort((a, b) => a.path.localeCompare(b.path));
160
+
161
+ const primaryHumanArtifact = reportStatus === 'written'
162
+ ? artifactPath('report.html')
163
+ : artifactPath('brief.md');
164
+ const primaryAgentArtifact = artifactPath('agent-context.json');
165
+ const artifactReadOrder = [
166
+ artifactPath('manifest.json'),
167
+ artifactPath('brief.md'),
168
+ artifactPath('agent-context.json'),
169
+ ...(statusFor('pr-impact', 'deferred') === 'written' ? [artifactPath('pr-impact/pr-impact.json')] : []),
170
+ ];
171
+
172
+ return {
173
+ schemaVersion: '1.0',
174
+ command: 'scan',
175
+ generatedAt: getGeneratedAt(deterministic),
176
+ deterministic: Boolean(deterministic),
177
+ project: {
178
+ label: path.basename(root) || '.',
179
+ path: '.',
180
+ },
181
+ outputDirectory: diagramDir,
182
+ primaryHumanArtifact,
183
+ primaryAgentArtifact,
184
+ artifactReadOrder,
185
+ artifacts,
186
+ subordinateDirectories: [
187
+ unixPath(path.join(diagramDir, 'contracts')),
188
+ unixPath(path.join(diagramDir, 'context')),
189
+ unixPath(path.join(diagramDir, 'migration')),
190
+ unixPath(path.join(diagramDir, 'pr-impact')),
191
+ ].sort(),
192
+ validation: {
193
+ status: 'not_run',
194
+ summary: 'scan does not run validation automatically',
195
+ },
196
+ warnings: sortStrings(warnings),
197
+ };
198
+ }
199
+
200
+ module.exports = {
201
+ FIXED_DETERMINISTIC_TIMESTAMP,
202
+ createGenerateAllManifest,
203
+ createScanEvidenceManifest,
204
+ getGeneratedAt,
205
+ writeJsonFile,
206
+ };
@@ -0,0 +1,29 @@
1
+ function countBy(items, selectKey) {
2
+ const counts = new Map();
3
+ for (const item of items || []) {
4
+ const key = selectKey(item);
5
+ if (!key) continue;
6
+ counts.set(key, (counts.get(key) || 0) + 1);
7
+ }
8
+ return [...counts.entries()].sort((left, right) =>
9
+ left[0].localeCompare(right[0])
10
+ );
11
+ }
12
+
13
+ function summarizeAnalysis(analysis = {}) {
14
+ const components = Array.isArray(analysis.components) ? analysis.components : [];
15
+ return {
16
+ componentCount: components.length,
17
+ entryPointCount: Array.isArray(analysis.entryPoints) ? analysis.entryPoints.length : 0,
18
+ totalFilesFound: Number.isFinite(Number(analysis.totalFilesFound))
19
+ ? Number(analysis.totalFilesFound)
20
+ : components.length,
21
+ languages: countBy(components, (component) => component.language),
22
+ areas: countBy(components, (component) => component.type),
23
+ };
24
+ }
25
+
26
+ module.exports = {
27
+ countBy,
28
+ summarizeAnalysis,
29
+ };