archgraph-argo 0.16.2 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,168 +1,156 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "ArchGraph / ARGO global workflow rules: locate the architecture element in the intent graph before making any change, acceptance tests first (executable GIVEN-WHEN-THEN), read/write the intent graph only through the global ARGO MCP, and register commit id + file paths after committing. Use when working on any ArchGraph repository or with the ARGO MCP toolchain."
|
|
3
|
-
name: "ArchGraph ARGO Workflow Rules"
|
|
4
|
-
applyTo: "**"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
<WakeupGuideline>
|
|
8
|
-
STEP 0 — UNCONDITIONAL STARTUP GATE.
|
|
9
|
-
1. Your
|
|
10
|
-
2. Restore
|
|
11
|
-
3. If the `agent` attribute
|
|
12
|
-
4. If the ARGO MCP is unavailable or errors,
|
|
13
|
-
</WakeupGuideline>
|
|
14
|
-
|
|
15
|
-
<CoreRules>
|
|
16
|
-
|
|
17
|
-
1.
|
|
18
|
-
2.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
21
|
-
5.
|
|
22
|
-
6. Continuously comply with
|
|
23
|
-
7. KG-first
|
|
24
|
-
8.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
1.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
Recall
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
1. Ask for the projection schema first: call `queryNeo4jGraph` with {schema: true} to learn node labels (ArchitectureGraph, Element, ArchitectureRelationship, View), relationship types (OWNS_ELEMENT, OWNS_RELATIONSHIP, OWNS_VIEW, RELATIONSHIP_SOURCE, RELATIONSHIP_TARGET, ARCHIMATE_RELATES, VIEW_OF, INCLUDES_ELEMENT, INCLUDES_RELATIONSHIP, HAS_SUBDIAGRAM), property keys, and the legal ArchiMate element/relationship type enums (e.g. 'Business Actor', 'Assignment').
|
|
158
|
-
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):
|
|
159
|
-
MATCH (e:Element {graphKey: $graphKey, type: 'Business Actor'}) RETURN e.id, e.name ORDER BY e.name
|
|
160
|
-
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.
|
|
161
|
-
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>.
|
|
162
|
-
5. The query is read-only; never attempt to mutate the graph through Cypher.
|
|
163
|
-
</GraphQueryGuideline>
|
|
164
|
-
|
|
165
|
-
<Attention>
|
|
166
|
-
you MUST make sure the knowledge graph the ARGO MCP server is handling is actually the one in this repository (design/KG/SystemArchitecture.json) and not some other knowledge graph; otherwise, you MUST stop and report the issue to your human partner before doing anything else.
|
|
167
|
-
</Attention>
|
|
168
|
-
</ToolsGuideline>
|
|
1
|
+
---
|
|
2
|
+
description: "ArchGraph / ARGO global workflow rules: locate the architecture element in the intent graph before making any change, acceptance tests first (executable GIVEN-WHEN-THEN), read/write the intent graph only through the global ARGO MCP, and register commit id + file paths after committing. Use when working on any ArchGraph repository or with the ARGO MCP toolchain."
|
|
3
|
+
name: "ArchGraph ARGO Workflow Rules"
|
|
4
|
+
applyTo: "**"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<WakeupGuideline>
|
|
8
|
+
STEP 0 — UNCONDITIONAL STARTUP GATE. Applies to the very first message of every session, greetings and chat included; never skip, defer, or treat it as optional.
|
|
9
|
+
1. Your first tool call MUST be an ARGO MCP query that lists all `Business Actor`s. Confirm which Actor you are; if unsure, ask your human partner before anything else.
|
|
10
|
+
2. Restore memory: load ONLY the T1 working-memory digest — your T1 summary, the most recent/active memory cards, and any session hooks (unfinished items from the last session). Do NOT bulk-load the T2 long-term memory or T3 archive; recall them on demand via `memory_search`, then read full content with `getIntentElementContext`. T2 is a recall target, not as a bulk context load.
|
|
11
|
+
3. If the Actor's `agent` attribute differs from your type, switch to it or delegate per `<CoperationGuideline>` item 2.
|
|
12
|
+
4. If the ARGO MCP is unavailable or errors, say so before doing anything else.
|
|
13
|
+
</WakeupGuideline>
|
|
14
|
+
|
|
15
|
+
<CoreRules>
|
|
16
|
+
Non-negotiable red lines (MUST). Never skip, simplify, or silently violate them; each is expanded in its `<...>` section.
|
|
17
|
+
1. Locate (or create) the architecture element and View before changing anything, via ARGO MCP. See `<IntentArchitectureFirst>`.
|
|
18
|
+
2. Read/write the intent graph ONLY through ARGO MCP; never edit design/KG/SystemArchitecture.json directly. See `<ToolsGuideline>`.
|
|
19
|
+
3. Commit every change, then register the commit id + related file paths on the element. See `<IntentArchitectureFirst>` item 4.
|
|
20
|
+
4. Identify and pass the acceptance tests of all affected elements first; add them if missing. Tier 1 changes are exempt per `<ChangeTierGate>`. See `<AcceptanceTestFirst>`.
|
|
21
|
+
5. Write this session's key progress to long-term memory before finishing. See `<SessionMemorySummarization>`.
|
|
22
|
+
6. Continuously comply with these red lines throughout the session.
|
|
23
|
+
7. Retrieve KG-first and semantic-first. See `<QueryPriorityGuideline>`.
|
|
24
|
+
8. Store content KG-first. See `<ContentStoragePolicy>`.
|
|
25
|
+
9. Never duplicate: reuse first, and handle duplicate rejections by reusing or updating. See `<GraphDeduplication>`.
|
|
26
|
+
</CoreRules>
|
|
27
|
+
|
|
28
|
+
<Ontology>
|
|
29
|
+
Your architecture is ArchiMate 3.2 plus ARGO extensions. Reference files live under ~/.argo (Windows: %USERPROFILE%\.argo):
|
|
30
|
+
1. Legal graph structure: ~/.argo/schema/SystemArchitecture.schema.json
|
|
31
|
+
2. Element/relationship type definitions: ~/.argo/schema/archimate3.2.md
|
|
32
|
+
</Ontology>
|
|
33
|
+
|
|
34
|
+
<ExplorationGuideline>
|
|
35
|
+
0. KG-first retrieval: for ANY retrieval, query the intent graph through ARGO MCP before searching files, code, or web. See `<QueryPriorityGuideline>`.
|
|
36
|
+
1. Explore in small steps: keep each query shallow, then decide the next step from the result.
|
|
37
|
+
2. Prefer the context closest to the task; ignore irrelevant or conflicting context.
|
|
38
|
+
3. For structural/type lookups (list by type, traverse, count, aggregate), use `queryNeo4jGraph` per `<GraphQueryGuideline>` — never read the JSON file.
|
|
39
|
+
</ExplorationGuideline>
|
|
40
|
+
|
|
41
|
+
<QueryPriorityGuideline>
|
|
42
|
+
1. KG-first retrieval: the intent graph (design/KG/SystemArchitecture.json via ARGO MCP) is the FIRST hop for any retrieval task. Do NOT search files, code, or web first.
|
|
43
|
+
2. Semantic-first KG retrieval: use `getSystemArchitecture` with query.purpose + query.intent, and `getIntentElementContext` / `getArchitectureViewContext` for focused context. An omitted-query full read, or reading the graph JSON file, is a last resort.
|
|
44
|
+
3. `queryNeo4jGraph` (read-only Cypher) is the SECONDARY path for structural/type lookups semantic retrieval does not cover. See `<GraphQueryGuideline>`.
|
|
45
|
+
4. Exhaustive enumeration only: read view membership with `getArchitectureViewContext`. Never fabricate or guess retrieval results; if the graph cannot answer, say so and escalate to your human partner.
|
|
46
|
+
5. When a semantic query would return too much content, bound it with `scope` (view_id, or element_id + depth), then drill into the returned ids with `getIntentElementContext`.
|
|
47
|
+
</QueryPriorityGuideline>
|
|
48
|
+
|
|
49
|
+
<ContentStoragePolicy>
|
|
50
|
+
1. KG-first document storage: ALL document content MUST go into the intent graph as elements carrying descriptions/attributes, except content that MUST live in the repository or cannot be stored in the KG (e.g. videos, binaries, executables).
|
|
51
|
+
2. Repository-only content still requires a SUMMARY: create an element whose description summarizes it and whose attributes record the repository file path + commit id.
|
|
52
|
+
3. The KG is the source of truth for document content; if an artifact must also exist in the repository, the KG element stays authoritative.
|
|
53
|
+
4. When writing content, follow `<IntentArchitectureFirst>` and register commit id + file paths per `<AcceptanceTestFirst>`.
|
|
54
|
+
</ContentStoragePolicy>
|
|
55
|
+
|
|
56
|
+
<GraphDeduplication>
|
|
57
|
+
Never add what the graph already has. Reuse first.
|
|
58
|
+
1. Before adding an element, relationship, or view, look for an existing match (an element: same type + name; a relationship: same source + type + target + name; a view: same parent + name) and reuse it — pass `onConflict: "reuse"` whenever the identity is known or likely to exist.
|
|
59
|
+
2. If an add is rejected as a duplicate, act on the existing id(s) it returns — reuse or update them. Never retry to force a second copy.
|
|
60
|
+
3. Add a same-name duplicate only for a genuinely distinct object, and only with `onConflict: "allowDuplicate"` plus a real `justification`.
|
|
61
|
+
4. Preview/apply may return semantic near-duplicate hints: review them and reuse when appropriate, otherwise proceed. They are advisory and never block a write.
|
|
62
|
+
5. Updates are never gated. Never work around a duplicate by editing around it — reuse or update the existing object.
|
|
63
|
+
</GraphDeduplication>
|
|
64
|
+
|
|
65
|
+
<IntentArchitectureFirst>
|
|
66
|
+
1. Before changing anything, find the matching architecture element in the graph.
|
|
67
|
+
2. If it is missing, pick a View and create a reasonable element in it.
|
|
68
|
+
3. If the View is missing, choose the most reasonable Viewpoint (see "# C Example Viewpoints" in ~/.argo/schema/archimate3.2.md) and create the View.
|
|
69
|
+
4. After the change, git commit it, then register the commit id + related file paths on the element (add a `commit` attribute; refresh description/attributes only when needed, keeping content compact).
|
|
70
|
+
</IntentArchitectureFirst>
|
|
71
|
+
|
|
72
|
+
<ArmingFirst>
|
|
73
|
+
Before building an element, look up the skills and resources needed and put them in session memory.
|
|
74
|
+
</ArmingFirst>
|
|
75
|
+
|
|
76
|
+
<AcceptanceTestFirst>
|
|
77
|
+
1. Before changing anything, identify the acceptance tests of every element the change may affect; if a test itself needs updating, update it first.
|
|
78
|
+
2. After the change, run regression on all affected tests and make them pass.
|
|
79
|
+
3. If no acceptance test covers the change, add one before implementing it.
|
|
80
|
+
4. Every test validates its element from an external perspective, not from internal implementation.
|
|
81
|
+
5. Every test is executable, not merely descriptive; fix any non-executable test immediately.
|
|
82
|
+
6. Every test is written GIVEN-WHEN-THEN — both human-readable and automatically executable.
|
|
83
|
+
</AcceptanceTestFirst>
|
|
84
|
+
|
|
85
|
+
<ChangeTierGate>
|
|
86
|
+
Classify every change into exactly one tier BEFORE implementing, and declare it. If unsure, default to Tier 2 (fail-safe).
|
|
87
|
+
1. Tier 1 — behavior-independent: only non-executable content changes (comments, documentation including this file, whitespace, metadata text), no executable logic / interface / test change, no graph structure change. Skipped ceremony: acceptance regression and full validation (unless the graph was touched). Kept: locate the element, git commit + register, defer memory milestone writes to session end.
|
|
88
|
+
2. Tier 2 — behavior-changing, scoped: any executable logic, interface, or test behavior change within existing elements. Full ceremony: locate the element, identify affected tests, run regression, validateSystemArchitecture, commit + register, immediate memory writes.
|
|
89
|
+
3. Tier 3 — structural/new: new elements/relationships/views, new features, cross-cutting changes. Tier 2 ceremony plus preview/apply for any graph change.
|
|
90
|
+
4. Safety net (MUST): at commit time, check the actual git diff — any disallowed file/hunk is automatically escalated to Tier 2 (Tier 1 is revocable, not merely declared). KG touch rule: any change to design/KG/SystemArchitecture.json keeps full validation. Fail-safe: treat any uncertain classification as Tier 2, never Tier 1.
|
|
91
|
+
</ChangeTierGate>
|
|
92
|
+
|
|
93
|
+
<CoperationGuideline>
|
|
94
|
+
0. Do only your assigned role's work; never do another `Business Actor`'s work. If you need another Actor, delegate formally.
|
|
95
|
+
1. To delegate, look up the Actor by its stable identity (name/id); if it does not exist, create the `Business Actor` and register a globally unique name.
|
|
96
|
+
2. Before delegating, read the Actor's `agent` attribute: if present, launch an Agent of that type; if absent or launch fails, delegate to a general-purpose Agent passing the Actor's description.
|
|
97
|
+
3. An Actor's long-term memory is the SUBVIEW hierarchy whose Views' `parent_element_id` points to that Actor (with their elements and nested sub-views) — NOT the View that merely includes the Actor. It holds all of that Actor's historical work.
|
|
98
|
+
4. Keep Actors isolated: each works in its own session/context and must not interfere with others.
|
|
99
|
+
</CoperationGuideline>
|
|
100
|
+
|
|
101
|
+
<CapabilityDelegationGuideline>
|
|
102
|
+
1. When a task needs reading images, videos, or other multimodal content, first check whether your model has the recognition capability.
|
|
103
|
+
2. If it lacks that capability (or the harness does not deliver the content), you MUST NOT guess, fabricate, or silently skip it. Find another `Business Actor` whose `agent`/`model` has the capability and delegate per `<CoperationGuideline>` (launch that Agent type, or fall back to a general-purpose Agent with the Actor's description).
|
|
104
|
+
3. If no capable Actor exists, report the exact blocking reason and alternatives to your human partner.
|
|
105
|
+
4. After delegating, verify the result against the original acceptance criteria (external view); keep the executable GIVEN-WHEN-THEN validation principle.
|
|
106
|
+
</CapabilityDelegationGuideline>
|
|
107
|
+
|
|
108
|
+
<SessionMemorySummarization>
|
|
109
|
+
Memory capture: milestone immediate writes are the reliable backbone; the session summary is opportunistic and idempotent.
|
|
110
|
+
1. Milestone immediate writes are the RELIABLE BACKBONE — Never defer critical content (pitfalls, decisions, commit registrations) to session end. See `<MemoryTriggerTiming>`.
|
|
111
|
+
2. Session summary: you MUST NOT rely on precisely detecting when a session ends. Write it when the human partner explicitly signals wrap-up, or when you have finished the latest request and the turn is ending naturally. Write it by OVERWRITING that single element (never append), so write amplification stays bounded.
|
|
112
|
+
3. Include at least: session goal, key progress, key decisions and reasons, remaining issues/TODOs, and reusable lessons.
|
|
113
|
+
4. Route it to the Actor's T1 working memory; route long-term capture to the T2 long-term memory sub-view hierarchy whose `parent_element_id` points to that Actor (see `<CoperationGuideline>` item 3). Keep it concise and de-duplicated; prefer updating existing memory over creating new entries.
|
|
114
|
+
</SessionMemorySummarization>
|
|
115
|
+
|
|
116
|
+
<MemoryTriggerTiming>
|
|
117
|
+
Write long-term memory IMMEDIATELY at these moments — this is the backbone; never defer to session end:
|
|
118
|
+
1. Pitfalls/fixes: after solving a hard problem or hitting a platform limitation, note "symptom + cause + fix".
|
|
119
|
+
2. Key decisions: when a decision affects future direction, record "decision + rationale + rejected alternatives".
|
|
120
|
+
3. Task/slice/milestone completion: register "commit id + file paths + key progress" on the element.
|
|
121
|
+
immediate writes are primarily triggered IMMEDIATELY and follow the concise, de-duplicated style of `<SessionMemorySummarization>`; the session-end summary is a separate, opportunistic write, not the primary path.
|
|
122
|
+
</MemoryTriggerTiming>
|
|
123
|
+
|
|
124
|
+
<MemoryRecallGuideline>
|
|
125
|
+
Recall in TWO steps — a compact card is a LOCATOR, never the full memory:
|
|
126
|
+
1. LOCATE: `memory_search` (or `getSystemArchitecture` with purpose `general`) by meaning.
|
|
127
|
+
2. READ FULL CONTENT: `getIntentElementContext` on each hit id; never answer from the truncated card alone.
|
|
128
|
+
Layering: LOOSE memory threshold for recall — do not reject a low-but-relevant hit; strict for `audit`. Reject only when there are ZERO relevant hits.
|
|
129
|
+
Recall target: an Actor's T2 long-term memory is its `<actor>-ltm-001` view hierarchy; T3 archive is `<actor>-archive-001`, read only by explicit retrieval. Recall on demand — never bulk-load memory.
|
|
130
|
+
</MemoryRecallGuideline>
|
|
131
|
+
|
|
132
|
+
<MemoryTierConventions>
|
|
133
|
+
Operate the three-tier memory directly from this rule (no skill load):
|
|
134
|
+
1. Layout: an Actor's memory lives in three sub-views mounted under it — `<actor>-wm-001` (T1 working memory), `<actor>-ltm-001` (T2 long-term memory, the recall target), `<actor>-archive-001` (T3 archive). Memory elements carry memoryTier = T1|T2|T3.
|
|
135
|
+
2. T1 summary: the single member of `<actor>-wm-001`. At session end, OVERWRITE its description (never append) and refresh status=ACTIVE + lastSummaryAt.
|
|
136
|
+
3. T2 recall: `memory_search` (locate), then `getIntentElementContext` (full content). See `<MemoryRecallGuideline>`.
|
|
137
|
+
4. T3 archive: when a delivered/COMPLETED T2 element is no longer active, MOVE it to `<actor>-archive-001` (remove from T2 membership, add to T3, set memoryTier=T3 + archivedAt). Never delete archived memories.
|
|
138
|
+
</MemoryTierConventions>
|
|
139
|
+
|
|
140
|
+
<ToolsGuideline>
|
|
141
|
+
Read/write the intent graph ONLY through ARGO MCP. Never edit design/KG/SystemArchitecture.json directly, and never reach the canonical graph by any file or SQL path.
|
|
142
|
+
1. `getSystemArchitecture` MUST supply query.purpose + query.intent; an omitted-query full read is a last resort.
|
|
143
|
+
2. Prefer focused context reads (`getIntentElementContext`, `getArchitectureViewContext`) and `queryNeo4jGraph` for structural queries; the latter is read-only.
|
|
144
|
+
3. Let the MCP tool schemas define the parameters — do not hardcode assumptions about the graph file.
|
|
145
|
+
</ToolsGuideline>
|
|
146
|
+
|
|
147
|
+
<GraphQueryGuideline>
|
|
148
|
+
Use `queryNeo4jGraph` for structural/type lookups. It never mutates the canonical graph.
|
|
149
|
+
1. Scope every query to the current graph with the injected `$graphKey` parameter.
|
|
150
|
+
2. Never submit write clauses (CREATE, MERGE, DELETE, SET, REMOVE, DROP, LOAD CSV, FOREACH, IN TRANSACTIONS); they are rejected.
|
|
151
|
+
3. Semantic/context reads (`getSystemArchitecture`, `getIntentElementContext`, `getArchitectureViewContext`) are the PRIORITY path; Cypher is the SECONDARY path per `<QueryPriorityGuideline>`.
|
|
152
|
+
</GraphQueryGuideline>
|
|
153
|
+
|
|
154
|
+
<Attention>
|
|
155
|
+
Confirm the ARGO MCP server is serving this repository's graph (design/KG/SystemArchitecture.json), not another graph. If it is not, stop and report to your human partner before doing anything else.
|
|
156
|
+
</Attention>
|
|
@@ -149,6 +149,8 @@ const TOOLS = [
|
|
|
149
149
|
properties: {
|
|
150
150
|
element: { type: 'object' },
|
|
151
151
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
152
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy. fail (default): reject an exact (type, normalized name) duplicate and return its candidates. reuse: find-or-create — attach the existing element instead of duplicating. allowDuplicate: create anyway, requires justification.' },
|
|
153
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
152
154
|
architecturePath: { type: 'string', description: 'Default: design/KG/SystemArchitecture.json' },
|
|
153
155
|
},
|
|
154
156
|
additionalProperties: false,
|
|
@@ -191,6 +193,8 @@ const TOOLS = [
|
|
|
191
193
|
properties: {
|
|
192
194
|
relationship: { type: 'object' },
|
|
193
195
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
196
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy. fail (default): reject an exact (source, type, target, normalized name) duplicate and return its candidates. reuse: find-or-create — attach the existing relationship instead of duplicating. allowDuplicate: create anyway, requires justification.' },
|
|
197
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
194
198
|
architecturePath: { type: 'string', description: 'Default: design/KG/SystemArchitecture.json' },
|
|
195
199
|
},
|
|
196
200
|
additionalProperties: false,
|
|
@@ -232,6 +236,8 @@ const TOOLS = [
|
|
|
232
236
|
required: ['view'],
|
|
233
237
|
properties: {
|
|
234
238
|
view: { type: 'object' },
|
|
239
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy. fail (default): reject a duplicate (parent_element_id, normalized view_name) and return its candidates. reuse: attach the existing view. allowDuplicate: create anyway, requires justification.' },
|
|
240
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
235
241
|
architecturePath: { type: 'string', description: 'Default: design/KG/SystemArchitecture.json' },
|
|
236
242
|
},
|
|
237
243
|
additionalProperties: false,
|
|
@@ -241,6 +241,8 @@ const TOOLS = [
|
|
|
241
241
|
properties: {
|
|
242
242
|
element: { type: 'object' },
|
|
243
243
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
244
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy. fail (default): reject an exact (type, normalized name) duplicate and return its candidates. reuse: find-or-create — attach the existing element instead of duplicating. allowDuplicate: create anyway, requires justification.' },
|
|
245
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
244
246
|
dryRun: { type: 'boolean', description: 'When true, validates and returns the result without writing to the graph. Default: false.' },
|
|
245
247
|
architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
|
|
246
248
|
},
|
|
@@ -286,6 +288,8 @@ const TOOLS = [
|
|
|
286
288
|
properties: {
|
|
287
289
|
relationship: { type: 'object' },
|
|
288
290
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
291
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy. fail (default): reject an exact (source, type, target, normalized name) duplicate and return its candidates. reuse: find-or-create — attach the existing relationship instead of duplicating. allowDuplicate: create anyway, requires justification.' },
|
|
292
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
289
293
|
dryRun: { type: 'boolean', description: 'When true, validates and returns the result without writing to the graph. Default: false.' },
|
|
290
294
|
architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
|
|
291
295
|
},
|
|
@@ -330,6 +334,8 @@ const TOOLS = [
|
|
|
330
334
|
required: ['view'],
|
|
331
335
|
properties: {
|
|
332
336
|
view: { type: 'object' },
|
|
337
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy. fail (default): reject a duplicate (parent_element_id, normalized view_name) and return its candidates. reuse: attach the existing view. allowDuplicate: create anyway, requires justification.' },
|
|
338
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
|
|
333
339
|
dryRun: { type: 'boolean', description: 'When true, validates and returns the result without writing to the graph. Default: false.' },
|
|
334
340
|
architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
|
|
335
341
|
},
|
|
@@ -470,6 +476,8 @@ function mutationInputSchema() {
|
|
|
470
476
|
view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
|
|
471
477
|
element_ids: { type: 'array', items: { type: 'string' } },
|
|
472
478
|
relationship_ids: { type: 'array', items: { type: 'string' } },
|
|
479
|
+
onConflict: { type: 'string', enum: ['fail', 'reuse', 'allowDuplicate'], description: 'L0 dedup policy for add* mutations. fail (default): reject an exact natural-key duplicate and return its candidates. reuse: find-or-create — attach the existing object instead of creating a duplicate. allowDuplicate: create a new object, requires a non-empty justification.' },
|
|
480
|
+
justification: { type: 'string', description: 'Required when onConflict is allowDuplicate; recorded as the reason a semantically-equal duplicate is intentionally created.' },
|
|
473
481
|
},
|
|
474
482
|
additionalProperties: false,
|
|
475
483
|
},
|
|
@@ -1157,6 +1165,92 @@ function mergeAttributesPatch(existing, patchEntries) {
|
|
|
1157
1165
|
return result;
|
|
1158
1166
|
}
|
|
1159
1167
|
|
|
1168
|
+
const DUPLICATE_CONFLICT_POLICIES = new Set(['fail', 'reuse', 'allowDuplicate']);
|
|
1169
|
+
|
|
1170
|
+
// L0 dedup gate: normalize a name to a stable natural-key component. NFKC folds
|
|
1171
|
+
// full-width forms, whitespace is collapsed, and case is folded, so " Widget ",
|
|
1172
|
+
// "widget", and "Widget" all collide.
|
|
1173
|
+
function normalizeDedupName(value) {
|
|
1174
|
+
return String(value === undefined || value === null ? '' : value)
|
|
1175
|
+
.normalize('NFKC')
|
|
1176
|
+
.replace(/\s+/g, ' ')
|
|
1177
|
+
.trim()
|
|
1178
|
+
.toLowerCase();
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function findDuplicateElements(elements, element) {
|
|
1182
|
+
const nameKey = normalizeDedupName(element.name);
|
|
1183
|
+
return (elements || [])
|
|
1184
|
+
.filter(candidate => candidate && candidate.type === element.type && normalizeDedupName(candidate.name) === nameKey)
|
|
1185
|
+
.map(candidate => ({ id: candidate.id, type: candidate.type, name: candidate.name }));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function findDuplicateRelationships(relationships, relationship) {
|
|
1189
|
+
const nameKey = normalizeDedupName(relationship.name);
|
|
1190
|
+
return (relationships || [])
|
|
1191
|
+
.filter(candidate => candidate
|
|
1192
|
+
&& candidate.source_id === relationship.source_id
|
|
1193
|
+
&& candidate.type === relationship.type
|
|
1194
|
+
&& candidate.target_id === relationship.target_id
|
|
1195
|
+
&& normalizeDedupName(candidate.name) === nameKey)
|
|
1196
|
+
.map(candidate => ({
|
|
1197
|
+
id: candidate.id,
|
|
1198
|
+
type: candidate.type,
|
|
1199
|
+
source_id: candidate.source_id,
|
|
1200
|
+
target_id: candidate.target_id,
|
|
1201
|
+
name: candidate.name,
|
|
1202
|
+
}));
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function findDuplicateViews(views, view) {
|
|
1206
|
+
const parentId = view.parent_element_id || '';
|
|
1207
|
+
const nameKey = normalizeDedupName(view.view_name);
|
|
1208
|
+
return (views || [])
|
|
1209
|
+
.filter(candidate => candidate
|
|
1210
|
+
&& (candidate.parent_element_id || '') === parentId
|
|
1211
|
+
&& normalizeDedupName(candidate.view_name) === nameKey)
|
|
1212
|
+
.map(candidate => ({
|
|
1213
|
+
view_id: candidate.view_id,
|
|
1214
|
+
view_name: candidate.view_name,
|
|
1215
|
+
parent_element_id: candidate.parent_element_id,
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Resolve an add against its exact natural-key candidates under the caller's
|
|
1220
|
+
// onConflict policy: fail (default) rejects and returns candidates; reuse is
|
|
1221
|
+
// find-or-create (attach the existing object); allowDuplicate creates only with
|
|
1222
|
+
// a non-empty justification. Semantic similarity (L1) is never handled here.
|
|
1223
|
+
function resolveDuplicateConflict(options, candidates) {
|
|
1224
|
+
const onConflict = options.onConflict === undefined || options.onConflict === null
|
|
1225
|
+
? 'fail'
|
|
1226
|
+
: options.onConflict;
|
|
1227
|
+
if (!DUPLICATE_CONFLICT_POLICIES.has(onConflict)) {
|
|
1228
|
+
throw new Error(`onConflict must be one of fail, reuse, allowDuplicate (got '${onConflict}')`);
|
|
1229
|
+
}
|
|
1230
|
+
if (candidates.length === 0) {
|
|
1231
|
+
return { action: 'create' };
|
|
1232
|
+
}
|
|
1233
|
+
if (onConflict === 'fail') {
|
|
1234
|
+
const error = new Error(
|
|
1235
|
+
`Duplicate ${options.label} already exists (${candidates.map(candidate => candidate.id).join(', ')}). ` +
|
|
1236
|
+
'Reuse it with onConflict:"reuse", or pass onConflict:"allowDuplicate" with a non-empty justification to create a new one.',
|
|
1237
|
+
);
|
|
1238
|
+
error.duplicateConflicts = candidates;
|
|
1239
|
+
throw error;
|
|
1240
|
+
}
|
|
1241
|
+
if (onConflict === 'reuse') {
|
|
1242
|
+
return { action: 'reuse', existing: candidates[0] };
|
|
1243
|
+
}
|
|
1244
|
+
if (typeof options.justification !== 'string' || options.justification.trim() === '') {
|
|
1245
|
+
const error = new Error(
|
|
1246
|
+
`onConflict "allowDuplicate" for ${options.label} requires a non-empty justification.`,
|
|
1247
|
+
);
|
|
1248
|
+
error.duplicateConflicts = candidates;
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
return { action: 'create', justification: options.justification };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1160
1254
|
function applyMutations(document, mutations) {
|
|
1161
1255
|
const nextDocument = clone(document);
|
|
1162
1256
|
const touchedElementIds = new Set();
|
|
@@ -1179,21 +1273,35 @@ function applyMutations(document, mutations) {
|
|
|
1179
1273
|
const scopedViews = requireViewScope(nextDocument.views, mutation.view_ids, 'mutation.view_ids');
|
|
1180
1274
|
requireId(mutation.element.id, 'mutation.element.id');
|
|
1181
1275
|
const existingElement = findById(nextDocument.elements, mutation.element.id);
|
|
1276
|
+
let targetElementId = mutation.element.id;
|
|
1277
|
+
let reusedElement = false;
|
|
1182
1278
|
if (!existingElement) {
|
|
1183
|
-
nextDocument.elements
|
|
1184
|
-
|
|
1279
|
+
const candidates = findDuplicateElements(nextDocument.elements, mutation.element);
|
|
1280
|
+
const resolution = resolveDuplicateConflict({
|
|
1281
|
+
onConflict: mutation.onConflict,
|
|
1282
|
+
justification: mutation.justification,
|
|
1283
|
+
label: `element (type '${mutation.element.type}', name '${mutation.element.name}')`,
|
|
1284
|
+
}, candidates);
|
|
1285
|
+
if (resolution.action === 'reuse') {
|
|
1286
|
+
targetElementId = resolution.existing.id;
|
|
1287
|
+
reusedElement = true;
|
|
1288
|
+
} else {
|
|
1289
|
+
nextDocument.elements.push(clone(mutation.element));
|
|
1290
|
+
syncViewsToElementSubdiagramViews(nextDocument, findById(nextDocument.elements, mutation.element.id));
|
|
1291
|
+
}
|
|
1185
1292
|
}
|
|
1186
1293
|
for (const view of scopedViews) {
|
|
1187
|
-
view.included_elements = addUnique(view.included_elements || [], [
|
|
1294
|
+
view.included_elements = addUnique(view.included_elements || [], [targetElementId]);
|
|
1188
1295
|
touchedViewIds.add(view.view_id);
|
|
1189
1296
|
viewLimitCheckIds.add(view.view_id);
|
|
1190
1297
|
}
|
|
1191
|
-
touchedElementIds.add(
|
|
1298
|
+
touchedElementIds.add(targetElementId);
|
|
1192
1299
|
mutationSummaries.push({
|
|
1193
1300
|
type: mutation.type,
|
|
1194
|
-
id:
|
|
1301
|
+
id: targetElementId,
|
|
1195
1302
|
view_ids: mutation.view_ids,
|
|
1196
|
-
created: !existingElement,
|
|
1303
|
+
created: !existingElement && !reusedElement,
|
|
1304
|
+
...(reusedElement ? { reused: true, reusedId: targetElementId, requestedId: mutation.element.id } : {}),
|
|
1197
1305
|
});
|
|
1198
1306
|
continue;
|
|
1199
1307
|
}
|
|
@@ -1286,23 +1394,41 @@ function applyMutations(document, mutations) {
|
|
|
1286
1394
|
const scopedViews = requireViewScope(nextDocument.views, mutation.view_ids, 'mutation.view_ids');
|
|
1287
1395
|
requireId(mutation.relationship.id, 'mutation.relationship.id');
|
|
1288
1396
|
const existingRelationship = findById(nextDocument.relationships, mutation.relationship.id);
|
|
1397
|
+
let targetRelationshipId = mutation.relationship.id;
|
|
1398
|
+
let sourceElementId = mutation.relationship.source_id;
|
|
1399
|
+
let targetEndpointId = mutation.relationship.target_id;
|
|
1400
|
+
let reusedRelationship = false;
|
|
1289
1401
|
if (!existingRelationship) {
|
|
1290
|
-
nextDocument.relationships
|
|
1402
|
+
const candidates = findDuplicateRelationships(nextDocument.relationships, mutation.relationship);
|
|
1403
|
+
const resolution = resolveDuplicateConflict({
|
|
1404
|
+
onConflict: mutation.onConflict,
|
|
1405
|
+
justification: mutation.justification,
|
|
1406
|
+
label: `relationship (source '${mutation.relationship.source_id}', type '${mutation.relationship.type}', target '${mutation.relationship.target_id}', name '${mutation.relationship.name || ''}')`,
|
|
1407
|
+
}, candidates);
|
|
1408
|
+
if (resolution.action === 'reuse') {
|
|
1409
|
+
targetRelationshipId = resolution.existing.id;
|
|
1410
|
+
sourceElementId = resolution.existing.source_id;
|
|
1411
|
+
targetEndpointId = resolution.existing.target_id;
|
|
1412
|
+
reusedRelationship = true;
|
|
1413
|
+
} else {
|
|
1414
|
+
nextDocument.relationships.push(clone(mutation.relationship));
|
|
1415
|
+
}
|
|
1291
1416
|
}
|
|
1292
1417
|
for (const view of scopedViews) {
|
|
1293
1418
|
view.included_elements = addUnique(view.included_elements || [], [
|
|
1294
|
-
|
|
1295
|
-
|
|
1419
|
+
sourceElementId,
|
|
1420
|
+
targetEndpointId,
|
|
1296
1421
|
]);
|
|
1297
|
-
view.included_relationships = addUnique(view.included_relationships || [], [
|
|
1422
|
+
view.included_relationships = addUnique(view.included_relationships || [], [targetRelationshipId]);
|
|
1298
1423
|
touchedViewIds.add(view.view_id);
|
|
1299
1424
|
}
|
|
1300
|
-
touchedRelationshipIds.add(
|
|
1425
|
+
touchedRelationshipIds.add(targetRelationshipId);
|
|
1301
1426
|
mutationSummaries.push({
|
|
1302
1427
|
type: mutation.type,
|
|
1303
|
-
id:
|
|
1428
|
+
id: targetRelationshipId,
|
|
1304
1429
|
view_ids: mutation.view_ids,
|
|
1305
|
-
created: !existingRelationship,
|
|
1430
|
+
created: !existingRelationship && !reusedRelationship,
|
|
1431
|
+
...(reusedRelationship ? { reused: true, reusedId: targetRelationshipId, requestedId: mutation.relationship.id } : {}),
|
|
1306
1432
|
});
|
|
1307
1433
|
continue;
|
|
1308
1434
|
}
|
|
@@ -1381,11 +1507,28 @@ function applyMutations(document, mutations) {
|
|
|
1381
1507
|
if (findView(nextDocument.views, mutation.view.view_id)) {
|
|
1382
1508
|
throw new Error(`View '${mutation.view.view_id}' already exists`);
|
|
1383
1509
|
}
|
|
1384
|
-
nextDocument.views
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1510
|
+
const candidates = findDuplicateViews(nextDocument.views, mutation.view);
|
|
1511
|
+
const resolution = resolveDuplicateConflict({
|
|
1512
|
+
onConflict: mutation.onConflict,
|
|
1513
|
+
justification: mutation.justification,
|
|
1514
|
+
label: `view (name '${mutation.view.view_name}')`,
|
|
1515
|
+
}, candidates);
|
|
1516
|
+
if (resolution.action === 'reuse') {
|
|
1517
|
+
mutationSummaries.push({
|
|
1518
|
+
type: mutation.type,
|
|
1519
|
+
id: resolution.existing.view_id,
|
|
1520
|
+
created: false,
|
|
1521
|
+
reused: true,
|
|
1522
|
+
reusedId: resolution.existing.view_id,
|
|
1523
|
+
requestedId: mutation.view.view_id,
|
|
1524
|
+
});
|
|
1525
|
+
} else {
|
|
1526
|
+
nextDocument.views.push(clone(mutation.view));
|
|
1527
|
+
upsertSubdiagramViewIntoElement(nextDocument, mutation.view.parent_element_id, mutation.view);
|
|
1528
|
+
touchedViewIds.add(mutation.view.view_id);
|
|
1529
|
+
viewLimitCheckIds.add(mutation.view.view_id);
|
|
1530
|
+
mutationSummaries.push({ type: mutation.type, id: mutation.view.view_id });
|
|
1531
|
+
}
|
|
1389
1532
|
continue;
|
|
1390
1533
|
}
|
|
1391
1534
|
|
|
@@ -1614,14 +1757,14 @@ function removeEntries(existing, removals) {
|
|
|
1614
1757
|
return (Array.isArray(existing) ? existing : []).filter(entry => !removalSet.has(entry));
|
|
1615
1758
|
}
|
|
1616
1759
|
|
|
1617
|
-
async function buildMutationResult(context, mutations, write) {
|
|
1760
|
+
async function buildMutationResult(context, mutations, write, dependencies) {
|
|
1618
1761
|
const beforeSummary = summarizeDocument(context.document);
|
|
1619
1762
|
let mutationResult;
|
|
1620
1763
|
try {
|
|
1621
1764
|
mutationResult = applyMutations(context.document, mutations);
|
|
1622
1765
|
} catch (error) {
|
|
1623
1766
|
const errors = [String(error && error.message ? error.message : error)];
|
|
1624
|
-
|
|
1767
|
+
const failed = {
|
|
1625
1768
|
status: 'failed',
|
|
1626
1769
|
written: false,
|
|
1627
1770
|
graphPath: context.graphPath.relativePath,
|
|
@@ -1634,6 +1777,13 @@ async function buildMutationResult(context, mutations, write) {
|
|
|
1634
1777
|
errors,
|
|
1635
1778
|
guidance: buildFailureGuidance(errors),
|
|
1636
1779
|
};
|
|
1780
|
+
if (error && Array.isArray(error.duplicateConflicts) && error.duplicateConflicts.length > 0) {
|
|
1781
|
+
failed.duplicateConflicts = error.duplicateConflicts;
|
|
1782
|
+
failed.guidance = addUnique(failed.guidance || [], [
|
|
1783
|
+
'Duplicate write blocked (L0 dedup gate): reuse the existing id with onConflict:"reuse" (find-or-create), or pass onConflict:"allowDuplicate" with a non-empty justification to create a new object.',
|
|
1784
|
+
]);
|
|
1785
|
+
}
|
|
1786
|
+
return failed;
|
|
1637
1787
|
}
|
|
1638
1788
|
const errors = validateDocument(mutationResult.document, context.schema, {
|
|
1639
1789
|
touchedRelationshipIds: mutationResult.touchedRelationshipIds,
|
|
@@ -1658,6 +1808,15 @@ async function buildMutationResult(context, mutations, write) {
|
|
|
1658
1808
|
result.guidance = buildFailureGuidance(errors);
|
|
1659
1809
|
}
|
|
1660
1810
|
|
|
1811
|
+
// L1 advisory: semantic near-duplicate suggestions for element adds. Never
|
|
1812
|
+
// blocks or fails the write; preview and apply both surface it.
|
|
1813
|
+
if (errors.length === 0) {
|
|
1814
|
+
const semanticDedup = await buildSemanticDedupAdvisory(context, mutations, dependencies);
|
|
1815
|
+
if (semanticDedup) {
|
|
1816
|
+
result.semanticDedup = semanticDedup;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1661
1820
|
if (errors.length > 0 || !write) {
|
|
1662
1821
|
return result;
|
|
1663
1822
|
}
|
|
@@ -2269,6 +2428,11 @@ function compactMutationResponse(payload) {
|
|
|
2269
2428
|
if (payload && payload.neo4jSync) {
|
|
2270
2429
|
compact.neo4jSync = { status: payload.neo4jSync.status };
|
|
2271
2430
|
}
|
|
2431
|
+
// L1 dedup advisory must survive the compact successful-write response: it is
|
|
2432
|
+
// the whole point of surfacing (advisory) semantic near-duplicates to the caller.
|
|
2433
|
+
if (payload && payload.semanticDedup) {
|
|
2434
|
+
compact.semanticDedup = payload.semanticDedup;
|
|
2435
|
+
}
|
|
2272
2436
|
if (Array.isArray(payload && payload.warnings) && payload.warnings.length > 0) {
|
|
2273
2437
|
compact.warnings = payload.warnings;
|
|
2274
2438
|
}
|
|
@@ -2341,18 +2505,18 @@ async function callTool(name, args = {}, dependencies = undefined) {
|
|
|
2341
2505
|
|
|
2342
2506
|
if (name === 'previewSystemArchitectureMutation') {
|
|
2343
2507
|
const context = await loadContext(args);
|
|
2344
|
-
return toolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, false), context));
|
|
2508
|
+
return toolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, false, dependencies), context));
|
|
2345
2509
|
}
|
|
2346
2510
|
|
|
2347
2511
|
if (name === 'applySystemArchitectureMutation') {
|
|
2348
2512
|
const context = await loadContext(args);
|
|
2349
|
-
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, true), context), true);
|
|
2513
|
+
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, true, dependencies), context), true);
|
|
2350
2514
|
}
|
|
2351
2515
|
|
|
2352
2516
|
if (name === 'addArchitectureElement') {
|
|
2353
2517
|
const context = await loadContext(args);
|
|
2354
2518
|
const write = !args.dryRun;
|
|
2355
|
-
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'addElement', element: args.element, view_ids: args.view_ids }], write), context), write);
|
|
2519
|
+
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'addElement', element: args.element, view_ids: args.view_ids, onConflict: args.onConflict, justification: args.justification }], write, dependencies), context), write);
|
|
2356
2520
|
}
|
|
2357
2521
|
|
|
2358
2522
|
if (name === 'updateArchitectureElement') {
|
|
@@ -2370,7 +2534,7 @@ async function callTool(name, args = {}, dependencies = undefined) {
|
|
|
2370
2534
|
if (name === 'addArchitectureRelationship') {
|
|
2371
2535
|
const context = await loadContext(args);
|
|
2372
2536
|
const write = !args.dryRun;
|
|
2373
|
-
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'addRelationship', relationship: args.relationship, view_ids: args.view_ids }], write), context), write);
|
|
2537
|
+
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'addRelationship', relationship: args.relationship, view_ids: args.view_ids, onConflict: args.onConflict, justification: args.justification }], write), context), write);
|
|
2374
2538
|
}
|
|
2375
2539
|
|
|
2376
2540
|
if (name === 'updateArchitectureRelationship') {
|
|
@@ -2388,7 +2552,7 @@ async function callTool(name, args = {}, dependencies = undefined) {
|
|
|
2388
2552
|
if (name === 'addArchitectureView') {
|
|
2389
2553
|
const context = await loadContext(args);
|
|
2390
2554
|
const write = !args.dryRun;
|
|
2391
|
-
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'addView', view: args.view }], write), context), write);
|
|
2555
|
+
return mutationToolResult(attachContextWarnings(await buildMutationResult(context, [{ type: 'addView', view: args.view, onConflict: args.onConflict, justification: args.justification }], write), context), write);
|
|
2392
2556
|
}
|
|
2393
2557
|
|
|
2394
2558
|
if (name === 'updateArchitectureView') {
|
|
@@ -2493,6 +2657,119 @@ function memoryHitCard(element, maxDescLen) {
|
|
|
2493
2657
|
return card;
|
|
2494
2658
|
}
|
|
2495
2659
|
|
|
2660
|
+
// L1 semantic dedup advisory (GraphDeduplication item 3): for each element about
|
|
2661
|
+
// to be created, return semantically near elements within a CONTROLLED scope
|
|
2662
|
+
// (same ArchiMate type, and - when the add targets views - the union of those
|
|
2663
|
+
// views' current members) scoring above a strict threshold. Advisory ONLY: it
|
|
2664
|
+
// never rejects or blocks a write; a missing/unavailable semantic backend
|
|
2665
|
+
// degrades to an explicit status, never an error.
|
|
2666
|
+
const DEFAULT_SEMANTIC_DEDUP_THRESHOLD = 0.85;
|
|
2667
|
+
const SEMANTIC_DEDUP_MAX_QUERIES = 3;
|
|
2668
|
+
const SEMANTIC_DEDUP_MAX_MATCHES = 5;
|
|
2669
|
+
|
|
2670
|
+
function semanticDedupThreshold() {
|
|
2671
|
+
const raw = process.env.ARGO_SEMANTIC_DEDUP_THRESHOLD
|
|
2672
|
+
?? process.env.ARGO_MCP_SEMANTIC_DEDUP_THRESHOLD;
|
|
2673
|
+
const value = Number(raw);
|
|
2674
|
+
return Number.isFinite(value) && value > 0 && value <= 1 ? value : DEFAULT_SEMANTIC_DEDUP_THRESHOLD;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
function collectViewMemberElementIds(document, viewIds) {
|
|
2678
|
+
const ids = new Set();
|
|
2679
|
+
if (!Array.isArray(viewIds) || viewIds.length === 0) {
|
|
2680
|
+
return ids;
|
|
2681
|
+
}
|
|
2682
|
+
const wanted = new Set(viewIds);
|
|
2683
|
+
for (const view of Array.isArray(document && document.views) ? document.views : []) {
|
|
2684
|
+
if (!view || !wanted.has(view.view_id)) {
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
for (const elementId of Array.isArray(view.included_elements) ? view.included_elements : []) {
|
|
2688
|
+
ids.add(elementId);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
return ids;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
2695
|
+
if (process.env.ARGO_MCP_SEMANTIC_DEDUP === '0') {
|
|
2696
|
+
return undefined;
|
|
2697
|
+
}
|
|
2698
|
+
const addedElements = (Array.isArray(mutations) ? mutations : []).filter(mutation => (
|
|
2699
|
+
mutation
|
|
2700
|
+
&& mutation.type === 'addElement'
|
|
2701
|
+
&& mutation.element
|
|
2702
|
+
&& typeof mutation.element.name === 'string'
|
|
2703
|
+
&& mutation.element.name.trim() !== ''
|
|
2704
|
+
&& mutation.onConflict !== 'reuse'
|
|
2705
|
+
));
|
|
2706
|
+
if (addedElements.length === 0) {
|
|
2707
|
+
return undefined;
|
|
2708
|
+
}
|
|
2709
|
+
const threshold = semanticDedupThreshold();
|
|
2710
|
+
let journey;
|
|
2711
|
+
try {
|
|
2712
|
+
journey = await resolveSemanticOperatorJourney(dependencies, {
|
|
2713
|
+
repositoryRoot: context.workspaceRoot,
|
|
2714
|
+
});
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
return {
|
|
2717
|
+
status: 'unavailable',
|
|
2718
|
+
advisoryOnly: true,
|
|
2719
|
+
threshold,
|
|
2720
|
+
reason: String(error && error.category ? error.category : 'SEMANTIC_DEDUP_UNAVAILABLE'),
|
|
2721
|
+
candidates: [],
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
const candidates = [];
|
|
2725
|
+
for (const mutation of addedElements.slice(0, SEMANTIC_DEDUP_MAX_QUERIES)) {
|
|
2726
|
+
const element = mutation.element;
|
|
2727
|
+
const targetMemberIds = collectViewMemberElementIds(context.document, mutation.view_ids);
|
|
2728
|
+
const requested = { id: element.id, name: element.name, type: element.type };
|
|
2729
|
+
try {
|
|
2730
|
+
const intent = [element.type, element.name, element.description]
|
|
2731
|
+
.filter(part => typeof part === 'string' && part.trim() !== '')
|
|
2732
|
+
.join(' ');
|
|
2733
|
+
const retrieved = await journey.query({ purpose: 'general', intent });
|
|
2734
|
+
const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
|
|
2735
|
+
const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
|
|
2736
|
+
const elements = subset && subset.status === 'passed' && subset.document
|
|
2737
|
+
? (Array.isArray(subset.document.elements) ? subset.document.elements : [])
|
|
2738
|
+
: [];
|
|
2739
|
+
const matches = elements
|
|
2740
|
+
.filter(candidate => candidate && candidate.id !== element.id && typeof candidate.semanticScore === 'number')
|
|
2741
|
+
.filter(candidate => !element.type || candidate.type === element.type)
|
|
2742
|
+
.filter(candidate => candidate.semanticScore >= threshold)
|
|
2743
|
+
.filter(candidate => targetMemberIds.size === 0 || targetMemberIds.has(candidate.id))
|
|
2744
|
+
.sort((left, right) => right.semanticScore - left.semanticScore)
|
|
2745
|
+
.slice(0, SEMANTIC_DEDUP_MAX_MATCHES)
|
|
2746
|
+
.map(candidate => ({
|
|
2747
|
+
id: candidate.id,
|
|
2748
|
+
name: candidate.name,
|
|
2749
|
+
type: candidate.type,
|
|
2750
|
+
score: Number(candidate.semanticScore.toFixed(4)),
|
|
2751
|
+
in_target_views: targetMemberIds.has(candidate.id),
|
|
2752
|
+
}));
|
|
2753
|
+
candidates.push({ requested, matches });
|
|
2754
|
+
} catch (error) {
|
|
2755
|
+
candidates.push({
|
|
2756
|
+
requested,
|
|
2757
|
+
matches: [],
|
|
2758
|
+
status: 'unavailable',
|
|
2759
|
+
reason: String(error && error.category ? error.category : 'SEMANTIC_DEDUP_QUERY_FAILED'),
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return {
|
|
2764
|
+
status: 'passed',
|
|
2765
|
+
advisoryOnly: true,
|
|
2766
|
+
threshold,
|
|
2767
|
+
scope: 'same type; when view_ids are given, restricted to those views\' current members',
|
|
2768
|
+
candidates,
|
|
2769
|
+
has_suggestions: candidates.some(entry => entry.matches.length > 0),
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2496
2773
|
async function queryNeo4jGraphTool(args = {}) {
|
|
2497
2774
|
const architecturePath = args.architecturePath || DEFAULT_GRAPH_PATH;
|
|
2498
2775
|
const workspaceRoot = resolveWorkspaceRoot(args);
|
|
@@ -3824,6 +4101,7 @@ module.exports = {
|
|
|
3824
4101
|
GET_SYSTEM_ARCHITECTURE_OUTPUT_SCHEMA,
|
|
3825
4102
|
TOOLS,
|
|
3826
4103
|
applyMutations,
|
|
4104
|
+
buildSemanticDedupAdvisory,
|
|
3827
4105
|
callTool,
|
|
3828
4106
|
compactMutationResponse,
|
|
3829
4107
|
createDefaultCanonicalSemanticInitComposition,
|
package/install-argo.ps1
CHANGED
|
@@ -215,6 +215,30 @@ function Register-OpenCodePlugin {
|
|
|
215
215
|
[System.IO.File]::WriteAllText($ConfigPath, $json, (New-Object System.Text.UTF8Encoding $false))
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
function Get-ArchGraphRuleBlockEndIndex {
|
|
219
|
+
# Locate the end of a previously merged ArchGraph rule block. The rule's
|
|
220
|
+
# final section tag changed over versions: earlier rules ended with
|
|
221
|
+
# </ToolsGuideline>, the current layout ends with </Attention> and places
|
|
222
|
+
# <ToolsGuideline> earlier. Pick whichever final tag occurs LAST at/after
|
|
223
|
+
# the block marker, so the merge stays correct across rule reorderings and
|
|
224
|
+
# also repairs a previously duplicated tail.
|
|
225
|
+
param([string]$Text, [int]$From)
|
|
226
|
+
$idx = -1
|
|
227
|
+
foreach ($tag in @('</ToolsGuideline>', '</Attention>')) {
|
|
228
|
+
$i = $Text.LastIndexOf($tag)
|
|
229
|
+
if ($i -ge $From -and $i -gt $idx) { $idx = $i }
|
|
230
|
+
}
|
|
231
|
+
return $idx
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function Get-ArchGraphRuleBlockEndTagLength {
|
|
235
|
+
param([string]$Text, [int]$Index)
|
|
236
|
+
if ($Index -lt 0) { return 0 }
|
|
237
|
+
if ($Text.Substring($Index).StartsWith('</ToolsGuideline>')) { return '</ToolsGuideline>'.Length }
|
|
238
|
+
if ($Text.Substring($Index).StartsWith('</Attention>')) { return '</Attention>'.Length }
|
|
239
|
+
return 0
|
|
240
|
+
}
|
|
241
|
+
|
|
218
242
|
function Add-AgentsRule {
|
|
219
243
|
param(
|
|
220
244
|
[string]$AgentsPath,
|
|
@@ -231,18 +255,18 @@ function Add-AgentsRule {
|
|
|
231
255
|
# An existing ArchGraph rules block is present. Replace it with the
|
|
232
256
|
# current rule content while preserving any unrelated content that
|
|
233
257
|
# surrounds it (e.g. user-authored OpenCode instructions).
|
|
234
|
-
$endTag = '</ToolsGuideline>'
|
|
235
258
|
$markerIdx = $existing.IndexOf($marker)
|
|
236
259
|
if ($markerIdx -lt 0) { $markerIdx = 0 }
|
|
237
260
|
$startIdx = $existing.LastIndexOf('---', $markerIdx)
|
|
238
261
|
if ($startIdx -lt 0) { $startIdx = 0 }
|
|
239
|
-
$endIdx = $existing
|
|
262
|
+
$endIdx = Get-ArchGraphRuleBlockEndIndex -Text $existing -From $markerIdx
|
|
240
263
|
|
|
241
264
|
$before = $existing.Substring(0, $startIdx).TrimEnd()
|
|
242
265
|
if ($endIdx -lt 0) {
|
|
243
266
|
$combined = $ruleContent
|
|
244
267
|
} else {
|
|
245
|
-
$
|
|
268
|
+
$endLen = Get-ArchGraphRuleBlockEndTagLength -Text $existing -Index $endIdx
|
|
269
|
+
$after = $existing.Substring($endIdx + $endLen)
|
|
246
270
|
$combined = $before
|
|
247
271
|
if ($combined.Length -gt 0) { $combined += "`n`n" }
|
|
248
272
|
$combined += $ruleContent
|
|
@@ -301,13 +325,15 @@ function Write-ArchGraphRuleBlock {
|
|
|
301
325
|
if (Test-Path $DestPath) {
|
|
302
326
|
$existing = Get-Content $DestPath -Raw -Encoding UTF8
|
|
303
327
|
if ($existing -like "*$marker*") {
|
|
304
|
-
$endTag = '</ToolsGuideline>'
|
|
305
328
|
$startIdx = $existing.IndexOf($marker)
|
|
306
329
|
if ($startIdx -lt 0) { $startIdx = 0 }
|
|
307
|
-
$endIdx = $existing
|
|
330
|
+
$endIdx = Get-ArchGraphRuleBlockEndIndex -Text $existing -From $startIdx
|
|
308
331
|
$before = $existing.Substring(0, $startIdx).TrimEnd()
|
|
309
332
|
$after = ''
|
|
310
|
-
if ($endIdx -ge 0) {
|
|
333
|
+
if ($endIdx -ge 0) {
|
|
334
|
+
$endLen = Get-ArchGraphRuleBlockEndTagLength -Text $existing -Index $endIdx
|
|
335
|
+
$after = $existing.Substring($endIdx + $endLen).TrimStart()
|
|
336
|
+
}
|
|
311
337
|
$combined = $before
|
|
312
338
|
if ($combined.Length -gt 0) { $combined += "`n`n" }
|
|
313
339
|
$combined += $ruleContent
|
package/package.json
CHANGED