archgraph-argo 0.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 (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +115 -0
  3. package/argo/package.json +8 -0
  4. package/argo/rules/intent-architecture-global-rule.md +45 -0
  5. package/argo/schema/ImplementationToCodingHandoff.schema.json +252 -0
  6. package/argo/schema/ImplementationToIntentTraceProposal.schema.json +180 -0
  7. package/argo/schema/IntentToImplementationHandoff.schema.json +75 -0
  8. package/argo/schema/SystemArchitecture.schema.json +378 -0
  9. package/argo/schema/archimate3.2.pdf +0 -0
  10. package/argo/scripts/ARCHITECTURE.md +57 -0
  11. package/argo/scripts/archimate32-rules.js +12301 -0
  12. package/argo/scripts/argo-mcp-server.js +629 -0
  13. package/argo/scripts/argo-paths.js +77 -0
  14. package/argo/scripts/ensureArgoHarnessEnvironment.js +340 -0
  15. package/argo/scripts/generateArchitectureDiffPlantuml.js +466 -0
  16. package/argo/scripts/graph-rag/ARCHITECTURE.md +192 -0
  17. package/argo/scripts/graph-rag/canonicalProjectionAuthority.js +45 -0
  18. package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +969 -0
  19. package/argo/scripts/graph-rag/embeddingQualificationGate.js +59 -0
  20. package/argo/scripts/graph-rag/externalProductionConfig.js +74 -0
  21. package/argo/scripts/graph-rag/liveEmbeddingIndexGate.js +129 -0
  22. package/argo/scripts/graph-rag/liveEmbeddingNeo4jBoundary.js +137 -0
  23. package/argo/scripts/graph-rag/liveEmbeddingProviderClient.js +49 -0
  24. package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +481 -0
  25. package/argo/scripts/graph-rag/mutationEmbeddingVectorLifecycle.js +1261 -0
  26. package/argo/scripts/graph-rag/neo4jNativeRetrieval.js +37 -0
  27. package/argo/scripts/graph-rag/productionGraphRagRuntime.js +1624 -0
  28. package/argo/scripts/graph-rag/semantic-persistence/ARCHITECTURE.md +51 -0
  29. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +241 -0
  30. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticCheckpointStore.js +99 -0
  31. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticNeo4jAdapter.js +149 -0
  32. package/argo/scripts/graph-rag/semantic-persistence/productionSemanticProjectionStore.js +171 -0
  33. package/argo/scripts/graph-rag/semanticOperatorError.js +38 -0
  34. package/argo/scripts/graph-rag/semanticOperatorJourney.js +459 -0
  35. package/argo/scripts/graph-rag/semanticReadinessAttestationStore.js +398 -0
  36. package/argo/scripts/graph-rag/systemMetadataCommandAdapter.js +269 -0
  37. package/argo/scripts/graph-semantics.js +220 -0
  38. package/argo/scripts/neo4j-system-architecture-store.js +777 -0
  39. package/argo/scripts/repositoryArgoEnvironment.js +101 -0
  40. package/argo/scripts/runArchitectureTests.js +583 -0
  41. package/argo/scripts/semanticOperatorJourneyCli.js +91 -0
  42. package/argo/scripts/syncSystemArchitectureToNeo4j.js +67 -0
  43. package/argo/scripts/systemarchitecture-mcp-server.js +2965 -0
  44. package/argo/scripts/test-executors/_template.js +58 -0
  45. package/argo/scripts/test-executors/default.js +199 -0
  46. package/argo/scripts/validateStageHandoff.js +459 -0
  47. package/argo/scripts/validateSystemArchitecture.js +254 -0
  48. package/argo/scripts/validateTraceProposal.js +181 -0
  49. package/argo/scripts/validator-mcp-server.js +377 -0
  50. package/argo/skills/argo-init/SKILL.md +110 -0
  51. package/bin/argo-deploy.js +12 -0
  52. package/install-argo.ps1 +112 -0
  53. package/package.json +28 -0
  54. package/vendor/neo4j-driver-6.2.0.tgz +0 -0
@@ -0,0 +1,101 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const {
5
+ getArgoEnvPath,
6
+ } = require('./argo-paths.js');
7
+
8
+ function loadRepositoryArgoEnvironment(workspaceRoot) {
9
+ const argoEnvPath = getArgoEnvPath();
10
+ const workspaceEnvPath = path.join(workspaceRoot, '.argo', '.env');
11
+ const result = {
12
+ status: 'missing',
13
+ path: normalizeRelativePath(path.relative(workspaceRoot, argoEnvPath)),
14
+ loadedBeforeProjection: true,
15
+ assignedCount: 0,
16
+ preservedProcessCount: 0,
17
+ sourcePaths: [],
18
+ };
19
+
20
+ const candidatePaths = [argoEnvPath];
21
+ if (workspaceEnvPath !== argoEnvPath) {
22
+ candidatePaths.push(workspaceEnvPath);
23
+ }
24
+
25
+ const merged = new Map();
26
+ const loadedPaths = [];
27
+ for (const envPath of candidatePaths) {
28
+ if (!fs.existsSync(envPath)) {
29
+ continue;
30
+ }
31
+ for (const [key, value] of parseRepositoryEnvFile(fs.readFileSync(envPath, 'utf8'))) {
32
+ merged.set(key, value);
33
+ }
34
+ loadedPaths.push(envPath);
35
+ }
36
+
37
+ if (loadedPaths.length === 0) {
38
+ return result;
39
+ }
40
+
41
+ result.status = 'loaded';
42
+ result.sourcePaths = loadedPaths.map(
43
+ envPath => normalizeRelativePath(path.relative(workspaceRoot, envPath)),
44
+ );
45
+
46
+ for (const [key, value] of merged) {
47
+ if (process.env[key] === undefined) {
48
+ process.env[key] = value;
49
+ result.assignedCount += 1;
50
+ } else {
51
+ result.preservedProcessCount += 1;
52
+ }
53
+ }
54
+
55
+ return result;
56
+ }
57
+
58
+ function parseRepositoryEnvFile(content) {
59
+ const entries = [];
60
+ for (const rawLine of String(content).split(/\r?\n/)) {
61
+ const line = rawLine.trim();
62
+ if (!line || line.startsWith('#')) {
63
+ continue;
64
+ }
65
+
66
+ const separatorIndex = line.indexOf('=');
67
+ if (separatorIndex <= 0) {
68
+ continue;
69
+ }
70
+
71
+ const key = line.slice(0, separatorIndex).trim();
72
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
73
+ continue;
74
+ }
75
+
76
+ const value = parseRepositoryEnvValue(line.slice(separatorIndex + 1).trim());
77
+ entries.push([key, value]);
78
+ }
79
+ return entries;
80
+ }
81
+
82
+ function parseRepositoryEnvValue(value) {
83
+ if (value.length >= 2) {
84
+ const quote = value[0];
85
+ if ((quote === '"' || quote === "'") && value[value.length - 1] === quote) {
86
+ const inner = value.slice(1, -1);
87
+ return quote === '"' ? inner.replace(/\\n/g, '\n').replace(/\\r/g, '\r') : inner;
88
+ }
89
+ }
90
+ const commentIndex = value.search(/\s#/);
91
+ return commentIndex >= 0 ? value.slice(0, commentIndex).trimEnd() : value;
92
+ }
93
+
94
+ function normalizeRelativePath(value) {
95
+ return String(value).replace(/\\/g, '/');
96
+ }
97
+
98
+ module.exports = {
99
+ loadRepositoryArgoEnvironment,
100
+ parseRepositoryEnvFile,
101
+ };
@@ -0,0 +1,583 @@
1
+ const { execFile } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { promisify } = require('util');
5
+
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ const {
9
+ getWorkspaceRoot,
10
+ } = require('./argo-paths.js');
11
+
12
+ const repoRoot = getWorkspaceRoot();
13
+ const PYTHON_EXECUTABLE = resolvePythonExecutable(repoRoot);
14
+ const DEFAULT_ARCHITECTURE_GRAPH_PATH = 'design/KG/SystemArchitecture.json';
15
+ const FAILURE_RECORDS_PATH = 'design/KG/test-failure-records.json';
16
+ const DEFAULT_TEST_TIMEOUT_MS = 60000000;
17
+ const TEST_TIMEOUT_MS = readPositiveInteger(process.env.ARGO_TEST_TIMEOUT_MS, DEFAULT_TEST_TIMEOUT_MS);
18
+ const TEST_EXECUTORS_DIR = path.join(__dirname, 'test-executors');
19
+ const DISALLOWED_ACCEPTANCE_CRITERIA_PATTERNS = [
20
+ /[\r\n]/,
21
+ /[|&;<>]/,
22
+ /^['"].*['"]$/,
23
+ /^(?:npm|pnpm|yarn|npx|node|python|py|powershell|pwsh|cmd|bash|sh)\b/i,
24
+ ];
25
+
26
+ // --- Test Executor Registry ---
27
+
28
+ /** @type {Array<{name:string, canHandle:(criteria:string, wsRoot:string)=>boolean, execute:(criteria:string, wsRoot:string)=>Promise<{exitCode:number|null, stdout:string, stderr:string}>, getCommandPreview?:(criteria:string, wsRoot:string)=>string|null}>} */
29
+ let executors = [];
30
+
31
+ function loadExecutors() {
32
+ if (executors.length > 0) return;
33
+
34
+ // Always load the built-in default executor first
35
+ const defaultExecutor = require('./test-executors/default.js');
36
+ executors.push(defaultExecutor);
37
+
38
+ // Discover additional executors from the test-executors directory
39
+ if (!fs.existsSync(TEST_EXECUTORS_DIR)) return;
40
+
41
+ const entries = fs.readdirSync(TEST_EXECUTORS_DIR);
42
+ for (const entry of entries) {
43
+ if (entry === 'default.js') continue; // already loaded
44
+ if (entry.startsWith('_') || entry.startsWith('.')) continue; // templates / hidden files
45
+ if (!entry.endsWith('.js') && !entry.endsWith('.cjs') && !entry.endsWith('.mjs')) continue;
46
+
47
+ try {
48
+ const mod = require(path.join(TEST_EXECUTORS_DIR, entry));
49
+ if (mod && typeof mod.canHandle === 'function' && typeof mod.execute === 'function') {
50
+ executors.push(mod);
51
+ console.log(`[EXECUTOR] Loaded custom executor: ${mod.name || entry}`);
52
+ }
53
+ } catch (err) {
54
+ console.error(`[EXECUTOR] Failed to load ${entry}: ${err.message}`);
55
+ }
56
+ }
57
+
58
+ // Sort: default executor last (fallback), custom executors first
59
+ executors.sort((a, b) => {
60
+ if (a.name === 'default') return 1;
61
+ if (b.name === 'default') return -1;
62
+ return 0;
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Find the first executor that can handle the given acceptanceCriteria.
68
+ * Returns null if no executor matches.
69
+ */
70
+ function findExecutor(acceptanceCriteria) {
71
+ loadExecutors();
72
+ for (const executor of executors) {
73
+ if (executor.canHandle(acceptanceCriteria, repoRoot)) {
74
+ return executor;
75
+ }
76
+ }
77
+ return null;
78
+ }
79
+
80
+ async function main() {
81
+ const architecturePath = normalizeRelativePath(process.argv[2] || DEFAULT_ARCHITECTURE_GRAPH_PATH);
82
+ let summary;
83
+ try {
84
+ summary = await runArchitectureTests(repoRoot, architecturePath);
85
+ } catch (error) {
86
+ console.error(`Argo architecture test execution failed: ${String(error && error.stack ? error.stack : error)}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ printSummary(summary);
91
+ if (summary.failedCount > 0) {
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ async function runArchitectureTests(workspaceRoot, architecturePath) {
97
+ const resolvedArchitecturePath = normalizeRelativePath(architecturePath || DEFAULT_ARCHITECTURE_GRAPH_PATH);
98
+ const graphPath = path.join(workspaceRoot, ...resolvedArchitecturePath.split('/'));
99
+ const graph = await readArchitectureGraph(graphPath);
100
+ const explicitTestcases = collectExplicitTestcases(graph);
101
+ const results = [];
102
+ const failureRecords = [];
103
+
104
+ for (const [index, testcase] of explicitTestcases.entries()) {
105
+ logTestcaseStart(index, explicitTestcases.length, testcase);
106
+ const resolvedScriptPath = testcase.acceptanceCriteria
107
+ ? normalizeRelativePath(testcase.acceptanceCriteria)
108
+ : '';
109
+
110
+ if (!testcase.acceptanceCriteria) {
111
+ const result = buildExecutionResult({
112
+ testcase,
113
+ resolvedScriptPath: '',
114
+ executionCommand: '',
115
+ status: 'missing-criteria',
116
+ exitCode: null,
117
+ durationMs: 0,
118
+ stdout: '',
119
+ stderr: 'acceptanceCriteria is empty',
120
+ });
121
+ results.push(result);
122
+ logTestcaseFinish(index, explicitTestcases.length, result);
123
+ failureRecords.push(toFailedTestRecord(result));
124
+ continue;
125
+ }
126
+
127
+ const validation = validateAcceptanceCriteria(resolvedScriptPath);
128
+ if (!validation.valid) {
129
+ const result = buildExecutionResult({
130
+ testcase,
131
+ resolvedScriptPath,
132
+ executionCommand: '',
133
+ status: 'invalid-criteria',
134
+ exitCode: null,
135
+ durationMs: 0,
136
+ stdout: '',
137
+ stderr: validation.reason || 'acceptanceCriteria must be a direct script file path',
138
+ });
139
+ results.push(result);
140
+ logTestcaseFinish(index, explicitTestcases.length, result);
141
+ failureRecords.push(toFailedTestRecord(result));
142
+ continue;
143
+ }
144
+
145
+ const executor = findExecutor(resolvedScriptPath);
146
+ if (!executor) {
147
+ const result = buildExecutionResult({
148
+ testcase,
149
+ resolvedScriptPath,
150
+ executionCommand: '',
151
+ status: 'invalid-criteria',
152
+ exitCode: null,
153
+ durationMs: 0,
154
+ stdout: '',
155
+ stderr: `no test executor can handle: ${resolvedScriptPath}`,
156
+ });
157
+ results.push(result);
158
+ logTestcaseFinish(index, explicitTestcases.length, result);
159
+ failureRecords.push(toFailedTestRecord(result));
160
+ continue;
161
+ }
162
+
163
+ const executionCommand = typeof executor.getCommandPreview === 'function'
164
+ ? executor.getCommandPreview(resolvedScriptPath, workspaceRoot)
165
+ : `[executor: ${executor.name || 'unknown'}] ${resolvedScriptPath}`;
166
+
167
+ const start = Date.now();
168
+ const execution = await executor.execute(resolvedScriptPath, workspaceRoot);
169
+ const passed = execution.exitCode === 0;
170
+ const result = buildExecutionResult({
171
+ testcase,
172
+ resolvedScriptPath,
173
+ executionCommand,
174
+ status: passed ? 'passed' : 'failed',
175
+ exitCode: execution.exitCode,
176
+ durationMs: Date.now() - start,
177
+ stdout: execution.stdout,
178
+ stderr: execution.stderr,
179
+ });
180
+ results.push(result);
181
+ logTestcaseFinish(index, explicitTestcases.length, result);
182
+ if (!passed) {
183
+ failureRecords.push(toFailedTestRecord(result));
184
+ }
185
+ }
186
+
187
+ await writeFailureRecords(workspaceRoot, failureRecords);
188
+
189
+ const deliveryChanges = refreshDeliveryStatus(graph, results);
190
+ if (deliveryChanges.length > 0) {
191
+ await writeArchitectureGraph(graphPath, graph);
192
+ console.log(`[DELIVERY] Refreshed delivery status: ${deliveryChanges.length} element(s) changed`);
193
+ for (const change of deliveryChanges) {
194
+ const direction = change.deliveryStatus === 'delivered' ? 'DELIVERED' : 'NOT_DELIVERED';
195
+ console.log(`[DELIVERY] ${change.id} "${change.name}" [${direction}] ${change.previousStatus || '(none)'} → ${change.deliveryStatus}`);
196
+ }
197
+ }
198
+
199
+ return {
200
+ architecturePath: resolvedArchitecturePath,
201
+ failureRecordsPath: FAILURE_RECORDS_PATH,
202
+ totalTestCases: explicitTestcases.length,
203
+ passedCount: results.filter(result => result.passed).length,
204
+ failedCount: failureRecords.length,
205
+ missingCriteriaCount: results.filter(result => result.status === 'missing-criteria').length,
206
+ deliveryChanges,
207
+ results,
208
+ failureRecords,
209
+ };
210
+ }
211
+
212
+ async function readArchitectureGraph(graphPath) {
213
+ try {
214
+ return JSON.parse(await fs.promises.readFile(graphPath, 'utf8'));
215
+ } catch (error) {
216
+ throw new Error(`Failed to read architecture graph: ${graphPath}. ${String(error)}`);
217
+ }
218
+ }
219
+
220
+ function buildExecutionResult(input) {
221
+ // Truncate stdout/stderr to bound memory: 32 tests × 4KB each = 128KB max.
222
+ // Error details are typically at the tail; keep the last portion.
223
+ const MAX_OUTPUT_CHARS = 4096;
224
+ const truncate = (s) => {
225
+ const str = String(s || '');
226
+ return str.length > MAX_OUTPUT_CHARS
227
+ ? '...(truncated)...\n' + str.slice(str.length - MAX_OUTPUT_CHARS)
228
+ : str;
229
+ };
230
+ return {
231
+ testcaseName: input.testcase.testcaseName,
232
+ testDescription: input.testcase.testDescription,
233
+ acceptanceCriteria: input.testcase.acceptanceCriteria,
234
+ elementId: input.testcase.elementId,
235
+ resolvedScriptPath: input.resolvedScriptPath,
236
+ executionCommand: input.executionCommand,
237
+ status: input.status,
238
+ passed: input.status === 'passed',
239
+ exitCode: input.exitCode,
240
+ durationMs: input.durationMs,
241
+ stdout: truncate(input.stdout),
242
+ stderr: truncate(input.stderr),
243
+ };
244
+ }
245
+
246
+ async function writeFailureRecords(workspaceRoot, records) {
247
+ const targetPath = path.join(workspaceRoot, ...FAILURE_RECORDS_PATH.split('/'));
248
+ await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
249
+ await fs.promises.writeFile(targetPath, JSON.stringify(records, null, 2) + '\n', 'utf8');
250
+ }
251
+
252
+ function toFailedTestRecord(result) {
253
+ return {
254
+ testcasename: result.testcaseName,
255
+ testdescription: result.testDescription,
256
+ acceptanceCriteria: result.acceptanceCriteria,
257
+ relatedIntentElementId: result.elementId,
258
+ status: result.status,
259
+ resolvedScriptPath: result.resolvedScriptPath,
260
+ executionCommand: result.executionCommand,
261
+ exitCode: result.exitCode,
262
+ failureError: buildFailureError(result),
263
+ stdout: result.stdout,
264
+ stderr: result.stderr,
265
+ };
266
+ }
267
+
268
+ function buildFailureError(result) {
269
+ const stderr = result.stderr.trim();
270
+ if (stderr) {
271
+ return stderr;
272
+ }
273
+ const stdout = result.stdout.trim();
274
+ if (stdout) {
275
+ return stdout;
276
+ }
277
+ if (result.exitCode !== null) {
278
+ return `Command exited with code ${result.exitCode}`;
279
+ }
280
+ return `Test status: ${result.status}`;
281
+ }
282
+
283
+ function resolvePythonExecutable(workspaceRoot) {
284
+ const candidates = process.platform === 'win32'
285
+ ? [
286
+ path.join(workspaceRoot, '.venv', 'Scripts', 'python.exe'),
287
+ path.join(workspaceRoot, 'venv', 'Scripts', 'python.exe'),
288
+ ]
289
+ : [
290
+ path.join(workspaceRoot, '.venv', 'bin', 'python'),
291
+ path.join(workspaceRoot, 'venv', 'bin', 'python'),
292
+ ];
293
+
294
+ for (const candidate of candidates) {
295
+ if (fs.existsSync(candidate)) {
296
+ return candidate;
297
+ }
298
+ }
299
+
300
+ return 'python';
301
+ }
302
+
303
+ async function runCommand(command, args, cwd) {
304
+ try {
305
+ const { stdout, stderr } = await execFileAsync(command, args, {
306
+ cwd,
307
+ windowsHide: true,
308
+ maxBuffer: 1024 * 1024 * 10,
309
+ timeout: TEST_TIMEOUT_MS,
310
+ });
311
+ return {
312
+ exitCode: 0,
313
+ stdout: stdout.trim(),
314
+ stderr: stderr.trim(),
315
+ };
316
+ } catch (error) {
317
+ const timedOut = error && (error.killed || error.signal === 'SIGTERM' || error.code === 'ETIMEDOUT');
318
+ return {
319
+ exitCode: typeof error.code === 'number' ? error.code : 1,
320
+ stdout: String(error.stdout || '').trim(),
321
+ stderr: timedOut
322
+ ? `Command timed out after ${TEST_TIMEOUT_MS}ms: ${[command, ...args].join(' ')}`
323
+ : String(error.stderr || error.message || error).trim(),
324
+ };
325
+ }
326
+ }
327
+
328
+ function validateAcceptanceCriteria(value) {
329
+ if (!value) {
330
+ return { valid: false, reason: 'acceptanceCriteria is empty' };
331
+ }
332
+
333
+ for (const pattern of DISALLOWED_ACCEPTANCE_CRITERIA_PATTERNS) {
334
+ if (pattern.test(value)) {
335
+ return {
336
+ valid: false,
337
+ reason: 'acceptanceCriteria must be a single workspace-relative test entry only, without extra command wrappers or arguments',
338
+ };
339
+ }
340
+ }
341
+
342
+ // Format-specific validation is delegated to test executors via canHandle().
343
+ // If no executor matches, the test loop reports 'invalid-criteria'.
344
+ return { valid: true };
345
+ }
346
+
347
+ function collectExplicitTestcases(graph) {
348
+ const testcases = [];
349
+ for (const element of graph.elements || []) {
350
+ const elementId = String(element.id || '');
351
+ for (const testcase of element.testcases || []) {
352
+ testcases.push({
353
+ elementId,
354
+ testcaseName: String(testcase.name || ''),
355
+ testDescription: String(testcase.description || ''),
356
+ acceptanceCriteria: String(testcase.acceptanceCriteria || '').trim(),
357
+ });
358
+ }
359
+ }
360
+ return testcases;
361
+ }
362
+
363
+ function normalizeRelativePath(value) {
364
+ return String(value).replace(/\\/g, '/').replace(/^\.\//, '').trim();
365
+ }
366
+
367
+ function readPositiveInteger(value, fallback) {
368
+ const parsed = Number.parseInt(String(value || ''), 10);
369
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
370
+ }
371
+
372
+ function logTestcaseStart(index, total, testcase) {
373
+ const label = formatTestcaseLabel(index, total, testcase.testcaseName);
374
+ console.log(`[START] ${label}`);
375
+ console.log(` script: ${testcase.acceptanceCriteria || '(missing acceptanceCriteria)'}`);
376
+ }
377
+
378
+ function logTestcaseFinish(index, total, result) {
379
+ const label = formatTestcaseLabel(index, total, result.testcaseName);
380
+ const exitCode = result.exitCode === null ? 'n/a' : String(result.exitCode);
381
+ console.log(`[END] ${label}`);
382
+ console.log(` result: ${result.status}; exitCode=${exitCode}; durationMs=${result.durationMs}`);
383
+ console.log(` command: ${result.executionCommand || '(n/a)'}`);
384
+ if (result.stderr) {
385
+ console.log(` stderr: ${truncateSingleLine(result.stderr)}`);
386
+ }
387
+ console.log(`[PROGRESS] ${JSON.stringify(buildProgressPayload(index, total, result))}`);
388
+ }
389
+
390
+ function buildProgressPayload(index, total, result) {
391
+ return {
392
+ index,
393
+ total,
394
+ };
395
+ }
396
+
397
+ function formatTestcaseLabel(index, total, testcaseName) {
398
+ return `[${index + 1}/${total}] ${testcaseName || '(unnamed testcase)'}`;
399
+ }
400
+
401
+ function truncateSingleLine(value) {
402
+ const singleLine = String(value).replace(/\s+/g, ' ').trim();
403
+ return singleLine.length > 240 ? `${singleLine.slice(0, 237)}...` : singleLine;
404
+ }
405
+
406
+ function printSummary(summary) {
407
+ console.log(`Argo architecture tests from: ${summary.architecturePath}`);
408
+ console.log(`Failure records: ${summary.failureRecordsPath}`);
409
+ console.log(`Total: ${summary.totalTestCases}; Passed: ${summary.passedCount}; Failed or missing: ${summary.failedCount}; Missing acceptanceCriteria: ${summary.missingCriteriaCount}`);
410
+ for (const result of summary.results) {
411
+ const exitCode = result.exitCode === null ? 'n/a' : String(result.exitCode);
412
+ console.log(`- ${result.testcaseName || '(unnamed testcase)'}: ${result.status} | ${result.resolvedScriptPath || '(missing)'} | ${result.executionCommand || '(n/a)'} | exitCode: ${exitCode}`);
413
+ }
414
+ }
415
+
416
+ async function writeArchitectureGraph(graphPath, graph) {
417
+ await fs.promises.writeFile(graphPath, JSON.stringify(graph, null, 2) + '\n', 'utf8');
418
+ }
419
+
420
+ // --- Delivery Status Refresh (hard guardrail: computed by test runner, not by agents) ---
421
+
422
+ /**
423
+ * Dependency direction for delivery:
424
+ * For element X, its upstream dependencies = elements X needs to be delivered first.
425
+ * Mirrors resolveSemanticEdges from systemarchitecture-mcp-server.js.
426
+ *
427
+ * - Access, Assignment, Specialization, Composition, Aggregation: source depends on target
428
+ * - Serving, Realization, Flow, Triggering, Influence: target depends on source
429
+ */
430
+ const DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET = new Set(['Access', 'Assignment', 'Specialization', 'Composition', 'Aggregation']);
431
+ const DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE = new Set(['Serving', 'Realization', 'Flow', 'Triggering', 'Influence']);
432
+
433
+ /**
434
+ * Resolve upstream dependencies for a single element.
435
+ * Returns the set of element IDs that this element depends on.
436
+ */
437
+ function resolveUpstreamDependencies(elementId, relationships) {
438
+ const dependencies = new Set();
439
+ for (const rel of relationships || []) {
440
+ const sourceId = String(rel.source_id || rel.source || '');
441
+ const targetId = String(rel.target_id || rel.target || '');
442
+ const relType = String(rel.type || '');
443
+
444
+ if (elementId === sourceId && elementId === targetId) continue;
445
+
446
+ if (DEPENDENCY_TYPES_SOURCE_DEPENDS_ON_TARGET.has(relType) && elementId === sourceId) {
447
+ dependencies.add(targetId);
448
+ continue;
449
+ }
450
+
451
+ if (DEPENDENCY_TYPES_TARGET_DEPENDS_ON_SOURCE.has(relType) && elementId === targetId) {
452
+ dependencies.add(sourceId);
453
+ }
454
+ }
455
+ return dependencies;
456
+ }
457
+
458
+ /**
459
+ * Refresh delivery status for elements with mounted testcases based on test results and dependency topology.
460
+ * An element with mounted testcases is:
461
+ * - "delivered" when:
462
+ * 1. It has at least one testcase AND all its testcases passed
463
+ * 2. All its upstream dependencies are also "delivered"
464
+ * - "not_delivered" otherwise
465
+ *
466
+ * Elements without mounted testcases are left untouched: no new deliveryStatus
467
+ * is added, and any existing status remains as-is.
468
+ * Returns the list of elements whose delivery status changed (additions and regressions).
469
+ */
470
+ function refreshDeliveryStatus(graph, testResults) {
471
+ if (!graph || !graph.elements) return [];
472
+
473
+ // Build test-results map: elementId → { allPassed, hasTestcases }
474
+ const testResultByElement = new Map();
475
+ for (const result of testResults) {
476
+ const eid = String(result.elementId || '');
477
+ if (!testResultByElement.has(eid)) {
478
+ testResultByElement.set(eid, { allPassed: true, hasTestcases: false });
479
+ }
480
+ const entry = testResultByElement.get(eid);
481
+ entry.hasTestcases = true;
482
+ if (!result.passed) {
483
+ entry.allPassed = false;
484
+ }
485
+ }
486
+
487
+ // Build upstream dependency map for all elements
488
+ const upstreamDeps = new Map();
489
+ for (const element of graph.elements) {
490
+ const eid = String(element.id || '');
491
+ upstreamDeps.set(eid, resolveUpstreamDependencies(eid, graph.relationships));
492
+ }
493
+
494
+ // Record previous delivery status, then strip only mounted-testcase elements.
495
+ // Untested architectural scaffolding is not marked by this runner.
496
+ const previousStatus = new Map();
497
+ for (const element of graph.elements) {
498
+ const eid = String(element.id || '');
499
+ const attr = (element.attributes || []).find(a => a.name === 'deliveryStatus');
500
+ previousStatus.set(eid, attr ? attr.value : '');
501
+ const testInfo = testResultByElement.get(eid);
502
+ if (testInfo && testInfo.hasTestcases && element.attributes) {
503
+ element.attributes = element.attributes.filter(a => a.name !== 'deliveryStatus');
504
+ }
505
+ }
506
+
507
+ // Fresh delivery status map — tested elements start as not_delivered.
508
+ // Untested elements preserve their previous status for reporting/dependency
509
+ // bookkeeping but do not block dependents.
510
+ const deliveryStatus = new Map();
511
+ for (const element of graph.elements) {
512
+ const eid = String(element.id || '');
513
+ const testInfo = testResultByElement.get(eid);
514
+ deliveryStatus.set(eid, testInfo && testInfo.hasTestcases ? 'not_delivered' : (previousStatus.get(eid) || ''));
515
+ }
516
+
517
+ // Iterate to fixed point: mark elements whose tests pass AND whose upstream deps are delivered
518
+ // Fixed-point iteration: an element becomes 'delivered' when its own tests pass
519
+ // AND all its upstream dependencies are already 'delivered'. We must guard against
520
+ // re-processing already-delivered elements; otherwise the loop never terminates
521
+ // (every iteration re-enters the marking block for delivered elements, sets
522
+ // changed=true, and pushes duplicate deliveryStatus attributes — OOM on 32 tests).
523
+ let changed = true;
524
+ while (changed) {
525
+ changed = false;
526
+ for (const element of graph.elements) {
527
+ const eid = String(element.id || '');
528
+
529
+ // Skip elements already marked as delivered in a previous iteration
530
+ if (deliveryStatus.get(eid) === 'delivered') continue;
531
+
532
+ const testInfo = testResultByElement.get(eid);
533
+ // Only mark elements that have testcases.
534
+ if (!testInfo || !testInfo.hasTestcases) continue;
535
+ if (!testInfo.allPassed) continue;
536
+
537
+ // Check upstream deps: only those that have testcases block delivery.
538
+ // Elements without testcases (e.g. architectural scaffolding) cannot
539
+ // be marked delivered themselves, so they should not block dependents.
540
+ const deps = upstreamDeps.get(eid) || new Set();
541
+ let allRelevantDepsDelivered = true;
542
+ for (const depId of deps) {
543
+ const depInfo = testResultByElement.get(depId);
544
+ if (depInfo && depInfo.hasTestcases && deliveryStatus.get(depId) !== 'delivered') {
545
+ allRelevantDepsDelivered = false;
546
+ break;
547
+ }
548
+ }
549
+ if (!allRelevantDepsDelivered) continue;
550
+
551
+ deliveryStatus.set(eid, 'delivered');
552
+ changed = true;
553
+ }
554
+ }
555
+
556
+ // Persist final status for every mounted-testcase element.
557
+ for (const element of graph.elements) {
558
+ const eid = String(element.id || '');
559
+ const testInfo = testResultByElement.get(eid);
560
+ if (!testInfo || !testInfo.hasTestcases) continue;
561
+
562
+ if (!element.attributes) element.attributes = [];
563
+ element.attributes.push({ name: 'deliveryStatus', value: deliveryStatus.get(eid) || 'not_delivered' });
564
+ }
565
+
566
+ // Compute changes for mounted-testcase elements only.
567
+ const changes = [];
568
+ for (const element of graph.elements) {
569
+ const eid = String(element.id || '');
570
+ const testInfo = testResultByElement.get(eid);
571
+ if (!testInfo || !testInfo.hasTestcases) continue;
572
+
573
+ const prev = previousStatus.get(eid) || '';
574
+ const curr = deliveryStatus.get(eid) || 'not_delivered';
575
+ if (prev !== curr) {
576
+ changes.push({ id: eid, name: element.name, previousStatus: prev, deliveryStatus: curr });
577
+ }
578
+ }
579
+
580
+ return changes;
581
+ }
582
+
583
+ main();