archgraph-argo 0.20.1 → 0.20.3

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.
@@ -240,12 +240,12 @@ async function executeWpP2Retrieval({
240
240
  const lexicalTopK = hybridTopK();
241
241
  const fusionK = rrfK();
242
242
  const fusionWeights = hybridWeights();
243
- const rerank = isRerankEnabled();
243
+ const rerank = isRerankEnabled() && request.rerank !== false;
244
244
  const rerankOptions = rerankConfig();
245
245
  const rerankProvider = rerank ? resolveRerankProvider(configurationEvidence.configuration) : null;
246
246
  // Rerank needs a larger candidate pool than the final top-K.
247
247
  const pool = rerank ? Math.max(topK, rerankOptions.poolSize) : topK;
248
- const seedsByType = {};
248
+ const channelSeeds = [];
249
249
  for (const channel of CHANNELS) {
250
250
  const vectorSeeds = await exhaustChannel({
251
251
  channel,
@@ -266,17 +266,27 @@ async function executeWpP2Retrieval({
266
266
  });
267
267
  seeds = fuseChannelSeeds({ vectorSeeds, lexicalSeeds, k: fusionK, limit: Math.max(pool, lexicalTopK), weights: fusionWeights });
268
268
  }
269
- if (rerank && seeds.length > 1) {
270
- const ordered = await rerankCandidates({
271
- query: request.intent,
272
- candidates: seeds,
273
- provider: rerankProvider,
274
- transport: composition.transport,
275
- maxReturn: rerankOptions.maxReturn,
276
- });
277
- // fail-open: a null/empty order keeps the original ordering
278
- seeds = applyRerankOrder(seeds, ordered, topK);
279
- }
269
+ channelSeeds.push({ channel, seeds });
270
+ }
271
+ if (rerank) {
272
+ // Rerank every channel CONCURRENTLY: the LLM calls dominate latency and are
273
+ // independent, so parallelizing turns the cost from sum(channels) into
274
+ // ~one call. fail-open: a null/empty order keeps the original ordering.
275
+ const rerankedSeeds = await Promise.all(channelSeeds.map(({ seeds }) => (
276
+ seeds.length > 1
277
+ ? rerankCandidates({
278
+ query: request.intent,
279
+ candidates: seeds,
280
+ provider: rerankProvider,
281
+ transport: composition.transport,
282
+ maxReturn: rerankOptions.maxReturn,
283
+ }).then(ordered => applyRerankOrder(seeds, ordered, topK))
284
+ : seeds
285
+ )));
286
+ channelSeeds.forEach((entry, index) => { entry.seeds = rerankedSeeds[index]; });
287
+ }
288
+ const seedsByType = {};
289
+ for (const { channel, seeds } of channelSeeds) {
280
290
  seedsByType[channel.key] = seeds;
281
291
  }
282
292
  return completeSemanticResult({
@@ -835,15 +845,26 @@ function scopeCanonicalIdentitiesForChannel(identities, channel) {
835
845
  });
836
846
  }
837
847
 
848
+ // Semantic vector records store their canonicalIdentity channel-prefixed
849
+ // (e.g. "Element:widget-1") so the scoped Cypher `node.canonicalIdentity IN
850
+ // $canonicalIdentities` matches. Every consumer of a seed's `id` — purpose
851
+ // closure anchors, provenance keys, and the canonical-subset score lookup —
852
+ // however keys on the BARE canonical graph id. Expose the bare id as `id` and
853
+ // keep the prefixed value as `canonicalIdentity` so a seed resolves against the
854
+ // canonical graph instead of a phantom prefixed id.
855
+ function bareCanonicalId(id) {
856
+ return String(id).replace(/^[^:]*:/, '');
857
+ }
858
+
838
859
  function normalizeVectorRecord(raw, channel) {
839
860
  if (!raw || typeof raw !== 'object') return undefined;
840
- const id = raw.canonicalIdentity || raw.id || raw.objectId;
861
+ const canonicalIdentity = raw.canonicalIdentity || raw.id || raw.objectId;
841
862
  const score = numberValue(raw.score);
842
- if (!id || !Number.isFinite(score)) return undefined;
863
+ if (!canonicalIdentity || !Number.isFinite(score)) return undefined;
843
864
  return {
844
865
  ...raw,
845
- id,
846
- canonicalIdentity: id,
866
+ id: bareCanonicalId(canonicalIdentity),
867
+ canonicalIdentity,
847
868
  objectType: channel.objectType,
848
869
  channel: channel.channel,
849
870
  score,
@@ -1157,5 +1178,7 @@ module.exports = {
1157
1178
  auditThresholdFor,
1158
1179
  resolveTopK,
1159
1180
  scopeCanonicalIdentitiesForChannel,
1181
+ bareCanonicalId,
1182
+ normalizeVectorRecord,
1160
1183
  AUDIT_PURPOSES,
1161
1184
  };
@@ -6,9 +6,18 @@
6
6
  // the caller keeps the original ordering. Pure helpers are exported for tests.
7
7
 
8
8
  const DEFAULT_RERANK_MODEL = 'qwen-turbo';
9
+ // Candidate pool size. The pool is the recall CEILING: a target the seed stage
10
+ // ranks outside the pool can never be recovered by rerank, so it must NOT be
11
+ // shrunk for speed. Keep the original 20. With DeepSeek thinking disabled a
12
+ // single rerank call is ~flat across candidate counts (8-40 all ~1s), so a
13
+ // larger pool costs ~nothing -- never trade recall for latency here.
9
14
  const DEFAULT_RERANK_POOL = 20;
10
15
  const DEFAULT_RERANK_RETURN = 8;
11
- const DEFAULT_RERANK_TIMEOUT_MS = 8000;
16
+ // Per-call timeout. Channel reranks run concurrently, so the end-to-end rerank
17
+ // cost is ~one timeout, not N. 3.5s keeps the whole semantic query under ~5s
18
+ // while still letting typical calls (1-4s) complete; slower calls fail open to
19
+ // the pre-rerank (fused) order. Overridable via ARGO_SEMANTIC_RERANK_TIMEOUT_MS.
20
+ const DEFAULT_RERANK_TIMEOUT_MS = 3500;
12
21
 
13
22
  function rerankTimeoutMs(env = process.env) {
14
23
  const value = Number(env && env.ARGO_SEMANTIC_RERANK_TIMEOUT_MS);
@@ -110,6 +119,11 @@ async function rerankCandidates({ query, candidates, provider, transport, maxRet
110
119
  model,
111
120
  temperature: 0,
112
121
  response_format: { type: 'json_object' },
122
+ // DeepSeek's default is a "thinking" model: for a listwise ranking it spends
123
+ // 5k-10k reasoning tokens per call (measured 6-12s, highly variable) with no
124
+ // accuracy gain. Disable thinking for a fast, deterministic rerank
125
+ // (deepseek-flash: ~6s -> ~0.7s, reasoning tokens -> 0).
126
+ ...(provider.provider === 'deepseek' ? { thinking: { type: 'disabled' } } : {}),
113
127
  messages: [
114
128
  { role: 'system', content: 'You rank architecture elements by relevance to a query. Return ONLY JSON {"order":[ids best-first]} using only the candidate ids.' },
115
129
  { role: 'user', content: `Query: ${query}\n\nCandidates (id\\ttext):\n${list.map(candidate => `${candidate.id}\t${candidateText(candidate)}`).join('\n')}\n\nReturn up to ${Math.min(maxReturn || DEFAULT_RERANK_RETURN, list.length)} ids best-first.` },
@@ -2801,7 +2801,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2801
2801
  const intent = [element.type, element.name, element.description]
2802
2802
  .filter(part => typeof part === 'string' && part.trim() !== '')
2803
2803
  .join(' ');
2804
- const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K });
2804
+ const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K, rerank: false });
2805
2805
  const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
2806
2806
  const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
2807
2807
  const elements = subset && subset.status === 'passed' && subset.document
@@ -3462,7 +3462,11 @@ function buildCanonicalSemanticDocumentSubset(source, canonicalDocument = undefi
3462
3462
  if (!item) continue;
3463
3463
  const numericScore = Number(item.semanticScore);
3464
3464
  if (!Number.isFinite(numericScore)) continue;
3465
- const key = String(item.id !== undefined ? item.id : item.view_id);
3465
+ // Evidence ids may still carry the channel prefix (real Neo4j shape); the
3466
+ // seed-score map and canonical lookups are keyed on the bare canonical id,
3467
+ // so normalize before comparing (otherwise the rank-derived score lands on
3468
+ // a phantom prefixed key and the tiny fused RRF score wins for the element).
3469
+ const key = String(item.id !== undefined ? item.id : item.view_id).replace(/^[^:]*:/, '');
3466
3470
  if (key && (!semanticScoreById.has(key) || numericScore > semanticScoreById.get(key))) {
3467
3471
  semanticScoreById.set(key, numericScore);
3468
3472
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
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": {