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.
- package/LICENSE +201 -0
- package/README.md +115 -0
- package/argo/package.json +8 -0
- package/argo/rules/intent-architecture-global-rule.md +45 -0
- package/argo/schema/ImplementationToCodingHandoff.schema.json +252 -0
- package/argo/schema/ImplementationToIntentTraceProposal.schema.json +180 -0
- package/argo/schema/IntentToImplementationHandoff.schema.json +75 -0
- package/argo/schema/SystemArchitecture.schema.json +378 -0
- package/argo/schema/archimate3.2.pdf +0 -0
- package/argo/scripts/ARCHITECTURE.md +57 -0
- package/argo/scripts/archimate32-rules.js +12301 -0
- package/argo/scripts/argo-mcp-server.js +629 -0
- package/argo/scripts/argo-paths.js +77 -0
- package/argo/scripts/ensureArgoHarnessEnvironment.js +340 -0
- package/argo/scripts/generateArchitectureDiffPlantuml.js +466 -0
- package/argo/scripts/graph-rag/ARCHITECTURE.md +192 -0
- package/argo/scripts/graph-rag/canonicalProjectionAuthority.js +45 -0
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +969 -0
- package/argo/scripts/graph-rag/embeddingQualificationGate.js +59 -0
- package/argo/scripts/graph-rag/externalProductionConfig.js +74 -0
- package/argo/scripts/graph-rag/liveEmbeddingIndexGate.js +129 -0
- package/argo/scripts/graph-rag/liveEmbeddingNeo4jBoundary.js +137 -0
- package/argo/scripts/graph-rag/liveEmbeddingProviderClient.js +49 -0
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +481 -0
- package/argo/scripts/graph-rag/mutationEmbeddingVectorLifecycle.js +1261 -0
- package/argo/scripts/graph-rag/neo4jNativeRetrieval.js +37 -0
- package/argo/scripts/graph-rag/productionGraphRagRuntime.js +1624 -0
- package/argo/scripts/graph-rag/semantic-persistence/ARCHITECTURE.md +51 -0
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +241 -0
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticCheckpointStore.js +99 -0
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticNeo4jAdapter.js +149 -0
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticProjectionStore.js +171 -0
- package/argo/scripts/graph-rag/semanticOperatorError.js +38 -0
- package/argo/scripts/graph-rag/semanticOperatorJourney.js +459 -0
- package/argo/scripts/graph-rag/semanticReadinessAttestationStore.js +398 -0
- package/argo/scripts/graph-rag/systemMetadataCommandAdapter.js +269 -0
- package/argo/scripts/graph-semantics.js +220 -0
- package/argo/scripts/neo4j-system-architecture-store.js +777 -0
- package/argo/scripts/repositoryArgoEnvironment.js +101 -0
- package/argo/scripts/runArchitectureTests.js +583 -0
- package/argo/scripts/semanticOperatorJourneyCli.js +91 -0
- package/argo/scripts/syncSystemArchitectureToNeo4j.js +67 -0
- package/argo/scripts/systemarchitecture-mcp-server.js +2965 -0
- package/argo/scripts/test-executors/_template.js +58 -0
- package/argo/scripts/test-executors/default.js +199 -0
- package/argo/scripts/validateStageHandoff.js +459 -0
- package/argo/scripts/validateSystemArchitecture.js +254 -0
- package/argo/scripts/validateTraceProposal.js +181 -0
- package/argo/scripts/validator-mcp-server.js +377 -0
- package/argo/skills/argo-init/SKILL.md +110 -0
- package/bin/argo-deploy.js +12 -0
- package/install-argo.ps1 +112 -0
- package/package.json +28 -0
- 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();
|