archgraph-argo 0.20.7 → 0.20.9

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.
@@ -154,65 +154,83 @@ function createDefaultSemanticRetrieval(dependencies = {}) {
154
154
  return Object.freeze({
155
155
  async retrieve(request = {}) {
156
156
  const composition = await resolveRetrievalComposition(dependencies);
157
- const activeTestComposition = testCompositionStorage.getStore();
158
- const activeReadinessBoundary = activeTestComposition
159
- && activeTestComposition.useReadinessBoundary !== true
160
- ? undefined
161
- : readinessBoundary;
162
- let configurationEvidence = await composition.resolveConfiguration();
163
- let evidence = await readAndEvaluatePersistentReadiness(
164
- composition,
165
- canonicalGraph,
166
- activeReadinessBoundary,
167
- );
168
- if (!evidence.alignment.aligned) {
169
- await attemptAutomaticAlignment({
170
- composition,
171
- request,
172
- alignment: evidence.alignment,
173
- });
174
- evidence = await readAndEvaluatePersistentReadiness(
157
+ try {
158
+ const activeTestComposition = testCompositionStorage.getStore();
159
+ const activeReadinessBoundary = activeTestComposition
160
+ && activeTestComposition.useReadinessBoundary !== true
161
+ ? undefined
162
+ : readinessBoundary;
163
+ let configurationEvidence = await composition.resolveConfiguration();
164
+ let evidence = await readAndEvaluatePersistentReadiness(
175
165
  composition,
176
166
  canonicalGraph,
177
167
  activeReadinessBoundary,
178
168
  );
179
169
  if (!evidence.alignment.aligned) {
180
- throw semanticAutomaticAlignmentFailed(evidence.alignment);
170
+ await attemptAutomaticAlignment({
171
+ composition,
172
+ request,
173
+ alignment: evidence.alignment,
174
+ });
175
+ evidence = await readAndEvaluatePersistentReadiness(
176
+ composition,
177
+ canonicalGraph,
178
+ activeReadinessBoundary,
179
+ );
180
+ if (!evidence.alignment.aligned) {
181
+ throw semanticAutomaticAlignmentFailed(evidence.alignment);
182
+ }
183
+ }
184
+ return await executeWpP2Retrieval({
185
+ composition,
186
+ request,
187
+ canonicalGraph,
188
+ readiness: evidence.readiness,
189
+ configurationEvidence,
190
+ });
191
+ } finally {
192
+ if (typeof composition.dispose === 'function') {
193
+ await composition.dispose();
181
194
  }
182
195
  }
183
- return executeWpP2Retrieval({
184
- composition,
185
- request,
186
- canonicalGraph,
187
- readiness: evidence.readiness,
188
- configurationEvidence,
189
- });
190
196
  },
191
197
  async probeQueryability(request = {}, readiness = {}) {
192
198
  const composition = await resolveRetrievalComposition(dependencies);
193
- const configurationEvidence = await composition.resolveConfiguration();
194
- return executeWpP2Retrieval({
195
- composition,
196
- request,
197
- canonicalGraph,
198
- readiness,
199
- configurationEvidence,
200
- });
199
+ try {
200
+ const configurationEvidence = await composition.resolveConfiguration();
201
+ return await executeWpP2Retrieval({
202
+ composition,
203
+ request,
204
+ canonicalGraph,
205
+ readiness,
206
+ configurationEvidence,
207
+ });
208
+ } finally {
209
+ if (typeof composition.dispose === 'function') {
210
+ await composition.dispose();
211
+ }
212
+ }
201
213
  },
202
214
  async readReadiness() {
203
215
  const composition = await resolveRetrievalComposition(dependencies);
204
- const activeTestComposition = testCompositionStorage.getStore();
205
- const activeReadinessBoundary = activeTestComposition
206
- && activeTestComposition.useReadinessBoundary !== true
207
- ? undefined
208
- : readinessBoundary;
209
- await composition.resolveConfiguration();
210
- const evidence = await readAndEvaluatePersistentReadiness(
211
- composition,
212
- canonicalGraph,
213
- activeReadinessBoundary,
214
- );
215
- return publicReadinessOutcome(evidence.alignment);
216
+ try {
217
+ const activeTestComposition = testCompositionStorage.getStore();
218
+ const activeReadinessBoundary = activeTestComposition
219
+ && activeTestComposition.useReadinessBoundary !== true
220
+ ? undefined
221
+ : readinessBoundary;
222
+ await composition.resolveConfiguration();
223
+ const evidence = await readAndEvaluatePersistentReadiness(
224
+ composition,
225
+ canonicalGraph,
226
+ activeReadinessBoundary,
227
+ );
228
+ return publicReadinessOutcome(evidence.alignment);
229
+ } finally {
230
+ if (typeof composition.dispose === 'function') {
231
+ await composition.dispose();
232
+ }
233
+ }
216
234
  },
217
235
  });
218
236
  }
@@ -244,7 +262,7 @@ async function executeWpP2Retrieval({
244
262
  const strict = AUDIT_PURPOSES.has(purpose);
245
263
  const topK = Number.isInteger(request.topK) && request.topK > 0 ? request.topK : resolveTopK();
246
264
  const scoped = Array.isArray(canonicalIdentities) && canonicalIdentities.length > 0;
247
- const hybrid = isHybridEnabled();
265
+ const hybrid = isHybridEnabled() && request.hybrid !== false;
248
266
  const lexicalTopK = hybridTopK();
249
267
  const fusionK = rrfK();
250
268
  const fusionWeights = hybridWeights();
@@ -333,6 +351,28 @@ async function createProductionComposition(dependencies) {
333
351
  const repositoryRoot = dependencies.repositoryRoot
334
352
  || getWorkspaceRoot();
335
353
  let configurationEvidence;
354
+ let neo4jHandle = null;
355
+ async function disposeNeo4j() {
356
+ const handle = neo4jHandle;
357
+ neo4jHandle = null;
358
+ if (!handle) return;
359
+ try { await handle.session.close(); } catch { /* ignore */ }
360
+ try { await handle.driver.close(); } catch { /* ignore */ }
361
+ }
362
+ function ensureNeo4jSession() {
363
+ if (neo4jHandle) return neo4jHandle.session;
364
+ const configuration = configurationEvidence.configuration;
365
+ const neo4j = require('neo4j-driver');
366
+ const driver = neo4j.driver(
367
+ configuration.neo4jDatabaseUrl,
368
+ neo4j.auth.basic(configuration.neo4jDatabaseUsername, configuration.neo4jDatabasePassword),
369
+ );
370
+ const session = driver.session(configuration.neo4jDatabase === undefined
371
+ ? undefined
372
+ : { database: configuration.neo4jDatabase });
373
+ neo4jHandle = { driver, session };
374
+ return session;
375
+ }
336
376
  return Object.freeze({
337
377
  async resolveConfiguration() {
338
378
  configurationEvidence = await resolveApprovedLiveConfiguration({
@@ -354,59 +394,61 @@ async function createProductionComposition(dependencies) {
354
394
  if (!configurationEvidence) {
355
395
  throw safeError('EXTERNAL_CREDENTIALS_REQUIRED');
356
396
  }
357
- return executeProductionNeo4jOperation(configurationEvidence.configuration, operation);
397
+ if (operation && operation.kind === 'semantic-auto-alignment-attempt') {
398
+ return runScriptOwnedSemanticAlignment(operation);
399
+ }
400
+ try {
401
+ return await runOperationOnSession(ensureNeo4jSession(), operation);
402
+ } catch (error) {
403
+ // A failed run may leave the session unusable; drop the handle so the
404
+ // rest of this retrieval (or the next one) reconnects cleanly.
405
+ await disposeNeo4j();
406
+ throw error;
407
+ }
358
408
  },
359
409
  }),
410
+ async dispose() {
411
+ await disposeNeo4j();
412
+ },
360
413
  });
361
414
  }
362
415
 
363
- async function executeProductionNeo4jOperation(configuration, operation) {
364
- if (operation && operation.kind === 'semantic-auto-alignment-attempt') {
365
- return runScriptOwnedSemanticAlignment(operation);
416
+ // Pure operation runner: runs one Cypher operation on a caller-owned Neo4j
417
+ // session and maps the result. The session/driver lifecycle lives in the
418
+ // production composition (ONE handle per retrieval, reused across every vector
419
+ // window, then closed) -- correctly reused without leaving a lingering handle,
420
+ // which would keep short-lived processes alive. Creating AND closing a driver
421
+ // per operation caused the rapid handle churn that intermittently aborted the
422
+ // MCP with a native libuv assertion on Windows (confirmed crash phase:
423
+ // retrieval:vector-window:ArchitectureRelationship).
424
+ async function runOperationOnSession(session, operation) {
425
+ const result = await session.run(operation.cypher, operation.parameters);
426
+ if (operation.kind === 'semantic-readiness-read') {
427
+ const readiness = result.records[0] && result.records[0].get('readiness');
428
+ return { records: readiness ? [readiness] : [] };
366
429
  }
367
- const neo4j = require('neo4j-driver');
368
- const driver = neo4j.driver(
369
- configuration.neo4jDatabaseUrl,
370
- neo4j.auth.basic(
371
- configuration.neo4jDatabaseUsername,
372
- configuration.neo4jDatabasePassword,
373
- ),
374
- );
375
- const session = driver.session(configuration.neo4jDatabase === undefined
376
- ? undefined
377
- : { database: configuration.neo4jDatabase });
378
- try {
379
- const result = await session.run(operation.cypher, operation.parameters);
380
- if (operation.kind === 'semantic-readiness-read') {
381
- const readiness = result.records[0] && result.records[0].get('readiness');
382
- return { records: readiness ? [readiness] : [] };
383
- }
384
- const records = result.records.map(record => ({
385
- ...record.get('record'),
386
- score: numberValue(record.get('score')),
387
- }));
388
- if (operation.kind === 'semantic-lexical-query') {
389
- return { records };
390
- }
391
- const offset = operation.parameters.offset;
392
- const windowSize = operation.parameters.windowSize;
393
- const returnedCount = Math.max(0, records.length - offset);
394
- const hasMore = records.length === operation.parameters.topK;
395
- return {
396
- records,
397
- windowEvidence: {
398
- offset,
399
- windowSize,
400
- returnedCount,
401
- hasMore,
402
- nextOffset: hasMore ? operation.parameters.topK : null,
403
- windowExhausted: !hasMore,
404
- },
405
- };
406
- } finally {
407
- await session.close();
408
- await driver.close();
430
+ const records = result.records.map(record => ({
431
+ ...record.get('record'),
432
+ score: numberValue(record.get('score')),
433
+ }));
434
+ if (operation.kind === 'semantic-lexical-query') {
435
+ return { records };
409
436
  }
437
+ const offset = operation.parameters.offset;
438
+ const windowSize = operation.parameters.windowSize;
439
+ const returnedCount = Math.max(0, records.length - offset);
440
+ const hasMore = records.length === operation.parameters.topK;
441
+ return {
442
+ records,
443
+ windowEvidence: {
444
+ offset,
445
+ windowSize,
446
+ returnedCount,
447
+ hasMore,
448
+ nextOffset: hasMore ? operation.parameters.topK : null,
449
+ windowExhausted: !hasMore,
450
+ },
451
+ };
410
452
  }
411
453
 
412
454
  async function attemptAutomaticAlignment({ composition, request, alignment }) {
@@ -46,6 +46,18 @@ function isProjectInitialized(repositoryRoot) {
46
46
  }
47
47
  }
48
48
 
49
+ // A readiness record proves the workspace was initialized (argo init) before.
50
+ // The preheat only RECONCILES a previously-initialized-but-now-stale workspace;
51
+ // a never-aligned workspace (brand-new project, or a synthetic test copy) must
52
+ // be initialized explicitly, never rebuilt in the background.
53
+ function hasReadinessRecord(repositoryRoot) {
54
+ try {
55
+ return fs.existsSync(readinessRecordPath(repositoryRoot));
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
49
61
  function alignmentError() {
50
62
  const error = new Error('SEMANTIC_AUTO_ALIGNMENT_FAILED');
51
63
  error.category = 'SEMANTIC_AUTO_ALIGNMENT_FAILED';
@@ -57,7 +69,7 @@ function alignmentError() {
57
69
 
58
70
  // Run the alignment (once). Returns a promise resolving to { status: 'aligned' }
59
71
  // or rejecting with the SEMANTIC_AUTO_ALIGNMENT_FAILED envelope.
60
- function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
72
+ function runSemanticAlignment(repositoryRoot = getWorkspaceRoot(), options = {}) {
61
73
  if (inFlight) {
62
74
  return inFlight;
63
75
  }
@@ -70,6 +82,11 @@ function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
70
82
  env: process.env,
71
83
  stdio: ['ignore', 'ignore', 'inherit'],
72
84
  });
85
+ if (options.unref && typeof child.unref === 'function') {
86
+ // Background preheat must NOT keep the MCP process alive (a spawnSync-driven
87
+ // caller waits for the server to exit; an attached child would hang it).
88
+ child.unref();
89
+ }
73
90
  const finish = (failed, cause) => {
74
91
  inFlight = null;
75
92
  const ms = Date.now() - startedAt;
@@ -94,9 +111,14 @@ function preheatSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
94
111
  if (preheated || !repositoryRoot || !isProjectInitialized(repositoryRoot) || isSemanticReady(repositoryRoot)) {
95
112
  return;
96
113
  }
114
+ // Only reconcile a workspace that was initialized before (a readiness record
115
+ // exists); never rebuild a never-aligned project in the background.
116
+ if (!hasReadinessRecord(repositoryRoot)) {
117
+ return;
118
+ }
97
119
  preheated = true;
98
120
  console.error('[argo] readiness not aligned at startup; preheating semantic alignment in background…');
99
- runSemanticAlignment(repositoryRoot).catch(() => {});
121
+ runSemanticAlignment(repositoryRoot, { unref: true }).catch(() => {});
100
122
  }
101
123
 
102
124
  module.exports = {
@@ -104,5 +126,6 @@ module.exports = {
104
126
  preheatSemanticAlignment,
105
127
  isSemanticReady,
106
128
  isProjectInitialized,
129
+ hasReadinessRecord,
107
130
  readinessRecordPath,
108
131
  };
@@ -2810,7 +2810,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2810
2810
  const intent = [element.type, element.name, element.description]
2811
2811
  .filter(part => typeof part === 'string' && part.trim() !== '')
2812
2812
  .join(' ');
2813
- const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K, rerank: false });
2813
+ const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K, rerank: false, hybrid: false, scoreMode: 'similarity' });
2814
2814
  const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
2815
2815
  const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
2816
2816
  const elements = subset && subset.status === 'passed' && subset.document
@@ -3001,7 +3001,11 @@ async function executeSemanticSystemArchitectureQuery(args, dependencies) {
3001
3001
  try {
3002
3002
  const retrieved = await semanticRetrievalBoundary.retrieve(queryForRetrieval);
3003
3003
  if (canonicalSubsetContract) {
3004
- const subset = buildCanonicalSemanticDocumentSubset(retrieved, context.document);
3004
+ // scoreMode:'similarity' (used by the write-path dedup gate) returns the
3005
+ // true vector similarity instead of the closure rank-derived score.
3006
+ const subset = buildCanonicalSemanticDocumentSubset(retrieved, context.document, {
3007
+ preferSeedSimilarity: !!(query && query.scoreMode === 'similarity'),
3008
+ });
3005
3009
  if (subset.status === 'failed') {
3006
3010
  return getSystemArchitectureResult(subset);
3007
3011
  }
@@ -3306,7 +3310,7 @@ function normalizeFailedSemanticResponse(payload, fallbackResponse) {
3306
3310
  });
3307
3311
  }
3308
3312
 
3309
- function buildCanonicalSemanticDocumentSubset(source, canonicalDocument = undefined) {
3313
+ function buildCanonicalSemanticDocumentSubset(source, canonicalDocument = undefined, options = {}) {
3310
3314
  const evidence = source && typeof source === 'object' ? source : {};
3311
3315
  const endpointClosureRelationships = arrayAt(evidence, ['endpointClosure', 'relationships']);
3312
3316
  const viewClosureViews = arrayAt(evidence, ['viewClosure', 'views']);
@@ -3464,10 +3468,12 @@ function buildCanonicalSemanticDocumentSubset(source, canonicalDocument = undefi
3464
3468
  }
3465
3469
  }
3466
3470
  }
3467
- // buildCanonicalSemanticDocumentSubset may run a second time over a prior
3468
- // subset document (which no longer carries seedsByType); carry scores forward
3469
- // from evidence elements that already expose semanticScore.
3470
- for (const item of evidenceElements) {
3471
+ // The closure's rank-derived score (0.99..0.8, by closure rank) must NOT
3472
+ // overwrite a seed's TRUE similarity: when preferSeedSimilarity is set the
3473
+ // caller needs the cosine, so this override is skipped (otherwise the dedup
3474
+ // gate would flag every same-type seed in the top window regardless of actual
3475
+ // similarity -- the false-positive defect).
3476
+ if (!options.preferSeedSimilarity) for (const item of evidenceElements) {
3471
3477
  if (!item) continue;
3472
3478
  const numericScore = Number(item.semanticScore);
3473
3479
  if (!Number.isFinite(numericScore)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.20.7",
3
+ "version": "0.20.9",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {