@brainwav/diagram 1.0.8 → 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 +178 -1761
  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,328 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const YAML = require('yaml');
5
+ const { analyze } = require('../core/analysis-generation');
6
+ const { RulesEngine } = require('../rules');
7
+ const { ComponentGraph } = require('../graph');
8
+ const { RuleFactory } = require('../rules/factory');
9
+ const { formatResults } = require('../formatters/index');
10
+ const { buildJSONOutput } = require('../formatters/json');
11
+ const { validateConfig, getDefaultConfig } = require('../schema/rules-schema');
12
+ const {
13
+ applyDiagramRcDefaults,
14
+ getDiagramRcFromProgram,
15
+ resolveRootPathOrExit,
16
+ validateOutputPath,
17
+ } = require('./shared');
18
+ const { buildMachineEnvelope } = require('./output');
19
+
20
+ /**
21
+ * Apply configured baselines to validation results and optionally persist updated baselines to the config file.
22
+ *
23
+ * Updates each result rule with baseline-related fields (`baseline`, `baselineWarning`, `baselineExceeded`, `status`)
24
+ * based on the matching rule in `config`. When `saveBaseline` is enabled, updates `config.rules[*].baseline` to the
25
+ * observed violation counts and writes the YAML back to `configPath` provided that `configPath` is inside `root`.
26
+ *
27
+ * @param {Object} results - Validation results object containing a `rules` array of rule result objects.
28
+ * @param {Object} config - Loaded configuration object containing a `rules` array of configured rules.
29
+ * @param {boolean} saveBaseline - If `true`, update and persist observed baseline values into the config file.
30
+ * @param {string} configPath - Absolute path to the configuration file to write when saving baselines.
31
+ * @param {string} root - Project root directory; `configPath` must reside inside this directory to be written.
32
+ * @param {boolean} [quiet=false] - If `true`, suppress console informational and error messages.
33
+ * @returns {{ updated: boolean, counts: Object }} `updated` is `true` if the config file was modified and written, `false` otherwise; `counts` maps rule names to observed violation counts.
34
+ */
35
+ function applyBaseline(results, config, saveBaseline, configPath, root, quiet = false) {
36
+ const baselineCounts = {};
37
+ let configModified = false;
38
+
39
+ for (const rule of results.rules || []) {
40
+ const configRule = config.rules?.find((candidate) => candidate.name === rule.name);
41
+ const violationCount = rule.violations?.length || 0;
42
+ const baseline = configRule?.baseline;
43
+ baselineCounts[rule.name] = violationCount;
44
+
45
+ if (baseline !== undefined) {
46
+ rule.baseline = baseline;
47
+ if (violationCount <= baseline) {
48
+ rule.status = 'passed';
49
+ if (violationCount > 0) {
50
+ rule.baselineWarning = `Baseline allows ${baseline} violation(s), found ${violationCount}`;
51
+ }
52
+ } else {
53
+ rule.baselineExceeded = violationCount - baseline;
54
+ rule.status = 'failed';
55
+ }
56
+ }
57
+ }
58
+
59
+ if (saveBaseline) {
60
+ for (const rule of config.rules || []) {
61
+ const count = baselineCounts[rule.name] ?? 0;
62
+ if (rule.baseline !== count) {
63
+ rule.baseline = count;
64
+ configModified = true;
65
+ }
66
+ }
67
+
68
+ if (configModified) {
69
+ const relativePath = path.relative(root, configPath);
70
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
71
+ if (!quiet) console.error(chalk.red('❌ Cannot save baseline: config path outside project'));
72
+ return { updated: false, counts: baselineCounts };
73
+ }
74
+ const yaml = YAML.stringify(config, { indent: 2, lineWidth: 0 });
75
+ fs.writeFileSync(configPath, yaml);
76
+ if (!quiet) {
77
+ console.log(chalk.green('✅ Baseline saved:'), configPath);
78
+ console.log(chalk.gray(' Run `archscope validate` to verify'));
79
+ }
80
+ } else if (!quiet) {
81
+ console.log(chalk.gray('ℹ️ Baseline already up to date'));
82
+ }
83
+ }
84
+
85
+ return { updated: configModified, counts: baselineCounts };
86
+ }
87
+
88
+ /**
89
+ * Resolve a configuration file path inside the given project root, exiting the process if the resolved path lies outside the root.
90
+ *
91
+ * Resolves `configPathInput` (or `.architecture.yml` when omitted) to an absolute path relative to `root`. If the resolved path would traverse outside `root` or otherwise be invalid, an error is printed and the process exits with code `2`.
92
+ * @param {string} root - Project root directory used as the base for relative resolution.
93
+ * @param {string|undefined} configPathInput - Optional user-supplied config path; may be absolute or relative to `root`.
94
+ * @returns {string} The resolved absolute path to the configuration file.
95
+ */
96
+ function resolveConfigPathOrExit(root, configPathInput) {
97
+ const absoluteRoot = path.resolve(root);
98
+ let realRoot;
99
+ try {
100
+ realRoot = fs.realpathSync(absoluteRoot);
101
+ } catch (_error) {
102
+ console.error(chalk.red('❌ Invalid project path:'), absoluteRoot);
103
+ process.exit(2);
104
+ }
105
+ const requestedPath = configPathInput || '.architecture.yml';
106
+ const resolvedPath = path.isAbsolute(requestedPath)
107
+ ? path.resolve(requestedPath)
108
+ : path.resolve(absoluteRoot, requestedPath);
109
+
110
+ const resolveViaExistingAncestor = (targetPath) => {
111
+ const pending = [];
112
+ let probe = targetPath;
113
+ while (!fs.existsSync(probe)) {
114
+ pending.unshift(path.basename(probe));
115
+ const parent = path.dirname(probe);
116
+ if (parent === probe) {
117
+ break;
118
+ }
119
+ probe = parent;
120
+ }
121
+ const canonicalBase = fs.realpathSync(probe);
122
+ return path.join(canonicalBase, ...pending);
123
+ };
124
+
125
+ const canonicalConfigPath = resolveViaExistingAncestor(resolvedPath);
126
+ const relativeConfigPath = path.relative(realRoot, canonicalConfigPath);
127
+ if (relativeConfigPath.startsWith('..') || path.isAbsolute(relativeConfigPath)) {
128
+ console.error(chalk.red('❌ Invalid config path: directory traversal detected'));
129
+ process.exit(2);
130
+ }
131
+ return canonicalConfigPath;
132
+ }
133
+
134
+ /**
135
+ * Attach the `validate [path]` command to the given Commander program, enabling architecture validation driven by `.architecture.yml`.
136
+ *
137
+ * The command supports config discovery/initialisation, analysis, dry-run previews, baseline management, formatted output, and exit codes for CI integration.
138
+ * @param {import('commander').Command} program - Commander program instance to augment with the `validate` command.
139
+ */
140
+ function registerValidateCommand(program) {
141
+ program
142
+ .command('validate [path]')
143
+ .description('Validate architecture against .architecture.yml rules')
144
+ .option('-q, --quiet', 'Suppress non-essential logging', false)
145
+ .option('-c, --config <file>', 'Config file path', '.architecture.yml')
146
+ .option('-f, --format <format>', 'Output format: console, json, junit', 'console')
147
+ .option('-o, --output <file>', 'Output file (for json/junit formats)')
148
+ .option('-p, --patterns <list>', 'File patterns')
149
+ .option('-e, --exclude <list>', 'Exclude patterns')
150
+ .option('-m, --max-files <n>', 'Max files to analyze')
151
+ .option('--dry-run', 'Preview file matching without validation', false)
152
+ .option('--verbose', 'Show detailed output', false)
153
+ .option('--deterministic', 'Use deterministic output where supported', false)
154
+ .option('--init', 'Generate starter configuration file', false)
155
+ .option('--force', 'Overwrite existing configuration with --init', false)
156
+ .option('--save-baseline', 'Save current violation counts as baseline', false)
157
+ .action(async (targetPath, rawOptions) => {
158
+ const configValueSource = typeof rawOptions?.getOptionValueSource === 'function'
159
+ ? rawOptions.getOptionValueSource('config')
160
+ : null;
161
+ const configProvidedByCli = configValueSource
162
+ ? configValueSource === 'cli'
163
+ : (process.argv.includes('--config') || process.argv.includes('-c'));
164
+ const options = applyDiagramRcDefaults(rawOptions, getDiagramRcFromProgram(program), ['patterns', 'exclude', 'maxFiles']);
165
+ const root = resolveRootPathOrExit(targetPath);
166
+ const engine = new RulesEngine();
167
+ const startTime = Date.now();
168
+ const format = String(options.format || 'console').toLowerCase();
169
+ const outputsMachineFormat = !options.output && (format === 'json' || format === 'junit');
170
+ const quietMachineOutput = options.quiet || outputsMachineFormat;
171
+
172
+ if (options.init) {
173
+ const configPath = resolveConfigPathOrExit(root, options.config);
174
+ const defaultConfig = getDefaultConfig();
175
+ const yaml = YAML.stringify(defaultConfig, {
176
+ indent: 2,
177
+ lineWidth: 0,
178
+ });
179
+
180
+ try {
181
+ fs.writeFileSync(configPath, yaml, { flag: options.force ? 'w' : 'wx' });
182
+ } catch (error) {
183
+ if (error.code === 'EEXIST') {
184
+ console.error(chalk.yellow('⚠️ Configuration already exists:'), configPath);
185
+ console.log(chalk.gray(' Use --force to overwrite'));
186
+ process.exit(2);
187
+ }
188
+ throw error;
189
+ }
190
+ console.log(chalk.green('✅ Created configuration:'), configPath);
191
+ console.log(chalk.gray('\nEdit the file to define your architecture rules, then run:'));
192
+ console.log(chalk.cyan(' archscope validate'));
193
+ process.exit(0);
194
+ }
195
+
196
+ let configPath = resolveConfigPathOrExit(root, options.config);
197
+
198
+ if (!fs.existsSync(configPath)) {
199
+ if (configProvidedByCli) {
200
+ console.error(chalk.red('❌ Config file not found:'), configPath);
201
+ console.error(chalk.gray('Fix: run `archscope init .` or `archscope validate --init` to scaffold rules.'));
202
+ process.exit(2);
203
+ }
204
+ const found = engine.findConfig(root);
205
+ if (!found) {
206
+ console.error(chalk.red('❌ No .architecture.yml found.'));
207
+ console.error(chalk.gray('Fix: run `archscope init .` or `archscope validate --init` to scaffold rules.'));
208
+ process.exit(2);
209
+ }
210
+ configPath = resolveConfigPathOrExit(root, found);
211
+ }
212
+
213
+ let config;
214
+ try {
215
+ config = engine.loadConfig(configPath);
216
+ } catch (error) {
217
+ console.error(chalk.red('❌ Config error:'), error.message);
218
+ process.exit(2);
219
+ }
220
+
221
+ const validation = validateConfig(config);
222
+ if (!validation.valid) {
223
+ console.error(chalk.red('❌ Schema validation failed:'));
224
+ for (const error of validation.errors) {
225
+ console.error(chalk.red(` • ${error.path}: ${error.message}`));
226
+ }
227
+ process.exit(2);
228
+ }
229
+
230
+ if (!quietMachineOutput) {
231
+ console.log(chalk.blue('🔍 Analyzing'), root);
232
+ }
233
+ const data = await analyze(root, options);
234
+ const graph = new ComponentGraph(data);
235
+
236
+ let rules;
237
+ try {
238
+ rules = RuleFactory.createRules(config);
239
+ } catch (error) {
240
+ console.error(chalk.red('❌ Rule error:'), error.message);
241
+ process.exit(2);
242
+ }
243
+
244
+ if (options.dryRun) {
245
+ const preview = engine.previewMatches(rules, graph);
246
+ console.log(chalk.cyan('\n📋 Dry Run - File Matching Preview\n'));
247
+ for (const rule of preview.rules) {
248
+ console.log(chalk.bold(rule.name));
249
+ console.log(' Layer:', chalk.gray(Array.isArray(rule.layer) ? rule.layer.join(', ') : rule.layer));
250
+ console.log(' Matched files:', rule.matchedFiles.length);
251
+ if (options.verbose) {
252
+ for (const file of rule.matchedFiles) {
253
+ console.log(' -', file);
254
+ }
255
+ }
256
+ console.log();
257
+ }
258
+ process.exit(0);
259
+ }
260
+
261
+ if (!quietMachineOutput) {
262
+ console.log(chalk.blue('🧪 Validating'), rules.length, 'rules...\n');
263
+ }
264
+ const results = engine.validate(rules, graph);
265
+ if (options.deterministic) {
266
+ results.generatedAt = undefined;
267
+ }
268
+ applyBaseline(results, config, options.saveBaseline, configPath, root, quietMachineOutput);
269
+
270
+ let safeOutput = options.output;
271
+ if (safeOutput) {
272
+ try {
273
+ safeOutput = validateOutputPath(safeOutput, root);
274
+ } catch (error) {
275
+ console.error(chalk.red('❌ Output path error:'), error.message);
276
+ process.exit(2);
277
+ }
278
+ }
279
+
280
+ if (format === 'json' && !safeOutput) {
281
+ const validationOutput = buildJSONOutput(results, options.deterministic ? Date.now() : startTime);
282
+ const exitCode = validationOutput.summary.exitCode;
283
+ const payload = buildMachineEnvelope({
284
+ schemaVersion: '1.0',
285
+ command: 'validate',
286
+ rootPath: root,
287
+ status: exitCode === 0 ? 'success' : 'failure',
288
+ deterministic: Boolean(options.deterministic),
289
+ data: {
290
+ validation: validationOutput,
291
+ },
292
+ errors: exitCode === 0
293
+ ? []
294
+ : [{
295
+ code: 'architecture_validation_failed',
296
+ message: 'Architecture validation failed',
297
+ }],
298
+ agentSummary: {
299
+ changedComponents: 0,
300
+ riskReasons: exitCode === 0 ? [] : ['architecture_validation_failed'],
301
+ suggestedReviewerChecks: exitCode === 0
302
+ ? ['Architecture rules passed for this workspace.']
303
+ : ['Review failed architecture rules before merging.'],
304
+ },
305
+ });
306
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
307
+ process.exit(exitCode);
308
+ }
309
+
310
+ const exitCode = formatResults(results, format, {
311
+ output: safeOutput,
312
+ verbose: options.verbose,
313
+ }, startTime);
314
+
315
+ if (!quietMachineOutput && exitCode === 0) {
316
+ console.log(chalk.cyan('\nNext steps:'));
317
+ console.log(' 1) Run `archscope workflow pr . --base origin/main --head HEAD` before opening a PR.');
318
+ console.log(' 2) Add `archscope validate` to CI using the `archscope init` sample workflow.');
319
+ }
320
+
321
+ process.exit(exitCode);
322
+ });
323
+ }
324
+
325
+ module.exports = {
326
+ applyBaseline,
327
+ registerValidateCommand,
328
+ };
@@ -0,0 +1,105 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { analyze, generate } = require('../core/analysis-generation');
5
+ const {
6
+ applyDiagramRcDefaults,
7
+ getDiagramRcFromProgram,
8
+ normalizeThemeOption,
9
+ resolveRootPathOrExit,
10
+ validateOutputPath,
11
+ } = require('./shared');
12
+
13
+ let videoModule;
14
+
15
+ /**
16
+ * Lazily requires and caches the project's video module.
17
+ *
18
+ * If requiring the module fails because the Playwright runtime is missing, logs the provided message
19
+ * and a "Fix: npm install playwright" hint, then exits the process with code 2. Any other require
20
+ * errors are rethrown.
21
+ *
22
+ * @param {string} missingRuntimeMessage - Message to display when Playwright is not installed.
23
+ * @returns {*} The loaded video module.
24
+ */
25
+ function getVideoModule(missingRuntimeMessage) {
26
+ if (!videoModule) {
27
+ try {
28
+ videoModule = require('../video.js');
29
+ } catch (error) {
30
+ const message = String(error?.message || '');
31
+ const missingPlaywrightRuntime = error?.code === 'MODULE_NOT_FOUND'
32
+ && message.toLowerCase().includes('playwright');
33
+ if (!missingPlaywrightRuntime) {
34
+ throw error;
35
+ }
36
+ console.error(chalk.red(`❌ ${missingRuntimeMessage}`));
37
+ console.error(chalk.gray('Fix: npm install playwright'));
38
+ process.exit(2);
39
+ }
40
+ }
41
+ return videoModule;
42
+ }
43
+
44
+ /**
45
+ * Constructs and validates the command execution context for media-to-diagram operations.
46
+ *
47
+ * @param {Object} program - CLI program instance from which diagram RC may be read.
48
+ * @param {string} targetPath - Path used to resolve the project root.
49
+ * @param {Object} rawOptions - CLI options to merge with diagram RC defaults; keys considered: `patterns`, `exclude`, `maxFiles`, `theme` (default theme is `dark`).
50
+ * @returns {{options: Object, root: string, safeTheme: string, safeOutput: string}} An object containing:
51
+ * - `options`: the merged and finalised options,
52
+ * - `root`: the resolved project root path,
53
+ * - `safeTheme`: the normalised theme value,
54
+ * - `safeOutput`: the validated output file path.
55
+ */
56
+ function resolveMediaCommandContext(program, targetPath, rawOptions) {
57
+ const options = applyDiagramRcDefaults(
58
+ rawOptions,
59
+ getDiagramRcFromProgram(program),
60
+ ['patterns', 'exclude', 'maxFiles', 'theme'],
61
+ { theme: 'dark' }
62
+ );
63
+ const root = resolveRootPathOrExit(targetPath);
64
+ const safeTheme = normalizeThemeOption(options.theme, 'dark');
65
+
66
+ let safeOutput;
67
+ try {
68
+ safeOutput = validateOutputPath(options.output, root);
69
+ } catch (error) {
70
+ console.error(chalk.red('❌ Output path error:'), error.message);
71
+ process.exit(2);
72
+ }
73
+
74
+ if (fs.existsSync(safeOutput) && !options.force) {
75
+ console.error(chalk.red(`❌ Target file exists: ${safeOutput}`));
76
+ console.error(chalk.gray('Fix: rerun with `--force` to overwrite.'));
77
+ process.exit(1);
78
+ }
79
+
80
+ fs.mkdirSync(path.dirname(safeOutput), { recursive: true, mode: 0o755 });
81
+
82
+ return {
83
+ options,
84
+ root,
85
+ safeTheme,
86
+ safeOutput,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Generate Mermaid content from media files located under the given root.
92
+ * @param {string} root - Filesystem path used as the analysis root.
93
+ * @param {Object} options - Options that control analysis and generation. `options.type` selects the generation output type.
94
+ * @returns {string} The generated Mermaid output.
95
+ */
96
+ async function buildMermaidForMedia(root, options) {
97
+ const analysis = await analyze(root, options);
98
+ return generate(analysis, options.type);
99
+ }
100
+
101
+ module.exports = {
102
+ buildMermaidForMedia,
103
+ getVideoModule,
104
+ resolveMediaCommandContext,
105
+ };
@@ -0,0 +1,26 @@
1
+ const { registerWorkflowCommands } = require('../workflow/pr-command');
2
+ const {
3
+ applyDiagramRcDefaults,
4
+ getDiagramRcFromProgram,
5
+ resolveRootPathOrExit,
6
+ splitList,
7
+ validateOutputPath,
8
+ } = require('./shared');
9
+
10
+ /**
11
+ * Register PR workflow commands on the provided CLI `program`.
12
+ * @param {object} program - CLI program instance to attach the workflow PR commands to.
13
+ */
14
+ function registerWorkflowPrCommand(program) {
15
+ registerWorkflowCommands(program, {
16
+ resolveRootPathOrExit,
17
+ validateOutputPath,
18
+ applyDiagramRcDefaults,
19
+ getDiagramRc: () => getDiagramRcFromProgram(program),
20
+ splitList,
21
+ });
22
+ }
23
+
24
+ module.exports = {
25
+ registerWorkflowPrCommand,
26
+ };
@@ -0,0 +1,186 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+ const { getNpxCommandCandidates } = require('../utils/commands');
5
+
6
+ const CONFIDENCE_SCHEMA_VERSION = '1.0';
7
+
8
+ function runCommand(command, args, options = {}) {
9
+ const result = spawnSync(command, args, {
10
+ encoding: 'utf8',
11
+ timeout: options.timeoutMs || 15000,
12
+ windowsHide: true,
13
+ ...options,
14
+ });
15
+
16
+ return {
17
+ ok: result.status === 0,
18
+ status: result.status,
19
+ stdout: String(result.stdout || '').trim(),
20
+ stderr: String(result.stderr || '').trim(),
21
+ error: result.error ? String(result.error.message || result.error) : null,
22
+ };
23
+ }
24
+
25
+ function probeNodeRuntime() {
26
+ return {
27
+ id: 'node_runtime',
28
+ required: true,
29
+ status: 'pass',
30
+ message: `Node runtime detected: ${process.version}`,
31
+ details: { version: process.version },
32
+ };
33
+ }
34
+
35
+ function probeGitCapability() {
36
+ const result = runCommand('git', ['--version']);
37
+ if (!result.ok) {
38
+ return {
39
+ id: 'git_cli',
40
+ required: true,
41
+ status: 'fail',
42
+ message: 'Git CLI unavailable',
43
+ details: result,
44
+ };
45
+ }
46
+
47
+ return {
48
+ id: 'git_cli',
49
+ required: true,
50
+ status: 'pass',
51
+ message: result.stdout || 'Git CLI available',
52
+ details: result,
53
+ };
54
+ }
55
+
56
+ function probeMermaidCli() {
57
+ const candidates = getNpxCommandCandidates(process.platform);
58
+ let lastFailure = null;
59
+
60
+ for (const candidate of candidates) {
61
+ const result = runCommand(candidate, ['-y', '@mermaid-js/mermaid-cli', '--version'], { timeoutMs: 30000 });
62
+ if (result.ok) {
63
+ return {
64
+ id: 'mermaid_cli',
65
+ required: false,
66
+ status: 'pass',
67
+ message: `Mermaid CLI available via ${candidate}`,
68
+ details: { candidate, version: result.stdout || 'unknown' },
69
+ };
70
+ }
71
+ lastFailure = { candidate, ...result };
72
+ }
73
+
74
+ return {
75
+ id: 'mermaid_cli',
76
+ required: false,
77
+ status: 'warn',
78
+ message: 'Mermaid CLI unavailable — SVG/PNG rendering disabled; .mmd/.md output still works',
79
+ details: lastFailure,
80
+ };
81
+ }
82
+
83
+ function probeCapabilities(command, context = {}) {
84
+ const checks = [probeNodeRuntime()];
85
+
86
+ if (command === 'workflow-pr') {
87
+ checks.push(probeGitCapability());
88
+ }
89
+
90
+ if (context.requiresMermaidCli) {
91
+ checks.push(probeMermaidCli());
92
+ }
93
+
94
+ return {
95
+ command,
96
+ generatedAt: new Date().toISOString(),
97
+ checks,
98
+ };
99
+ }
100
+
101
+ function evaluateConfidence({ capabilities, validation, fallback }) {
102
+ const checks = Array.isArray(capabilities?.checks) ? capabilities.checks : [];
103
+ const failures = checks.filter((check) => check.required && check.status === 'fail');
104
+ const warnings = checks.filter((check) => check.status === 'warn');
105
+
106
+ const validationFailed = Boolean(validation?.enabled && validation?.valid === false);
107
+ const fallbackUsed = Boolean(fallback?.used);
108
+
109
+ let verdict = 'pass';
110
+ const reasons = [];
111
+
112
+ if (failures.length > 0 || validationFailed) {
113
+ verdict = 'fail';
114
+ } else if (warnings.length > 0 || fallbackUsed) {
115
+ verdict = 'warn';
116
+ }
117
+
118
+ if (failures.length > 0) {
119
+ reasons.push(...failures.map((failure) => `${failure.id}: ${failure.message}`));
120
+ }
121
+ if (validationFailed) {
122
+ reasons.push('validation: Mermaid validation reported errors');
123
+ }
124
+ if (fallbackUsed) {
125
+ reasons.push('fallback: degraded fallback path used');
126
+ }
127
+
128
+ let score = 100;
129
+ score -= failures.length * 30;
130
+ score -= warnings.length * 10;
131
+ if (validationFailed) score -= 30;
132
+ if (fallbackUsed) score -= 15;
133
+ score = Math.max(0, Math.min(100, score));
134
+
135
+ return {
136
+ verdict,
137
+ score,
138
+ reasons,
139
+ summary: {
140
+ requiredFailures: failures.length,
141
+ warnings: warnings.length,
142
+ validationFailed,
143
+ fallbackUsed,
144
+ },
145
+ };
146
+ }
147
+
148
+ function buildConfidenceReport({ command, rootPath, capabilities, validation, fallback, notes = [] }) {
149
+ const confidence = evaluateConfidence({ capabilities, validation, fallback });
150
+
151
+ return {
152
+ schemaVersion: CONFIDENCE_SCHEMA_VERSION,
153
+ generatedAt: new Date().toISOString(),
154
+ command,
155
+ rootPath,
156
+ capabilities,
157
+ validation,
158
+ fallback,
159
+ confidence,
160
+ notes,
161
+ };
162
+ }
163
+
164
+ function writeConfidenceReport(rootPath, report, explicitPath) {
165
+ const destination = explicitPath
166
+ ? path.resolve(explicitPath)
167
+ : path.join(rootPath, '.diagram', 'confidence', 'confidence-report.json');
168
+
169
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
170
+ fs.writeFileSync(destination, `${JSON.stringify(report, null, 2)}\n`);
171
+ return destination;
172
+ }
173
+
174
+ function shouldFailStrictConfidence(report) {
175
+ const summary = report?.confidence?.summary || {};
176
+ return Boolean(summary.requiredFailures > 0 || summary.validationFailed || summary.fallbackUsed);
177
+ }
178
+
179
+ module.exports = {
180
+ CONFIDENCE_SCHEMA_VERSION,
181
+ probeCapabilities,
182
+ evaluateConfidence,
183
+ buildConfidenceReport,
184
+ writeConfidenceReport,
185
+ shouldFailStrictConfidence,
186
+ };