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.
- package/argo/rules/archgraph.instructions.md +29 -11
- package/argo/scripts/argo-mcp-server.js +11 -0
- package/argo/scripts/graph-rag/defaultSemanticRetrieval.js +68 -6
- package/argo/scripts/graph-rag/liveEmbeddingIndexGate.js +2 -2
- package/argo/scripts/graph-rag/liveEmbeddingNeo4jBoundary.js +2 -2
- package/argo/scripts/graph-rag/liveEmbeddingProviderConfig.js +46 -10
- package/argo/scripts/graph-rag/mutationEmbeddingVectorLifecycle.js +1 -1
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticBackfill.js +41 -0
- package/argo/scripts/graph-rag/semantic-persistence/productionSemanticNeo4jAdapter.js +18 -1
- package/argo/scripts/systemarchitecture-mcp-server.js +272 -5
- package/package.json +1 -1
|
@@ -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 —
|
|
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
|
|
64
|
-
2. Tier 2 —
|
|
65
|
-
3. Tier 3 —
|
|
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
|
|
68
|
-
- KG
|
|
69
|
-
-
|
|
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
|
|
82
|
-
2. If the Agent's model lacks that capability
|
|
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
|
|
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
|
|
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:
|
|
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({
|
|
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 (
|
|
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 >=
|
|
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 !==
|
|
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 !==
|
|
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 !==
|
|
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`:
|
|
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: '
|
|
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:
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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