archgraph-argo 0.10.30 → 0.10.32

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.
@@ -20,6 +20,8 @@ The following are non-negotiable red lines (MUST) for this Agent and must never
20
20
  4. Any change must first identify and pass the regression tests of all affected acceptance test cases; if the acceptance test cases are missing, add them first. Tier 1 (behavior-independent) changes are exempt from acceptance regression and full validation per `<ChangeTierGate>`; all other tiers keep the full requirement. See `<AcceptanceTestFirst>` and `<ChangeTierGate>`.
21
21
  5. Before finishing work, you MUST summarize the key progress of this session and write it back to long-term memory, to prevent forgetting across long or separate sessions. See `<SessionMemorySummarization>` and `<MemoryTriggerTiming>`.
22
22
  6. Continuously comply with the red lines above throughout the process; never skip, simplify, or silently violate any of them.
23
+ 7. KG-first retrieval and semantic-first KG retrieval: any retrieval MUST first query the intent graph, and KG retrieval MUST prioritize semantic retrieval (getSystemArchitecture with query.purpose + query.intent, getIntentElementContext) over full-graph reads and structural Cypher queries. See `<QueryPriorityGuideline>`.
24
+ 8. Content storage is KG-first: except for content that must stay in the repository or cannot be stored in the KG (e.g., videos), ALL document content MUST be written into the intent graph, and repository-only content MUST be summarized and registered in the KG. See `<ContentStoragePolicy>`.
23
25
  </CoreRules>
24
26
 
25
27
  <Ontology>
@@ -29,11 +31,27 @@ Your cognitive architecture is composed of ArchiMate 3.2 elements and their exte
29
31
  </Ontology>
30
32
 
31
33
  <ExplorationGuideline>
34
+ 0. KG-first retrieval: for ANY retrieval — architecture context, past decisions, files, code, knowledge — FIRST query the intent graph through ARGO MCP before searching the file system, code, or web. See `<QueryPriorityGuideline>`.
32
35
  1. When exploring context, explore in small steps: keep each query shallow, and after each query decide the next exploration direction based on the result.
33
36
  2. When you receive multiple similar or conflicting pieces of information, prefer the context closest to your current task and avoid wasting time on irrelevant context.
34
37
  3. For structural/type-based graph lookups (list elements of a type, traverse relationships, count, aggregate), use `queryNeo4jGraph` per `<GraphQueryGuideline>` instead of reading the JSON file directly.
35
38
  </ExplorationGuideline>
36
39
 
40
+ <QueryPriorityGuideline>
41
+ 1. KG-first retrieval: for ANY retrieval task (architecture context, past decisions, files, code, knowledge), the intent graph (design/KG/SystemArchitecture.json via ARGO MCP) is the FIRST hop. Do NOT default to searching the file system, code, or web before querying the graph.
42
+ 2. Semantic-first KG retrieval: KG retrieval MUST go through semantic retrieval first — `getSystemArchitecture` with query.purpose + query.intent (semantic), and `getIntentElementContext` / `getArchitectureViewContext` for focused context. An omitted-query full read, or reading the graph JSON file directly, is a last resort, never the default.
43
+ 3. `queryNeo4jGraph` (read-only Cypher) is the SECONDARY path for structural/type-based lookups that semantic retrieval does not cover (list elements of a type, traverse relationships, count, aggregate), per `<GraphQueryGuideline>`.
44
+ 4. Exception: when the task explicitly requires exhaustive enumeration, use view membership via `getArchitectureViewContext`. Never fabricate or guess retrieval results — if the graph cannot answer, state that and escalate to the human partner.
45
+ 5. Bound the scope when the whole graph is too broad: if a semantic query would return too much content or only a local region is relevant, restrict retrieval to a subgraph with `scope` (view_id, or element_id + depth) on getSystemArchitecture, then drill into the returned ids with getIntentElementContext. Prefer a scoped read over an unbounded whole-graph read.
46
+ </QueryPriorityGuideline>
47
+
48
+ <ContentStoragePolicy>
49
+ 1. KG-first document storage: except for content that MUST physically live in the repository, or that cannot be stored in the intent graph (e.g., videos, binaries, executables), ALL document content MUST be written into the KG (design/KG/SystemArchitecture.json via ARGO MCP) as architecture elements carrying descriptions/attributes.
50
+ 2. Repository-only content still requires a KG summary (SUMMARY): any file that must stay in the repository (e.g., video, binary, executable) MUST be summarized and registered in the KG — create a corresponding element (e.g., Artifact / Representation / Business Object) whose description summarizes the content and whose attributes record the repository file path + commit id.
51
+ 3. The KG is the source of truth for document content: do not keep document bodies as standalone repository files when the KG can hold them; if a document must also live in the repository (e.g., a rendered/exported artifact), the KG element remains authoritative.
52
+ 4. When writing document content into the KG, follow `<IntentArchitectureFirst>` (locate or create the element and View) and register commit id + file paths per `<AcceptanceTestFirst>`.
53
+ </ContentStoragePolicy>
54
+
37
55
  <IntentArchitectureFirst>
38
56
  1. Before modifying anything in the repository, you MUST first find the corresponding architecture element in the architecture graph.
39
57
  2. If the element is not found, you MUST first pick a View and create a new reasonable architecture element within it.
@@ -56,17 +74,17 @@ When you are about to build an element, first look up the skills and resources n
56
74
 
57
75
  <ChangeTierGate>
58
76
  Every repository change MUST be classified into exactly one tier BEFORE implementation; the tier is declared explicitly. Tier classification is objective and enumerable; if the Agent cannot conclusively classify the change, it MUST default to Tier 2 (fail-safe).
59
- 1. Tier 1 — 行为无关(behavior-independent): qualifies ONLY if ALL of the following hold:
77
+ 1. Tier 1 — behavior-independent: qualifies ONLY if ALL of the following hold:
60
78
  - The diff touches only non-executable content: comments, documentation (including this rules file), whitespace/formatting-only hunks, or descriptive metadata text in the intent graph.
61
79
  - No executable logic, public interface/API surface, or test logic is changed (test files untouched).
62
80
  - No graph structure change: no element/relationship/view added, removed, renamed, or retyped.
63
- - Skipped ceremony(跳过验收回归与全量校验): acceptance test identification and regression, and full validateSystemArchitecture, are skipped (unless the graph was touched). Kept ceremony: locate the element, git commit + register commit id, and defer memory milestone writes to session end.
64
- 2. Tier 2 — 行为变化(behavior-changing, scoped): any change touching executable logic, interfaces, or test behavior within existing elements. Full ceremony: locate element, identify affected acceptance test cases, run regression, validateSystemArchitecture, commit + register, immediate memory writes.
65
- 3. Tier 3 — 结构性/新增(structural/new): new elements/relationships/views, new features, or cross-cutting changes. Full Tier 2 ceremony plus preview/apply mutation for any graph change.
81
+ - Skipped ceremony (acceptance regression and full validation): acceptance test identification and regression, and full validateSystemArchitecture, are skipped (unless the graph was touched). Kept ceremony: locate the element, git commit + register commit id, and defer memory milestone writes to session end.
82
+ 2. Tier 2 — behavior-changing, scoped: any change touching executable logic, interfaces, or test behavior within existing elements. Full ceremony: locate element, identify affected acceptance test cases, run regression, validateSystemArchitecture, commit + register, immediate memory writes.
83
+ 3. Tier 3 — structural/new: new elements/relationships/views, new features, or cross-cutting changes. Full Tier 2 ceremony plus preview/apply mutation for any graph change.
66
84
  4. Safety net (MUST, non-negotiable):
67
- - Declared tier is verified at commit time: the actual git diff file list is checked against the Tier 1 allowlist; if any disallowed file/hunk appears, the change automatically 升为 Tier 2 and MUST complete the acceptance regression and validation before finishing. Tier 1 is revocable, not merely declared.
68
- - KG 触线规则: any diff touching design/KG/SystemArchitecture.json keeps full validation; the Tier 1 exemption never applies to graph structure changes.
69
- - 零歧义默认升档(fail-safe): any uncertain classification MUST be treated as Tier 2, never Tier 1.
85
+ - Declared tier is verified at commit time: the actual git diff file list is checked against the Tier 1 allowlist; if any disallowed file/hunk appears, the change is automatically escalated to Tier 2 and MUST complete the acceptance regression and validation before finishing. Tier 1 is revocable, not merely declared.
86
+ - KG touch rule: any diff touching design/KG/SystemArchitecture.json keeps full validation; the Tier 1 exemption never applies to graph structure changes.
87
+ - Fail-safe (zero-ambiguity default escalation): any uncertain classification MUST be treated as Tier 2, never Tier 1.
70
88
  </ChangeTierGate>
71
89
 
72
90
  <CoperationGuideline>
@@ -78,8 +96,8 @@ Every repository change MUST be classified into exactly one tier BEFORE implemen
78
96
  </CoperationGuideline>
79
97
 
80
98
  <CapabilityDelegationGuideline>
81
- 1. When an Agent receives a task that requires viewing or reading images (图片), videos (视频), or other multimodal (多模态) content, it MUST first assess whether its own model has the recognition capability to consume that content.
82
- 2. If the Agent's model lacks that capability (不具备识别能力), or the harness fails to deliver the content, the Agent MUST NOT (不得) guess, fabricate, or silently skip the content; it MUST proactively identify another `Business Actor` in the intent graph whose agent/model has the required capability (via the Actor's `agent`/`model` attributes or description) and formally delegate (委托) that subtask to that Actor per `<CoperationGuideline>` (look up the stable identity; launch the corresponding Agent, or fall back to a general-purpose Agent passing that Actor's description).
99
+ 1. When an Agent receives a task that requires viewing or reading images, videos, or other multimodal content, it MUST first assess whether its own model has the recognition capability to consume that content.
100
+ 2. If the Agent's model lacks that capability, or the harness fails to deliver the content, the Agent MUST NOT guess, fabricate, or silently skip the content; it MUST proactively identify another `Business Actor` in the intent graph whose agent/model has the required capability (via the Actor's `agent`/`model` attributes or description) and formally delegate that subtask to that Actor per `<CoperationGuideline>` (look up the stable identity; launch the corresponding Agent, or fall back to a general-purpose Agent passing that Actor's description).
83
101
  3. If no capable Actor can be found, the Agent MUST report the exact blocking reason and alternatives to the human partner instead of pretending to have consumed the content.
84
102
  4. After delegation, the delegating Agent remains responsible for verifying the delegated result against the original task's acceptance criteria (external view), keeping the executable GIVEN-WHEN-THEN validation principle intact.
85
103
  </CapabilityDelegationGuideline>
@@ -103,7 +121,7 @@ The above immediate records also follow the conciseness and de-duplication requi
103
121
 
104
122
  <ToolsGuideline>
105
123
  You MUST read/write the intent architecture through the tools provided by the ARGO MCP server; direct modification of the intent architecture source file is forbidden:
106
- 1. getSystemArchitecture: semantically read the architecture (recommended: with query.purpose + query.intent, rather than a full read).
124
+ 1. getSystemArchitecture: semantically read the architecture — MUST supply query.purpose + query.intent (semantic retrieval per <QueryPriorityGuideline>); an omitted-query full read is a last resort, not the default.
107
125
  2. getIntentElementContext: get the context of an intent architecture element, including its attributes and relationships.
108
126
  3. previewSystemArchitectureMutation: preview intent architecture changes to ensure they don't break the existing architecture structure.
109
127
  4. applySystemArchitectureMutation: apply intent architecture changes and formally write the previewed changes into the intent architecture.
@@ -126,7 +144,7 @@ For structural/type-based graph lookups, use the read-only Neo4j Cypher interfac
126
144
  2. Construct a read-only Cypher statement and scope every pattern to the current graph with the server-injected `$graphKey` parameter (the value is filled by the server; the agent only writes the placeholder):
127
145
  MATCH (e:Element {graphKey: $graphKey, type: 'Business Actor'}) RETURN e.id, e.name ORDER BY e.name
128
146
  3. Never submit write clauses (CREATE, MERGE, DELETE, SET, REMOVE, DROP, LOAD CSV, FOREACH, IN TRANSACTIONS); the interface rejects them to protect the canonical JSON single source of truth.
129
- 4. Use it for structural queries: list elements of a type, traverse ARCHIMATE_RELATES edges, count and aggregate. For semantic/context reading (subgraph context, dependency traversal, view membership), keep using getSystemArchitecture (semantic query), getIntentElementContext, and getArchitectureViewContext.
147
+ 4. Use it as the SECONDARY path for structural/type-based lookups that semantic retrieval does not cover: list elements of a type, traverse ARCHIMATE_RELATES edges, count and aggregate. Semantic-first KG retrieval — semantic/context reading (getSystemArchitecture with query.purpose + query.intent, getIntentElementContext, getArchitectureViewContext) is the PRIORITY path per <QueryPriorityGuideline>.
130
148
  5. The query is read-only; never attempt to mutate the graph through Cypher.
131
149
  </GraphQueryGuideline>
132
150
 
@@ -66,6 +66,7 @@ const SYSTEM_ARCHITECTURE_TOOL_NAMES = new Set([
66
66
  'removeArchitectureView',
67
67
  'generateArchitectureDiffPlantuml',
68
68
  'queryNeo4jGraph',
69
+ 'memory_search',
69
70
  ]);
70
71
 
71
72
  const TOOLS = [
@@ -157,6 +158,16 @@ const TOOLS = [
157
158
  },
158
159
  intent: { type: 'string', description: 'Natural-language intent for semantic retrieval, for example "summarize business features for high-risk audit".' },
159
160
  subject: { type: 'string', description: 'Required for audit; optional anchor/focus id for other semantic purposes.' },
161
+ scope: {
162
+ type: 'object',
163
+ description: 'Optional subgraph scope to bound semantic retrieval and limit the returned content to a local region. Provide view_id to search within one view membership, or element_id (+ depth) to search within an element subtree.',
164
+ properties: {
165
+ view_id: { type: 'string', description: 'Restrict retrieval to the members of this view.' },
166
+ element_id: { type: 'string', description: 'Restrict retrieval to this element and its mounted sub-view subtree.' },
167
+ depth: { type: 'number', description: 'Default: 2. Subtree depth for element scope.' },
168
+ },
169
+ additionalProperties: false,
170
+ },
160
171
  },
161
172
  additionalProperties: true,
162
173
  },
@@ -36,7 +36,7 @@ const APPROVED_PROFILE = Object.freeze({
36
36
  model: 'qwen3.7-text-embedding',
37
37
  provider: 'alibaba-cloud-model-studio-openai-compatible-cn-beijing',
38
38
  version: 'qualification-2026-07-25',
39
- dimensions: 1024,
39
+ dimensions: 1536,
40
40
  });
41
41
  const CHANNELS = Object.freeze([
42
42
  Object.freeze({
@@ -68,12 +68,49 @@ const VECTOR_QUERY_CYPHER = [
68
68
  'RETURN properties(node) AS record, score',
69
69
  'ORDER BY score DESC',
70
70
  ].join('\n');
71
+ const VECTOR_QUERY_CYPHER_SCOPED = [
72
+ 'CALL db.index.vector.queryNodes($indexName, $topK, $vector)',
73
+ 'YIELD node, score',
74
+ 'WHERE node.channel = $channel AND node.canonicalIdentity IN $canonicalIdentities',
75
+ 'RETURN properties(node) AS record, score',
76
+ 'ORDER BY score DESC',
77
+ ].join('\n');
71
78
  const READINESS_QUERY_CYPHER = [
72
79
  'MATCH (readiness:ArgoProductionSemanticReadiness {identity: $identity})',
73
80
  'RETURN properties(readiness) AS readiness',
74
81
  ].join('\n');
75
82
  const INITIAL_WINDOW_SIZE = 2;
83
+ // Purpose-aware, env-configurable similarity thresholds + bounded top-K recall.
84
+ // Audit keeps the strict threshold (precision); memory-retrieval purposes use a
85
+ // looser threshold so relevant-but-paraphrased memory is recalled, while results
86
+ // are bounded to ARGO_SEMANTIC_TOP_K so noise stays bounded. Thresholds are
87
+ // overridable per channel via ARGO_SEMANTIC_{MEMORY|AUDIT}_THRESHOLD_<CHANNEL>.
88
+ const AUDIT_PURPOSES = new Set(['audit']);
89
+ const DEFAULT_MEMORY_THRESHOLD = 0.55;
90
+ const DEFAULT_AUDIT_THRESHOLD = 0.8;
91
+ const DEFAULT_TOP_K = 8;
76
92
  const SELECTED_VIEW_ID = 'semprod-wp2-default-retrieval-readiness';
93
+
94
+ function envNumber(key, fallback) {
95
+ const value = Number(process.env[key]);
96
+ return Number.isFinite(value) ? value : fallback;
97
+ }
98
+ function memoryThresholdFor(channel) {
99
+ return envNumber(
100
+ `ARGO_SEMANTIC_MEMORY_THRESHOLD_${channel.channel.toUpperCase()}`,
101
+ envNumber('ARGO_SEMANTIC_MEMORY_THRESHOLD', DEFAULT_MEMORY_THRESHOLD),
102
+ );
103
+ }
104
+ function auditThresholdFor(channel) {
105
+ return envNumber(
106
+ `ARGO_SEMANTIC_AUDIT_THRESHOLD_${channel.channel.toUpperCase()}`,
107
+ envNumber('ARGO_SEMANTIC_AUDIT_THRESHOLD', DEFAULT_AUDIT_THRESHOLD),
108
+ );
109
+ }
110
+ function resolveTopK() {
111
+ const k = envNumber('ARGO_SEMANTIC_TOP_K', DEFAULT_TOP_K);
112
+ return Number.isInteger(k) && k > 0 ? k : DEFAULT_TOP_K;
113
+ }
77
114
  const testCompositionStorage = new AsyncLocalStorage();
78
115
 
79
116
  function createDefaultSemanticRetrieval(dependencies = {}) {
@@ -159,22 +196,31 @@ async function executeWpP2Retrieval({
159
196
  readiness,
160
197
  configurationEvidence,
161
198
  }) {
199
+ const { canonicalIdentities, ...completeRequest } = request;
162
200
  const provider = createLiveEmbeddingProviderClient({
163
201
  configuration: configurationEvidence.configuration,
164
202
  transport: composition.transport,
165
203
  });
166
204
  const vector = await provider.embed(request.intent);
167
205
  requireQualifiedVector(vector);
206
+ const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
207
+ const strict = AUDIT_PURPOSES.has(purpose);
208
+ const topK = resolveTopK();
168
209
  const seedsByType = {};
169
210
  for (const channel of CHANNELS) {
170
211
  seedsByType[channel.key] = await exhaustChannel({
171
212
  channel,
172
213
  neo4jDriver: composition.neo4jDriver,
173
214
  vector,
215
+ threshold: strict ? auditThresholdFor(channel) : memoryThresholdFor(channel),
216
+ maxSeeds: topK,
217
+ ...(Array.isArray(canonicalIdentities) && canonicalIdentities.length > 0
218
+ ? { canonicalIdentities }
219
+ : {}),
174
220
  });
175
221
  }
176
222
  return completeSemanticResult({
177
- request,
223
+ request: completeRequest,
178
224
  canonicalGraph,
179
225
  readiness,
180
226
  seedsByType,
@@ -609,11 +655,21 @@ function publicReadinessOutcome(alignment) {
609
655
  };
610
656
  }
611
657
 
612
- async function exhaustChannel({ channel, neo4jDriver, vector }) {
658
+ async function exhaustChannel({
659
+ channel,
660
+ neo4jDriver,
661
+ vector,
662
+ canonicalIdentities,
663
+ threshold,
664
+ maxSeeds,
665
+ }) {
666
+ const scoped = Array.isArray(canonicalIdentities) && canonicalIdentities.length > 0;
613
667
  const accepted = [];
614
668
  const seen = new Set();
669
+ const effectiveThreshold = typeof threshold === 'number' ? threshold : channel.threshold;
670
+ const effectiveMax = Number.isInteger(maxSeeds) && maxSeeds > 0 ? maxSeeds : Number.POSITIVE_INFINITY;
615
671
  let offset = 0;
616
- while (true) {
672
+ while (accepted.length < effectiveMax) {
617
673
  const parameters = Object.freeze({
618
674
  indexName: channel.indexName,
619
675
  channel: channel.channel,
@@ -621,19 +677,21 @@ async function exhaustChannel({ channel, neo4jDriver, vector }) {
621
677
  windowSize: INITIAL_WINDOW_SIZE,
622
678
  topK: offset + INITIAL_WINDOW_SIZE,
623
679
  vector,
680
+ ...(scoped ? { canonicalIdentities } : {}),
624
681
  });
625
682
  const result = await neo4jDriver.execute(Object.freeze({
626
683
  kind: 'semantic-vector-window-query',
627
684
  channel: channel.channel,
628
685
  indexName: channel.indexName,
629
- cypher: VECTOR_QUERY_CYPHER,
686
+ cypher: scoped ? VECTOR_QUERY_CYPHER_SCOPED : VECTOR_QUERY_CYPHER,
630
687
  parameters,
631
688
  }));
632
689
  const records = Array.isArray(result && result.records) ? result.records : [];
633
690
  const newlyVisible = records.slice(offset);
634
691
  for (const raw of newlyVisible) {
692
+ if (accepted.length >= effectiveMax) break;
635
693
  const record = normalizeVectorRecord(raw, channel);
636
- if (record && record.score >= channel.threshold && !seen.has(record.id)) {
694
+ if (record && record.score >= effectiveThreshold && !seen.has(record.id)) {
637
695
  seen.add(record.id);
638
696
  accepted.push(Object.freeze(record));
639
697
  }
@@ -966,4 +1024,8 @@ function safeError(category) {
966
1024
  module.exports = {
967
1025
  createDefaultSemanticRetrieval,
968
1026
  withDefaultSemanticRetrievalTestComposition,
1027
+ memoryThresholdFor,
1028
+ auditThresholdFor,
1029
+ resolveTopK,
1030
+ AUDIT_PURPOSES,
969
1031
  };
@@ -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 !== 1024
49
+ vector.length !== 1536
50
50
  || vector.some(value => typeof value !== 'number' || !Number.isFinite(value))
51
51
  ) {
52
52
  throw safeError('LIVE_PROVIDER_RESPONSE_INVALID');
@@ -79,7 +79,7 @@ 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 !== 1024
82
+ || qualification.dimensions !== 1536
83
83
  ) {
84
84
  throw safeError('LIVE_PROVIDER_INDEX_WRITE_PROHIBITED');
85
85
  }
@@ -64,11 +64,11 @@ async function createApprovedNeo4jBoundary({ configuration, neo4j, logger }) {
64
64
  return result.records.map(record => normalizeNeo4jValue(record.get('evidence')));
65
65
  },
66
66
  async queryVectorEvidence(runId, vector, canonicalIdentities) {
67
- if (!Array.isArray(vector) || vector.length !== 1024 || !Array.isArray(canonicalIdentities)) {
67
+ if (!Array.isArray(vector) || vector.length !== 1536 || !Array.isArray(canonicalIdentities)) {
68
68
  throw safeError('LIVE_PROVIDER_OPERATION_FAILED');
69
69
  }
70
70
  await query(
71
- 'CREATE VECTOR INDEX argo_live_embedding_vector IF NOT EXISTS FOR (e:ArgoLiveEmbeddingEvidence) ON (e.vector) OPTIONS { indexConfig: { `vector.dimensions`: 1024, `vector.similarity_function`: "cosine" } }',
71
+ 'CREATE VECTOR INDEX argo_live_embedding_vector IF NOT EXISTS FOR (e:ArgoLiveEmbeddingEvidence) ON (e.vector) OPTIONS { indexConfig: { `vector.dimensions`: 1536, `vector.similarity_function`: "cosine" } }',
72
72
  {},
73
73
  );
74
74
  const result = await query(
@@ -21,6 +21,20 @@ const CONFIG_KEYS = Object.freeze([
21
21
  const OPTIONAL_CONFIG_KEYS = Object.freeze([
22
22
  'ARGO_NEO4J_DATABASE',
23
23
  ]);
24
+ // Semantic-retrieval tuning knobs (purpose-aware thresholds + bounded top-K).
25
+ // They are read by defaultSemanticRetrieval from process.env and whitelisted here
26
+ // so an approved env file may carry them without tripping SECRET_FILE_UNKNOWN_KEY.
27
+ const RETRIEVAL_TUNING_KEYS = Object.freeze([
28
+ 'ARGO_SEMANTIC_MEMORY_THRESHOLD',
29
+ 'ARGO_SEMANTIC_MEMORY_THRESHOLD_ELEMENT',
30
+ 'ARGO_SEMANTIC_MEMORY_THRESHOLD_RELATIONSHIP',
31
+ 'ARGO_SEMANTIC_MEMORY_THRESHOLD_VIEW',
32
+ 'ARGO_SEMANTIC_AUDIT_THRESHOLD',
33
+ 'ARGO_SEMANTIC_AUDIT_THRESHOLD_ELEMENT',
34
+ 'ARGO_SEMANTIC_AUDIT_THRESHOLD_RELATIONSHIP',
35
+ 'ARGO_SEMANTIC_AUDIT_THRESHOLD_VIEW',
36
+ 'ARGO_SEMANTIC_TOP_K',
37
+ ]);
24
38
  const OPT_IN_KEYS = Object.freeze({
25
39
  ARGO_LIVE_PROVIDER_E2E: 'LIVE_PROVIDER_E2E_OPT_IN_REQUIRED',
26
40
  ARGO_W31_LIVE_MUTATION_VECTOR_E2E: 'W31_MUTATION_VECTOR_E2E_OPT_IN_REQUIRED',
@@ -28,6 +42,7 @@ const OPT_IN_KEYS = Object.freeze({
28
42
  const READABLE_KEYS = Object.freeze([
29
43
  ...CONFIG_KEYS,
30
44
  ...OPTIONAL_CONFIG_KEYS,
45
+ ...RETRIEVAL_TUNING_KEYS,
31
46
  ...Object.keys(OPT_IN_KEYS),
32
47
  ]);
33
48
  const LEGACY_KEYS = Object.freeze(['ARGO_NEO4J_URI', 'ARGO_NEO4J_USERNAME', 'ARGO_NEO4J_PASSWORD']);
@@ -38,7 +53,7 @@ const APPROVED = Object.freeze({
38
53
  ARGO_EMBEDDING_MODEL: 'qwen3.7-text-embedding',
39
54
  ARGO_EMBEDDING_PROVIDER: 'alibaba-cloud-model-studio-openai-compatible-cn-beijing',
40
55
  ARGO_EMBEDDING_MODEL_VERSION: 'qualification-2026-07-25',
41
- ARGO_EMBEDDING_DIMENSIONS: '1024',
56
+ ARGO_EMBEDDING_DIMENSIONS: '1536',
42
57
  });
43
58
  const issuedAdapters = new WeakSet();
44
59
  const issuedTraces = new WeakSet();
@@ -167,7 +182,7 @@ async function resolveTrusted({
167
182
  embeddingModel: normalized.ARGO_EMBEDDING_MODEL,
168
183
  embeddingProvider: normalized.ARGO_EMBEDDING_PROVIDER,
169
184
  embeddingModelVersion: normalized.ARGO_EMBEDDING_MODEL_VERSION,
170
- embeddingDimensions: 1024,
185
+ embeddingDimensions: 1536,
171
186
  neo4jDatabaseUrl: normalized.ARGO_NEO4J_DATABASE_URL,
172
187
  neo4jDatabaseUsername: normalized.ARGO_NEO4J_DATABASE_USERNAME,
173
188
  neo4jDatabasePassword: normalized.ARGO_NEO4J_DATABASE_PASSWORD,
@@ -342,13 +357,15 @@ function preflightFile({ canonicalFilePath, configuredFilePath, filesystem, adap
342
357
  const insideRepository = adapters.systemMetadata.isSecretFileInsideGitRepository().status === 0;
343
358
  ignored = insideRepository ? adapters.systemMetadata.isSecretFileIgnored() : true;
344
359
  tracked = insideRepository ? adapters.systemMetadata.isSecretFileTracked() : false;
345
- const identityResult = adapters.systemMetadata.readCurrentIdentity();
346
- const aclResult = adapters.systemMetadata.readSecretFileAcl();
347
- aclEvidence = {
348
- status: aclResult.status,
349
- stdout: aclResult.stdout,
350
- identity: identityResult.status === 0 ? identityResult.stdout.trim() : '',
351
- };
360
+ if (process.platform === 'win32') {
361
+ const identityResult = adapters.systemMetadata.readCurrentIdentity();
362
+ const aclResult = adapters.systemMetadata.readSecretFileAcl();
363
+ aclEvidence = {
364
+ status: aclResult.status,
365
+ stdout: aclResult.stdout,
366
+ identity: identityResult.status === 0 ? identityResult.stdout.trim() : '',
367
+ };
368
+ }
352
369
  } else {
353
370
  ignored = adapters.git.isIgnored(configuredFilePath);
354
371
  tracked = adapters.git.isTracked(configuredFilePath);
@@ -356,7 +373,11 @@ function preflightFile({ canonicalFilePath, configuredFilePath, filesystem, adap
356
373
  }
357
374
  if (tracked) throw safeError('SECRET_FILE_TRACKED');
358
375
  if (!ignored) throw safeError('SECRET_FILE_NOT_IGNORED');
359
- validateAcl(aclEvidence);
376
+ if (process.platform === 'win32') {
377
+ validateAcl(aclEvidence);
378
+ } else {
379
+ validatePosixSecretFileAcl(configuredFilePath, filesystem);
380
+ }
360
381
  }
361
382
 
362
383
  function validateAcl(result) {
@@ -389,6 +410,20 @@ function parseAcl(output) {
389
410
  return result;
390
411
  }
391
412
 
413
+ function posixModeIsSecretSafe(mode) {
414
+ return (mode & 0o077) === 0 && (mode & 0o600) === 0o600;
415
+ }
416
+
417
+ function validatePosixSecretFileAcl(configuredFilePath, filesystem) {
418
+ const stat = filesystem.lstatSync(configuredFilePath);
419
+ if (!posixModeIsSecretSafe(stat.mode)) {
420
+ throw safeError('SECRET_FILE_ACL_UNSAFE');
421
+ }
422
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
423
+ throw safeError('SECRET_FILE_ACL_UNSAFE');
424
+ }
425
+ }
426
+
392
427
  function productionSourceBehavior(repositoryRoot) {
393
428
  return Object.freeze({
394
429
  expectedFilePath: getArgoEnvPath(),
@@ -478,4 +513,5 @@ function safeError(category) {
478
513
  module.exports = {
479
514
  resolveApprovedLiveConfiguration,
480
515
  withApprovedLiveConfigurationTestComposition,
516
+ posixModeIsSecretSafe,
481
517
  };
@@ -1143,7 +1143,7 @@ function createSyntheticVectorTransport() {
1143
1143
  return {
1144
1144
  data: [
1145
1145
  {
1146
- embedding: Array.from({ length: 1024 }, (_, index) => index / 2048),
1146
+ embedding: Array.from({ length: 1536 }, (_, index) => index / 2048),
1147
1147
  },
1148
1148
  ],
1149
1149
  };
@@ -15,6 +15,8 @@ function createProductionSemanticBackfill(dependencies = {}) {
15
15
  requireBoundary(dependencies.structuralProjection, 'requireComplete', 'structuralProjection');
16
16
  requireBoundary(dependencies.embeddingProvider, 'embedBatch', 'embeddingProvider');
17
17
  requireBoundary(dependencies.projectionStore, 'upsertRecords', 'projectionStore');
18
+ requireBoundary(dependencies.projectionStore, 'readRecords', 'projectionStore');
19
+ requireBoundary(dependencies.projectionStore, 'deleteTombstones', 'projectionStore');
18
20
  requireBoundary(dependencies.checkpointStore, 'readCheckpoint', 'checkpointStore');
19
21
  requireBoundary(dependencies.checkpointStore, 'writeCheckpoint', 'checkpointStore');
20
22
  const batchSize = dependencies.batchSize;
@@ -55,6 +57,11 @@ function createProductionSemanticBackfill(dependencies = {}) {
55
57
  checkpointStore: dependencies.checkpointStore,
56
58
  });
57
59
  }
60
+ const reconciled = await reconcileRemovals({
61
+ snapshot,
62
+ canonicalVersion,
63
+ projectionStore: dependencies.projectionStore,
64
+ });
58
65
  const aligned = CHANNELS.every(channel => (
59
66
  channels[channel].status === 'complete'
60
67
  && channels[channel].canonicalVersion === canonicalVersion
@@ -64,6 +71,7 @@ function createProductionSemanticBackfill(dependencies = {}) {
64
71
  canonicalVersion,
65
72
  alignmentState: aligned ? 'Aligned' : 'Updating',
66
73
  channels: Object.freeze(channels),
74
+ reconciled,
67
75
  });
68
76
  },
69
77
  });
@@ -204,6 +212,39 @@ function mergeFailures(existing, current) {
204
212
  return [...merged.values()];
205
213
  }
206
214
 
215
+ // Full-reconciliation removal pass: after all channels are upserted, delete any
216
+ // persisted semantic record whose canonical identity no longer exists in the
217
+ // local JSON snapshot. This guarantees argo-init rebuilds a semantic projection
218
+ // that is exactly consistent with the canonical graph (no stale vectors).
219
+ async function reconcileRemovals({ snapshot, canonicalVersion, projectionStore }) {
220
+ const currentByChannel = new Map();
221
+ for (const channel of CHANNELS) {
222
+ const source = CHANNEL_SOURCES[channel];
223
+ const identities = new Set(
224
+ (snapshot[source.property] || []).map(record => `${channel}:${source.identity(record)}`),
225
+ );
226
+ currentByChannel.set(channel, identities);
227
+ }
228
+ const stored = await projectionStore.readRecords();
229
+ const allStored = Array.isArray(stored) ? stored : [];
230
+ const stale = allStored.filter(record => {
231
+ const identities = currentByChannel.get(record.channel);
232
+ return identities && !identities.has(record.canonicalIdentity);
233
+ });
234
+ const tombstones = stale.map(record => Object.freeze({
235
+ canonicalIdentity: record.canonicalIdentity,
236
+ channel: record.channel,
237
+ canonicalVersion: record.canonicalVersion || canonicalVersion,
238
+ }));
239
+ if (tombstones.length > 0) {
240
+ await projectionStore.deleteTombstones(tombstones);
241
+ }
242
+ return Object.freeze({
243
+ scanned: allStored.length,
244
+ tombstoned: tombstones.length,
245
+ });
246
+ }
247
+
207
248
  function requireBoundary(boundary, method, name) {
208
249
  if (!boundary || typeof boundary[method] !== 'function') {
209
250
  throw new TypeError(`${name}.${method} is required`);
@@ -90,14 +90,31 @@ function createProductionSemanticNeo4jAdapter(dependencies = {}) {
90
90
  });
91
91
  }
92
92
 
93
+ const EMBEDDING_DIMENSIONS = 1536;
94
+
93
95
  async function ensureVectorIndexes(session) {
94
96
  for (const definition of Object.values(CHANNEL_INDEXES)) {
97
+ // CREATE ... IF NOT EXISTS cannot change an existing index's dimensions, so
98
+ // when the approved embedding dimension changes (e.g. 1024 -> 1536) drop the
99
+ // stale index first and recreate it at the approved dimension.
100
+ const existing = await executeRead(
101
+ session,
102
+ 'SHOW INDEXES YIELD name, type, options WHERE type = \'VECTOR\' AND name = $name RETURN name, options',
103
+ { name: definition.indexName },
104
+ );
105
+ if (existing.records.length > 0) {
106
+ const options = existing.records[0].get('options');
107
+ const dim = options && options.indexConfig && options.indexConfig['vector.dimensions'];
108
+ if (dim !== EMBEDDING_DIMENSIONS) {
109
+ await executeWrite(session, `DROP INDEX ${definition.indexName} IF EXISTS`, {});
110
+ }
111
+ }
95
112
  await executeWrite(
96
113
  session,
97
114
  [
98
115
  `CREATE VECTOR INDEX ${definition.indexName} IF NOT EXISTS`,
99
116
  `FOR (semantic:${definition.label}) ON (semantic.vector)`,
100
- 'OPTIONS { indexConfig: { `vector.dimensions`: 1024, `vector.similarity_function`: "cosine" } }',
117
+ `OPTIONS { indexConfig: { \`vector.dimensions\`: ${EMBEDDING_DIMENSIONS}, \`vector.similarity_function\`: "cosine" } }`,
101
118
  ].join('\n'),
102
119
  {},
103
120
  );
@@ -128,7 +128,7 @@ const W31_APPROVED_PROFILE = Object.freeze({
128
128
  baseUrl: 'https://llm-clids9mqc5o1mbvb.cn-beijing.maas.aliyuncs.com/compatible-mode/v1',
129
129
  model: 'qwen3.7-text-embedding',
130
130
  version: 'qualification-2026-07-25',
131
- dimensions: 1024,
131
+ dimensions: 1536,
132
132
  source: 'explicit-human-approval',
133
133
  });
134
134
 
@@ -204,6 +204,16 @@ const TOOLS = [
204
204
  },
205
205
  intent: { type: 'string', description: 'Natural-language intent for semantic retrieval, for example "summarize business features for high-risk audit".' },
206
206
  subject: { type: 'string', description: 'Required for audit; optional anchor/focus id for other semantic purposes.' },
207
+ scope: {
208
+ type: 'object',
209
+ description: 'Optional subgraph scope to bound semantic retrieval and limit the returned content to a local region. Provide view_id to search within one view membership, or element_id (+ depth) to search within an element subtree.',
210
+ properties: {
211
+ view_id: { type: 'string', description: 'Restrict retrieval to the members of this view.' },
212
+ element_id: { type: 'string', description: 'Restrict retrieval to this element and its mounted sub-view subtree.' },
213
+ depth: { type: 'number', description: 'Default: 2. Subtree depth for element scope.' },
214
+ },
215
+ additionalProperties: false,
216
+ },
207
217
  },
208
218
  additionalProperties: true,
209
219
  },
@@ -396,6 +406,20 @@ const TOOLS = [
396
406
  additionalProperties: false,
397
407
  },
398
408
  },
409
+ {
410
+ name: 'memory_search',
411
+ description: 'Semantic memory search: retrieve the user\'s memory by natural-language query. Runs embedding-based semantic retrieval (memory-retrieval purpose) over the intent graph and returns the top-k relevant memory items (id, name, type, similarity score, and a description excerpt with its full length). Each hit is a lightweight card: to keep the tool result compact, description is truncated to max_desc_len characters (default 800) and the full text length is reported as description_length. To read the full text of a hit, call getIntentElementContext with its id. Agents SHOULD call this tool to look up the user\'s memory before answering a question about the user.',
412
+ inputSchema: {
413
+ type: 'object',
414
+ properties: {
415
+ query: { type: 'string', description: 'Natural-language query describing the user memory to retrieve.' },
416
+ top_k: { type: 'integer', description: 'Optional max number of hits to return (default 8).' },
417
+ max_desc_len: { type: 'integer', description: 'Optional max characters of description to return per hit. Default 800. Use 0 for the full description, or -1 to omit description and return only the length.' },
418
+ architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
419
+ },
420
+ additionalProperties: false,
421
+ },
422
+ },
399
423
  ];
400
424
 
401
425
  // Every tool accepts an optional per-call `workspaceRoot` (absolute path,
@@ -2178,9 +2202,91 @@ async function callTool(name, args = {}, dependencies = undefined) {
2178
2202
  return queryNeo4jGraphTool(args);
2179
2203
  }
2180
2204
 
2205
+ if (name === 'memory_search') {
2206
+ return memorySearchTool(args, dependencies);
2207
+ }
2208
+
2181
2209
  throw new Error(`Unknown tool: ${name}`);
2182
2210
  }
2183
2211
 
2212
+ // memory_search: natural-language semantic memory retrieval. Reuses the
2213
+ // memory-retrieval purpose (implementation-design) of the semantic journey
2214
+ // (same path as getSystemArchitecture, so readiness/closure behave identically)
2215
+ // and reshapes the retrieved elements into a memory-oriented hit list with
2216
+ // content + similarity score, so an agent can look up the user's memory in one
2217
+ // call.
2218
+ async function memorySearchTool(args = {}, dependencies = undefined) {
2219
+ const query = typeof args.query === 'string' ? args.query.trim() : '';
2220
+ if (!query) {
2221
+ return { status: 'failed', error: { category: 'MEMORY_QUERY_REQUIRED', message: 'query is required' } };
2222
+ }
2223
+ const topK = Number.isInteger(args.top_k) && args.top_k > 0 ? args.top_k : 8;
2224
+ // max_desc_len: default 800 (compact excerpt). 0 = full description;
2225
+ // -1 = omit description entirely and return only the length. Keeps the tool
2226
+ // result compact (production memory cards, not full session dumps).
2227
+ let maxDescLen = 800;
2228
+ if (Number.isInteger(args.max_desc_len) && args.max_desc_len !== 800) {
2229
+ maxDescLen = args.max_desc_len;
2230
+ }
2231
+ const context = await loadContext(args);
2232
+ let retrieved;
2233
+ try {
2234
+ const journey = await resolveSemanticOperatorJourney(dependencies);
2235
+ retrieved = await journey.query({ purpose: 'implementation-design', intent: query });
2236
+ } catch (error) {
2237
+ return {
2238
+ status: 'failed',
2239
+ error: {
2240
+ category: error && error.category ? error.category : 'MEMORY_RETRIEVAL_FAILED',
2241
+ message: error && error.message ? error.message : 'Memory retrieval failed',
2242
+ },
2243
+ };
2244
+ }
2245
+ const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
2246
+ const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
2247
+ if (subset.status !== 'passed') {
2248
+ return { status: 'failed', error: subset.error || { category: 'MEMORY_RETRIEVAL_FAILED' } };
2249
+ }
2250
+ const hits = (Array.isArray(subset.document && subset.document.elements) ? subset.document.elements : [])
2251
+ .filter(element => element && typeof element.semanticScore === 'number')
2252
+ .sort((left, right) => right.semanticScore - left.semanticScore)
2253
+ .slice(0, topK)
2254
+ .map(element => Object.freeze(memoryHitCard(element, maxDescLen)));
2255
+ return {
2256
+ status: 'passed',
2257
+ query,
2258
+ max_desc_len: maxDescLen,
2259
+ fullTextHint: 'To read the full text of a hit, call getIntentElementContext with its id (elementId).',
2260
+ hits: Object.freeze(hits),
2261
+ };
2262
+ }
2263
+
2264
+ // Build one compact memory hit: id/name/type/score + an excerpt of the
2265
+ // description bounded by maxDescLen, plus the full text length so the caller
2266
+ // knows how much content exists and can decide whether to expand.
2267
+ function memoryHitCard(element, maxDescLen) {
2268
+ const description = typeof element.description === 'string' ? element.description : '';
2269
+ const descriptionLength = description.length;
2270
+ let excerpt = '';
2271
+ if (maxDescLen !== -1) {
2272
+ excerpt = maxDescLen === 0 ? description : description.slice(0, maxDescLen);
2273
+ }
2274
+ const card = {
2275
+ id: element.id,
2276
+ name: element.name,
2277
+ type: element.type,
2278
+ score: element.semanticScore,
2279
+ description_length: descriptionLength,
2280
+ };
2281
+ if (maxDescLen !== -1) {
2282
+ card.description = excerpt;
2283
+ if (maxDescLen > 0 && descriptionLength > maxDescLen) {
2284
+ card.truncated = true;
2285
+ }
2286
+ }
2287
+ return card;
2288
+ }
2289
+
2184
2290
  async function queryNeo4jGraphTool(args = {}) {
2185
2291
  const architecturePath = args.architecturePath || DEFAULT_GRAPH_PATH;
2186
2292
  const workspaceRoot = resolveWorkspaceRoot(args);
@@ -2291,6 +2397,16 @@ async function executeSemanticSystemArchitectureQuery(args, dependencies) {
2291
2397
  : {}),
2292
2398
  });
2293
2399
  const query = args.query;
2400
+ const scopeResolution = resolveSemanticScope(context.document, query && query.scope);
2401
+ if (scopeResolution.status === 'failed') {
2402
+ return getSystemArchitectureResult(queryError(
2403
+ scopeResolution.error.category,
2404
+ scopeResolution.error.message,
2405
+ ));
2406
+ }
2407
+ const queryForRetrieval = Array.isArray(scopeResolution.identities) && scopeResolution.identities.length > 0
2408
+ ? { ...(query || {}), canonicalIdentities: scopeResolution.identities }
2409
+ : query;
2294
2410
  const contractOptions = semanticContractOptions(args, dependencies);
2295
2411
  const canonicalSubsetContract = isCanonicalSubsetSemanticContract(query, contractOptions);
2296
2412
  if (canonicalSubsetContract) {
@@ -2310,7 +2426,7 @@ async function executeSemanticSystemArchitectureQuery(args, dependencies) {
2310
2426
  }
2311
2427
  let document;
2312
2428
  try {
2313
- const retrieved = await semanticRetrievalBoundary.retrieve(query);
2429
+ const retrieved = await semanticRetrievalBoundary.retrieve(queryForRetrieval);
2314
2430
  if (canonicalSubsetContract) {
2315
2431
  const subset = buildCanonicalSemanticDocumentSubset(retrieved, context.document);
2316
2432
  if (subset.status === 'failed') {
@@ -2352,6 +2468,14 @@ async function executeSemanticSystemArchitectureQuery(args, dependencies) {
2352
2468
  semanticErrorEvidence,
2353
2469
  ));
2354
2470
  }
2471
+ if (
2472
+ Array.isArray(scopeResolution.identities)
2473
+ && scopeResolution.identities.length > 0
2474
+ && document
2475
+ && typeof document === 'object'
2476
+ ) {
2477
+ document = applySemanticScopeFilter(document, scopeResolution.identities);
2478
+ }
2355
2479
  const semanticPayload = {
2356
2480
  status: 'passed',
2357
2481
  graphPath: context.graphPath.relativePath,
@@ -2381,6 +2505,109 @@ function shouldReturnDebugSemanticResult(query) {
2381
2505
  ).toLowerCase());
2382
2506
  }
2383
2507
 
2508
+ function resolveSemanticScope(document, scope) {
2509
+ if (!scope || typeof scope !== 'object') {
2510
+ return { identities: undefined };
2511
+ }
2512
+ const elements = new Set();
2513
+ const relationships = new Set();
2514
+ const views = new Set();
2515
+
2516
+ if (typeof scope.view_id === 'string' && scope.view_id) {
2517
+ const view = (document.views || []).find(item => item && item.view_id === scope.view_id);
2518
+ if (!view) {
2519
+ return {
2520
+ status: 'failed',
2521
+ error: {
2522
+ category: 'SCOPE_VIEW_NOT_FOUND',
2523
+ message: `Scope view '${scope.view_id}' does not exist in the canonical graph`,
2524
+ },
2525
+ };
2526
+ }
2527
+ (view.included_elements || []).forEach(id => elements.add(String(id)));
2528
+ (view.included_relationships || []).forEach(id => relationships.add(String(id)));
2529
+ views.add(String(scope.view_id));
2530
+ return { identities: scopeIdentitiesOf(elements, relationships, views) };
2531
+ }
2532
+
2533
+ if (typeof scope.element_id === 'string' && scope.element_id) {
2534
+ const root = (document.elements || []).find(item => item && item.id === scope.element_id);
2535
+ if (!root) {
2536
+ return {
2537
+ status: 'failed',
2538
+ error: {
2539
+ category: 'SCOPE_ELEMENT_NOT_FOUND',
2540
+ message: `Scope element '${scope.element_id}' does not exist in the canonical graph`,
2541
+ },
2542
+ };
2543
+ }
2544
+ const depth = Number.isInteger(scope.depth) && scope.depth > 0 ? scope.depth : 2;
2545
+ elements.add(String(scope.element_id));
2546
+ const queue = [{ elementId: scope.element_id, level: 0 }];
2547
+ const visited = new Set();
2548
+ while (queue.length > 0) {
2549
+ const { elementId, level } = queue.shift();
2550
+ if (visited.has(elementId)) continue;
2551
+ visited.add(elementId);
2552
+ const element = (document.elements || []).find(item => item && item.id === elementId);
2553
+ const subViews = Array.isArray(element && element.subdiagram_views)
2554
+ ? element.subdiagram_views
2555
+ : [];
2556
+ for (const subView of subViews) {
2557
+ const view = (document.views || []).find(item => item && item.view_id === subView.view_id);
2558
+ if (!view) continue;
2559
+ views.add(String(view.view_id));
2560
+ (view.included_relationships || []).forEach(id => relationships.add(String(id)));
2561
+ for (const childId of (view.included_elements || [])) {
2562
+ elements.add(String(childId));
2563
+ if (level < depth - 1) {
2564
+ queue.push({ elementId: childId, level: level + 1 });
2565
+ }
2566
+ }
2567
+ }
2568
+ }
2569
+ return { identities: scopeIdentitiesOf(elements, relationships, views) };
2570
+ }
2571
+
2572
+ return { identities: undefined };
2573
+ }
2574
+
2575
+ function scopeIdentitiesOf(elements, relationships, views) {
2576
+ return [...elements, ...relationships, ...views];
2577
+ }
2578
+
2579
+ function applySemanticScopeFilter(document, identities) {
2580
+ if (!document || typeof document !== 'object' || !Array.isArray(identities)) {
2581
+ return document;
2582
+ }
2583
+ const allowed = new Set(identities.map(id => String(id)));
2584
+ if (
2585
+ Array.isArray(document.elements)
2586
+ || Array.isArray(document.relationships)
2587
+ || Array.isArray(document.views)
2588
+ ) {
2589
+ return Object.freeze({
2590
+ ...document,
2591
+ elements: Object.freeze((document.elements || []).filter(item => item && allowed.has(String(item.id)))),
2592
+ relationships: Object.freeze((document.relationships || []).filter(item => item && allowed.has(String(item.id)))),
2593
+ views: Object.freeze((document.views || []).filter(item => item && allowed.has(String(item.view_id)))),
2594
+ });
2595
+ }
2596
+ if (document.businessObjects) {
2597
+ return Object.freeze({
2598
+ ...document,
2599
+ businessObjects: Object.freeze({
2600
+ elements: Object.freeze((document.businessObjects.elements || []).filter(item => item && allowed.has(String(item.id)))),
2601
+ relationships: Object.freeze((document.businessObjects.relationships || []).filter(item => item && allowed.has(String(item.id)))),
2602
+ views: Object.freeze((document.businessObjects.views || []).filter(item => item && allowed.has(String(item.view_id)))),
2603
+ }),
2604
+ semanticSeeds: Object.freeze((document.semanticSeeds || []).filter(seed => seed && allowed.has(String(seed.objectId)))),
2605
+ hitReasons: Object.freeze((document.hitReasons || []).filter(reason => reason && allowed.has(String(reason.objectId)))),
2606
+ });
2607
+ }
2608
+ return document;
2609
+ }
2610
+
2384
2611
  function buildBusinessSemanticSummary(retrieved, query = {}) {
2385
2612
  if (retrieved && retrieved.responseProfile === 'business-summary') return retrieved;
2386
2613
  if (retrieved && retrieved.result && retrieved.result.responseProfile === 'business-summary') return retrieved.result;
@@ -2649,15 +2876,52 @@ function buildCanonicalSemanticDocumentSubset(source, canonicalDocument = undefi
2649
2876
  }
2650
2877
  }
2651
2878
 
2879
+ // Attach the semantic similarity score to each retrieved object so callers can
2880
+ // judge relevance (top-K bounded recall may include borderline neighbours).
2881
+ const semanticScoreById = new Map();
2882
+ const seedsByType = evidence.seedsByType || {};
2883
+ for (const channelSeeds of Object.values(seedsByType)) {
2884
+ for (const seed of Array.isArray(channelSeeds) ? channelSeeds : []) {
2885
+ if (!seed || !seed.id) continue;
2886
+ const numericScore = Number(seed.score);
2887
+ if (!Number.isFinite(numericScore)) continue;
2888
+ const bareId = String(seed.id).replace(/^[^:]*:/, '');
2889
+ if (bareId && (!semanticScoreById.has(bareId) || numericScore > semanticScoreById.get(bareId))) {
2890
+ semanticScoreById.set(bareId, numericScore);
2891
+ }
2892
+ }
2893
+ }
2894
+ // buildCanonicalSemanticDocumentSubset may run a second time over a prior
2895
+ // subset document (which no longer carries seedsByType); carry scores forward
2896
+ // from evidence elements that already expose semanticScore.
2897
+ for (const item of evidenceElements) {
2898
+ if (!item) continue;
2899
+ const numericScore = Number(item.semanticScore);
2900
+ if (!Number.isFinite(numericScore)) continue;
2901
+ const key = String(item.id !== undefined ? item.id : item.view_id);
2902
+ if (key && (!semanticScoreById.has(key) || numericScore > semanticScoreById.get(key))) {
2903
+ semanticScoreById.set(key, numericScore);
2904
+ }
2905
+ }
2906
+ const withSemanticScore = item => {
2907
+ if (!item) return item;
2908
+ const score = item.id !== undefined
2909
+ ? semanticScoreById.get(String(item.id))
2910
+ : semanticScoreById.get(String(item.view_id));
2911
+ return typeof score === 'number' ? Object.freeze({ ...item, semanticScore: score }) : item;
2912
+ };
2652
2913
  const elements = [...elementIds]
2653
2914
  .map(id => canonicalElementById.get(id))
2654
- .filter(Boolean);
2915
+ .filter(Boolean)
2916
+ .map(withSemanticScore);
2655
2917
  const relationships = [...relationshipIds]
2656
2918
  .map(id => canonicalRelationshipById.get(id))
2657
- .filter(Boolean);
2919
+ .filter(Boolean)
2920
+ .map(withSemanticScore);
2658
2921
  const views = [...viewIds]
2659
2922
  .map(id => canonicalViewById.get(id))
2660
- .filter(Boolean);
2923
+ .filter(Boolean)
2924
+ .map(withSemanticScore);
2661
2925
 
2662
2926
  return {
2663
2927
  status: 'passed',
@@ -3348,8 +3612,11 @@ module.exports = {
3348
3612
  compactMutationResponse,
3349
3613
  createDefaultCanonicalSemanticInitComposition,
3350
3614
  createDefaultProductionSemanticOperatorJourney,
3615
+ applySemanticScopeFilter,
3351
3616
  handleRequest,
3352
3617
  loadContext,
3353
3618
  main,
3619
+ memoryHitCard,
3620
+ resolveSemanticScope,
3354
3621
  validateDocument,
3355
3622
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.10.30",
3
+ "version": "0.10.32",
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": {