archgraph-argo 0.24.0 → 0.24.1

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/README.md CHANGED
@@ -57,12 +57,15 @@ Everything works out of the box except **semantic (Graph RAG) queries**, which n
57
57
  - **Embedding / vector engine** — powers semantic Graph RAG retrieval. Configure
58
58
  `ARGO_EMBEDDING_BASE_URL`, `ARGO_EMBEDDING_MODEL`, `ARGO_EMBEDDING_PROVIDER`,
59
59
  `ARGO_EMBEDDING_MODEL_VERSION`, `ARGO_EMBEDDING_DIMENSIONS`, plus the API key `QWEN_KEY`.
60
+ It points at **any OpenAI-compatible embedding endpoint** — a cloud provider, or a self-hosted
61
+ server for offline / intranet / private deployments via `ARGO_EMBEDDING_PROFILE=openai-compatible`
62
+ (see the [self-hosted embedding guide](docs/self-hosted-embedding-deployment.md)).
60
63
 
61
64
  Where do the values come from? The Neo4j credentials come from the Neo4j instance you own or
62
65
  provision (URI, username, password). The embedding configuration and `QWEN_KEY` come from your
63
- embedding provider's dashboard — for example Alibaba DashScope. `argo-deploy` walks you through the
64
- prompt (existing non-empty values in `~/.argo/.env` are kept); you can also edit the file afterwards
65
- and re-run.
66
+ embedding provider's dashboard — for example Alibaba DashScope or from a self-hosted
67
+ OpenAI-compatible server. `argo-deploy` walks you through the prompt (existing non-empty values in
68
+ `~/.argo/.env` are kept); you can also edit the file afterwards and re-run.
66
69
 
67
70
  ## How to use
68
71
 
@@ -1,89 +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
- };
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
+ };
@@ -77,6 +77,27 @@ const READABLE_KEYS = Object.freeze([
77
77
  ...Object.keys(OPT_IN_KEYS),
78
78
  ]);
79
79
  const LEGACY_KEYS = Object.freeze(['ARGO_NEO4J_URI', 'ARGO_NEO4J_USERNAME', 'ARGO_NEO4J_PASSWORD']);
80
+ // Host / process-level keys: read from the environment at launch, but NOT
81
+ // accepted inside the .env file (see argo/.env.example Part 2). Kept explicit so
82
+ // the coverage test (tests/env-key-coverage.test.js) can prove every env key
83
+ // referenced by argo/scripts is classified — either a .env key, a host-only key,
84
+ // or a legacy alias. This is what prevents "a key the code already uses is still
85
+ // rejected as unknown".
86
+ const HOST_ONLY_ENV_KEYS = Object.freeze([
87
+ 'ARGO_ENV_FILE',
88
+ 'ARGO_REPO_ROOT',
89
+ 'ARGO_EA_QEA',
90
+ 'ARGO_WORKSPACE_ROOTS',
91
+ 'ARGO_SERVER_PATH',
92
+ 'GRAPH_MCP_URL',
93
+ 'EA_QEA_DEBUG',
94
+ 'WORKSPACE_FOLDER',
95
+ 'ARGO_TEST_TIMEOUT_MS',
96
+ 'ARGO_MCP_MUTATION_RESPONSE_DEBUG',
97
+ 'ARGO_MCP_SEMANTIC_DEDUP',
98
+ 'ARGO_MCP_SEMANTIC_DEDUP_THRESHOLD',
99
+ 'ARGO_SEMANTIC_DEDUP_THRESHOLD',
100
+ ]);
80
101
  const PROHIBITED_RUNTIME_FIELD_KEYS = Object.freeze(['neo4jUri', 'embeddingCredential']);
81
102
  const SECRET_KEYS = new Set(['ARGO_NEO4J_DATABASE_PASSWORD', 'QWEN_KEY', 'ARGO_RERANK_API_KEY', EMBEDDING_API_KEY_KEY]);
82
103
  const APPROVED = Object.freeze({
@@ -600,4 +621,6 @@ module.exports = {
600
621
  // preflighted as secrets.
601
622
  ENV_FILE_KEYS: READABLE_KEYS,
602
623
  ENV_FILE_SECRET_KEYS: Object.freeze(Array.from(SECRET_KEYS)),
624
+ HOST_ONLY_ENV_KEYS,
625
+ ENV_FILE_LEGACY_KEYS: LEGACY_KEYS,
603
626
  };
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ // Maps profile-aware configuration evidence (from resolveApprovedLiveConfiguration)
4
+ // into the shape the canonical semantic-init / backfill runtime consumes.
5
+ //
6
+ // This is the single place that turns "which embedding profile" into the init
7
+ // backfill's configuration, so argo init embeds through the SAME provider the
8
+ // retrieval path uses (approved cloud by default, or a self-hosted endpoint via
9
+ // ARGO_EMBEDDING_PROFILE=openai-compatible) — instead of a hardcoded profile.
10
+
11
+ function buildDefaultSemanticConfiguration(evidence) {
12
+ const configuration = evidence && typeof evidence === 'object' && evidence.configuration
13
+ ? evidence.configuration
14
+ : evidence;
15
+ if (!configuration || typeof configuration !== 'object') {
16
+ throw new TypeError('resolved configuration evidence is required');
17
+ }
18
+ return Object.freeze({
19
+ ...configuration,
20
+ embeddingCredential: configuration.embeddingApiKey || configuration.qwenKey,
21
+ });
22
+ }
23
+
24
+ module.exports = { buildDefaultSemanticConfiguration };
@@ -144,12 +144,12 @@ const {
144
144
  const {
145
145
  semanticOperatorErrorResult,
146
146
  } = require('./graph-rag/semanticOperatorError.js');
147
- const {
148
- resolveExternalProductionConfig,
149
- } = require('./graph-rag/externalProductionConfig.js');
150
147
  const {
151
148
  resolveApprovedLiveConfiguration,
152
149
  } = require('./graph-rag/liveEmbeddingProviderConfig.js');
150
+ const {
151
+ buildDefaultSemanticConfiguration,
152
+ } = require('./graph-rag/semanticInitConfiguration.js');
153
153
  const {
154
154
  createLiveEmbeddingProviderClient,
155
155
  } = require('./graph-rag/liveEmbeddingProviderClient.js');
@@ -4056,59 +4056,17 @@ async function createDefaultProductionSemanticRuntime(options = {}) {
4056
4056
  });
4057
4057
  }
4058
4058
 
4059
+ // Resolve the canonical semantic-init/backfill configuration through the SAME
4060
+ // profile-aware resolver the retrieval path uses, so argo init embeds via the
4061
+ // configured provider (ARGO_EMBEDDING_PROFILE) instead of a hardcoded cloud
4062
+ // profile. `resolveApprovedLiveConfiguration` still fails closed when a required
4063
+ // key is missing or an unknown key is present in the .env file.
4059
4064
  async function resolveDefaultSemanticConfiguration(repositoryRoot) {
4060
- let external;
4061
- try {
4062
- external = resolveExternalProductionConfig({
4063
- neo4jUri: process.env.ARGO_NEO4J_DATABASE_URL,
4064
- neo4jUsername: process.env.ARGO_NEO4J_DATABASE_USERNAME,
4065
- neo4jPassword: process.env.ARGO_NEO4J_DATABASE_PASSWORD,
4066
- embeddingCredential: process.env.QWEN_KEY,
4067
- neo4jDatabase: process.env.ARGO_NEO4J_DATABASE || getDefaultSemanticNeo4jDatabaseName(repositoryRoot),
4068
- }, {
4069
- operation: 'semantic-backfill',
4070
- sourceKeys: new Map([
4071
- ['neo4jUri', 'ARGO_NEO4J_DATABASE_URL'],
4072
- ['neo4jUsername', 'ARGO_NEO4J_DATABASE_USERNAME'],
4073
- ['neo4jPassword', 'ARGO_NEO4J_DATABASE_PASSWORD'],
4074
- ['embeddingCredential', 'QWEN_KEY'],
4075
- ]),
4076
- });
4077
- } catch (error) {
4078
- if (error && error.category === 'EXTERNAL_CREDENTIALS_REQUIRED') {
4079
- const missing = new Error('EXTERNAL_CREDENTIALS_REQUIRED');
4080
- missing.category = 'EXTERNAL_CREDENTIALS_REQUIRED';
4081
- missing.field = error.field;
4082
- throw missing;
4083
- }
4084
- throw error;
4085
- }
4086
- return Object.freeze({
4087
- embeddingBaseUrl: W31_APPROVED_PROFILE.baseUrl,
4088
- embeddingModel: W31_APPROVED_PROFILE.model,
4089
- embeddingProvider: W31_APPROVED_PROFILE.provider,
4090
- embeddingModelVersion: W31_APPROVED_PROFILE.version,
4091
- embeddingDimensions: W31_APPROVED_PROFILE.dimensions,
4092
- neo4jDatabaseUrl: external.neo4jUri,
4093
- neo4jDatabaseUsername: external.neo4jUsername,
4094
- neo4jDatabasePassword: external.neo4jPassword,
4095
- qwenKey: external.embeddingCredential,
4096
- embeddingCredential: external.embeddingCredential,
4097
- ...(external.neo4jDatabase === undefined ? {} : { neo4jDatabase: external.neo4jDatabase }),
4065
+ const evidence = await resolveApprovedLiveConfiguration({
4066
+ repositoryRoot,
4067
+ requiredOptIns: [LIVE_PROVIDER_OPT_IN, W31_LIVE_OPT_IN],
4098
4068
  });
4099
- }
4100
-
4101
- function getDefaultSemanticNeo4jDatabaseName(repositoryRoot) {
4102
- const repoName = path.basename(repositoryRoot || resolveWorkspaceRoot());
4103
- const normalized = String(repoName)
4104
- .toLowerCase()
4105
- .replace(/[^a-z0-9.-]+/g, '-')
4106
- .replace(/^-+|-+$/g, '')
4107
- .replace(/\.{2,}/g, '.')
4108
- .replace(/-{2,}/g, '-');
4109
- const safe = normalized || 'workspace';
4110
- const prefixed = /^[a-z]/.test(safe) ? safe : `db-${safe}`;
4111
- return prefixed.slice(0, 63);
4069
+ return buildDefaultSemanticConfiguration(evidence);
4112
4070
  }
4113
4071
 
4114
4072
  function deriveSemanticCanonicalVersion(document) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
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": {