archgraph-argo 0.23.0 → 0.24.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/argo/.env.example CHANGED
@@ -19,6 +19,10 @@
19
19
  # -----------------------------------------------------------------------------
20
20
 
21
21
  # --- Embedding provider (required) — powers vector semantic retrieval -------
22
+ # Provider profile: "approved" (default) = the human-approved cloud profile
23
+ # below; "openai-compatible" = a self-hosted OpenAI-compatible endpoint
24
+ # (intranet/offline) whose URL/model/label/dimension are read verbatim.
25
+ ARGO_EMBEDDING_PROFILE=
22
26
  # OpenAI-compatible embedding endpoint base URL (no trailing slash).
23
27
  ARGO_EMBEDDING_BASE_URL=
24
28
  # Embedding model id (e.g. qwen3.7-text-embedding).
@@ -27,8 +31,17 @@ ARGO_EMBEDDING_MODEL=
27
31
  ARGO_EMBEDDING_PROVIDER=
28
32
  # Model version / qualification label (recorded evidence only).
29
33
  ARGO_EMBEDDING_MODEL_VERSION=
30
- # Embedding vector dimension; must match the model (current profiles: 1536).
34
+ # Embedding vector dimension; must match the model and the vector index
35
+ # (current profiles: 1536).
31
36
  ARGO_EMBEDDING_DIMENSIONS=
37
+ # Query-side instruction prefix for instruction-tuned embedding models (e.g.
38
+ # gte-Qwen2: "Instruct: <task>\nQuery: "). Empty = no prefix. Documents are
39
+ # never prefixed; only the query side is.
40
+ ARGO_EMBEDDING_QUERY_INSTRUCTION=
41
+ # Optional embedding API key. SECRET: overrides QWEN_KEY as the Bearer token for
42
+ # the embeddings call when set (useful for a self-hosted endpoint). Leave empty
43
+ # to use QWEN_KEY.
44
+ ARGO_EMBEDDING_API_KEY=
32
45
 
33
46
  # --- Neo4j (required) — structural projection + vector/full-text store ------
34
47
  # Neo4j connection URI (e.g. neo4j://127.0.0.1:7687).
@@ -137,7 +137,7 @@ Successful query evidence identifies `nodejs` as runtime, `neo4j-native` as retr
137
137
  - Frozen accepted source fixtures supply all five `ARGO_EMBEDDING_*` keys, `ARGO_NEO4J_DATABASE_URL`, `ARGO_NEO4J_DATABASE_USERNAME`, and both secret keys from process-only, file-only, or matching dual sources. Every rejected fixture starts from that complete profile and mutates only its named rejection dimension; no implicit default may satisfy normalization.
138
138
  - Loader provenance rejects root/alternate/tracked files, CLI, literal/default/fallback, alias, destructured, generated, or indirect secret sources.
139
139
  - The resolver validates every trace before consuming its value using its private composition capability: trace/alias-chain immutability, exact field set with no extras, field types, issued-object identity, requested-key equality, exact requested file path, source-kind/path/operation correlation, and a one-element alias chain equal to the requested key for approved direct reads. Trusted test composition can deliberately issue key/path mismatch, missing/extra-field, or wrong-type traces; each must fail exactly `SOURCE_TRACE_INVALID`. The same value with CLI/literal operation methods, fallback/indirect operation methods, or alias methods remains prohibited.
140
- - The approved live profile is provider `alibaba-cloud-model-studio-openai-compatible-cn-beijing`, endpoint `https://llm-clids9mqc5o1mbvb.cn-beijing.maas.aliyuncs.com/compatible-mode/v1`, model `qwen3.7-text-embedding`, qualification `qualification-2026-07-25`, dimensions `1024`.
140
+ - The approved live profile is provider `alibaba-cloud-model-studio-openai-compatible-cn-beijing`, endpoint `https://llm-clids9mqc5o1mbvb.cn-beijing.maas.aliyuncs.com/compatible-mode/v1`, model `qwen3.7-text-embedding`, qualification `qualification-2026-07-25`, dimensions `1536`. The embedding backend is profile-selectable via `ARGO_EMBEDDING_PROFILE`: `approved` (default) enforces those values byte-for-byte; `openai-compatible` reads a self-hosted OpenAI-compatible endpoint verbatim but still fails closed on a missing key or an invalid dimension, with the vector-index dimension driven by `ARGO_EMBEDDING_DIMENSIONS`. `ARGO_EMBEDDING_QUERY_INSTRUCTION` prefixes the query side only (instruction-tuned models), never stored documents; `ARGO_EMBEDDING_API_KEY` (optional secret) overrides `QWEN_KEY` as the embeddings Bearer token.
141
141
  - Live network access requires explicit opt-in through the approved configuration source, normally the ignored/untracked `.argo/.env` containing `ARGO_LIVE_PROVIDER_E2E=1` and, for W3.1, `ARGO_W31_LIVE_MUTATION_VECTOR_E2E=1`; controlled process injection is allowed only through the same provenance-checked resolver. Default/offline CI remains deterministic but never substitutes fake evidence for a live pass.
142
142
  - Redaction verification includes a synthetic-success recording boundary that captures full Cypher text/parameter and graph-evidence values, detects canaries in neutral fields, and clears all in-memory persistence before inspecting generated artifacts.
143
143
  - The controlled Neo4j test boundary uses `ARGO_NEO4J_DATABASE_URL`, `ARGO_NEO4J_DATABASE_USERNAME`, and `ARGO_NEO4J_DATABASE_PASSWORD`; the password flows only to `neo4j.auth.basic`, never to Cypher or evidence.
@@ -35,6 +35,9 @@ const {
35
35
  const {
36
36
  markPhase,
37
37
  } = require('./mcpCrashDiagnostics.js');
38
+ const {
39
+ composeQueryEmbeddingInput,
40
+ } = require('./embeddingProviderProfile.js');
38
41
 
39
42
  const APPROVED_SOURCE_KEYS = Object.freeze([
40
43
  'ARGO_EMBEDDING_BASE_URL',
@@ -165,6 +168,7 @@ function createDefaultSemanticRetrieval(dependencies = {}) {
165
168
  composition,
166
169
  canonicalGraph,
167
170
  activeReadinessBoundary,
171
+ expectedEmbeddingQualification(configurationEvidence.configuration),
168
172
  );
169
173
  if (!evidence.alignment.aligned) {
170
174
  await attemptAutomaticAlignment({
@@ -176,6 +180,7 @@ function createDefaultSemanticRetrieval(dependencies = {}) {
176
180
  composition,
177
181
  canonicalGraph,
178
182
  activeReadinessBoundary,
183
+ expectedEmbeddingQualification(configurationEvidence.configuration),
179
184
  );
180
185
  if (!evidence.alignment.aligned) {
181
186
  throw semanticAutomaticAlignmentFailed(evidence.alignment);
@@ -219,11 +224,12 @@ function createDefaultSemanticRetrieval(dependencies = {}) {
219
224
  && activeTestComposition.useReadinessBoundary !== true
220
225
  ? undefined
221
226
  : readinessBoundary;
222
- await composition.resolveConfiguration();
227
+ const configurationEvidence = await composition.resolveConfiguration();
223
228
  const evidence = await readAndEvaluatePersistentReadiness(
224
229
  composition,
225
230
  canonicalGraph,
226
231
  activeReadinessBoundary,
232
+ expectedEmbeddingQualification(configurationEvidence.configuration),
227
233
  );
228
234
  return publicReadinessOutcome(evidence.alignment);
229
235
  } finally {
@@ -255,9 +261,12 @@ async function executeWpP2Retrieval({
255
261
  transport: composition.transport,
256
262
  });
257
263
  markPhase('retrieval:embed:start');
258
- const vector = await provider.embed(request.intent);
264
+ const vector = await provider.embed(composeQueryEmbeddingInput(
265
+ request.intent,
266
+ configurationEvidence.configuration.embeddingQueryInstruction,
267
+ ));
259
268
  markPhase('retrieval:embed:done');
260
- requireQualifiedVector(vector);
269
+ requireQualifiedVector(vector, configurationEvidence.configuration.embeddingDimensions);
261
270
  const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
262
271
  const strict = AUDIT_PURPOSES.has(purpose);
263
272
  const topK = Number.isInteger(request.topK) && request.topK > 0 ? request.topK : resolveTopK();
@@ -652,7 +661,7 @@ async function readPersistentReadiness(neo4jDriver, readinessBoundary) {
652
661
  return { readiness, requireQualification: false };
653
662
  }
654
663
 
655
- function evaluatePersistentReadiness(readiness, canonicalGraph, requireQualification = false) {
664
+ function evaluatePersistentReadiness(readiness, canonicalGraph, requireQualification = false, expectedQualification = APPROVED_PROFILE) {
656
665
  const expectedCanonicalVersion = deriveCanonicalVersion(canonicalGraph);
657
666
  const records = new Map((Array.isArray(readiness.channels) ? readiness.channels : [])
658
667
  .map(record => [record.channel, record]));
@@ -669,10 +678,10 @@ function evaluatePersistentReadiness(readiness, canonicalGraph, requireQualifica
669
678
  || record.contentVersion !== readiness.contentVersion
670
679
  || record.indexVersion !== readiness.indexVersion
671
680
  || (requireQualification && (
672
- record.provider !== APPROVED_PROFILE.provider
673
- || record.model !== APPROVED_PROFILE.model
674
- || record.modelVersion !== APPROVED_PROFILE.version
675
- || record.dimensions !== APPROVED_PROFILE.dimensions
681
+ record.provider !== expectedQualification.provider
682
+ || record.model !== expectedQualification.model
683
+ || record.modelVersion !== expectedQualification.version
684
+ || record.dimensions !== expectedQualification.dimensions
676
685
  || record.queryable !== true
677
686
  || record.coherent !== true
678
687
  ))
@@ -720,17 +729,27 @@ function arrayEvidence(value, fallback = []) {
720
729
  : fallback;
721
730
  }
722
731
 
723
- async function readAndEvaluatePersistentReadiness(composition, canonicalGraph, readinessBoundary) {
732
+ async function readAndEvaluatePersistentReadiness(composition, canonicalGraph, readinessBoundary, expectedQualification) {
724
733
  const persistent = await readPersistentReadiness(composition.neo4jDriver, readinessBoundary);
725
734
  const readiness = persistent.readiness;
726
735
  const alignment = evaluatePersistentReadiness(
727
736
  readiness,
728
737
  canonicalGraph,
729
738
  persistent.requireQualification,
739
+ expectedQualification,
730
740
  );
731
741
  return { composition, readiness, alignment };
732
742
  }
733
743
 
744
+ function expectedEmbeddingQualification(configuration) {
745
+ return {
746
+ provider: configuration.embeddingProvider,
747
+ model: configuration.embeddingModel,
748
+ version: configuration.embeddingModelVersion,
749
+ dimensions: configuration.embeddingDimensions,
750
+ };
751
+ }
752
+
734
753
  function publicFailureEvidence(readiness) {
735
754
  const categories = new Set([
736
755
  'APPROVED_SECRET_REQUIRED',
@@ -929,10 +948,10 @@ async function completeSemanticResult({
929
948
  canonicalGraph,
930
949
  embeddingQualification: {
931
950
  approvedByHuman: true,
932
- provider: APPROVED_PROFILE.provider,
933
- model: APPROVED_PROFILE.model,
934
- version: APPROVED_PROFILE.version,
935
- dimensions: APPROVED_PROFILE.dimensions,
951
+ provider: configurationEvidence.configuration.embeddingProvider,
952
+ model: configurationEvidence.configuration.embeddingModel,
953
+ version: configurationEvidence.configuration.embeddingModelVersion,
954
+ dimensions: configurationEvidence.configuration.embeddingDimensions,
936
955
  },
937
956
  neo4jRetrievalBoundary: {
938
957
  async retrieve() {
@@ -967,10 +986,10 @@ async function completeSemanticResult({
967
986
  seedsByType: Object.freeze(seedsByType),
968
987
  configurationEvidence: Object.freeze({
969
988
  attribution: configurationEvidence.attribution,
970
- provider: APPROVED_PROFILE.provider,
971
- model: APPROVED_PROFILE.model,
972
- modelVersion: APPROVED_PROFILE.version,
973
- dimensions: APPROVED_PROFILE.dimensions,
989
+ provider: configurationEvidence.configuration.embeddingProvider,
990
+ model: configurationEvidence.configuration.embeddingModel,
991
+ modelVersion: configurationEvidence.configuration.embeddingModelVersion,
992
+ dimensions: configurationEvidence.configuration.embeddingDimensions,
974
993
  }),
975
994
  canonicalVersion: versions.canonicalVersion,
976
995
  contentVersion: versions.contentVersion,
@@ -1159,10 +1178,10 @@ function semanticAutomaticAlignmentFailed(alignment, sourceError) {
1159
1178
  return error;
1160
1179
  }
1161
1180
 
1162
- function requireQualifiedVector(vector) {
1181
+ function requireQualifiedVector(vector, dimensions = APPROVED_PROFILE.dimensions) {
1163
1182
  if (
1164
1183
  !Array.isArray(vector)
1165
- || vector.length !== APPROVED_PROFILE.dimensions
1184
+ || vector.length !== dimensions
1166
1185
  || vector.some(value => typeof value !== 'number' || !Number.isFinite(value))
1167
1186
  ) {
1168
1187
  throw safeError('LIVE_PROVIDER_RESPONSE_INVALID');
@@ -0,0 +1,89 @@
1
+ 'use strict';
2
+
3
+ // Embedding provider profile (ArchGraph ARGO).
4
+ //
5
+ // The semantic-retrieval embedding backend is either the human-approved cloud
6
+ // profile (`approved`, the default, byte-for-byte unchanged) or a self-hosted
7
+ // OpenAI-compatible endpoint (`openai-compatible`) for intranet / offline
8
+ // deployments. This module owns the profile vocabulary, the dimension contract,
9
+ // and the query-side instruction composition so the live configuration resolver
10
+ // and the retrieval runtime share one implementation.
11
+
12
+ const PROFILE_KEY = 'ARGO_EMBEDDING_PROFILE';
13
+ const QUERY_INSTRUCTION_KEY = 'ARGO_EMBEDDING_QUERY_INSTRUCTION';
14
+ const API_KEY = 'ARGO_EMBEDDING_API_KEY';
15
+ const PROFILE_APPROVED = 'approved';
16
+ const PROFILE_OPENAI_COMPATIBLE = 'openai-compatible';
17
+ const SUPPORTED_EMBEDDING_PROFILES = Object.freeze([PROFILE_APPROVED, PROFILE_OPENAI_COMPATIBLE]);
18
+ const DEFAULT_EMBEDDING_DIMENSIONS = 1536;
19
+
20
+ function resolveEmbeddingProfile(value) {
21
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
22
+ return normalized === '' ? PROFILE_APPROVED : normalized;
23
+ }
24
+
25
+ function isApprovedEmbeddingProfile(value) {
26
+ return resolveEmbeddingProfile(value) === PROFILE_APPROVED;
27
+ }
28
+
29
+ function requireSupportedEmbeddingProfile(value) {
30
+ const profile = resolveEmbeddingProfile(value);
31
+ if (!SUPPORTED_EMBEDDING_PROFILES.includes(profile)) {
32
+ throw profileError('EMBEDDING_PROFILE_UNSUPPORTED');
33
+ }
34
+ return profile;
35
+ }
36
+
37
+ function resolveEmbeddingDimensions(value, fallback = DEFAULT_EMBEDDING_DIMENSIONS) {
38
+ const parsed = typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN;
39
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
40
+ }
41
+
42
+ function requireEmbeddingDimensions(value) {
43
+ const parsed = typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN;
44
+ if (!Number.isInteger(parsed) || parsed <= 0) {
45
+ throw profileError('EMBEDDING_DIMENSIONS_INVALID');
46
+ }
47
+ return parsed;
48
+ }
49
+
50
+ // The query-side input may carry an instruction prefix (e.g. gte-Qwen2's
51
+ // "Instruct: <task>\nQuery: <query>"); document-side input stays raw so stored
52
+ // vectors are never polluted by a query-only prefix. The prefix commonly comes
53
+ // from a single-line `.env` value, so `\n`/`\r`/`\t` escapes are decoded here.
54
+ function normalizeInstruction(instruction) {
55
+ if (typeof instruction !== 'string') return '';
56
+ return instruction
57
+ .replace(/\\r\\n/g, '\n')
58
+ .replace(/\\n/g, '\n')
59
+ .replace(/\\r/g, '\r')
60
+ .replace(/\\t/g, '\t');
61
+ }
62
+
63
+ function composeQueryEmbeddingInput(text, instruction) {
64
+ const query = text == null ? '' : String(text);
65
+ const prefix = normalizeInstruction(instruction);
66
+ return prefix === '' ? query : `${prefix}${query}`;
67
+ }
68
+
69
+ function profileError(category) {
70
+ const error = new Error(category);
71
+ error.category = category;
72
+ return error;
73
+ }
74
+
75
+ module.exports = {
76
+ PROFILE_KEY,
77
+ QUERY_INSTRUCTION_KEY,
78
+ API_KEY,
79
+ PROFILE_APPROVED,
80
+ PROFILE_OPENAI_COMPATIBLE,
81
+ SUPPORTED_EMBEDDING_PROFILES,
82
+ DEFAULT_EMBEDDING_DIMENSIONS,
83
+ resolveEmbeddingProfile,
84
+ isApprovedEmbeddingProfile,
85
+ requireSupportedEmbeddingProfile,
86
+ resolveEmbeddingDimensions,
87
+ requireEmbeddingDimensions,
88
+ composeQueryEmbeddingInput,
89
+ };
@@ -46,7 +46,7 @@ function createLiveEmbeddingIndexGate(dependencies = {}) {
46
46
  requireInput(input);
47
47
  const vector = await client.embed(input.input);
48
48
  if (
49
- vector.length !== 1536
49
+ vector.length !== configuration.embeddingDimensions
50
50
  || vector.some(value => typeof value !== 'number' || !Number.isFinite(value))
51
51
  ) {
52
52
  throw safeError('LIVE_PROVIDER_RESPONSE_INVALID');
@@ -79,7 +79,6 @@ function requireApprovedConfiguration(configuration, qualification) {
79
79
  || qualification.model !== configuration.embeddingModel
80
80
  || qualification.version !== configuration.embeddingModelVersion
81
81
  || qualification.dimensions !== configuration.embeddingDimensions
82
- || qualification.dimensions !== 1536
83
82
  ) {
84
83
  throw safeError('LIVE_PROVIDER_INDEX_WRITE_PROHIBITED');
85
84
  }
@@ -11,7 +11,7 @@ function createLiveEmbeddingProviderClient({ configuration, transport }) {
11
11
  {
12
12
  method: 'POST',
13
13
  headers: {
14
- Authorization: `Bearer ${configuration.qwenKey}`,
14
+ Authorization: `Bearer ${configuration.embeddingApiKey || configuration.qwenKey}`,
15
15
  'Content-Type': 'application/json',
16
16
  },
17
17
  body: JSON.stringify({
@@ -6,6 +6,14 @@ const {
6
6
  const {
7
7
  getArgoEnvPath,
8
8
  } = require('../argo-paths.js');
9
+ const {
10
+ PROFILE_KEY,
11
+ QUERY_INSTRUCTION_KEY,
12
+ API_KEY: EMBEDDING_API_KEY_KEY,
13
+ PROFILE_APPROVED,
14
+ requireSupportedEmbeddingProfile,
15
+ requireEmbeddingDimensions,
16
+ } = require('./embeddingProviderProfile.js');
9
17
 
10
18
  const CONFIG_KEYS = Object.freeze([
11
19
  'ARGO_EMBEDDING_BASE_URL',
@@ -63,11 +71,14 @@ const READABLE_KEYS = Object.freeze([
63
71
  ...CONFIG_KEYS,
64
72
  ...OPTIONAL_CONFIG_KEYS,
65
73
  ...RETRIEVAL_TUNING_KEYS,
74
+ PROFILE_KEY,
75
+ QUERY_INSTRUCTION_KEY,
76
+ EMBEDDING_API_KEY_KEY,
66
77
  ...Object.keys(OPT_IN_KEYS),
67
78
  ]);
68
79
  const LEGACY_KEYS = Object.freeze(['ARGO_NEO4J_URI', 'ARGO_NEO4J_USERNAME', 'ARGO_NEO4J_PASSWORD']);
69
80
  const PROHIBITED_RUNTIME_FIELD_KEYS = Object.freeze(['neo4jUri', 'embeddingCredential']);
70
- const SECRET_KEYS = new Set(['ARGO_NEO4J_DATABASE_PASSWORD', 'QWEN_KEY', 'ARGO_RERANK_API_KEY']);
81
+ const SECRET_KEYS = new Set(['ARGO_NEO4J_DATABASE_PASSWORD', 'QWEN_KEY', 'ARGO_RERANK_API_KEY', EMBEDDING_API_KEY_KEY]);
71
82
  const APPROVED = Object.freeze({
72
83
  ARGO_EMBEDDING_BASE_URL: 'https://llm-clids9mqc5o1mbvb.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',
73
84
  ARGO_EMBEDDING_MODEL: 'qwen3.7-text-embedding',
@@ -194,15 +205,16 @@ async function resolveTrusted({
194
205
  }
195
206
  attribution[key] = present(processValue) ? 'process' : 'file';
196
207
  }
197
- for (const [key, expected] of Object.entries(APPROVED)) {
198
- if (normalized[key] !== expected) throw safeError('LIVE_PROVIDER_CONFIGURATION_REQUIRED');
199
- }
208
+ const profile = requireSupportedEmbeddingProfile(
209
+ selectOptional(processValues, fileValues, PROFILE_KEY),
210
+ );
211
+ const profileConfiguration = buildProfileConfiguration(normalized, profile);
212
+ const embeddingQueryInstruction = selectOptional(processValues, fileValues, QUERY_INSTRUCTION_KEY);
213
+ const explicitEmbeddingApiKey = selectOptional(processValues, fileValues, EMBEDDING_API_KEY_KEY);
200
214
  const configuration = Object.freeze({
201
- embeddingBaseUrl: normalized.ARGO_EMBEDDING_BASE_URL,
202
- embeddingModel: normalized.ARGO_EMBEDDING_MODEL,
203
- embeddingProvider: normalized.ARGO_EMBEDDING_PROVIDER,
204
- embeddingModelVersion: normalized.ARGO_EMBEDDING_MODEL_VERSION,
205
- embeddingDimensions: 1536,
215
+ ...profileConfiguration,
216
+ embeddingQueryInstruction,
217
+ embeddingApiKey: explicitEmbeddingApiKey || normalized.QWEN_KEY,
206
218
  neo4jDatabaseUrl: normalized.ARGO_NEO4J_DATABASE_URL,
207
219
  neo4jDatabaseUsername: normalized.ARGO_NEO4J_DATABASE_USERNAME,
208
220
  neo4jDatabasePassword: normalized.ARGO_NEO4J_DATABASE_PASSWORD,
@@ -216,6 +228,52 @@ async function resolveTrusted({
216
228
  });
217
229
  }
218
230
 
231
+ // Profile-aware embedding identity. `approved` (the default) enforces the
232
+ // human-approved cloud values byte-for-byte; `openai-compatible` accepts the
233
+ // configured self-hosted endpoint verbatim but still fails closed on any missing
234
+ // key or invalid dimension (no implicit default).
235
+ function buildProfileConfiguration(normalized, profileValue) {
236
+ const profile = requireSupportedEmbeddingProfile(profileValue);
237
+ for (const key of [
238
+ 'ARGO_EMBEDDING_BASE_URL',
239
+ 'ARGO_EMBEDDING_MODEL',
240
+ 'ARGO_EMBEDDING_PROVIDER',
241
+ 'ARGO_EMBEDDING_MODEL_VERSION',
242
+ ]) {
243
+ if (!present(normalized[key])) {
244
+ const error = safeError('LIVE_PROVIDER_CONFIGURATION_REQUIRED');
245
+ error.field = key;
246
+ throw error;
247
+ }
248
+ }
249
+ if (profile === PROFILE_APPROVED) {
250
+ for (const [key, expected] of Object.entries(APPROVED)) {
251
+ if (normalized[key] !== expected) throw safeError('LIVE_PROVIDER_CONFIGURATION_REQUIRED');
252
+ }
253
+ }
254
+ return Object.freeze({
255
+ embeddingProfile: profile,
256
+ embeddingBaseUrl: normalized.ARGO_EMBEDDING_BASE_URL,
257
+ embeddingModel: normalized.ARGO_EMBEDDING_MODEL,
258
+ embeddingProvider: normalized.ARGO_EMBEDDING_PROVIDER,
259
+ embeddingModelVersion: normalized.ARGO_EMBEDDING_MODEL_VERSION,
260
+ embeddingDimensions: requireEmbeddingDimensions(normalized.ARGO_EMBEDDING_DIMENSIONS),
261
+ });
262
+ }
263
+
264
+ function selectOptional(processValues, fileValues, key) {
265
+ const processValue = processValues.get(key);
266
+ const fileValue = fileValues.get(key);
267
+ if (present(processValue) && present(fileValue) && processValue !== fileValue) {
268
+ throw safeError(SECRET_KEYS.has(key)
269
+ ? 'SECRET_SOURCE_CONFLICT'
270
+ : 'LIVE_PROVIDER_CONFIGURATION_CONFLICT');
271
+ }
272
+ if (present(processValue)) return processValue;
273
+ if (present(fileValue)) return fileValue;
274
+ return '';
275
+ }
276
+
219
277
  function optionalDatabaseName(value, repositoryRoot) {
220
278
  if (present(value)) return String(value).trim();
221
279
  const repoName = path.basename(repositoryRoot);
@@ -534,6 +592,8 @@ module.exports = {
534
592
  resolveApprovedLiveConfiguration,
535
593
  withApprovedLiveConfigurationTestComposition,
536
594
  posixModeIsSecretSafe,
595
+ buildProfileConfiguration,
596
+ APPROVED_EMBEDDING_VALUES: APPROVED,
537
597
  // The authoritative set of keys an approved `.env` file may carry. The
538
598
  // committed `.env.example` must document exactly this set (see
539
599
  // tests/env-example.test.js); SECRET_KEYS marks the subset that must be
@@ -29,7 +29,7 @@ function createProductionSemanticNeo4jAdapter(dependencies = {}) {
29
29
  }));
30
30
  }
31
31
  return withSession(driver, dependencies.configuration, async session => {
32
- await ensureVectorIndexes(session);
32
+ await ensureVectorIndexes(session, resolveEmbeddingDimensions(dependencies.configuration));
33
33
  await ensureFulltextIndexes(session);
34
34
  const results = [];
35
35
  for (const [channel, definition] of Object.entries(CHANNEL_INDEXES)) {
@@ -93,11 +93,16 @@ function createProductionSemanticNeo4jAdapter(dependencies = {}) {
93
93
 
94
94
  const EMBEDDING_DIMENSIONS = 1536;
95
95
 
96
- async function ensureVectorIndexes(session) {
96
+ function resolveEmbeddingDimensions(configuration) {
97
+ const value = configuration && configuration.embeddingDimensions;
98
+ return Number.isInteger(value) && value > 0 ? value : EMBEDDING_DIMENSIONS;
99
+ }
100
+
101
+ async function ensureVectorIndexes(session, dimensions = EMBEDDING_DIMENSIONS) {
97
102
  for (const definition of Object.values(CHANNEL_INDEXES)) {
98
103
  // CREATE ... IF NOT EXISTS cannot change an existing index's dimensions, so
99
- // when the approved embedding dimension changes (e.g. 1024 -> 1536) drop the
100
- // stale index first and recreate it at the approved dimension.
104
+ // when the configured embedding dimension changes (e.g. 1024 -> 1536) drop
105
+ // the stale index first and recreate it at the configured dimension.
101
106
  const existing = await executeRead(
102
107
  session,
103
108
  'SHOW INDEXES YIELD name, type, options WHERE type = \'VECTOR\' AND name = $name RETURN name, options',
@@ -106,7 +111,7 @@ async function ensureVectorIndexes(session) {
106
111
  if (existing.records.length > 0) {
107
112
  const options = existing.records[0].get('options');
108
113
  const dim = options && options.indexConfig && options.indexConfig['vector.dimensions'];
109
- if (dim !== EMBEDDING_DIMENSIONS) {
114
+ if (dim !== dimensions) {
110
115
  await executeWrite(session, `DROP INDEX ${definition.indexName} IF EXISTS`, {});
111
116
  }
112
117
  }
@@ -115,7 +120,7 @@ async function ensureVectorIndexes(session) {
115
120
  [
116
121
  `CREATE VECTOR INDEX ${definition.indexName} IF NOT EXISTS`,
117
122
  `FOR (semantic:${definition.label}) ON (semantic.vector)`,
118
- `OPTIONS { indexConfig: { \`vector.dimensions\`: ${EMBEDDING_DIMENSIONS}, \`vector.similarity_function\`: "cosine" } }`,
123
+ `OPTIONS { indexConfig: { \`vector.dimensions\`: ${dimensions}, \`vector.similarity_function\`: "cosine" } }`,
119
124
  ].join('\n'),
120
125
  {},
121
126
  );
@@ -177,4 +182,5 @@ function cloneRecord(record) {
177
182
 
178
183
  module.exports = {
179
184
  createProductionSemanticNeo4jAdapter,
185
+ resolveEmbeddingDimensions,
180
186
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
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": {