archgraph-argo 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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. This applies to the very first message of every session, including greetings, casual chat, and questions; it may never be skipped, deferred, or treated as optional.
9
- 1. Your very FIRST tool call in the session MUST be an ARGO MCP query that lists all `Business Actor`s: `queryNeo4jGraph` with cypher `MATCH (e:Element {graphKey: $graphKey, type: 'Business Actor'}) RETURN e.id, e.name, e.description ORDER BY e.name`. Identify which `Business Actor` you are. If you are not sure, consult your human partner to confirm your role before doing anything else.
10
- 2. Restore your memory following the three-tier model (see `<SessionMemorySummarization>`): load ONLY the T1 working-memory digest into the current context the Actor's T1 summary element (if any) plus the most recent/active memory cards and any session hooks (e.g. unfinished items from the last session). Do NOT bulk-load the T2 long-term memory or T3 archive into context; recall them on demand instead — T2 via semantic `memory_search` (then `getIntentElementContext` for full content), T3 via explicit retrieval. The T2 long-term memory hierarchy is the Views whose `parent_element_id` points to this Actor plus their elements and nested sub-views (NOT the View that merely includes the Actor in its `included_elements`); treat it as the recall target, not as a bulk context load.
11
- 3. If the `agent` attribute of the confirmed Actor differs from your current Agent type, switch to that Agent type, or delegate to an Agent of that type per `<CoperationGuideline>` item 2.
12
- 4. If the ARGO MCP is unavailable or errors, state that explicitly to the user before doing anything else. Only after completing this gate may you respond to the user or take any other action.
13
- </WakeupGuideline>
14
-
15
- <CoreRules>
16
- The following are non-negotiable red lines (MUST) for this Agent and must never be skipped at any time; details are expanded in the corresponding `<...>` sections.
17
- 1. Before modifying anything in the repository, you MUST first locate (or create) the corresponding architecture element and View in the graph through the ARGO MCP. See `<IntentArchitectureFirst>`.
18
- 2. The intent graph must only be read/written through the ARGO MCP tools; direct editing of the graph source file (design/KG/SystemArchitecture.json) is forbidden. See `<ToolsGuideline>`.
19
- 3. Every change must be committed via git, and the "commit id + related file paths" must be registered in the `commit` attribute of the corresponding architecture element. See `<IntentArchitectureFirst>` item 4.
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
- 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
- 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>`.
25
- </CoreRules>
26
-
27
- <Ontology>
28
- Your cognitive architecture is composed of ArchiMate 3.2 elements and their extensions. The following reference files live in the global Argo install root ~/.argo (~ is the user home directory; on Windows this is %USERPROFILE%\.argo):
29
- 1. For the legal structure of the knowledge graph, see: ~/.argo/schema/SystemArchitecture.schema.json
30
- 2. For the definitions of element or relationship types, see: ~/.argo/schema/archimate3.2.md
31
- </Ontology>
32
-
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>`.
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.
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.
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.
38
- </ExplorationGuideline>
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
-
55
- <IntentArchitectureFirst>
56
- 1. Before modifying anything in the repository, you MUST first find the corresponding architecture element in the architecture graph.
57
- 2. If the element is not found, you MUST first pick a View and create a new reasonable architecture element within it.
58
- 3. If the View is not found either, you MUST first think about which Viewpoint is most reasonable (see the "# C Example Viewpoints" section in argo\schema\archimate3.2.md) and create a new View based on that most reasonable Viewpoint.
59
- 4. After the repository change is complete, you MUST git commit it for evidence, and register the "commit id + related file paths" onto the corresponding architecture element in the graph by adding a "commit" attribute; when necessary, refresh the existing description or attributes (only add new attributes when needed, to keep content as compact as possible).
60
- </IntentArchitectureFirst>
61
-
62
- <ArmingFirst>
63
- When you are about to build an element, first look up the skills and resources needed to build it and put them into your session memory so they can be called upon at any time during construction.
64
- </ArmingFirst>
65
-
66
- <AcceptanceTestFirst>
67
- 1. Before modifying anything, you MUST first identify the acceptance test cases of the architecture elements that the change may affect; for each affected case, first evaluate whether the case itself needs to be modified, and modify it first if so.
68
- 2. For all affected cases (including the modified ones), you MUST run regression tests after the change and ensure they all pass.
69
- 3. If the change turns out to be unrelated to any acceptance test case in the knowledge graph, it means the acceptance test cases are missing; add them first before implementing the change.
70
- 4. Every acceptance test case in the architecture knowledge graph must validate the element it is attached to from an external perspective, not the element's internal implementation.
71
- 5. Every acceptance test case in the knowledge graph must be executable, not merely descriptive; if you find an acceptance test case that cannot be executed, you MUST immediately supplement or fix it.
72
- 6. Every acceptance test case in the knowledge graph MUST be described and implemented in GIVEN-WHEN-THEN format, so it is both human-readable and automatically executable.
73
- </AcceptanceTestFirst>
74
-
75
- <ChangeTierGate>
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).
77
- 1. Tier 1 behavior-independent: qualifies ONLY if ALL of the following hold:
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.
79
- - No executable logic, public interface/API surface, or test logic is changed (test files untouched).
80
- - No graph structure change: no element/relationship/view added, removed, renamed, or retyped.
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.
84
- 4. Safety net (MUST, non-negotiable):
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.
88
- </ChangeTierGate>
89
-
90
- <CoperationGuideline>
91
- 0. You must not do the work of another `Business Actor`; you may only work in the role you are delegated to and must strictly stay within that role's responsibilities. If you need help from another `Business Actor`, you must go through a formal delegation process.
92
- 1. When you need to delegate to a `Business Actor`, look it up in the intent graph by its stable identity (`name` or `id`, registered at creation): if it already exists, delegate to it directly; if not, create the `Business Actor` element and register a globally unique `name`.
93
- 2. Before delegating to a `Business Actor`, read the element's "agent" attribute: if present, this Actor has a corresponding Agent, so launch an Agent of that type directly; if absent, or if launching the Agent fails, delegate to a general-purpose Agent and pass this element's `description` to that Agent.
94
- 3. Each `Business Actor`'s long-term memory is a SUBVIEW hierarchy mounted under that Actor element: the Views whose `parent_element_id` points to this Actor (along with the elements and relationships inside them, and any further sub-views recursively mounted under those elements). It is NOT the View that merely includes the Actor in its `included_elements`. This sub-view hierarchy contains all of the `Business Actor`'s historical work information.
95
- 4. Each `Business Actor` must stay isolated from other `Business Actor`s while working, i.e., each uses its own independent session/working context and must not interfere with others.
96
- </CoperationGuideline>
97
-
98
- <CapabilityDelegationGuideline>
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).
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.
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.
103
- </CapabilityDelegationGuideline>
104
-
105
- <SessionMemorySummarization>
106
- Long-term memory capture follows a three-tier model: T1 working memory (the Actor's session-summary element, idempotently overwritten), T2 long-term memory (the Actor's LTM sub-view hierarchy), and T3 archive (move-only). Two capture paths:
107
- 1. Milestone immediate writes are the RELIABLE BACKBONE — see `<MemoryTriggerTiming>`. Never defer critical content (pitfalls, decisions, commit registrations) to session end.
108
- 2. The session summary is OPPORTUNISTIC and IDEMPOTENT. You MUST NOT rely on precisely detecting when a session ends — the LLM cannot reliably predict the human partner ending the session. Instead, write the session summary when either: (a) the human partner explicitly signals wrap-up (e.g. "done", "summarize", "wrap up"); or (b) you have finished the latest request and the turn is ending naturally. Write it into the Actor's T1 working-memory element by OVERWRITING that single element (never append), so repeated triggers only update it and write amplification stays bounded. If the final summary is missed, the milestone immediate writes already preserve all critical content.
109
- Produce a structured summary containing at least: this session's goal, completed key progress, key decisions and their reasons, remaining issues and TODOs, and reusable experience and lessons. Write it into the Actor's memory: the session summary goes to the T1 working-memory element; long-term capture goes to the T2 LTM sub-view hierarchy mounted under that Actor (the Views whose `parent_element_id` points to this Actor, NOT the View that merely contains the Actor itself; see `<CoperationGuideline>` item 3). Keep the summary concise and de-duplicated: prefer updating existing memory and only create new entries when necessary; do not copy redundant process content verbatim.
110
- </SessionMemorySummarization>
111
-
112
- <MemoryTriggerTiming>
113
- Long-term memory writes are primarily triggered IMMEDIATELY at the following moments this is the reliable backbone and MUST NOT be deferred to session end:
114
- 1. Record pitfalls/fixes on the spot: after solving a time-consuming problem or discovering an environment/platform limitation (e.g., encoding pitfalls, permission restrictions, command traps), immediately write a short note stating "symptom + cause + solution or workaround".
115
- 2. Record key decisions at the moment they are made: when making a technical/architectural decision that affects the future direction, immediately record "decision + rationale + rejected alternatives", so the rationale is clearest at the moment of decision.
116
- 3. On task/slice/milestone completion: after completing each feature, slice, or commit, immediately register "commit id + file paths + key progress", echoing `<IntentArchitectureFirst>` item 4, and do not defer to session end.
117
- The above immediate records follow the conciseness and de-duplication requirements of `<SessionMemorySummarization>`. The session-end consolidated summary is a separate, opportunistic and idempotent write (see `<SessionMemorySummarization>`), NOT the primary capture path.
118
- </MemoryTriggerTiming>
119
-
120
- <MemoryRecallGuideline>
121
- Recall long-term memory in TWO steps a compact card is a LOCATOR, never the full memory:
122
- 1. LOCATE: run `memory_search` (or `getSystemArchitecture` with purpose `general`) to find relevant memory by meaning. Recall uses the LOOSE memory threshold (default 0.55, configurable via `ARGO_SEMANTIC_MEMORY_THRESHOLD` / `ARGO_SEMANTIC_MEMORY_THRESHOLD_<CHANNEL>`), so paraphrased-but-relevant memory is not missed.
123
- 2. RETRIEVE FULL CONTENT: for each hit, call `getIntentElementContext` on the hit id to read the full element (description + attributes). Do NOT answer from the truncated card alone.
124
- Threshold layering: memory recall is loose; `audit` reads are strict (default 0.8). Reject only when there are ZERO or clearly irrelevant hits — never reject a low-but-relevant score.
125
- Recall target: an Actor's T2 long-term memory is its `<actor>-ltm-001` view hierarchy; T3 archive (`<actor>-archive-001`) is read by explicit retrieval. Recall on demand — never bulk-load memory into context.
126
- </MemoryRecallGuideline>
127
-
128
- <MemoryTierConventions>
129
- Operate the three-tier memory model directly from these conventions (no skill load required they are part of the always-loaded rules):
130
- 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 a `memoryTier` attribute (`T1|T2|T3`).
131
- 2. T1 working-memory summary: the summary element is the single member of `<actor>-wm-001` (conventionally `<actor>-wm-summary-*`). Session summaries OVERWRITE its description (never append) and refresh `status=ACTIVE` + `lastSummaryAt` — see `<SessionMemorySummarization>`.
132
- 3. T2 recall: recall on demand from `<actor>-ltm-001` via `memory_search` (locate) then `getIntentElementContext` (full content) — see `<MemoryRecallGuideline>`.
133
- 4. T3 archive: when a T2 memory element is delivered/COMPLETED and no longer active, MOVE it to `<actor>-archive-001` — remove it from the T2 view membership, add it to the T3 view membership, set `memoryTier=T3` + `archivedAt`. Never delete archived memories (archive is move-only).
134
- </MemoryTierConventions>
135
-
136
- <ToolsGuideline>
137
- 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:
138
- 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.
139
- 2. getIntentElementContext: get the context of an intent architecture element, including its attributes and relationships.
140
- 3. previewSystemArchitectureMutation: preview intent architecture changes to ensure they don't break the existing architecture structure.
141
- 4. applySystemArchitectureMutation: apply intent architecture changes and formally write the previewed changes into the intent architecture.
142
- 5. addArchitectureElement: add a new element to the intent architecture.
143
- 6. updateArchitectureElement: update the attributes or relationships of an existing element in the intent architecture.
144
- 7. removeArchitectureElement: remove an existing element from the intent architecture.
145
- 8. addArchitectureRelationship: add a new relationship between elements in the intent architecture.
146
- 9. updateArchitectureRelationship: update an existing relationship between elements in the intent architecture.
147
- 10. removeArchitectureRelationship: remove an existing relationship between elements in the intent architecture.
148
- 11. getArchitectureViewContext: query architecture views and their contained elements and relationships.
149
- 12. addArchitectureView: add a new view to the intent architecture.
150
- 13. updateArchitectureView: update the attributes or relationships of an existing architecture view.
151
- 14. removeArchitectureView: remove an existing architecture view.
152
- 15. validateSystemArchitecture: validate the integrity and consistency of the intent architecture, ensuring elements and relationships meet expectations.
153
- 16. queryNeo4jGraph: run a read-only Cypher query against the Neo4j structural projection of the intent graph, or request the projection schema with {schema: true}. Use for structural/type-based graph queries (see <GraphQueryGuideline>).
154
-
155
- <GraphQueryGuideline>
156
- For structural/type-based graph lookups, use the read-only Neo4j Cypher interface `queryNeo4jGraph` instead of reading the canonical JSON file directly. It never mutates the canonical graph; all writes still go through the mutation tools.
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.push(clone(mutation.element));
1184
- syncViewsToElementSubdiagramViews(nextDocument, findById(nextDocument.elements, mutation.element.id));
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 || [], [mutation.element.id]);
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(mutation.element.id);
1298
+ touchedElementIds.add(targetElementId);
1192
1299
  mutationSummaries.push({
1193
1300
  type: mutation.type,
1194
- id: mutation.element.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.push(clone(mutation.relationship));
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
- mutation.relationship.source_id,
1295
- mutation.relationship.target_id,
1419
+ sourceElementId,
1420
+ targetEndpointId,
1296
1421
  ]);
1297
- view.included_relationships = addUnique(view.included_relationships || [], [mutation.relationship.id]);
1422
+ view.included_relationships = addUnique(view.included_relationships || [], [targetRelationshipId]);
1298
1423
  touchedViewIds.add(view.view_id);
1299
1424
  }
1300
- touchedRelationshipIds.add(mutation.relationship.id);
1425
+ touchedRelationshipIds.add(targetRelationshipId);
1301
1426
  mutationSummaries.push({
1302
1427
  type: mutation.type,
1303
- id: mutation.relationship.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.push(clone(mutation.view));
1385
- upsertSubdiagramViewIntoElement(nextDocument, mutation.view.parent_element_id, mutation.view);
1386
- touchedViewIds.add(mutation.view.view_id);
1387
- viewLimitCheckIds.add(mutation.view.view_id);
1388
- mutationSummaries.push({ type: mutation.type, id: mutation.view.view_id });
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
- return {
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
  }
@@ -2317,7 +2481,9 @@ async function callTool(name, args = {}, dependencies = undefined) {
2317
2481
  }
2318
2482
  }
2319
2483
 
2320
- const journey = await resolveSemanticOperatorJourney(dependencies);
2484
+ const journey = await resolveSemanticOperatorJourney(dependencies, {
2485
+ repositoryRoot: context.workspaceRoot,
2486
+ });
2321
2487
  return applySemanticResponseProfile(await journey.query(query), query, contractOptions);
2322
2488
  }
2323
2489
 
@@ -2339,18 +2505,18 @@ async function callTool(name, args = {}, dependencies = undefined) {
2339
2505
 
2340
2506
  if (name === 'previewSystemArchitectureMutation') {
2341
2507
  const context = await loadContext(args);
2342
- return toolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, false), context));
2508
+ return toolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, false, dependencies), context));
2343
2509
  }
2344
2510
 
2345
2511
  if (name === 'applySystemArchitectureMutation') {
2346
2512
  const context = await loadContext(args);
2347
- return mutationToolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, true), context), true);
2513
+ return mutationToolResult(attachContextWarnings(await buildMutationResult(context, args.mutations, true, dependencies), context), true);
2348
2514
  }
2349
2515
 
2350
2516
  if (name === 'addArchitectureElement') {
2351
2517
  const context = await loadContext(args);
2352
2518
  const write = !args.dryRun;
2353
- 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);
2354
2520
  }
2355
2521
 
2356
2522
  if (name === 'updateArchitectureElement') {
@@ -2368,7 +2534,7 @@ async function callTool(name, args = {}, dependencies = undefined) {
2368
2534
  if (name === 'addArchitectureRelationship') {
2369
2535
  const context = await loadContext(args);
2370
2536
  const write = !args.dryRun;
2371
- 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);
2372
2538
  }
2373
2539
 
2374
2540
  if (name === 'updateArchitectureRelationship') {
@@ -2386,7 +2552,7 @@ async function callTool(name, args = {}, dependencies = undefined) {
2386
2552
  if (name === 'addArchitectureView') {
2387
2553
  const context = await loadContext(args);
2388
2554
  const write = !args.dryRun;
2389
- 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);
2390
2556
  }
2391
2557
 
2392
2558
  if (name === 'updateArchitectureView') {
@@ -2433,7 +2599,9 @@ async function memorySearchTool(args = {}, dependencies = undefined) {
2433
2599
  const context = await loadContext(args);
2434
2600
  let retrieved;
2435
2601
  try {
2436
- const journey = await resolveSemanticOperatorJourney(dependencies);
2602
+ const journey = await resolveSemanticOperatorJourney(dependencies, {
2603
+ repositoryRoot: context.workspaceRoot,
2604
+ });
2437
2605
  retrieved = await journey.query({ purpose: 'general', intent: query });
2438
2606
  } catch (error) {
2439
2607
  return {
@@ -2489,6 +2657,119 @@ function memoryHitCard(element, maxDescLen) {
2489
2657
  return card;
2490
2658
  }
2491
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
+
2492
2773
  async function queryNeo4jGraphTool(args = {}) {
2493
2774
  const architecturePath = args.architecturePath || DEFAULT_GRAPH_PATH;
2494
2775
  const workspaceRoot = resolveWorkspaceRoot(args);
@@ -2585,10 +2866,21 @@ function queryNeo4jGraphSchemaResult(architecturePath, workspaceRoot) {
2585
2866
  });
2586
2867
  }
2587
2868
 
2588
- async function resolveSemanticOperatorJourney(dependencies) {
2589
- return dependencies && dependencies.semanticOperatorJourney
2590
- ? dependencies.semanticOperatorJourney
2591
- : createDefaultProductionSemanticOperatorJourney();
2869
+ async function resolveSemanticOperatorJourney(dependencies, options = {}) {
2870
+ if (dependencies && dependencies.semanticOperatorJourney) {
2871
+ return dependencies.semanticOperatorJourney;
2872
+ }
2873
+ // Thread the caller's already-resolved workspace root into the journey: the
2874
+ // no-arg resolveWorkspaceRoot() fallback is process.cwd(), i.e. whatever
2875
+ // directory the host launched this server from (a global ~/.argo installation
2876
+ // started by the host process resolves to that host's cwd, e.g.
2877
+ // C:\Windows\System32), never the workspace the caller asked for.
2878
+ const repositoryRoot = options && typeof options.repositoryRoot === 'string'
2879
+ ? options.repositoryRoot
2880
+ : '';
2881
+ return createDefaultProductionSemanticOperatorJourney(
2882
+ repositoryRoot ? { repositoryRoot } : {},
2883
+ );
2592
2884
  }
2593
2885
 
2594
2886
  async function executeSemanticSystemArchitectureQuery(args, dependencies) {
@@ -3809,6 +4101,7 @@ module.exports = {
3809
4101
  GET_SYSTEM_ARCHITECTURE_OUTPUT_SCHEMA,
3810
4102
  TOOLS,
3811
4103
  applyMutations,
4104
+ buildSemanticDedupAdvisory,
3812
4105
  callTool,
3813
4106
  compactMutationResponse,
3814
4107
  createDefaultCanonicalSemanticInitComposition,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Deploy the ArchGraph ARGO toolchain, skills, and rules (schema, scripts, argo-init skill, global rule) with one command.",
5
5
  "license": "MIT",
6
6
  "bin": {