archgraph-argo 0.10.43 → 0.10.44
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/LICENSE +201 -201
- package/argo/package.json +8 -8
- package/argo/schema/ImplementationToCodingHandoff.schema.json +251 -251
- package/argo/schema/ImplementationToIntentTraceProposal.schema.json +180 -180
- package/argo/schema/IntentToImplementationHandoff.schema.json +74 -74
- package/argo/schema/SystemArchitecture.schema.json +378 -378
- package/argo/schema/archimate3.2.md +7153 -7153
- package/argo/scripts/archimate32-rules.js +12301 -12301
- package/argo/scripts/generateArchitectureDiffPlantuml.js +466 -466
- package/argo/scripts/graph-rag/ARCHITECTURE.md +192 -192
- package/argo/scripts/graph-rag/canonicalProjectionAuthority.js +45 -45
- package/argo/scripts/graph-rag/embeddingQualificationGate.js +59 -59
- package/argo/scripts/graph-rag/externalProductionConfig.js +74 -74
- package/argo/scripts/graph-rag/liveEmbeddingProviderClient.js +49 -49
- package/argo/scripts/graph-rag/neo4jNativeRetrieval.js +37 -37
- package/argo/scripts/graph-rag/semantic-persistence/ARCHITECTURE.md +51 -51
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticCheckpointStore.js +99 -99
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticProjectionStore.js +171 -171
- package/argo/scripts/graph-rag/semanticOperatorError.js +38 -38
- package/argo/scripts/graph-rag/semanticOperatorJourney.js +459 -459
- package/argo/scripts/graph-rag/semanticReadinessAttestationStore.js +398 -398
- package/argo/scripts/graph-rag/systemMetadataCommandAdapter.js +269 -269
- package/argo/scripts/graph-semantics.js +220 -220
- package/argo/scripts/repositoryArgoEnvironment.js +101 -101
- package/argo/scripts/runArchitectureTests.js +583 -583
- package/argo/scripts/semanticOperatorJourneyCli.js +91 -91
- package/argo/scripts/syncSystemArchitectureToNeo4j.js +66 -66
- package/argo/scripts/test-executors/_template.js +58 -58
- package/argo/scripts/test-executors/default.js +199 -199
- package/argo/scripts/validateStageHandoff.js +458 -458
- package/argo/scripts/validateSystemArchitecture.js +253 -253
- package/argo/scripts/validateTraceProposal.js +181 -181
- package/bin/argo-deploy.js +12 -12
- package/install-argo.ps1 +27 -8
- package/package.json +1 -1
|
@@ -1,466 +1,466 @@
|
|
|
1
|
-
const { execFileSync } = require('node:child_process');
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
|
|
5
|
-
const {
|
|
6
|
-
getWorkspaceRoot,
|
|
7
|
-
} = require('./argo-paths.js');
|
|
8
|
-
|
|
9
|
-
const DEFAULT_ARCHITECTURE_PATH = 'design/KG/SystemArchitecture.json';
|
|
10
|
-
const DEFAULT_OUTPUT_DIR = '.argo/temp/architecture_analysis';
|
|
11
|
-
|
|
12
|
-
const ELEMENT_TYPE_BASE_G = {
|
|
13
|
-
Goal: 15,
|
|
14
|
-
Requirement: 15,
|
|
15
|
-
Constraint: 1.5,
|
|
16
|
-
'Business Process': 15,
|
|
17
|
-
'Application Component': 2,
|
|
18
|
-
'Application Function': 1.5,
|
|
19
|
-
'Application Interface': 2,
|
|
20
|
-
'Data Object': 1.5,
|
|
21
|
-
Assessment: 1.5,
|
|
22
|
-
Principle: 1.5,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const RELATIONSHIP_TYPE_BASE_G = {
|
|
26
|
-
Realization: 15,
|
|
27
|
-
Influence: 1.5,
|
|
28
|
-
Assignment: 3,
|
|
29
|
-
Serving: 1.5,
|
|
30
|
-
Access: 1.5,
|
|
31
|
-
Association: 2,
|
|
32
|
-
Composition: 10,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Keep dependency direction aligned with runArchitectureTests.js delivery semantics.
|
|
36
|
-
const DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET = new Set(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']);
|
|
37
|
-
const DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE = new Set(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']);
|
|
38
|
-
|
|
39
|
-
function main() {
|
|
40
|
-
const options = parseArgs(process.argv.slice(2));
|
|
41
|
-
const workspaceRoot = resolveWorkspaceRoot(options.workspaceRoot);
|
|
42
|
-
const architecturePath = normalizeRelativePath(options.architecturePath || DEFAULT_ARCHITECTURE_PATH);
|
|
43
|
-
const outputDir = normalizeRelativePath(options.outputDir || DEFAULT_OUTPUT_DIR);
|
|
44
|
-
|
|
45
|
-
const result = generateArchitectureDiffPlantuml({
|
|
46
|
-
workspaceRoot,
|
|
47
|
-
architecturePath,
|
|
48
|
-
outputDir,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
52
|
-
process.exit(result.status === 'passed' ? 0 : 1);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function generateArchitectureDiffPlantuml({ workspaceRoot, architecturePath, outputDir }) {
|
|
56
|
-
const normalizedArchitecturePath = normalizeRelativePath(architecturePath || DEFAULT_ARCHITECTURE_PATH);
|
|
57
|
-
const normalizedOutputDir = normalizeRelativePath(outputDir || DEFAULT_OUTPUT_DIR);
|
|
58
|
-
const absoluteArchitecturePath = resolveWorkspacePath(workspaceRoot, normalizedArchitecturePath);
|
|
59
|
-
const currentDocument = readJsonFile(absoluteArchitecturePath);
|
|
60
|
-
const baseDocument = readHeadJson(workspaceRoot, normalizedArchitecturePath);
|
|
61
|
-
const diff = diffArchitecture(baseDocument, currentDocument);
|
|
62
|
-
|
|
63
|
-
const totalG = estimateTotalG(diff, currentDocument, baseDocument);
|
|
64
|
-
const markdown = buildMarkdown({
|
|
65
|
-
architecturePath: normalizedArchitecturePath,
|
|
66
|
-
diff,
|
|
67
|
-
currentDocument,
|
|
68
|
-
baseDocument,
|
|
69
|
-
totalG,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const timestamp = buildTimestamp(new Date());
|
|
73
|
-
const relativeOutputPath = normalizeRelativePath(path.posix.join(normalizedOutputDir, `architecture-diff-${timestamp}.md`));
|
|
74
|
-
const absoluteOutputPath = resolveWorkspacePath(workspaceRoot, relativeOutputPath);
|
|
75
|
-
fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
|
|
76
|
-
fs.writeFileSync(absoluteOutputPath, markdown, 'utf8');
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
status: 'passed',
|
|
80
|
-
architecturePath: normalizedArchitecturePath,
|
|
81
|
-
outputPath: relativeOutputPath,
|
|
82
|
-
changedElementIds: diff.changedElements.map(entry => entry.id),
|
|
83
|
-
changedRelationshipIds: diff.changedRelationships.map(entry => entry.id),
|
|
84
|
-
totalGranularity: `${formatG(totalG)}G`,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function parseArgs(args) {
|
|
89
|
-
const options = {};
|
|
90
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
91
|
-
const arg = args[index];
|
|
92
|
-
if (arg === '--architecturePath') {
|
|
93
|
-
options.architecturePath = args[index + 1];
|
|
94
|
-
index += 1;
|
|
95
|
-
} else if (arg === '--outputDir') {
|
|
96
|
-
options.outputDir = args[index + 1];
|
|
97
|
-
index += 1;
|
|
98
|
-
} else if (arg === '--workspaceRoot') {
|
|
99
|
-
options.workspaceRoot = args[index + 1];
|
|
100
|
-
index += 1;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return options;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function resolveWorkspaceRoot(input) {
|
|
107
|
-
return path.resolve(input || getWorkspaceRoot());
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function resolveWorkspacePath(workspaceRoot, relativePath) {
|
|
111
|
-
const normalized = normalizeRelativePath(relativePath);
|
|
112
|
-
const absolutePath = path.resolve(workspaceRoot, normalized);
|
|
113
|
-
const normalizedRoot = path.resolve(workspaceRoot);
|
|
114
|
-
if (!absolutePath.startsWith(normalizedRoot)) {
|
|
115
|
-
throw new Error(`Path escapes workspace root: ${relativePath}`);
|
|
116
|
-
}
|
|
117
|
-
return absolutePath;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function normalizeRelativePath(value) {
|
|
121
|
-
return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function readJsonFile(absolutePath) {
|
|
125
|
-
return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function readHeadJson(workspaceRoot, architecturePath) {
|
|
129
|
-
try {
|
|
130
|
-
const content = execFileSync('git', ['show', `HEAD:${architecturePath}`], {
|
|
131
|
-
cwd: workspaceRoot,
|
|
132
|
-
encoding: 'utf8',
|
|
133
|
-
maxBuffer: 20 * 1024 * 1024,
|
|
134
|
-
});
|
|
135
|
-
return JSON.parse(content);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
return { elements: [], relationships: [], views: [] };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function diffArchitecture(baseDocument, currentDocument) {
|
|
142
|
-
const baseElements = mapById(baseDocument.elements || []);
|
|
143
|
-
const currentElements = mapById(currentDocument.elements || []);
|
|
144
|
-
const baseRelationships = mapById(baseDocument.relationships || []);
|
|
145
|
-
const currentRelationships = mapById(currentDocument.relationships || []);
|
|
146
|
-
|
|
147
|
-
const changedElements = diffCollection(baseElements, currentElements);
|
|
148
|
-
const changedRelationships = diffCollection(baseRelationships, currentRelationships);
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
changedElements,
|
|
152
|
-
changedRelationships,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function mapById(items) {
|
|
157
|
-
const map = new Map();
|
|
158
|
-
for (const item of items) {
|
|
159
|
-
if (item && item.id) {
|
|
160
|
-
map.set(String(item.id), item);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return map;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function diffCollection(baseMap, currentMap) {
|
|
167
|
-
const ids = new Set([...baseMap.keys(), ...currentMap.keys()]);
|
|
168
|
-
const entries = [];
|
|
169
|
-
for (const id of Array.from(ids).sort(compareIds)) {
|
|
170
|
-
const before = baseMap.get(id);
|
|
171
|
-
const after = currentMap.get(id);
|
|
172
|
-
if (!before && after) {
|
|
173
|
-
entries.push({ id, status: 'added', item: after });
|
|
174
|
-
} else if (before && !after) {
|
|
175
|
-
entries.push({ id, status: 'removed', item: before });
|
|
176
|
-
} else if (before && after && JSON.stringify(before) !== JSON.stringify(after)) {
|
|
177
|
-
entries.push({ id, status: 'affected', item: after, before });
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return entries;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function compareIds(left, right) {
|
|
184
|
-
const leftNumber = Number(left);
|
|
185
|
-
const rightNumber = Number(right);
|
|
186
|
-
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
|
|
187
|
-
return leftNumber - rightNumber;
|
|
188
|
-
}
|
|
189
|
-
return String(left).localeCompare(String(right));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function estimateTotalG(diff, currentDocument, baseDocument) {
|
|
193
|
-
const changedBusinessProcesses = diff.changedElements
|
|
194
|
-
.map(entry => entry.item)
|
|
195
|
-
.filter(element => element.type === 'Business Process');
|
|
196
|
-
const fpCount = changedBusinessProcesses
|
|
197
|
-
.map(countFunctionalPoints)
|
|
198
|
-
.reduce((sum, count) => sum + count, 0);
|
|
199
|
-
const interfaceCount = diff.changedElements
|
|
200
|
-
.map(entry => entry.item)
|
|
201
|
-
.filter(element => element.type === 'Application Interface')
|
|
202
|
-
.length;
|
|
203
|
-
const externalServiceInterfaceCount = diff.changedElements
|
|
204
|
-
.map(entry => entry.item)
|
|
205
|
-
.filter(element => element.type === 'Application Component')
|
|
206
|
-
.filter(element => /W3|IDASS|SSO/i.test(element.name || ''))
|
|
207
|
-
.length;
|
|
208
|
-
const calculated = (fpCount * 1.5) + ((interfaceCount + externalServiceInterfaceCount) * 2);
|
|
209
|
-
if (calculated > 0) {
|
|
210
|
-
return calculated;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const elementTotal = diff.changedElements.reduce((sum, entry) => sum + estimateElementG(entry.item), 0);
|
|
214
|
-
const relationshipTotal = diff.changedRelationships.reduce((sum, entry) => sum + estimateRelationshipG(entry.item), 0);
|
|
215
|
-
return Math.max(elementTotal, relationshipTotal, 1.5);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function countFunctionalPoints(element) {
|
|
219
|
-
const texts = [];
|
|
220
|
-
for (const attribute of element.attributes || []) {
|
|
221
|
-
if (attribute.name === 'functionalPoints' || /functional/i.test(attribute.name || '')) {
|
|
222
|
-
texts.push(attribute.description || '');
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
texts.push(element.description || '');
|
|
226
|
-
const joined = texts.join(' ');
|
|
227
|
-
const matches = joined.match(/\bFP(?:-\w+)?-\d+|\bFP\d+\b/g);
|
|
228
|
-
return matches ? new Set(matches).size : 0;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function estimateElementG(element) {
|
|
232
|
-
if (!element) {
|
|
233
|
-
return 1.5;
|
|
234
|
-
}
|
|
235
|
-
const fpCount = countFunctionalPoints(element);
|
|
236
|
-
if (fpCount > 0) {
|
|
237
|
-
return fpCount * 1.5;
|
|
238
|
-
}
|
|
239
|
-
return ELEMENT_TYPE_BASE_G[element.type] || 1.5;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function estimateRelationshipG(relationship) {
|
|
243
|
-
if (!relationship) {
|
|
244
|
-
return 1.5;
|
|
245
|
-
}
|
|
246
|
-
return RELATIONSHIP_TYPE_BASE_G[relationship.type] || 1.5;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function buildMarkdown({ architecturePath, diff, currentDocument, baseDocument, totalG }) {
|
|
250
|
-
const elementEntries = enrichElements(diff.changedElements, diff.changedRelationships, currentDocument, baseDocument, totalG);
|
|
251
|
-
const relationshipEntries = enrichRelationships(diff.changedRelationships, elementEntries);
|
|
252
|
-
const treeEdges = buildTreeEdges(elementEntries, relationshipEntries);
|
|
253
|
-
const plantuml = buildPlantuml(elementEntries, relationshipEntries, treeEdges, totalG);
|
|
254
|
-
|
|
255
|
-
return [
|
|
256
|
-
'# SystemArchitecture Diff PlantUML',
|
|
257
|
-
'',
|
|
258
|
-
`- Source: \`${architecturePath}\``,
|
|
259
|
-
`- Generated: \`${new Date().toISOString()}\``,
|
|
260
|
-
'- Color: added=pink, affected=blue, context=light yellow.',
|
|
261
|
-
'- Tree direction: root dependency -> depended-on elements; `G传递` means the child contributes granularity to its parent.',
|
|
262
|
-
'',
|
|
263
|
-
'```plantuml',
|
|
264
|
-
plantuml,
|
|
265
|
-
'```',
|
|
266
|
-
'',
|
|
267
|
-
].join('\n');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function enrichElements(changedElements, changedRelationships, currentDocument, baseDocument, totalG) {
|
|
271
|
-
const currentElements = mapById(currentDocument.elements || []);
|
|
272
|
-
const baseElements = mapById(baseDocument.elements || []);
|
|
273
|
-
const byId = new Map();
|
|
274
|
-
|
|
275
|
-
for (const entry of changedElements) {
|
|
276
|
-
byId.set(entry.id, {
|
|
277
|
-
id: entry.id,
|
|
278
|
-
status: entry.status,
|
|
279
|
-
item: entry.item,
|
|
280
|
-
g: ['Goal', 'Requirement', 'Business Process'].includes(entry.item.type)
|
|
281
|
-
? totalG
|
|
282
|
-
: estimateElementG(entry.item),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
for (const relationshipEntry of changedRelationships) {
|
|
287
|
-
const relationship = relationshipEntry.item;
|
|
288
|
-
for (const id of [relationship.source_id, relationship.target_id].filter(Boolean).map(String)) {
|
|
289
|
-
if (!byId.has(id)) {
|
|
290
|
-
const item = currentElements.get(id) || baseElements.get(id);
|
|
291
|
-
if (item) {
|
|
292
|
-
byId.set(id, {
|
|
293
|
-
id,
|
|
294
|
-
status: 'context',
|
|
295
|
-
item,
|
|
296
|
-
g: estimateElementG(item),
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
return Array.from(byId.values()).sort((left, right) => {
|
|
304
|
-
if (right.g !== left.g) {
|
|
305
|
-
return right.g - left.g;
|
|
306
|
-
}
|
|
307
|
-
return compareIds(left.id, right.id);
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function enrichRelationships(changedRelationships, elementEntries) {
|
|
312
|
-
const knownElementIds = new Set(elementEntries.map(entry => entry.id));
|
|
313
|
-
return changedRelationships
|
|
314
|
-
.filter(entry => knownElementIds.has(String(entry.item.source_id)) && knownElementIds.has(String(entry.item.target_id)))
|
|
315
|
-
.map(entry => ({
|
|
316
|
-
id: entry.id,
|
|
317
|
-
status: entry.status,
|
|
318
|
-
item: entry.item,
|
|
319
|
-
g: estimateRelationshipG(entry.item),
|
|
320
|
-
}))
|
|
321
|
-
.sort((left, right) => compareIds(left.id, right.id));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function buildTreeEdges(elementEntries, relationshipEntries) {
|
|
325
|
-
const entriesById = new Map(elementEntries.map(entry => [entry.id, entry]));
|
|
326
|
-
const edges = [];
|
|
327
|
-
const connectedElementIds = new Set();
|
|
328
|
-
|
|
329
|
-
for (const relationshipEntry of relationshipEntries) {
|
|
330
|
-
const source = entriesById.get(String(relationshipEntry.item.source_id));
|
|
331
|
-
const target = entriesById.get(String(relationshipEntry.item.target_id));
|
|
332
|
-
if (!source || !target) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
const dependencyEdge = orientDependencyEdge(relationshipEntry.item, source, target);
|
|
336
|
-
if (!dependencyEdge) {
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
const { parent, child } = dependencyEdge;
|
|
340
|
-
edges.push({
|
|
341
|
-
kind: 'relationship',
|
|
342
|
-
relationship: relationshipEntry,
|
|
343
|
-
parent,
|
|
344
|
-
child,
|
|
345
|
-
g: child.g,
|
|
346
|
-
});
|
|
347
|
-
connectedElementIds.add(parent.id);
|
|
348
|
-
connectedElementIds.add(child.id);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const root = elementEntries[0];
|
|
352
|
-
if (root) {
|
|
353
|
-
for (const entry of elementEntries) {
|
|
354
|
-
if (entry.id === root.id || connectedElementIds.has(entry.id)) {
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
edges.push({
|
|
358
|
-
kind: 'impact',
|
|
359
|
-
parent: root,
|
|
360
|
-
child: entry,
|
|
361
|
-
g: entry.g,
|
|
362
|
-
});
|
|
363
|
-
connectedElementIds.add(entry.id);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return edges;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function orientDependencyEdge(relationship, source, target) {
|
|
371
|
-
const relationshipType = String(relationship.type || '');
|
|
372
|
-
if (DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET.has(relationshipType)) {
|
|
373
|
-
return { parent: source, child: target };
|
|
374
|
-
}
|
|
375
|
-
if (DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE.has(relationshipType)) {
|
|
376
|
-
return { parent: target, child: source };
|
|
377
|
-
}
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function buildPlantuml(elementEntries, relationshipEntries, treeEdges, totalG) {
|
|
382
|
-
const lines = [
|
|
383
|
-
'@startuml',
|
|
384
|
-
'title SystemArchitecture 当前 Git Diff 变化树',
|
|
385
|
-
'',
|
|
386
|
-
'top to bottom direction',
|
|
387
|
-
'hide stereotype',
|
|
388
|
-
'skinparam shadowing false',
|
|
389
|
-
'skinparam linetype ortho',
|
|
390
|
-
'skinparam defaultTextAlignment center',
|
|
391
|
-
'skinparam wrapWidth 120',
|
|
392
|
-
'skinparam ArrowFontSize 10',
|
|
393
|
-
'skinparam nodesep 25',
|
|
394
|
-
'skinparam ranksep 55',
|
|
395
|
-
'',
|
|
396
|
-
'skinparam rectangle {',
|
|
397
|
-
' RoundCorner 12',
|
|
398
|
-
' BackgroundColor<<added>> #F8BBD0',
|
|
399
|
-
' BorderColor<<added>> #AD1457',
|
|
400
|
-
' BackgroundColor<<affected>> #BBDEFB',
|
|
401
|
-
' BorderColor<<affected>> #1565C0',
|
|
402
|
-
' BackgroundColor<<context>> #FFF9C4',
|
|
403
|
-
' BorderColor<<context>> #F9A825',
|
|
404
|
-
'}',
|
|
405
|
-
'',
|
|
406
|
-
'legend right',
|
|
407
|
-
' |= 颜色 |= 含义 |',
|
|
408
|
-
' | <back:#F8BBD0>新增</back> | Git diff 新增元素/关系 |',
|
|
409
|
-
' | <back:#BBDEFB>影响</back> | Git diff 修改既有元素/关系 |',
|
|
410
|
-
' | <back:#FFF9C4>上下文</back> | 被变更关系引用的上下文元素 |',
|
|
411
|
-
` | 估算 | 总计 ${formatG(totalG)}G |`,
|
|
412
|
-
'endlegend',
|
|
413
|
-
'',
|
|
414
|
-
];
|
|
415
|
-
|
|
416
|
-
for (const entry of elementEntries) {
|
|
417
|
-
lines.push(`rectangle "${escapePlantuml(entry.item.name || entry.id)}\\nid=${entry.id}\\n${escapePlantuml(entry.item.type || 'Element')}\\n${entry.status === 'affected' ? '影响粒度' : entry.status === 'context' ? '上下文粒度' : '任务粒度'}=${formatG(entry.g)}G" as E${sanitizeAlias(entry.id)} <<${entry.status}>>`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
lines.push('');
|
|
421
|
-
|
|
422
|
-
for (const edge of treeEdges) {
|
|
423
|
-
const color = edge.kind === 'impact' ? '#1565C0' : edge.relationship.status === 'affected' ? '#1565C0' : '#AD1457';
|
|
424
|
-
const style = edge.kind === 'impact' ? ',dotted' : '';
|
|
425
|
-
const label = edge.kind === 'impact'
|
|
426
|
-
? `影响项\\n非图谱关系\\nG传递=${formatG(edge.g)}G`
|
|
427
|
-
: `${edge.relationship.id} ${escapePlantuml(edge.relationship.item.name || '')}\\n${escapePlantuml(edge.relationship.item.type || '')}\\nG传递=${formatG(edge.g)}G`;
|
|
428
|
-
lines.push(`E${sanitizeAlias(edge.parent.id)} -[${color}${style}]-> E${sanitizeAlias(edge.child.id)} : ${label}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
lines.push('', '@enduml');
|
|
432
|
-
return lines.join('\n');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function escapePlantuml(value) {
|
|
436
|
-
return String(value || '').replace(/"/g, '\\"').replace(/\r?\n/g, '\\n');
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function sanitizeAlias(value) {
|
|
440
|
-
return String(value).replace(/[^A-Za-z0-9_]/g, '_');
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function formatG(value) {
|
|
444
|
-
return Number(value).toFixed(1).replace(/\.0$/, '');
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function buildTimestamp(date) {
|
|
448
|
-
const pad = value => String(value).padStart(2, '0');
|
|
449
|
-
return [
|
|
450
|
-
date.getFullYear(),
|
|
451
|
-
pad(date.getMonth() + 1),
|
|
452
|
-
pad(date.getDate()),
|
|
453
|
-
'-',
|
|
454
|
-
pad(date.getHours()),
|
|
455
|
-
pad(date.getMinutes()),
|
|
456
|
-
pad(date.getSeconds()),
|
|
457
|
-
].join('');
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (require.main === module) {
|
|
461
|
-
main();
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
module.exports = {
|
|
465
|
-
generateArchitectureDiffPlantuml,
|
|
466
|
-
};
|
|
1
|
+
const { execFileSync } = require('node:child_process');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
getWorkspaceRoot,
|
|
7
|
+
} = require('./argo-paths.js');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_ARCHITECTURE_PATH = 'design/KG/SystemArchitecture.json';
|
|
10
|
+
const DEFAULT_OUTPUT_DIR = '.argo/temp/architecture_analysis';
|
|
11
|
+
|
|
12
|
+
const ELEMENT_TYPE_BASE_G = {
|
|
13
|
+
Goal: 15,
|
|
14
|
+
Requirement: 15,
|
|
15
|
+
Constraint: 1.5,
|
|
16
|
+
'Business Process': 15,
|
|
17
|
+
'Application Component': 2,
|
|
18
|
+
'Application Function': 1.5,
|
|
19
|
+
'Application Interface': 2,
|
|
20
|
+
'Data Object': 1.5,
|
|
21
|
+
Assessment: 1.5,
|
|
22
|
+
Principle: 1.5,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const RELATIONSHIP_TYPE_BASE_G = {
|
|
26
|
+
Realization: 15,
|
|
27
|
+
Influence: 1.5,
|
|
28
|
+
Assignment: 3,
|
|
29
|
+
Serving: 1.5,
|
|
30
|
+
Access: 1.5,
|
|
31
|
+
Association: 2,
|
|
32
|
+
Composition: 10,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Keep dependency direction aligned with runArchitectureTests.js delivery semantics.
|
|
36
|
+
const DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET = new Set(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']);
|
|
37
|
+
const DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE = new Set(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']);
|
|
38
|
+
|
|
39
|
+
function main() {
|
|
40
|
+
const options = parseArgs(process.argv.slice(2));
|
|
41
|
+
const workspaceRoot = resolveWorkspaceRoot(options.workspaceRoot);
|
|
42
|
+
const architecturePath = normalizeRelativePath(options.architecturePath || DEFAULT_ARCHITECTURE_PATH);
|
|
43
|
+
const outputDir = normalizeRelativePath(options.outputDir || DEFAULT_OUTPUT_DIR);
|
|
44
|
+
|
|
45
|
+
const result = generateArchitectureDiffPlantuml({
|
|
46
|
+
workspaceRoot,
|
|
47
|
+
architecturePath,
|
|
48
|
+
outputDir,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
52
|
+
process.exit(result.status === 'passed' ? 0 : 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function generateArchitectureDiffPlantuml({ workspaceRoot, architecturePath, outputDir }) {
|
|
56
|
+
const normalizedArchitecturePath = normalizeRelativePath(architecturePath || DEFAULT_ARCHITECTURE_PATH);
|
|
57
|
+
const normalizedOutputDir = normalizeRelativePath(outputDir || DEFAULT_OUTPUT_DIR);
|
|
58
|
+
const absoluteArchitecturePath = resolveWorkspacePath(workspaceRoot, normalizedArchitecturePath);
|
|
59
|
+
const currentDocument = readJsonFile(absoluteArchitecturePath);
|
|
60
|
+
const baseDocument = readHeadJson(workspaceRoot, normalizedArchitecturePath);
|
|
61
|
+
const diff = diffArchitecture(baseDocument, currentDocument);
|
|
62
|
+
|
|
63
|
+
const totalG = estimateTotalG(diff, currentDocument, baseDocument);
|
|
64
|
+
const markdown = buildMarkdown({
|
|
65
|
+
architecturePath: normalizedArchitecturePath,
|
|
66
|
+
diff,
|
|
67
|
+
currentDocument,
|
|
68
|
+
baseDocument,
|
|
69
|
+
totalG,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const timestamp = buildTimestamp(new Date());
|
|
73
|
+
const relativeOutputPath = normalizeRelativePath(path.posix.join(normalizedOutputDir, `architecture-diff-${timestamp}.md`));
|
|
74
|
+
const absoluteOutputPath = resolveWorkspacePath(workspaceRoot, relativeOutputPath);
|
|
75
|
+
fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });
|
|
76
|
+
fs.writeFileSync(absoluteOutputPath, markdown, 'utf8');
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
status: 'passed',
|
|
80
|
+
architecturePath: normalizedArchitecturePath,
|
|
81
|
+
outputPath: relativeOutputPath,
|
|
82
|
+
changedElementIds: diff.changedElements.map(entry => entry.id),
|
|
83
|
+
changedRelationshipIds: diff.changedRelationships.map(entry => entry.id),
|
|
84
|
+
totalGranularity: `${formatG(totalG)}G`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseArgs(args) {
|
|
89
|
+
const options = {};
|
|
90
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
91
|
+
const arg = args[index];
|
|
92
|
+
if (arg === '--architecturePath') {
|
|
93
|
+
options.architecturePath = args[index + 1];
|
|
94
|
+
index += 1;
|
|
95
|
+
} else if (arg === '--outputDir') {
|
|
96
|
+
options.outputDir = args[index + 1];
|
|
97
|
+
index += 1;
|
|
98
|
+
} else if (arg === '--workspaceRoot') {
|
|
99
|
+
options.workspaceRoot = args[index + 1];
|
|
100
|
+
index += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function resolveWorkspaceRoot(input) {
|
|
107
|
+
return path.resolve(input || getWorkspaceRoot());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveWorkspacePath(workspaceRoot, relativePath) {
|
|
111
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
112
|
+
const absolutePath = path.resolve(workspaceRoot, normalized);
|
|
113
|
+
const normalizedRoot = path.resolve(workspaceRoot);
|
|
114
|
+
if (!absolutePath.startsWith(normalizedRoot)) {
|
|
115
|
+
throw new Error(`Path escapes workspace root: ${relativePath}`);
|
|
116
|
+
}
|
|
117
|
+
return absolutePath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeRelativePath(value) {
|
|
121
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readJsonFile(absolutePath) {
|
|
125
|
+
return JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readHeadJson(workspaceRoot, architecturePath) {
|
|
129
|
+
try {
|
|
130
|
+
const content = execFileSync('git', ['show', `HEAD:${architecturePath}`], {
|
|
131
|
+
cwd: workspaceRoot,
|
|
132
|
+
encoding: 'utf8',
|
|
133
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
134
|
+
});
|
|
135
|
+
return JSON.parse(content);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return { elements: [], relationships: [], views: [] };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function diffArchitecture(baseDocument, currentDocument) {
|
|
142
|
+
const baseElements = mapById(baseDocument.elements || []);
|
|
143
|
+
const currentElements = mapById(currentDocument.elements || []);
|
|
144
|
+
const baseRelationships = mapById(baseDocument.relationships || []);
|
|
145
|
+
const currentRelationships = mapById(currentDocument.relationships || []);
|
|
146
|
+
|
|
147
|
+
const changedElements = diffCollection(baseElements, currentElements);
|
|
148
|
+
const changedRelationships = diffCollection(baseRelationships, currentRelationships);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
changedElements,
|
|
152
|
+
changedRelationships,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function mapById(items) {
|
|
157
|
+
const map = new Map();
|
|
158
|
+
for (const item of items) {
|
|
159
|
+
if (item && item.id) {
|
|
160
|
+
map.set(String(item.id), item);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return map;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function diffCollection(baseMap, currentMap) {
|
|
167
|
+
const ids = new Set([...baseMap.keys(), ...currentMap.keys()]);
|
|
168
|
+
const entries = [];
|
|
169
|
+
for (const id of Array.from(ids).sort(compareIds)) {
|
|
170
|
+
const before = baseMap.get(id);
|
|
171
|
+
const after = currentMap.get(id);
|
|
172
|
+
if (!before && after) {
|
|
173
|
+
entries.push({ id, status: 'added', item: after });
|
|
174
|
+
} else if (before && !after) {
|
|
175
|
+
entries.push({ id, status: 'removed', item: before });
|
|
176
|
+
} else if (before && after && JSON.stringify(before) !== JSON.stringify(after)) {
|
|
177
|
+
entries.push({ id, status: 'affected', item: after, before });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return entries;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function compareIds(left, right) {
|
|
184
|
+
const leftNumber = Number(left);
|
|
185
|
+
const rightNumber = Number(right);
|
|
186
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) {
|
|
187
|
+
return leftNumber - rightNumber;
|
|
188
|
+
}
|
|
189
|
+
return String(left).localeCompare(String(right));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function estimateTotalG(diff, currentDocument, baseDocument) {
|
|
193
|
+
const changedBusinessProcesses = diff.changedElements
|
|
194
|
+
.map(entry => entry.item)
|
|
195
|
+
.filter(element => element.type === 'Business Process');
|
|
196
|
+
const fpCount = changedBusinessProcesses
|
|
197
|
+
.map(countFunctionalPoints)
|
|
198
|
+
.reduce((sum, count) => sum + count, 0);
|
|
199
|
+
const interfaceCount = diff.changedElements
|
|
200
|
+
.map(entry => entry.item)
|
|
201
|
+
.filter(element => element.type === 'Application Interface')
|
|
202
|
+
.length;
|
|
203
|
+
const externalServiceInterfaceCount = diff.changedElements
|
|
204
|
+
.map(entry => entry.item)
|
|
205
|
+
.filter(element => element.type === 'Application Component')
|
|
206
|
+
.filter(element => /W3|IDASS|SSO/i.test(element.name || ''))
|
|
207
|
+
.length;
|
|
208
|
+
const calculated = (fpCount * 1.5) + ((interfaceCount + externalServiceInterfaceCount) * 2);
|
|
209
|
+
if (calculated > 0) {
|
|
210
|
+
return calculated;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const elementTotal = diff.changedElements.reduce((sum, entry) => sum + estimateElementG(entry.item), 0);
|
|
214
|
+
const relationshipTotal = diff.changedRelationships.reduce((sum, entry) => sum + estimateRelationshipG(entry.item), 0);
|
|
215
|
+
return Math.max(elementTotal, relationshipTotal, 1.5);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function countFunctionalPoints(element) {
|
|
219
|
+
const texts = [];
|
|
220
|
+
for (const attribute of element.attributes || []) {
|
|
221
|
+
if (attribute.name === 'functionalPoints' || /functional/i.test(attribute.name || '')) {
|
|
222
|
+
texts.push(attribute.description || '');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
texts.push(element.description || '');
|
|
226
|
+
const joined = texts.join(' ');
|
|
227
|
+
const matches = joined.match(/\bFP(?:-\w+)?-\d+|\bFP\d+\b/g);
|
|
228
|
+
return matches ? new Set(matches).size : 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function estimateElementG(element) {
|
|
232
|
+
if (!element) {
|
|
233
|
+
return 1.5;
|
|
234
|
+
}
|
|
235
|
+
const fpCount = countFunctionalPoints(element);
|
|
236
|
+
if (fpCount > 0) {
|
|
237
|
+
return fpCount * 1.5;
|
|
238
|
+
}
|
|
239
|
+
return ELEMENT_TYPE_BASE_G[element.type] || 1.5;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function estimateRelationshipG(relationship) {
|
|
243
|
+
if (!relationship) {
|
|
244
|
+
return 1.5;
|
|
245
|
+
}
|
|
246
|
+
return RELATIONSHIP_TYPE_BASE_G[relationship.type] || 1.5;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildMarkdown({ architecturePath, diff, currentDocument, baseDocument, totalG }) {
|
|
250
|
+
const elementEntries = enrichElements(diff.changedElements, diff.changedRelationships, currentDocument, baseDocument, totalG);
|
|
251
|
+
const relationshipEntries = enrichRelationships(diff.changedRelationships, elementEntries);
|
|
252
|
+
const treeEdges = buildTreeEdges(elementEntries, relationshipEntries);
|
|
253
|
+
const plantuml = buildPlantuml(elementEntries, relationshipEntries, treeEdges, totalG);
|
|
254
|
+
|
|
255
|
+
return [
|
|
256
|
+
'# SystemArchitecture Diff PlantUML',
|
|
257
|
+
'',
|
|
258
|
+
`- Source: \`${architecturePath}\``,
|
|
259
|
+
`- Generated: \`${new Date().toISOString()}\``,
|
|
260
|
+
'- Color: added=pink, affected=blue, context=light yellow.',
|
|
261
|
+
'- Tree direction: root dependency -> depended-on elements; `G传递` means the child contributes granularity to its parent.',
|
|
262
|
+
'',
|
|
263
|
+
'```plantuml',
|
|
264
|
+
plantuml,
|
|
265
|
+
'```',
|
|
266
|
+
'',
|
|
267
|
+
].join('\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function enrichElements(changedElements, changedRelationships, currentDocument, baseDocument, totalG) {
|
|
271
|
+
const currentElements = mapById(currentDocument.elements || []);
|
|
272
|
+
const baseElements = mapById(baseDocument.elements || []);
|
|
273
|
+
const byId = new Map();
|
|
274
|
+
|
|
275
|
+
for (const entry of changedElements) {
|
|
276
|
+
byId.set(entry.id, {
|
|
277
|
+
id: entry.id,
|
|
278
|
+
status: entry.status,
|
|
279
|
+
item: entry.item,
|
|
280
|
+
g: ['Goal', 'Requirement', 'Business Process'].includes(entry.item.type)
|
|
281
|
+
? totalG
|
|
282
|
+
: estimateElementG(entry.item),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const relationshipEntry of changedRelationships) {
|
|
287
|
+
const relationship = relationshipEntry.item;
|
|
288
|
+
for (const id of [relationship.source_id, relationship.target_id].filter(Boolean).map(String)) {
|
|
289
|
+
if (!byId.has(id)) {
|
|
290
|
+
const item = currentElements.get(id) || baseElements.get(id);
|
|
291
|
+
if (item) {
|
|
292
|
+
byId.set(id, {
|
|
293
|
+
id,
|
|
294
|
+
status: 'context',
|
|
295
|
+
item,
|
|
296
|
+
g: estimateElementG(item),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return Array.from(byId.values()).sort((left, right) => {
|
|
304
|
+
if (right.g !== left.g) {
|
|
305
|
+
return right.g - left.g;
|
|
306
|
+
}
|
|
307
|
+
return compareIds(left.id, right.id);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function enrichRelationships(changedRelationships, elementEntries) {
|
|
312
|
+
const knownElementIds = new Set(elementEntries.map(entry => entry.id));
|
|
313
|
+
return changedRelationships
|
|
314
|
+
.filter(entry => knownElementIds.has(String(entry.item.source_id)) && knownElementIds.has(String(entry.item.target_id)))
|
|
315
|
+
.map(entry => ({
|
|
316
|
+
id: entry.id,
|
|
317
|
+
status: entry.status,
|
|
318
|
+
item: entry.item,
|
|
319
|
+
g: estimateRelationshipG(entry.item),
|
|
320
|
+
}))
|
|
321
|
+
.sort((left, right) => compareIds(left.id, right.id));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildTreeEdges(elementEntries, relationshipEntries) {
|
|
325
|
+
const entriesById = new Map(elementEntries.map(entry => [entry.id, entry]));
|
|
326
|
+
const edges = [];
|
|
327
|
+
const connectedElementIds = new Set();
|
|
328
|
+
|
|
329
|
+
for (const relationshipEntry of relationshipEntries) {
|
|
330
|
+
const source = entriesById.get(String(relationshipEntry.item.source_id));
|
|
331
|
+
const target = entriesById.get(String(relationshipEntry.item.target_id));
|
|
332
|
+
if (!source || !target) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const dependencyEdge = orientDependencyEdge(relationshipEntry.item, source, target);
|
|
336
|
+
if (!dependencyEdge) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const { parent, child } = dependencyEdge;
|
|
340
|
+
edges.push({
|
|
341
|
+
kind: 'relationship',
|
|
342
|
+
relationship: relationshipEntry,
|
|
343
|
+
parent,
|
|
344
|
+
child,
|
|
345
|
+
g: child.g,
|
|
346
|
+
});
|
|
347
|
+
connectedElementIds.add(parent.id);
|
|
348
|
+
connectedElementIds.add(child.id);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const root = elementEntries[0];
|
|
352
|
+
if (root) {
|
|
353
|
+
for (const entry of elementEntries) {
|
|
354
|
+
if (entry.id === root.id || connectedElementIds.has(entry.id)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
edges.push({
|
|
358
|
+
kind: 'impact',
|
|
359
|
+
parent: root,
|
|
360
|
+
child: entry,
|
|
361
|
+
g: entry.g,
|
|
362
|
+
});
|
|
363
|
+
connectedElementIds.add(entry.id);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return edges;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function orientDependencyEdge(relationship, source, target) {
|
|
371
|
+
const relationshipType = String(relationship.type || '');
|
|
372
|
+
if (DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET.has(relationshipType)) {
|
|
373
|
+
return { parent: source, child: target };
|
|
374
|
+
}
|
|
375
|
+
if (DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE.has(relationshipType)) {
|
|
376
|
+
return { parent: target, child: source };
|
|
377
|
+
}
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function buildPlantuml(elementEntries, relationshipEntries, treeEdges, totalG) {
|
|
382
|
+
const lines = [
|
|
383
|
+
'@startuml',
|
|
384
|
+
'title SystemArchitecture 当前 Git Diff 变化树',
|
|
385
|
+
'',
|
|
386
|
+
'top to bottom direction',
|
|
387
|
+
'hide stereotype',
|
|
388
|
+
'skinparam shadowing false',
|
|
389
|
+
'skinparam linetype ortho',
|
|
390
|
+
'skinparam defaultTextAlignment center',
|
|
391
|
+
'skinparam wrapWidth 120',
|
|
392
|
+
'skinparam ArrowFontSize 10',
|
|
393
|
+
'skinparam nodesep 25',
|
|
394
|
+
'skinparam ranksep 55',
|
|
395
|
+
'',
|
|
396
|
+
'skinparam rectangle {',
|
|
397
|
+
' RoundCorner 12',
|
|
398
|
+
' BackgroundColor<<added>> #F8BBD0',
|
|
399
|
+
' BorderColor<<added>> #AD1457',
|
|
400
|
+
' BackgroundColor<<affected>> #BBDEFB',
|
|
401
|
+
' BorderColor<<affected>> #1565C0',
|
|
402
|
+
' BackgroundColor<<context>> #FFF9C4',
|
|
403
|
+
' BorderColor<<context>> #F9A825',
|
|
404
|
+
'}',
|
|
405
|
+
'',
|
|
406
|
+
'legend right',
|
|
407
|
+
' |= 颜色 |= 含义 |',
|
|
408
|
+
' | <back:#F8BBD0>新增</back> | Git diff 新增元素/关系 |',
|
|
409
|
+
' | <back:#BBDEFB>影响</back> | Git diff 修改既有元素/关系 |',
|
|
410
|
+
' | <back:#FFF9C4>上下文</back> | 被变更关系引用的上下文元素 |',
|
|
411
|
+
` | 估算 | 总计 ${formatG(totalG)}G |`,
|
|
412
|
+
'endlegend',
|
|
413
|
+
'',
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
for (const entry of elementEntries) {
|
|
417
|
+
lines.push(`rectangle "${escapePlantuml(entry.item.name || entry.id)}\\nid=${entry.id}\\n${escapePlantuml(entry.item.type || 'Element')}\\n${entry.status === 'affected' ? '影响粒度' : entry.status === 'context' ? '上下文粒度' : '任务粒度'}=${formatG(entry.g)}G" as E${sanitizeAlias(entry.id)} <<${entry.status}>>`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
lines.push('');
|
|
421
|
+
|
|
422
|
+
for (const edge of treeEdges) {
|
|
423
|
+
const color = edge.kind === 'impact' ? '#1565C0' : edge.relationship.status === 'affected' ? '#1565C0' : '#AD1457';
|
|
424
|
+
const style = edge.kind === 'impact' ? ',dotted' : '';
|
|
425
|
+
const label = edge.kind === 'impact'
|
|
426
|
+
? `影响项\\n非图谱关系\\nG传递=${formatG(edge.g)}G`
|
|
427
|
+
: `${edge.relationship.id} ${escapePlantuml(edge.relationship.item.name || '')}\\n${escapePlantuml(edge.relationship.item.type || '')}\\nG传递=${formatG(edge.g)}G`;
|
|
428
|
+
lines.push(`E${sanitizeAlias(edge.parent.id)} -[${color}${style}]-> E${sanitizeAlias(edge.child.id)} : ${label}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
lines.push('', '@enduml');
|
|
432
|
+
return lines.join('\n');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function escapePlantuml(value) {
|
|
436
|
+
return String(value || '').replace(/"/g, '\\"').replace(/\r?\n/g, '\\n');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function sanitizeAlias(value) {
|
|
440
|
+
return String(value).replace(/[^A-Za-z0-9_]/g, '_');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function formatG(value) {
|
|
444
|
+
return Number(value).toFixed(1).replace(/\.0$/, '');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function buildTimestamp(date) {
|
|
448
|
+
const pad = value => String(value).padStart(2, '0');
|
|
449
|
+
return [
|
|
450
|
+
date.getFullYear(),
|
|
451
|
+
pad(date.getMonth() + 1),
|
|
452
|
+
pad(date.getDate()),
|
|
453
|
+
'-',
|
|
454
|
+
pad(date.getHours()),
|
|
455
|
+
pad(date.getMinutes()),
|
|
456
|
+
pad(date.getSeconds()),
|
|
457
|
+
].join('');
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (require.main === module) {
|
|
461
|
+
main();
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
module.exports = {
|
|
465
|
+
generateArchitectureDiffPlantuml,
|
|
466
|
+
};
|