archgraph-argo 0.20.4 → 0.20.6
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/argo/scripts/argo-mcp-server.js +13 -0
- package/argo/scripts/ensureArgoHarnessEnvironment.js +7 -1
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +5 -0
- package/argo/scripts/graph-rag/mcpCrashDiagnostics.js +112 -0
- package/argo/scripts/graph-rag/semanticAlignmentLock.js +76 -0
- package/argo/scripts/graph-rag/semanticAlignmentRunner.js +14 -1
- package/argo/scripts/systemarchitecture-mcp-server.js +37 -28
- package/package.json +1 -1
|
@@ -369,6 +369,13 @@ function resolveWorkspaceRoot(args) {
|
|
|
369
369
|
|
|
370
370
|
async function callTool(name, args = {}, progressToken = null, dependencies = undefined) {
|
|
371
371
|
loadRepositoryArgoEnvironment(resolveWorkspaceRoot(args));
|
|
372
|
+
try {
|
|
373
|
+
const crash = require('./graph-rag/mcpCrashDiagnostics.js');
|
|
374
|
+
crash.installCrashDiagnostics(resolveWorkspaceRoot(args));
|
|
375
|
+
crash.markPhase('tool:' + name);
|
|
376
|
+
} catch {
|
|
377
|
+
// diagnostics are best-effort; never block a tool call
|
|
378
|
+
}
|
|
372
379
|
if (name === 'initializeWorkspace') {
|
|
373
380
|
const workspace = await initializeWorkspace(resolveWorkspaceRoot(args));
|
|
374
381
|
// Deterministic argo-init harness report (Neo4j structural sync, semantic
|
|
@@ -777,6 +784,12 @@ async function main() {
|
|
|
777
784
|
// server starts, rebuild the index in the BACKGROUND (async, logged) so the
|
|
778
785
|
// first query rarely pays the multi-second reconstruction. No-op when aligned.
|
|
779
786
|
if (process.env.ARGO_REPO_ROOT && process.env.ARGO_REPO_ROOT.trim() !== '') {
|
|
787
|
+
try {
|
|
788
|
+
require('./graph-rag/mcpCrashDiagnostics.js')
|
|
789
|
+
.installCrashDiagnostics(process.env.ARGO_REPO_ROOT);
|
|
790
|
+
} catch {
|
|
791
|
+
// diagnostics are best-effort; never block server startup
|
|
792
|
+
}
|
|
780
793
|
try {
|
|
781
794
|
require('./graph-rag/semanticAlignmentRunner.js')
|
|
782
795
|
.preheatSemanticAlignment(process.env.ARGO_REPO_ROOT);
|
|
@@ -38,7 +38,13 @@ const REQUIRED_TOOL_NAMES = [
|
|
|
38
38
|
// `includeBootstrap` is true for the standalone CLI (it calls initializeWorkspace
|
|
39
39
|
// to bootstrap the workspace); the initializeWorkspace MCP tool passes false
|
|
40
40
|
// because it already bootstrapped — avoids re-entrancy through ensureWorkspaceBootstrap.
|
|
41
|
-
async function buildHarnessReport(
|
|
41
|
+
async function buildHarnessReport(options = {}) {
|
|
42
|
+
const { checkOnly = false, workspaceRoot, includeBootstrap = true } = options;
|
|
43
|
+
const { withAlignmentLock } = require('./graph-rag/semanticAlignmentLock.js');
|
|
44
|
+
return withAlignmentLock(workspaceRoot, () => runHarnessReport({ checkOnly, workspaceRoot, includeBootstrap }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runHarnessReport({ checkOnly, workspaceRoot, includeBootstrap }) {
|
|
42
48
|
const reportPath = path.join(workspaceRoot, '.argo', 'temp', 'argo-harness-init-report.json');
|
|
43
49
|
const report = {
|
|
44
50
|
status: 'ok',
|
|
@@ -32,6 +32,9 @@ const {
|
|
|
32
32
|
const {
|
|
33
33
|
runSemanticAlignment,
|
|
34
34
|
} = require('./semanticAlignmentRunner.js');
|
|
35
|
+
const {
|
|
36
|
+
markPhase,
|
|
37
|
+
} = require('./mcpCrashDiagnostics.js');
|
|
35
38
|
|
|
36
39
|
const APPROVED_SOURCE_KEYS = Object.freeze([
|
|
37
40
|
'ARGO_EMBEDDING_BASE_URL',
|
|
@@ -233,6 +236,7 @@ async function executeWpP2Retrieval({
|
|
|
233
236
|
configuration: configurationEvidence.configuration,
|
|
234
237
|
transport: composition.transport,
|
|
235
238
|
});
|
|
239
|
+
markPhase('retrieval:embed');
|
|
236
240
|
const vector = await provider.embed(request.intent);
|
|
237
241
|
requireQualifiedVector(vector);
|
|
238
242
|
const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
|
|
@@ -272,6 +276,7 @@ async function executeWpP2Retrieval({
|
|
|
272
276
|
channelSeeds.push({ channel, seeds });
|
|
273
277
|
}
|
|
274
278
|
if (rerank) {
|
|
279
|
+
markPhase('retrieval:rerank');
|
|
275
280
|
// Rerank every channel CONCURRENTLY: the LLM calls dominate latency and are
|
|
276
281
|
// independent, so parallelizing turns the cost from sum(channels) into
|
|
277
282
|
// ~one call. fail-open: a null/empty order keeps the original ordering.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Crash diagnostics for the ARGO MCP server. A native abort (libuv assertion) or
|
|
4
|
+
// an uncaught error currently kills the server with no trace in the host log.
|
|
5
|
+
// This module:
|
|
6
|
+
// - persists a "phase breadcrumb" synchronously on every phase change, so the
|
|
7
|
+
// LAST phase before a crash is always recoverable (even for native aborts
|
|
8
|
+
// that never fire JS handlers);
|
|
9
|
+
// - appends uncaught-exception (incl. unhandled-rejection, which Node raises as
|
|
10
|
+
// an uncaught exception) and process-exit records to a per-workspace crash
|
|
11
|
+
// log.
|
|
12
|
+
// It uses `uncaughtExceptionMonitor` (which logs WITHOUT changing Node's default
|
|
13
|
+
// crash behaviour) and never logs secret values.
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
|
|
18
|
+
let installed = false;
|
|
19
|
+
let crashLogPath = null;
|
|
20
|
+
let phasePath = null;
|
|
21
|
+
let currentPhase = 'startup';
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
|
|
24
|
+
function tempPath(workspaceRoot, name) {
|
|
25
|
+
return path.join(workspaceRoot, '.argo', 'temp', name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function crashLogFilePath() {
|
|
29
|
+
return crashLogPath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function appendCrash(entry) {
|
|
33
|
+
if (!crashLogPath) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
|
|
38
|
+
const error = entry && entry.error;
|
|
39
|
+
const line = JSON.stringify({
|
|
40
|
+
at: new Date().toISOString(),
|
|
41
|
+
pid: process.pid,
|
|
42
|
+
uptimeMs: Date.now() - startedAt,
|
|
43
|
+
phase: currentPhase,
|
|
44
|
+
kind: entry && entry.kind,
|
|
45
|
+
...(entry && entry.origin ? { origin: entry.origin } : {}),
|
|
46
|
+
...(entry && entry.code !== undefined ? { code: entry.code } : {}),
|
|
47
|
+
...(entry && entry.signal !== undefined ? { signal: entry.signal } : {}),
|
|
48
|
+
...(error ? {
|
|
49
|
+
category: error.category,
|
|
50
|
+
message: String(error.message || error).slice(0, 1000),
|
|
51
|
+
stack: String(error.stack || '').slice(0, 4000),
|
|
52
|
+
} : {}),
|
|
53
|
+
}) + '\n';
|
|
54
|
+
fs.appendFileSync(crashLogPath, line, 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
// best-effort diagnostics only; never throw from the crash path
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function installCrashDiagnostics(workspaceRoot) {
|
|
61
|
+
if (workspaceRoot) {
|
|
62
|
+
crashLogPath = tempPath(workspaceRoot, 'mcp-crash.log');
|
|
63
|
+
phasePath = tempPath(workspaceRoot, 'mcp-phase.json');
|
|
64
|
+
markPhase(currentPhase);
|
|
65
|
+
}
|
|
66
|
+
if (installed) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
installed = true;
|
|
70
|
+
// Logs without altering Node's default crash behaviour (unlike a bare
|
|
71
|
+
// 'uncaughtException' listener, which would swallow the crash).
|
|
72
|
+
process.on('uncaughtExceptionMonitor', (error, origin) => {
|
|
73
|
+
appendCrash({ kind: 'uncaughtException', origin, error });
|
|
74
|
+
});
|
|
75
|
+
process.on('exit', (code, signal) => {
|
|
76
|
+
appendCrash({ kind: 'exit', code: code === undefined ? null : code, signal: signal === undefined ? null : signal });
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Synchronous breadcrumb: guarantees the last phase survives a hard abort.
|
|
81
|
+
function markPhase(phase) {
|
|
82
|
+
currentPhase = String(phase === undefined || phase === null ? '' : phase);
|
|
83
|
+
if (!phasePath) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
fs.mkdirSync(path.dirname(phasePath), { recursive: true });
|
|
88
|
+
fs.writeFileSync(phasePath, JSON.stringify({
|
|
89
|
+
phase: currentPhase,
|
|
90
|
+
at: new Date().toISOString(),
|
|
91
|
+
pid: process.pid,
|
|
92
|
+
}) + '\n', 'utf8');
|
|
93
|
+
} catch {
|
|
94
|
+
// best-effort
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readLastPhase(workspaceRoot) {
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(fs.readFileSync(tempPath(workspaceRoot, 'mcp-phase.json'), 'utf8'));
|
|
101
|
+
return parsed && typeof parsed.phase === 'string' ? parsed.phase : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
installCrashDiagnostics,
|
|
109
|
+
markPhase,
|
|
110
|
+
readLastPhase,
|
|
111
|
+
crashLogFilePath,
|
|
112
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Cross-process, single-flight lock for the heavy semantic-index alignment
|
|
4
|
+
// (workspace bootstrap + semantic backfill + Neo4j sync + EA projection). Any
|
|
5
|
+
// invocation that can rebuild the index -- the MCP startup preheat, the
|
|
6
|
+
// query-path auto-alignment, and an explicit `argo init` (initializeWorkspace),
|
|
7
|
+
// in this process or another -- acquires this lock, so they serialize instead of
|
|
8
|
+
// racing (duplicate embeddings, concurrent Neo4j upserts, concurrent .qea
|
|
9
|
+
// rebuild, readiness/report races).
|
|
10
|
+
//
|
|
11
|
+
// The lock is a regular file created with `wx` under <workspace>/.argo/temp. A
|
|
12
|
+
// holder that crashes leaves a stale file; a lock older than `staleMs` is stolen.
|
|
13
|
+
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
|
|
17
|
+
const LOCK_RELATIVE_PATH = path.join('.argo', 'temp', 'semantic-alignment.lock');
|
|
18
|
+
const DEFAULT_WAIT_MS = 10 * 60 * 1000;
|
|
19
|
+
const DEFAULT_STALE_MS = 15 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
function alignmentLockPath(repositoryRoot) {
|
|
22
|
+
return path.join(repositoryRoot, ...LOCK_RELATIVE_PATH.split(path.sep));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sleep(ms) {
|
|
26
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function withAlignmentLock(repositoryRoot, action, options = {}) {
|
|
30
|
+
const waitMs = Number.isFinite(options.waitMs) && options.waitMs > 0 ? options.waitMs : DEFAULT_WAIT_MS;
|
|
31
|
+
const staleMs = Number.isFinite(options.staleMs) && options.staleMs > 0 ? options.staleMs : DEFAULT_STALE_MS;
|
|
32
|
+
const file = alignmentLockPath(repositoryRoot);
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
const deadline = Date.now() + waitMs;
|
|
35
|
+
let fd = null;
|
|
36
|
+
for (;;) {
|
|
37
|
+
try {
|
|
38
|
+
fd = fs.openSync(file, 'wx');
|
|
39
|
+
break;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (!error || error.code !== 'EEXIST') {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
let stale = false;
|
|
45
|
+
try {
|
|
46
|
+
stale = (Date.now() - fs.statSync(file).mtimeMs) > staleMs;
|
|
47
|
+
} catch {
|
|
48
|
+
stale = true;
|
|
49
|
+
}
|
|
50
|
+
if (stale) {
|
|
51
|
+
try { fs.unlinkSync(file); } catch { /* raced with another stealer */ }
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (Date.now() >= deadline) {
|
|
55
|
+
const timeout = new Error('SEMANTIC_ALIGNMENT_LOCK_TIMEOUT');
|
|
56
|
+
timeout.category = 'SEMANTIC_ALIGNMENT_LOCK_TIMEOUT';
|
|
57
|
+
timeout.message = 'Another semantic alignment is already running and did not finish in time.';
|
|
58
|
+
timeout.action = 'Wait for the running alignment (argo init) to finish, then retry.';
|
|
59
|
+
throw timeout;
|
|
60
|
+
}
|
|
61
|
+
await sleep(250);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
try { fs.writeSync(fd, String(process.pid)); } catch { /* best-effort owner marker */ }
|
|
66
|
+
return await action();
|
|
67
|
+
} finally {
|
|
68
|
+
try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
69
|
+
try { fs.unlinkSync(file); } catch { /* already gone */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
withAlignmentLock,
|
|
75
|
+
alignmentLockPath,
|
|
76
|
+
};
|
|
@@ -34,6 +34,18 @@ function isSemanticReady(repositoryRoot) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// A project is "initialized" once its canonical graph exists. The startup
|
|
38
|
+
// preheat must NEVER bootstrap a brand-new project: creating files / running a
|
|
39
|
+
// full backfill for an unknown workspace in the background is surprising and
|
|
40
|
+
// would race an explicit `argo init`. New projects initialize explicitly.
|
|
41
|
+
function isProjectInitialized(repositoryRoot) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.existsSync(path.join(repositoryRoot, 'design', 'KG', 'SystemArchitecture.json'));
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
function alignmentError() {
|
|
38
50
|
const error = new Error('SEMANTIC_AUTO_ALIGNMENT_FAILED');
|
|
39
51
|
error.category = 'SEMANTIC_AUTO_ALIGNMENT_FAILED';
|
|
@@ -79,7 +91,7 @@ function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
|
|
|
79
91
|
// otherwise it starts the rebuild in the background so the first query rarely
|
|
80
92
|
// pays for it. Safe to call repeatedly (guarded + de-duplicated).
|
|
81
93
|
function preheatSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
|
|
82
|
-
if (preheated || !repositoryRoot || isSemanticReady(repositoryRoot)) {
|
|
94
|
+
if (preheated || !repositoryRoot || !isProjectInitialized(repositoryRoot) || isSemanticReady(repositoryRoot)) {
|
|
83
95
|
return;
|
|
84
96
|
}
|
|
85
97
|
preheated = true;
|
|
@@ -91,5 +103,6 @@ module.exports = {
|
|
|
91
103
|
runSemanticAlignment,
|
|
92
104
|
preheatSemanticAlignment,
|
|
93
105
|
isSemanticReady,
|
|
106
|
+
isProjectInitialized,
|
|
94
107
|
readinessRecordPath,
|
|
95
108
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
-
const { spawn } = require('node:child_process');
|
|
3
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
4
|
+
const { markPhase } = require('./graph-rag/mcpCrashDiagnostics.js');
|
|
4
5
|
const readline = require('node:readline');
|
|
5
6
|
const crypto = require('node:crypto');
|
|
6
7
|
|
|
@@ -1768,6 +1769,7 @@ function removeEntries(existing, removals) {
|
|
|
1768
1769
|
}
|
|
1769
1770
|
|
|
1770
1771
|
async function buildMutationResult(context, mutations, write, dependencies) {
|
|
1772
|
+
markPhase('mutation:' + (write ? 'apply' : 'preview'));
|
|
1771
1773
|
const beforeSummary = summarizeDocument(context.document);
|
|
1772
1774
|
let mutationResult;
|
|
1773
1775
|
try {
|
|
@@ -1855,6 +1857,7 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1855
1857
|
// but ALWAYS reported on the result (passed / failed / noop+reason) so a missing EA
|
|
1856
1858
|
// update is never silent.
|
|
1857
1859
|
{
|
|
1860
|
+
markPhase('mutation:qeaProjection');
|
|
1858
1861
|
const resolved = resolveQeaProjectionTarget(context);
|
|
1859
1862
|
const qeaTarget = resolved && resolved.target;
|
|
1860
1863
|
if (qeaTarget) {
|
|
@@ -1883,6 +1886,7 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1883
1886
|
}
|
|
1884
1887
|
|
|
1885
1888
|
if (shouldSyncCanonicalGraphToNeo4j(context.graphPath.relativePath)) {
|
|
1889
|
+
markPhase('mutation:neo4jSync');
|
|
1886
1890
|
try {
|
|
1887
1891
|
const syncResult = await syncArchitectureToNeo4j({
|
|
1888
1892
|
architecturePath: context.graphPath.relativePath,
|
|
@@ -1907,7 +1911,9 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1907
1911
|
}
|
|
1908
1912
|
}
|
|
1909
1913
|
|
|
1914
|
+
markPhase('mutation:embeddingLifecycle');
|
|
1910
1915
|
await attachMutationEmbeddingLifecycle(context, result, mutationResult.document);
|
|
1916
|
+
markPhase('mutation:done');
|
|
1911
1917
|
|
|
1912
1918
|
return result;
|
|
1913
1919
|
}
|
|
@@ -2127,33 +2133,35 @@ function resolveQeaProjectionTarget(context) {
|
|
|
2127
2133
|
}
|
|
2128
2134
|
|
|
2129
2135
|
function runQeaProjection(target) {
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
}
|
|
2156
|
-
})
|
|
2136
|
+
// Synchronous child: this runs right before the async Neo4j sync + embedding
|
|
2137
|
+
// lifecycle. An async child leaves a libuv handle closing while subsequent
|
|
2138
|
+
// async I/O (fetch / neo4j-driver) proceeds, which on Windows can trip a
|
|
2139
|
+
// native `UV_HANDLE_CLOSING` assertion and abort the whole MCP. spawnSync has
|
|
2140
|
+
// no lingering handle (~150ms, bounded); the caller's `await` still applies.
|
|
2141
|
+
const script = path.join(__dirname, 'ea-qea-sync.js');
|
|
2142
|
+
const started = Date.now();
|
|
2143
|
+
if (!fs.existsSync(script)) {
|
|
2144
|
+
return { ok: false, error: 'argo/scripts/ea-qea-sync.js missing', ms: 0 };
|
|
2145
|
+
}
|
|
2146
|
+
const snapshotDir = path.join(target.workspaceRoot, '.argo', 'temp', 'qea-backups');
|
|
2147
|
+
// -y enables the projection-owned delete reconcile: objects that carry a schema anchor
|
|
2148
|
+
// (t_object.Alias / t_connectortag schema_id) but are no longer in canonical are removed
|
|
2149
|
+
// from the .qea, so a graph-side deletion actually disappears from EA on the next
|
|
2150
|
+
// projection. Human-drawn (un-anchored) content is never a delete candidate.
|
|
2151
|
+
const args = [script, '--mode', 'sync', '--graph', target.graphPath, '--qea', target.qeaPath, '--snapshot-dir', snapshotDir, '-y'];
|
|
2152
|
+
try {
|
|
2153
|
+
const res = spawnSync(process.execPath, args, { cwd: target.workspaceRoot, windowsHide: true, encoding: 'utf8', timeout: 120000 });
|
|
2154
|
+
const ok = res.status === 0;
|
|
2155
|
+
return {
|
|
2156
|
+
ok,
|
|
2157
|
+
code: res.status,
|
|
2158
|
+
ms: Date.now() - started,
|
|
2159
|
+
stderr: String(res.stderr || '').slice(0, 600),
|
|
2160
|
+
...(ok ? {} : { error: String((res.stderr || '').slice(0, 600) || ('exit ' + res.status)) }),
|
|
2161
|
+
};
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
return { ok: false, error: String(error && error.message ? error.message : error), ms: Date.now() - started };
|
|
2164
|
+
}
|
|
2157
2165
|
}
|
|
2158
2166
|
|
|
2159
2167
|
function writeGraph(graphPath, document) {
|
|
@@ -2764,6 +2772,7 @@ function evaluateSemanticDedupGate(advisory, mutations) {
|
|
|
2764
2772
|
}
|
|
2765
2773
|
|
|
2766
2774
|
async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
2775
|
+
markPhase('mutation:semanticDedup');
|
|
2767
2776
|
if (process.env.ARGO_MCP_SEMANTIC_DEDUP === '0') {
|
|
2768
2777
|
return undefined;
|
|
2769
2778
|
}
|
package/package.json
CHANGED