archgraph-argo 0.20.3 → 0.20.4
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/.env.example +11 -1
- package/argo/scripts/argo-mcp-server.js +11 -0
- package/argo/scripts/graph-rag/ARCHITECTURE.md +1 -1
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +7 -17
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +2 -0
- package/argo/scripts/graph-rag/rerankRetrieval.js +53 -16
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +56 -1
- package/argo/scripts/graph-rag/semanticAlignmentRunner.js +95 -0
- package/package.json +1 -1
package/argo/.env.example
CHANGED
|
@@ -89,7 +89,7 @@ ARGO_SEMANTIC_RERANK_MODEL=
|
|
|
89
89
|
ARGO_SEMANTIC_RERANK_POOL=
|
|
90
90
|
# Max ids the reranker may return. Default 8.
|
|
91
91
|
ARGO_SEMANTIC_RERANK_RETURN=
|
|
92
|
-
# Per-request rerank timeout in ms (AbortController). Default
|
|
92
|
+
# Per-request rerank timeout in ms (AbortController). Default 3500.
|
|
93
93
|
ARGO_SEMANTIC_RERANK_TIMEOUT_MS=
|
|
94
94
|
# Dedicated rerank provider (optional). When unset, rerank falls back to the
|
|
95
95
|
# embedding provider above. Use these to point rerank at another provider/model
|
|
@@ -101,6 +101,16 @@ ARGO_RERANK_API_KEY=
|
|
|
101
101
|
ARGO_RERANK_PROVIDER=
|
|
102
102
|
# Dedicated rerank model id (overrides ARGO_SEMANTIC_RERANK_MODEL).
|
|
103
103
|
ARGO_RERANK_MODEL=
|
|
104
|
+
# Disable the rerank model's hidden "thinking"/reasoning (reasoning models spend
|
|
105
|
+
# 6-12s per call for a listwise ranking with no accuracy gain). Default 1 =
|
|
106
|
+
# disabled; set 0 to send nothing. Applies to every provider so a model swap
|
|
107
|
+
# keeps the fast path. If a provider rejects the fragment it is retried without.
|
|
108
|
+
ARGO_RERANK_DISABLE_THINKING=
|
|
109
|
+
# Exact JSON body fragment merged into the rerank request to disable thinking
|
|
110
|
+
# (default {"thinking":{"type":"disabled"}}). Override for another model/provider
|
|
111
|
+
# that uses a different field, e.g. {"reasoning_effort":"none"} or
|
|
112
|
+
# {"enable_thinking":false}.
|
|
113
|
+
ARGO_RERANK_THINKING_PARAM=
|
|
104
114
|
|
|
105
115
|
# --- Live end-to-end opt-ins (optional; normally unset) ---------------------
|
|
106
116
|
# "1" allows the live embedding-provider E2E to hit the real network.
|
|
@@ -773,6 +773,17 @@ async function handleRequest(request, dependencies = undefined) {
|
|
|
773
773
|
}
|
|
774
774
|
|
|
775
775
|
async function main() {
|
|
776
|
+
// Recall-safe warm-up: if the semantic readiness record is not Aligned when the
|
|
777
|
+
// server starts, rebuild the index in the BACKGROUND (async, logged) so the
|
|
778
|
+
// first query rarely pays the multi-second reconstruction. No-op when aligned.
|
|
779
|
+
if (process.env.ARGO_REPO_ROOT && process.env.ARGO_REPO_ROOT.trim() !== '') {
|
|
780
|
+
try {
|
|
781
|
+
require('./graph-rag/semanticAlignmentRunner.js')
|
|
782
|
+
.preheatSemanticAlignment(process.env.ARGO_REPO_ROOT);
|
|
783
|
+
} catch {
|
|
784
|
+
// preheat is best-effort; never block server startup
|
|
785
|
+
}
|
|
786
|
+
}
|
|
776
787
|
const rl = readline.createInterface({
|
|
777
788
|
input: process.stdin,
|
|
778
789
|
crlfDelay: Infinity,
|
|
@@ -27,7 +27,7 @@ This local contract refines `OVERALL_ARCHITECTURE.md`.
|
|
|
27
27
|
- DT-19 capacity evidence begins after real DT-18 quality evidence. It must preserve explicit `observedResultIds` or equivalent result ids from actual phase-1 query observations, never use closure fallback as result evidence, record measured precision for every declared purpose, validate any provided result cardinality against the result ID count so cardinality mismatch blocks, block explicitly when quality evidence or result evidence is missing, and must not return or infer cap, budget, pagination, truncation, continuation, top-k, token-budget, result-limit, or capacity-policy decisions. This is evidence for later human capacity governance, not a release threshold.
|
|
28
28
|
- Seven-wave delivery gating must block W7 when W2-W6 prerequisite evidence is incomplete, block whole delivery when the W7 DT-18 business benchmark is missing, failing, incomplete, or carries precision outside `[0, 1]`, and permit overall delivery only after prerequisites and the W7 benchmark pass. DT-19 capacity controls remain evidence-only and cannot become a W7 release rule.
|
|
29
29
|
- WP-P1 delegates durable production persistence and full-backfill orchestration inward to `semantic-persistence/`. `productionGraphRagRuntime.js` composes concrete production Neo4j projection/checkpoint adapters from `semanticPersistence` dependencies and exposes `runSemanticBackfill(request)`; it must not absorb checkpoint, durable-store, tombstone, or production-cleanup policy.
|
|
30
|
-
- WP-P1 semantic backfill is invoked only by private canonical argo init composition after internal exact consent and verified same-version structural projection, never by a public standalone MCP route or inferred mutation signal. Missing consent, version match, external credentials, or provider qualification blocks before provider/store/checkpoint/index side effects. It independently completes Element, ArchitectureRelationship, and View through bounded durable checkpoints before Aligned.
|
|
30
|
+
- WP-P1 semantic backfill is invoked only by private canonical argo init composition after internal exact consent and verified same-version structural projection, never by a public standalone MCP route or inferred mutation signal. Missing consent, version match, external credentials, or provider qualification blocks before provider/store/checkpoint/index side effects. It independently completes Element, ArchitectureRelationship, and View through bounded durable checkpoints before Aligned. Its resume checkpoint is contentHash-incremental: when the canonical version changes, records whose curated semantic text (contentHash) and embedding provider/model/dimensions already match are skipped (not re-embedded); only changed/new records are embedded, so recall is preserved and a version bump stays cheap.
|
|
31
31
|
- The separate production store upserts changed stable canonical identities, deletes tombstones, preserves complete canonical/content/index/provider/model/version/dimensions/vector metadata across restart, rejects `runId` records, and exposes exactly four methods with no cleanup API. Existing `liveEmbeddingNeo4jBoundary.js` remains test-evidence infrastructure and unchanged.
|
|
32
32
|
- `defaultSemanticRetrieval.js` is the WP-P2 deep composition module for the shipped uninjected MCP semantic path. `createDefaultSemanticRetrieval(dependencies)` accepts the unified production `readinessBoundary` and returns `retrieve(request)` plus the narrow read-only `readReadiness()`. Both methods resolve through one private `readAndEvaluatePersistentReadiness` path that retains the existing approved composition, normalization, threshold filtering, expanding-window exhaustion, purpose closure, endpoint/View closure, provenance, and canonical completion. The safe failure envelope preserves stored `category`, `message`, and `action` plus state/version/channel evidence and `fullSnapshotFallback: false`; unknown diagnostic fields and secrets never cross the boundary.
|
|
33
33
|
- Approved external-source resolution must directly read and attribute all required embedding-profile, Neo4j, and provider-secret keys before readiness; success with zero or partial approved-source reads is forbidden. Production-semantic-query directly inspects all three legacy Neo4j alias keys and rejects legacy-only or mixed valid-canonical-plus-any-legacy-alias sources with `SECRET_SOURCE_PROVENANCE_PROHIBITED` before attribution, selection, readiness, provider, or driver effects. Inspection cannot attribute, normalize, return, or use a legacy value. It also rejects missing secrets, unsafe `.argo/.env` path/git/file/reparse/ACL state, conflicting process/file values, and test/default, literal, fallback, alias, or indirect credential operations. The test composition accepts raw `sourceBehavior` plus filesystem/git/ACL adapters; ready-made environment or normalized configuration objects are prohibited.
|
|
@@ -29,6 +29,9 @@ const {
|
|
|
29
29
|
rerankCandidates,
|
|
30
30
|
applyRerankOrder,
|
|
31
31
|
} = require('./rerankRetrieval.js');
|
|
32
|
+
const {
|
|
33
|
+
runSemanticAlignment,
|
|
34
|
+
} = require('./semanticAlignmentRunner.js');
|
|
32
35
|
|
|
33
36
|
const APPROVED_SOURCE_KEYS = Object.freeze([
|
|
34
37
|
'ARGO_EMBEDDING_BASE_URL',
|
|
@@ -423,25 +426,12 @@ async function attemptAutomaticAlignment({ composition, request, alignment }) {
|
|
|
423
426
|
}
|
|
424
427
|
|
|
425
428
|
function runScriptOwnedSemanticAlignment(operation) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const scriptPath = resolveArgoPath('scripts', 'ensureArgoHarnessEnvironment.js');
|
|
429
|
-
const result = childProcess.spawnSync(process.execPath, [scriptPath], {
|
|
430
|
-
cwd: repositoryRoot,
|
|
431
|
-
encoding: 'utf8',
|
|
432
|
-
env: process.env,
|
|
433
|
-
});
|
|
434
|
-
if (result.status === 0) {
|
|
435
|
-
return Object.freeze({
|
|
429
|
+
return runSemanticAlignment(getWorkspaceRoot()).then(
|
|
430
|
+
() => Object.freeze({
|
|
436
431
|
status: 'aligned',
|
|
437
432
|
originalQuery: operation && operation.originalQuery,
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
const error = safeError('SEMANTIC_AUTO_ALIGNMENT_FAILED');
|
|
441
|
-
error.message = 'Semantic automatic alignment failed before retry.';
|
|
442
|
-
error.action = 'Repair semantic lifecycle alignment, then retry the original query.';
|
|
443
|
-
error.fullSnapshotFallback = false;
|
|
444
|
-
return Promise.reject(error);
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
445
435
|
}
|
|
446
436
|
|
|
447
437
|
async function resolveRawTestConfiguration(sourceBehavior, sourceAdapters) {
|
|
@@ -52,6 +52,8 @@ const RETRIEVAL_TUNING_KEYS = Object.freeze([
|
|
|
52
52
|
'ARGO_RERANK_API_KEY',
|
|
53
53
|
'ARGO_RERANK_PROVIDER',
|
|
54
54
|
'ARGO_RERANK_MODEL',
|
|
55
|
+
'ARGO_RERANK_DISABLE_THINKING',
|
|
56
|
+
'ARGO_RERANK_THINKING_PARAM',
|
|
55
57
|
]);
|
|
56
58
|
const OPT_IN_KEYS = Object.freeze({
|
|
57
59
|
ARGO_LIVE_PROVIDER_E2E: 'LIVE_PROVIDER_E2E_OPT_IN_REQUIRED',
|
|
@@ -60,6 +60,32 @@ function rerankConfig(env = process.env) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Model-agnostic "disable thinking/reasoning" body fragment. Reasoning models
|
|
64
|
+
// spend seconds on hidden reasoning tokens for a simple listwise ranking with no
|
|
65
|
+
// accuracy gain, so disable it by default for EVERY rerank provider (not just
|
|
66
|
+
// deepseek), so switching models keeps the same behaviour. Providers use
|
|
67
|
+
// different field names, so it is fully overridable:
|
|
68
|
+
// ARGO_RERANK_DISABLE_THINKING=0 -> send nothing
|
|
69
|
+
// ARGO_RERANK_THINKING_PARAM='{...}' -> send this exact JSON fragment
|
|
70
|
+
// rerankCandidates retries WITHOUT the fragment if the provider rejects it, so an
|
|
71
|
+
// unsupported field can never break rerank (recall-first: never lose rerank to a
|
|
72
|
+
// 400).
|
|
73
|
+
const DEFAULT_RERANK_THINKING_OFF = Object.freeze({ thinking: { type: 'disabled' } });
|
|
74
|
+
|
|
75
|
+
function resolveRerankThinkingOff(env = process.env) {
|
|
76
|
+
if (String((env && env.ARGO_RERANK_DISABLE_THINKING) || '') === '0') return null;
|
|
77
|
+
const raw = env && env.ARGO_RERANK_THINKING_PARAM;
|
|
78
|
+
if (typeof raw === 'string' && raw.trim() !== '') {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(raw);
|
|
81
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return DEFAULT_RERANK_THINKING_OFF;
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
function candidateText(record) {
|
|
64
90
|
const raw = record && (record.searchText || record.description || record.name || '');
|
|
65
91
|
return String(raw).replace(/\s+/g, ' ').slice(0, 160);
|
|
@@ -115,37 +141,46 @@ async function rerankCandidates({ query, candidates, provider, transport, maxRet
|
|
|
115
141
|
const apiKey = typeof provider.apiKey === 'string' ? provider.apiKey : '';
|
|
116
142
|
if (baseUrl === '' || apiKey === '') return null;
|
|
117
143
|
const model = provider.model || DEFAULT_RERANK_MODEL;
|
|
118
|
-
const
|
|
144
|
+
const thinkingOff = resolveRerankThinkingOff();
|
|
145
|
+
const baseBody = {
|
|
119
146
|
model,
|
|
120
147
|
temperature: 0,
|
|
121
148
|
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' } } : {}),
|
|
127
149
|
messages: [
|
|
128
150
|
{ role: 'system', content: 'You rank architecture elements by relevance to a query. Return ONLY JSON {"order":[ids best-first]} using only the candidate ids.' },
|
|
129
151
|
{ 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.` },
|
|
130
152
|
],
|
|
131
153
|
};
|
|
132
|
-
const
|
|
133
|
-
const
|
|
154
|
+
const timeout = timeoutMs || rerankTimeoutMs();
|
|
155
|
+
const send = async (extra) => {
|
|
156
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
157
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeout) : null;
|
|
158
|
+
try {
|
|
159
|
+
return await transport.request(`${baseUrl}/chat/completions`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify({ ...baseBody, ...extra }),
|
|
163
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
} finally {
|
|
168
|
+
if (timer) clearTimeout(timer);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
134
171
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
172
|
+
let response = await send(thinkingOff || {});
|
|
173
|
+
// A provider may reject the thinking-off fragment (unknown field -> 400).
|
|
174
|
+
// Retry WITHOUT it so rerank still works: never lose rerank/recall to a 400.
|
|
175
|
+
if ((!response || response.ok !== true) && thinkingOff) {
|
|
176
|
+
response = await send({});
|
|
177
|
+
}
|
|
141
178
|
if (!response || response.ok !== true || typeof response.json !== 'function') return null;
|
|
142
179
|
const payload = await response.json();
|
|
143
180
|
const content = payload && payload.choices && payload.choices[0] && payload.choices[0].message && payload.choices[0].message.content;
|
|
144
181
|
return parseRerankOrder(content, list.map(candidate => candidate.id));
|
|
145
182
|
} catch {
|
|
146
183
|
return null;
|
|
147
|
-
} finally {
|
|
148
|
-
if (timer) clearTimeout(timer);
|
|
149
184
|
}
|
|
150
185
|
}
|
|
151
186
|
|
|
@@ -158,6 +193,8 @@ module.exports = {
|
|
|
158
193
|
rerankConfig,
|
|
159
194
|
rerankTimeoutMs,
|
|
160
195
|
resolveRerankProvider,
|
|
196
|
+
resolveRerankThinkingOff,
|
|
197
|
+
DEFAULT_RERANK_THINKING_OFF,
|
|
161
198
|
parseRerankOrder,
|
|
162
199
|
applyRerankOrder,
|
|
163
200
|
rerankCandidates,
|
|
@@ -90,7 +90,7 @@ async function processChannel(options) {
|
|
|
90
90
|
const stored = await options.checkpointStore.readCheckpoint(options.channel);
|
|
91
91
|
const checkpoint = stored && stored.canonicalVersion === options.canonicalVersion
|
|
92
92
|
? mutableCheckpoint(stored, canonicalRecords.length)
|
|
93
|
-
:
|
|
93
|
+
: await resumeCheckpointByContent(options, canonicalRecords);
|
|
94
94
|
const attempts = new Map();
|
|
95
95
|
|
|
96
96
|
while (checkpoint.completedCanonicalIdentities.length < canonicalRecords.length) {
|
|
@@ -153,6 +153,61 @@ async function processChannel(options) {
|
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// Cross-version incremental resume. The resume checkpoint is keyed by
|
|
157
|
+
// canonicalVersion, but ANY add/remove of an element/relationship/view changes
|
|
158
|
+
// that version. Re-embedding every record on every such change is wasteful and
|
|
159
|
+
// slow (~hundreds of provider calls: measured ~174s). Instead, when the stored
|
|
160
|
+
// checkpoint is for a different version, seed the completed set from the
|
|
161
|
+
// persisted records whose curated semantic text (contentHash) AND embedding
|
|
162
|
+
// provider/model/dimensions already match the current graph; only changed/new
|
|
163
|
+
// records get embedded. Recall is unchanged: changed content is ALWAYS
|
|
164
|
+
// re-embedded, only byte-identical text is skipped.
|
|
165
|
+
async function resumeCheckpointByContent(options, canonicalRecords) {
|
|
166
|
+
const checkpoint = emptyCheckpoint(options.channel, options.canonicalVersion, canonicalRecords.length);
|
|
167
|
+
let persisted = [];
|
|
168
|
+
try {
|
|
169
|
+
const stored = await options.projectionStore.readRecords();
|
|
170
|
+
persisted = Array.isArray(stored) ? stored : [];
|
|
171
|
+
} catch {
|
|
172
|
+
persisted = [];
|
|
173
|
+
}
|
|
174
|
+
const byIdentity = new Map();
|
|
175
|
+
for (const record of persisted) {
|
|
176
|
+
if (record && record.channel === options.channel && isNonBlankString(record.canonicalIdentity)) {
|
|
177
|
+
byIdentity.set(record.canonicalIdentity, record);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const completed = [];
|
|
181
|
+
for (const record of canonicalRecords) {
|
|
182
|
+
const existing = byIdentity.get(record.canonicalIdentity);
|
|
183
|
+
if (existing && persistedRecordMatches(existing, record, options.qualification)) {
|
|
184
|
+
completed.push(record.canonicalIdentity);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
checkpoint.completedCanonicalIdentities = completed;
|
|
188
|
+
checkpoint.completedCount = completed.length;
|
|
189
|
+
checkpoint.cursor = completed.length;
|
|
190
|
+
checkpoint.status = completed.length === canonicalRecords.length ? 'complete' : 'pending';
|
|
191
|
+
return checkpoint;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function persistedRecordMatches(existing, canonicalRecord, qualification) {
|
|
195
|
+
if (!Array.isArray(existing.vector)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const searchText = buildSemanticRecordText(canonicalRecord.channel, canonicalRecord.canonicalObject);
|
|
199
|
+
const contentHash = crypto.createHash('sha256').update(searchText).digest('hex');
|
|
200
|
+
return existing.contentVersion === `content:${contentHash}`
|
|
201
|
+
&& existing.provider === qualification.provider
|
|
202
|
+
&& existing.model === qualification.model
|
|
203
|
+
&& existing.modelVersion === qualification.version
|
|
204
|
+
&& toNumber(existing.dimensions) === toNumber(qualification.dimensions);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function toNumber(value) {
|
|
208
|
+
return value && typeof value.toNumber === 'function' ? value.toNumber() : Number(value);
|
|
209
|
+
}
|
|
210
|
+
|
|
156
211
|
function buildSemanticRecord(record, vector, options) {
|
|
157
212
|
const searchText = buildSemanticRecordText(record.channel, record.canonicalObject);
|
|
158
213
|
const contentHash = crypto.createHash('sha256')
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Owns the (expensive) semantic-index alignment child process. When readiness is
|
|
4
|
+
// NOT aligned, the index must be rebuilt to serve correct (full-recall) results;
|
|
5
|
+
// that rebuild can take seconds to ~90s. This module:
|
|
6
|
+
// - runs it ASYNCHRONOUSLY (child_process.spawn, not spawnSync) so the MCP
|
|
7
|
+
// event loop is never frozen while it runs;
|
|
8
|
+
// - de-duplicates concurrent requests (one rebuild shared by all waiters);
|
|
9
|
+
// - logs start/finish/failure for observability;
|
|
10
|
+
// - exposes preheatSemanticAlignment() for a background warm-up at startup.
|
|
11
|
+
// It never changes WHAT is retrieved (recall/precision), only how the rebuild is
|
|
12
|
+
// scheduled/observed.
|
|
13
|
+
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { spawn } = require('node:child_process');
|
|
17
|
+
const { getWorkspaceRoot, resolveArgoPath } = require('../argo-paths.js');
|
|
18
|
+
|
|
19
|
+
const READINESS_RELATIVE_PATH = path.join('.argo', 'temp', 'system-architecture-semantic-readiness.json');
|
|
20
|
+
|
|
21
|
+
let inFlight = null;
|
|
22
|
+
let preheated = false;
|
|
23
|
+
|
|
24
|
+
function readinessRecordPath(repositoryRoot) {
|
|
25
|
+
return path.join(repositoryRoot, ...READINESS_RELATIVE_PATH.split(path.sep));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSemanticReady(repositoryRoot) {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(fs.readFileSync(readinessRecordPath(repositoryRoot), 'utf8'));
|
|
31
|
+
return !!parsed && parsed.state === 'Aligned' && parsed.verified === true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function alignmentError() {
|
|
38
|
+
const error = new Error('SEMANTIC_AUTO_ALIGNMENT_FAILED');
|
|
39
|
+
error.category = 'SEMANTIC_AUTO_ALIGNMENT_FAILED';
|
|
40
|
+
error.message = 'Semantic automatic alignment failed before retry.';
|
|
41
|
+
error.action = 'Repair semantic lifecycle alignment, then retry the original query.';
|
|
42
|
+
error.fullSnapshotFallback = false;
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Run the alignment (once). Returns a promise resolving to { status: 'aligned' }
|
|
47
|
+
// or rejecting with the SEMANTIC_AUTO_ALIGNMENT_FAILED envelope.
|
|
48
|
+
function runSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
|
|
49
|
+
if (inFlight) {
|
|
50
|
+
return inFlight;
|
|
51
|
+
}
|
|
52
|
+
const scriptPath = resolveArgoPath('scripts', 'ensureArgoHarnessEnvironment.js');
|
|
53
|
+
const startedAt = Date.now();
|
|
54
|
+
console.error('[argo] semantic auto-alignment: index not aligned, rebuilding (recall-safe)…');
|
|
55
|
+
inFlight = new Promise((resolve, reject) => {
|
|
56
|
+
const child = spawn(process.execPath, [scriptPath], {
|
|
57
|
+
cwd: repositoryRoot,
|
|
58
|
+
env: process.env,
|
|
59
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
60
|
+
});
|
|
61
|
+
const finish = (failed, cause) => {
|
|
62
|
+
inFlight = null;
|
|
63
|
+
const ms = Date.now() - startedAt;
|
|
64
|
+
if (failed) {
|
|
65
|
+
console.error(`[argo] semantic auto-alignment FAILED in ${ms}ms${cause ? ': ' + cause : ''}`);
|
|
66
|
+
reject(alignmentError());
|
|
67
|
+
} else {
|
|
68
|
+
console.error(`[argo] semantic auto-alignment done in ${ms}ms`);
|
|
69
|
+
resolve(Object.freeze({ status: 'aligned' }));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
child.on('error', (error) => finish(true, error && error.message));
|
|
73
|
+
child.on('exit', (code, signal) => finish(code !== 0, `code=${code} signal=${signal || '-'}`));
|
|
74
|
+
});
|
|
75
|
+
return inFlight;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fire-and-forget warm-up: if readiness is already Aligned it does nothing;
|
|
79
|
+
// otherwise it starts the rebuild in the background so the first query rarely
|
|
80
|
+
// pays for it. Safe to call repeatedly (guarded + de-duplicated).
|
|
81
|
+
function preheatSemanticAlignment(repositoryRoot = getWorkspaceRoot()) {
|
|
82
|
+
if (preheated || !repositoryRoot || isSemanticReady(repositoryRoot)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
preheated = true;
|
|
86
|
+
console.error('[argo] readiness not aligned at startup; preheating semantic alignment in background…');
|
|
87
|
+
runSemanticAlignment(repositoryRoot).catch(() => {});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
runSemanticAlignment,
|
|
92
|
+
preheatSemanticAlignment,
|
|
93
|
+
isSemanticReady,
|
|
94
|
+
readinessRecordPath,
|
|
95
|
+
};
|
package/package.json
CHANGED