@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.
- package/.diagram/contracts/machine-command-coverage.json +73 -0
- package/.diagram/migration/finalization-policy.json +20 -0
- package/LICENSE +202 -21
- package/README.md +132 -339
- package/package.json +46 -13
- package/scripts/refresh-diagram-context.sh +274 -182
- package/src/analyzers/default-analyzer.js +11 -0
- package/src/analyzers/index.js +34 -0
- package/src/artifacts/agent-context.js +105 -0
- package/src/artifacts/artifact-budget.js +224 -0
- package/src/artifacts/brief.js +153 -0
- package/src/artifacts/evidence-manifest.js +206 -0
- package/src/artifacts/evidence-summary.js +29 -0
- package/src/commands/analyze.js +125 -0
- package/src/commands/changed.js +185 -0
- package/src/commands/context.js +110 -0
- package/src/commands/diff.js +142 -0
- package/src/commands/doctor.js +335 -0
- package/src/commands/explain.js +273 -0
- package/src/commands/generate-all.js +170 -0
- package/src/commands/generate-animated.js +50 -0
- package/src/commands/generate-video.js +65 -0
- package/src/commands/generate.js +522 -0
- package/src/commands/init.js +123 -0
- package/src/commands/output.js +76 -0
- package/src/commands/scan.js +624 -0
- package/src/commands/shared.js +396 -0
- package/src/commands/validate.js +328 -0
- package/src/commands/video-shared.js +105 -0
- package/src/commands/workflow-pr.js +26 -0
- package/src/confidence/pipeline.js +186 -0
- package/src/config/diagramrc.js +79 -0
- package/src/context/build-context-pack.js +291 -0
- package/src/context/normalize-diagram-manifest.js +282 -0
- package/src/core/analysis-generation-analyze-components.js +102 -0
- package/src/core/analysis-generation-analyze-dependencies.js +33 -0
- package/src/core/analysis-generation-analyze-files.js +48 -0
- package/src/core/analysis-generation-analyze-options.js +73 -0
- package/src/core/analysis-generation-analyze.js +63 -0
- package/src/core/analysis-generation-constants.js +53 -0
- package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
- package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
- package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
- package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
- package/src/core/analysis-generation-diagrams-core.js +12 -0
- package/src/core/analysis-generation-diagrams-empty.js +68 -0
- package/src/core/analysis-generation-diagrams-erd.js +59 -0
- package/src/core/analysis-generation-diagrams-limit.js +27 -0
- package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
- package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
- package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
- package/src/core/analysis-generation-diagrams-role-data.js +182 -0
- package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
- package/src/core/analysis-generation-diagrams-role-security.js +129 -0
- package/src/core/analysis-generation-diagrams-role.js +25 -0
- package/src/core/analysis-generation-diagrams.js +182 -0
- package/src/core/analysis-generation-role-tags-constants.js +55 -0
- package/src/core/analysis-generation-role-tags-imports.js +32 -0
- package/src/core/analysis-generation-role-tags-infer.js +49 -0
- package/src/core/analysis-generation-role-tags-match.js +19 -0
- package/src/core/analysis-generation-role-tags.js +7 -0
- package/src/core/analysis-generation-utils-core.js +308 -0
- package/src/core/analysis-generation-utils-graph.js +321 -0
- package/src/core/analysis-generation-utils-resolution.js +76 -0
- package/src/core/analysis-generation-utils.js +9 -0
- package/src/core/analysis-generation.js +44 -0
- package/src/diagram.js +180 -1760
- package/src/formatters/console.js +198 -0
- package/src/formatters/index.js +41 -0
- package/src/formatters/json.js +113 -0
- package/src/formatters/junit.js +123 -0
- package/src/graph.js +159 -0
- package/src/incremental/cache.js +210 -0
- package/src/ir/architecture-ir.js +48 -0
- package/src/migration/evidence.js +262 -0
- package/src/migration/finalization-policy.js +35 -0
- package/src/renderers/report-html.js +265 -0
- package/src/rules/factory.js +108 -0
- package/src/rules/types/base.js +54 -0
- package/src/rules/types/import-rule.js +286 -0
- package/src/rules.js +380 -0
- package/src/schema/erd-confidence.js +56 -0
- package/src/schema/erd-extractor.js +504 -0
- package/src/schema/erd-model.js +176 -0
- package/src/schema/rules-schema.js +170 -0
- package/src/utils/suggestions.js +67 -0
- package/src/video.js +4 -5
- package/src/workflow/git-helpers.js +576 -0
- package/src/workflow/pr-command.js +694 -0
- package/src/workflow/pr-impact.js +848 -0
- package/src/workflow/sort-utils.js +16 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const {
|
|
3
|
+
applyDiagramRcDefaults,
|
|
4
|
+
getDiagramRcFromProgram,
|
|
5
|
+
maybeWriteArchitectureIR,
|
|
6
|
+
resolveRootPathOrExit,
|
|
7
|
+
runAnalysisPipeline,
|
|
8
|
+
} = require('./shared');
|
|
9
|
+
const { buildMachineEnvelope } = require('./output');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register the `analyze [path]` CLI subcommand on the provided commander program.
|
|
13
|
+
*
|
|
14
|
+
* The subcommand analyzes a codebase root, runs the analysis pipeline, and emits
|
|
15
|
+
* either a human-readable summary or a machine-friendly JSON envelope. It also
|
|
16
|
+
* supports options for file patterns, exclusions, max files, analyzer selection,
|
|
17
|
+
* emitting a typed architecture IR, incremental analysis, output format,
|
|
18
|
+
* deterministic output and quiet mode.
|
|
19
|
+
*
|
|
20
|
+
* @param {import('commander').Command} program - Commander program instance to attach the subcommand to.
|
|
21
|
+
*/
|
|
22
|
+
function registerAnalyzeCommand(program) {
|
|
23
|
+
program
|
|
24
|
+
.command('analyze [path]')
|
|
25
|
+
.description('Analyze codebase structure')
|
|
26
|
+
.option('-p, --patterns <list>', 'File patterns (comma-separated)')
|
|
27
|
+
.option('-e, --exclude <list>', 'Exclude patterns')
|
|
28
|
+
.option('-m, --max-files <n>', 'Max files to analyze')
|
|
29
|
+
.option('--analyzer <name>', 'Analyzer plugin to use', 'default')
|
|
30
|
+
.option('--emit-ir', 'Write typed architecture IR artifact', false)
|
|
31
|
+
.option('--incremental', 'Use incremental cache when available', false)
|
|
32
|
+
.option('-f, --format <type>', 'Output format (text, json)', 'text')
|
|
33
|
+
.option('--deterministic', 'Use deterministic machine output', false)
|
|
34
|
+
.option('-q, --quiet', 'Suppress non-essential logging', false)
|
|
35
|
+
.action(async (targetPath, rawOptions) => {
|
|
36
|
+
const options = applyDiagramRcDefaults(rawOptions, getDiagramRcFromProgram(program), ['patterns', 'exclude', 'maxFiles']);
|
|
37
|
+
const root = resolveRootPathOrExit(targetPath);
|
|
38
|
+
const formatStr = String(options.format || 'text').toLowerCase().trim();
|
|
39
|
+
const allowedFormats = new Set(['text', 'json']);
|
|
40
|
+
if (!allowedFormats.has(formatStr)) {
|
|
41
|
+
console.error(chalk.red('❌ Invalid format:'), options.format);
|
|
42
|
+
console.error(chalk.gray('Fix: use --format text or --format json.'));
|
|
43
|
+
process.exit(2);
|
|
44
|
+
}
|
|
45
|
+
const isJson = formatStr === 'json';
|
|
46
|
+
if (!options.quiet) {
|
|
47
|
+
console.error(chalk.blue('Analyzing'), root);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pipeline = await runAnalysisPipeline(root, options, 'analyze');
|
|
51
|
+
const data = pipeline.analysis;
|
|
52
|
+
const irPath = options.emitIr
|
|
53
|
+
? maybeWriteArchitectureIR(root, data, pipeline.analyzer, true)
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
if (isJson) {
|
|
57
|
+
const payload = buildMachineEnvelope({
|
|
58
|
+
schemaVersion: '1.0',
|
|
59
|
+
command: 'analyze',
|
|
60
|
+
rootPath: root,
|
|
61
|
+
deterministic: Boolean(options.deterministic),
|
|
62
|
+
data: {
|
|
63
|
+
analysis: data,
|
|
64
|
+
analyzer: pipeline.analyzer,
|
|
65
|
+
incremental: pipeline.incremental,
|
|
66
|
+
artifacts: {
|
|
67
|
+
architectureIrPath: irPath || null,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
agentSummary: {
|
|
71
|
+
componentCount: data.components?.length || 0,
|
|
72
|
+
entryPoints: data.entryPoints || [],
|
|
73
|
+
dominantLanguages: Object.entries(data.languages || {})
|
|
74
|
+
.sort((a, b) => b[1] - a[1])
|
|
75
|
+
.slice(0, 3)
|
|
76
|
+
.map(([language]) => language),
|
|
77
|
+
suggestedReviewerChecks: [
|
|
78
|
+
'Inspect top dependency hubs for unwanted coupling.',
|
|
79
|
+
'Confirm entry points align with intended service boundaries.',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (irPath && !options.quiet) {
|
|
88
|
+
console.error(chalk.gray(' IR:'), irPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const components = data.components || [];
|
|
92
|
+
const languages = data.languages || {};
|
|
93
|
+
const entryPoints = data.entryPoints || [];
|
|
94
|
+
console.log(chalk.green('\n📊 Summary'));
|
|
95
|
+
console.log(` Files: ${components.length}`);
|
|
96
|
+
console.log(` Languages: ${Object.entries(languages).map(([k, v]) => `${k}(${v})`).join(', ') || 'none'}`);
|
|
97
|
+
console.log(` Entry points: ${entryPoints.join(', ') || 'none'}`);
|
|
98
|
+
console.log(`\n${chalk.yellow('Components:')}`);
|
|
99
|
+
components.slice(0, 15).forEach((component) => {
|
|
100
|
+
const deps = component.dependencies.length > 0
|
|
101
|
+
? ` → ${component.dependencies.slice(0, 3).join(', ')}`
|
|
102
|
+
: '';
|
|
103
|
+
console.log(` ${component.originalName} (${component.type})${deps}`);
|
|
104
|
+
});
|
|
105
|
+
if (components.length > 15) {
|
|
106
|
+
console.log(chalk.gray(` ... and ${components.length - 15} more`));
|
|
107
|
+
}
|
|
108
|
+
if (pipeline.incremental.requested) {
|
|
109
|
+
const message = pipeline.incremental.used
|
|
110
|
+
? `cache hit (${pipeline.incremental.reason})`
|
|
111
|
+
: `fallback full scan (${pipeline.incremental.reason})`;
|
|
112
|
+
console.log(chalk.gray(` Incremental: ${message}`));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!options.quiet) {
|
|
116
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
117
|
+
console.log(' 1) Run `archscope generate . --type architecture` to visualize structure.');
|
|
118
|
+
console.log(' 2) Run `archscope validate .` to enforce architecture policy.');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
registerAnalyzeCommand,
|
|
125
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { generate } = require('../core/analysis-generation');
|
|
3
|
+
const { validateGitRef, getChangedFiles, runGitCommand } = require('../workflow/git-helpers');
|
|
4
|
+
const {
|
|
5
|
+
applyDiagramRcDefaults,
|
|
6
|
+
getDiagramRcFromProgram,
|
|
7
|
+
resolveRootPathOrExit,
|
|
8
|
+
runAnalysisPipeline,
|
|
9
|
+
} = require('./shared');
|
|
10
|
+
const { buildMachineEnvelope } = require('./output');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Collects file paths changed in the working tree for the repository at `root`.
|
|
14
|
+
*
|
|
15
|
+
* Gathers tracked unstaged changes, staged changes and untracked files, then returns a deduplicated, alphabetically sorted list of file paths. If the repository has no commits (invalid `HEAD` / bad revision), tracked changes are treated as empty rather than causing an error.
|
|
16
|
+
* @param {string} root - Path to the git repository root.
|
|
17
|
+
* @returns {string[]} Deduplicated, alphabetically sorted file paths that are added, modified, renamed or untracked.
|
|
18
|
+
*/
|
|
19
|
+
function listWorkingTreeChangedFiles(root) {
|
|
20
|
+
let tracked = [];
|
|
21
|
+
try {
|
|
22
|
+
tracked = runGitCommand(['diff', '--name-only', '--diff-filter=ACMR', 'HEAD'], root)
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map((line) => line.trim())
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// Repos with no commits will fail on HEAD reference
|
|
28
|
+
if (error.message && (error.message.includes('HEAD') || error.message.includes('bad revision'))) {
|
|
29
|
+
tracked = [];
|
|
30
|
+
} else {
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const staged = runGitCommand(['diff', '--cached', '--name-only', '--diff-filter=ACMR'], root)
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map((line) => line.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
const untracked = runGitCommand(['ls-files', '--others', '--exclude-standard'], root)
|
|
39
|
+
.split('\n')
|
|
40
|
+
.map((line) => line.trim())
|
|
41
|
+
.filter(Boolean);
|
|
42
|
+
return [...new Set([...tracked, ...staged, ...untracked])].sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register the `changed` CLI subcommand to analyse git-changed files.
|
|
47
|
+
*
|
|
48
|
+
* The command computes the set of changed files (either from a base..head git delta
|
|
49
|
+
* when `--base` is provided, or from the working tree otherwise), runs the analysis
|
|
50
|
+
* pipeline restricted to those files, and prints a summary in either text or JSON.
|
|
51
|
+
* When a diagram type is requested via `--type` a preview diagram may be included
|
|
52
|
+
* in the output. The command also supports common options for patterns, excludes,
|
|
53
|
+
* max files, analyzer selection, deterministic output and quiet mode.
|
|
54
|
+
*
|
|
55
|
+
* @param {Object} program - Commander `program` instance to attach the `changed` subcommand to.
|
|
56
|
+
*/
|
|
57
|
+
function registerChangedCommand(program) {
|
|
58
|
+
program
|
|
59
|
+
.command('changed [path]')
|
|
60
|
+
.description('Analyze only git-changed files')
|
|
61
|
+
.option('--base <ref>', 'Base git ref')
|
|
62
|
+
.option('--head <ref>', 'Head git ref', 'HEAD')
|
|
63
|
+
.option('-m, --max-files <n>', 'Max files to analyze')
|
|
64
|
+
.option('-p, --patterns <list>', 'File patterns (comma-separated)')
|
|
65
|
+
.option('-e, --exclude <list>', 'Exclude patterns')
|
|
66
|
+
.option('--analyzer <name>', 'Analyzer plugin to use', 'default')
|
|
67
|
+
.option('--type <diagramType>', 'Optional diagram type preview (e.g. architecture, dependency)')
|
|
68
|
+
.option('--format <type>', 'Output format (text, json)', 'text')
|
|
69
|
+
.option('--deterministic', 'Use deterministic machine output', false)
|
|
70
|
+
.option('-q, --quiet', 'Suppress non-essential logging', false)
|
|
71
|
+
.action(async (targetPath, rawOptions) => {
|
|
72
|
+
const options = applyDiagramRcDefaults(rawOptions, getDiagramRcFromProgram(program), ['patterns', 'exclude', 'maxFiles']);
|
|
73
|
+
const root = resolveRootPathOrExit(targetPath);
|
|
74
|
+
const formatStr = (options.format || 'text').toLowerCase();
|
|
75
|
+
const isJson = formatStr === 'json';
|
|
76
|
+
|
|
77
|
+
let changedFiles = [];
|
|
78
|
+
if (options.base) {
|
|
79
|
+
const baseSha = validateGitRef(options.base, root);
|
|
80
|
+
const headSha = validateGitRef(options.head || 'HEAD', root);
|
|
81
|
+
const delta = getChangedFiles(baseSha, headSha, root);
|
|
82
|
+
changedFiles = [
|
|
83
|
+
...(delta.changed || []),
|
|
84
|
+
...(delta.added || []),
|
|
85
|
+
...((delta.renamed || []).map((entry) => entry?.to).filter(Boolean)),
|
|
86
|
+
];
|
|
87
|
+
} else {
|
|
88
|
+
changedFiles = listWorkingTreeChangedFiles(root);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const includeFiles = [...new Set(changedFiles.filter((filePath) => filePath && !filePath.endsWith('/')))];
|
|
92
|
+
if (includeFiles.length === 0) {
|
|
93
|
+
if (isJson) {
|
|
94
|
+
const payload = buildMachineEnvelope({
|
|
95
|
+
schemaVersion: '1.0',
|
|
96
|
+
command: 'changed',
|
|
97
|
+
rootPath: root,
|
|
98
|
+
deterministic: Boolean(options.deterministic),
|
|
99
|
+
data: {
|
|
100
|
+
changedFiles: [],
|
|
101
|
+
analysis: null,
|
|
102
|
+
diagram: null,
|
|
103
|
+
},
|
|
104
|
+
agentSummary: {
|
|
105
|
+
changedComponents: 0,
|
|
106
|
+
riskReasons: [],
|
|
107
|
+
suggestedReviewerChecks: ['No changed files detected.'],
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
console.log(chalk.green('✅ No changed files detected.'));
|
|
114
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
115
|
+
console.log(' 1) Continue with `archscope validate .` for full rule coverage.');
|
|
116
|
+
console.log(' 2) Re-run `archscope changed .` after new branch edits.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!options.quiet) {
|
|
121
|
+
console.error(chalk.blue('Analyzing changed files only:'), includeFiles.length);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pipeline = await runAnalysisPipeline(root, {
|
|
125
|
+
...options,
|
|
126
|
+
includeFiles,
|
|
127
|
+
}, 'changed');
|
|
128
|
+
const analysis = pipeline.analysis;
|
|
129
|
+
const diagram = options.type ? generate(analysis, options.type) : null;
|
|
130
|
+
|
|
131
|
+
if (isJson) {
|
|
132
|
+
const payload = buildMachineEnvelope({
|
|
133
|
+
schemaVersion: '1.0',
|
|
134
|
+
command: 'changed',
|
|
135
|
+
rootPath: root,
|
|
136
|
+
deterministic: Boolean(options.deterministic),
|
|
137
|
+
data: {
|
|
138
|
+
changedFiles: includeFiles,
|
|
139
|
+
analyzer: pipeline.analyzer,
|
|
140
|
+
incremental: pipeline.incremental,
|
|
141
|
+
analysis,
|
|
142
|
+
diagramType: options.type || null,
|
|
143
|
+
diagram,
|
|
144
|
+
},
|
|
145
|
+
agentSummary: {
|
|
146
|
+
changedComponents: analysis.components?.length || 0,
|
|
147
|
+
riskReasons: [],
|
|
148
|
+
suggestedReviewerChecks: [
|
|
149
|
+
'Review changed-file coupling for unexpected cross-layer imports.',
|
|
150
|
+
'Use `archscope workflow pr` for blast-radius scoring before merge.',
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const components = analysis.components || [];
|
|
159
|
+
const languages = analysis.languages || {};
|
|
160
|
+
console.log(chalk.green('\n📊 Changed-file Analysis'));
|
|
161
|
+
console.log(` Changed files: ${includeFiles.length}`);
|
|
162
|
+
console.log(` Modeled components: ${components.length}`);
|
|
163
|
+
console.log(` Languages: ${Object.entries(languages).map(([key, value]) => `${key}(${value})`).join(', ') || 'none'}`);
|
|
164
|
+
console.log(chalk.yellow('\nChanged files:'));
|
|
165
|
+
includeFiles.slice(0, 20).forEach((filePath) => console.log(` - ${filePath}`));
|
|
166
|
+
if (includeFiles.length > 20) {
|
|
167
|
+
console.log(chalk.gray(` ... and ${includeFiles.length - 20} more`));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (diagram) {
|
|
171
|
+
console.log(chalk.green(`\n📐 ${options.type} diagram for changed scope:\n`));
|
|
172
|
+
console.log('```mermaid');
|
|
173
|
+
console.log(diagram);
|
|
174
|
+
console.log('```');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
178
|
+
console.log(' 1) Run `archscope workflow pr . --base origin/main --head HEAD` for risk scoring.');
|
|
179
|
+
console.log(' 2) Run `archscope validate .` if changed scope touched architecture boundaries.');
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
registerChangedCommand,
|
|
185
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { resolveRootPathOrExit } = require('./shared');
|
|
6
|
+
const { buildMachineEnvelope } = require('./output');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register the `context [path]` CLI subcommand that refreshes AI-focused context pack artifacts under `.diagram/context`.
|
|
10
|
+
*
|
|
11
|
+
* The command accepts flags `--force`, `--dry-run`, `--check`, `--quiet`, `--format <type>` (text|json, default `text`) and
|
|
12
|
+
* `--deterministic`. In `text` mode it streams script output and prints a success or failure message; in `json` mode
|
|
13
|
+
* it emits a structured machine envelope containing execution results and parsed `.diagram/context/diagram-context.meta.json`.
|
|
14
|
+
*
|
|
15
|
+
* The process exits with the executed script's status code on completion. If the refresh script is missing it exits
|
|
16
|
+
* with code `2`; on other failures it exits with the script's exit code or `1` as a fallback.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('commander').Command} program - Commander program instance to register the command on.
|
|
19
|
+
*/
|
|
20
|
+
function registerContextCommand(program) {
|
|
21
|
+
program
|
|
22
|
+
.command('context [path]')
|
|
23
|
+
.description('Refresh AI-focused context pack artifacts under .diagram/context')
|
|
24
|
+
.option('--force', 'Force refresh even during cooldown', false)
|
|
25
|
+
.option('--dry-run', 'Preview actions without generating files', false)
|
|
26
|
+
.option('--check', 'Fail if context artifacts are stale without rewriting files', false)
|
|
27
|
+
.option('--quiet', 'Suppress script logs', false)
|
|
28
|
+
.option('--format <type>', 'Output format (text, json)', 'text')
|
|
29
|
+
.option('--deterministic', 'Use deterministic machine output', false)
|
|
30
|
+
.action((targetPath, options) => {
|
|
31
|
+
const root = resolveRootPathOrExit(targetPath);
|
|
32
|
+
const scriptPath = path.join(root, 'scripts', 'refresh-diagram-context.sh');
|
|
33
|
+
if (!fs.existsSync(scriptPath)) {
|
|
34
|
+
console.error(chalk.red('❌ Missing script:'), scriptPath);
|
|
35
|
+
console.error(chalk.gray('Fix: ensure repository scripts are intact and rerun.'));
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const args = [];
|
|
40
|
+
if (options.force) args.push('--force');
|
|
41
|
+
if (options.dryRun) args.push('--dry-run');
|
|
42
|
+
if (options.check) args.push('--check');
|
|
43
|
+
if (options.quiet) args.push('--quiet');
|
|
44
|
+
|
|
45
|
+
const run = spawnSync('bash', [scriptPath, ...args], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const stdout = run.stdout || '';
|
|
52
|
+
const stderr = run.stderr || '';
|
|
53
|
+
const formatStr = (options.format || 'text').toLowerCase();
|
|
54
|
+
const metaPath = path.join(root, '.diagram', 'context', 'diagram-context.meta.json');
|
|
55
|
+
let contextMeta = null;
|
|
56
|
+
if (fs.existsSync(metaPath)) {
|
|
57
|
+
try {
|
|
58
|
+
contextMeta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
59
|
+
} catch (_error) {
|
|
60
|
+
// Keep null on parse failures.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (formatStr === 'json') {
|
|
65
|
+
const payload = buildMachineEnvelope({
|
|
66
|
+
schemaVersion: '1.0',
|
|
67
|
+
command: 'context',
|
|
68
|
+
rootPath: root,
|
|
69
|
+
deterministic: Boolean(options.deterministic),
|
|
70
|
+
status: run.status === 0 ? 'success' : 'failure',
|
|
71
|
+
data: {
|
|
72
|
+
exitCode: run.status,
|
|
73
|
+
stdout,
|
|
74
|
+
stderr,
|
|
75
|
+
contextMeta,
|
|
76
|
+
},
|
|
77
|
+
errors: run.status === 0 ? [] : [{ message: stderr || `context refresh failed with code ${run.status}` }],
|
|
78
|
+
agentSummary: {
|
|
79
|
+
changedComponents: 0,
|
|
80
|
+
riskReasons: run.status === 0 ? [] : ['context_refresh_failed'],
|
|
81
|
+
suggestedReviewerChecks: [
|
|
82
|
+
'Verify `.diagram/context/diagram-context.md` is refreshed in CI artifacts.',
|
|
83
|
+
'Review omitted types in context metadata when compaction is applied.',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
88
|
+
process.exit(run.status || 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (stdout.trim()) process.stdout.write(stdout);
|
|
92
|
+
if (stderr.trim()) process.stderr.write(stderr);
|
|
93
|
+
if (run.status !== 0) {
|
|
94
|
+
console.error(chalk.red(`❌ Context refresh failed with code ${run.status}`));
|
|
95
|
+
process.exit(run.status || 1);
|
|
96
|
+
}
|
|
97
|
+
if (options.check) {
|
|
98
|
+
console.log(chalk.green('✅ Context pack is current.'));
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
console.log(chalk.green('✅ Context pack refreshed.'));
|
|
102
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
103
|
+
console.log(' 1) Attach `.diagram/context/diagram-context.md` to AI review workflows.');
|
|
104
|
+
console.log(' 2) Run `archscope generate-all . --artifact-profile agent` if source graph changed significantly.');
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
registerContextCommand,
|
|
110
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
const {
|
|
4
|
+
analyzeAtRef,
|
|
5
|
+
computeArchitectureDiff,
|
|
6
|
+
printArchitectureDiff,
|
|
7
|
+
} = require('../workflow/git-helpers');
|
|
8
|
+
const {
|
|
9
|
+
applyDiagramRcDefaults,
|
|
10
|
+
getDiagramRcFromProgram,
|
|
11
|
+
resolveRootPathOrExit,
|
|
12
|
+
splitList,
|
|
13
|
+
} = require('./shared');
|
|
14
|
+
const { buildMachineEnvelope } = require('./output');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register the `diff <base> <head>` CLI command to compare architecture diagrams between two git refs.
|
|
18
|
+
*
|
|
19
|
+
* The command validates both git refs, analyses repository snapshots at each ref, computes an architectural diff,
|
|
20
|
+
* and emits results either as a human-readable text summary or as a JSON machine envelope.
|
|
21
|
+
*
|
|
22
|
+
* Behaviour highlights:
|
|
23
|
+
* - Accepts options for output format (`text` or `json`), verbosity, quiet mode, file patterns/exclusions,
|
|
24
|
+
* maximum files to analyse and deterministic machine output.
|
|
25
|
+
* - Exits with code 2 and prints an error when a git ref is invalid or when analysis fails.
|
|
26
|
+
* - When `--format json` is used, prints a structured envelope containing `baseRef`, `headRef` and the `diff`.
|
|
27
|
+
* - When `--format text` is used, prints a textual diff and optional next-step guidance unless `--quiet` is set.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} program - The CLI program object (e.g. commander) to which the command will be attached.
|
|
30
|
+
*/
|
|
31
|
+
function registerDiffCommand(program) {
|
|
32
|
+
program
|
|
33
|
+
.command('diff <base> <head>')
|
|
34
|
+
.description('Compare architecture diagrams between two git refs')
|
|
35
|
+
.option('-f, --format <type>', 'Output format (text, json)', 'text')
|
|
36
|
+
.option('-q, --quiet', 'Suppress non-essential logging', false)
|
|
37
|
+
.option('-m, --max-files <n>', 'Max files to analyze per ref')
|
|
38
|
+
.option('-p, --patterns <list>', 'File patterns to include (comma-separated)')
|
|
39
|
+
.option('-e, --exclude <list>', 'Paths to exclude (comma-separated)')
|
|
40
|
+
.option('--deterministic', 'Use deterministic machine output', false)
|
|
41
|
+
.option('--verbose', 'Show detailed output')
|
|
42
|
+
.action(async (baseRef, headRef, rawOptions) => {
|
|
43
|
+
const options = applyDiagramRcDefaults(rawOptions, getDiagramRcFromProgram(program), ['patterns', 'exclude', 'maxFiles']);
|
|
44
|
+
const root = resolveRootPathOrExit('.');
|
|
45
|
+
const verbose = options.verbose || false;
|
|
46
|
+
|
|
47
|
+
const baseCheck = spawnSync('git', ['rev-parse', '--verify', baseRef], {
|
|
48
|
+
cwd: root,
|
|
49
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
});
|
|
52
|
+
if (baseCheck.status !== 0) {
|
|
53
|
+
console.error(chalk.red('❌ Invalid base ref:'), baseRef);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const headCheck = spawnSync('git', ['rev-parse', '--verify', headRef], {
|
|
58
|
+
cwd: root,
|
|
59
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
});
|
|
62
|
+
if (headCheck.status !== 0) {
|
|
63
|
+
console.error(chalk.red('❌ Invalid head ref:'), headRef);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const formatStr = String(options.format || 'text').toLowerCase().trim();
|
|
68
|
+
const allowedFormats = new Set(['text', 'json']);
|
|
69
|
+
if (!allowedFormats.has(formatStr)) {
|
|
70
|
+
console.error(chalk.red('❌ Invalid format:'), options.format);
|
|
71
|
+
console.error(chalk.gray('Fix: use --format text or --format json.'));
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
const isJson = formatStr === 'json';
|
|
75
|
+
if (!isJson && !options.quiet) {
|
|
76
|
+
console.error(chalk.blue('\n🔍 Architecture Diff'));
|
|
77
|
+
console.error(chalk.gray(` Base: ${baseRef}`));
|
|
78
|
+
console.error(chalk.gray(` Head: ${headRef}`));
|
|
79
|
+
console.error('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const analysisOptions = {
|
|
83
|
+
maxFiles: parseInt(options.maxFiles, 10) || 100,
|
|
84
|
+
patterns: Array.isArray(options.patterns) ? options.patterns : splitList(options.patterns),
|
|
85
|
+
exclude: Array.isArray(options.exclude) ? options.exclude : splitList(options.exclude),
|
|
86
|
+
deterministic: Boolean(options.deterministic),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let baseAnalysis;
|
|
90
|
+
let headAnalysis;
|
|
91
|
+
try {
|
|
92
|
+
baseAnalysis = await analyzeAtRef(baseRef, root, analysisOptions);
|
|
93
|
+
if (verbose && !isJson) {
|
|
94
|
+
console.error(chalk.gray(` Base components: ${baseAnalysis.components.length}`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
headAnalysis = await analyzeAtRef(headRef, root, analysisOptions);
|
|
98
|
+
if (verbose && !isJson) {
|
|
99
|
+
console.error(chalk.gray(` Head components: ${headAnalysis.components.length}`));
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(chalk.red('❌ Analysis error:'), error.message);
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const diff = computeArchitectureDiff(baseAnalysis, headAnalysis);
|
|
107
|
+
if (isJson) {
|
|
108
|
+
const payload = buildMachineEnvelope({
|
|
109
|
+
schemaVersion: '1.0',
|
|
110
|
+
command: 'diff',
|
|
111
|
+
rootPath: root,
|
|
112
|
+
deterministic: Boolean(options.deterministic),
|
|
113
|
+
data: {
|
|
114
|
+
baseRef,
|
|
115
|
+
headRef,
|
|
116
|
+
diff,
|
|
117
|
+
},
|
|
118
|
+
agentSummary: {
|
|
119
|
+
changedComponents: Array.isArray(diff?.components?.changed) ? diff.components.changed.length : 0,
|
|
120
|
+
riskReasons: [],
|
|
121
|
+
suggestedReviewerChecks: [
|
|
122
|
+
'Verify added/removed components are expected for this ref comparison.',
|
|
123
|
+
'Inspect dependency edge changes for accidental coupling.',
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
printArchitectureDiff(diff, baseRef, headRef);
|
|
132
|
+
if (!options.quiet) {
|
|
133
|
+
console.log(chalk.cyan('\nNext steps:'));
|
|
134
|
+
console.log(' 1) Run `archscope workflow pr . --base <base> --head <head>` for risk scoring.');
|
|
135
|
+
console.log(' 2) Use `archscope explain <component> .` to inspect local dependency neighborhoods.');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
registerDiffCommand,
|
|
142
|
+
};
|