archgraph-argo 0.18.0 → 0.19.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.
@@ -22,7 +22,7 @@ Non-negotiable red lines (MUST). Never skip, simplify, or silently violate them;
22
22
  6. Continuously comply with these red lines throughout the session.
23
23
  7. Retrieve KG-first and semantic-first. See `<QueryPriorityGuideline>`.
24
24
  8. Store content KG-first. See `<ContentStoragePolicy>`.
25
- 9. Never duplicate: reuse first, and handle duplicate rejections by reusing or updating. See `<GraphDeduplication>`.
25
+ 9. Never duplicate: reuse is the default; a create blocked as an exact or semantic duplicate must be reused, or explicitly overridden with `onConflict: "allowDuplicate"` + justification. See `<GraphDeduplication>`.
26
26
  </CoreRules>
27
27
 
28
28
  <Ontology>
@@ -54,11 +54,11 @@ Your architecture is ArchiMate 3.2 plus ARGO extensions. Reference files live un
54
54
  </ContentStoragePolicy>
55
55
 
56
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.
57
+ Never add what the graph already has. Reuse is the default; there is no reject mode.
58
+ 1. `addElement` / `addRelationship` / `addView` default to `onConflict: "reuse"` (find-or-create). An exact match (element: same type + name; relationship: same source + type + target + name; view: same parent + name) is reusedthe existing object is attached to the requested view(s) and nothing new is created.
59
+ 2. A new ELEMENT is also blocked when it is semantically near an existing element of the same type anywhere in the graph: the call writes nothing and returns `semanticConflicts` with the candidates.
60
+ 3. To create despite an exact or semantic duplicate you MUST pass `onConflict: "allowDuplicate"` with a real `justification`. Nothing else creates a duplicate.
61
+ 4. On a block, reuse the returned existing element: re-call `addElement` with that element's id (it is then attached to your view), or update it with `updateArchitectureElement`.
62
62
  5. Updates are never gated. Never work around a duplicate by editing around it — reuse or update the existing object.
63
63
  </GraphDeduplication>
64
64
 
@@ -149,7 +149,7 @@ 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.' },
152
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: find-or-create — attach an existing exact (type, name) match; a same-type semantic near-duplicate also blocks creation. allowDuplicate: create anyway (even if a duplicate exists), requires a justification.' },
153
153
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
154
154
  architecturePath: { type: 'string', description: 'Default: design/KG/SystemArchitecture.json' },
155
155
  },
@@ -193,7 +193,7 @@ const TOOLS = [
193
193
  properties: {
194
194
  relationship: { type: 'object' },
195
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.' },
196
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: find-or-create — attach an existing exact (source, type, target, name) match. allowDuplicate: create anyway, requires a justification.' },
197
197
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
198
198
  architecturePath: { type: 'string', description: 'Default: design/KG/SystemArchitecture.json' },
199
199
  },
@@ -236,7 +236,7 @@ const TOOLS = [
236
236
  required: ['view'],
237
237
  properties: {
238
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.' },
239
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: attach an existing exact (parent, view name) match. allowDuplicate: create anyway, requires a justification.' },
240
240
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
241
241
  architecturePath: { type: 'string', description: 'Default: design/KG/SystemArchitecture.json' },
242
242
  },
@@ -205,7 +205,7 @@ async function executeWpP2Retrieval({
205
205
  requireQualifiedVector(vector);
206
206
  const purpose = request && typeof request.purpose === 'string' ? request.purpose : '';
207
207
  const strict = AUDIT_PURPOSES.has(purpose);
208
- const topK = resolveTopK();
208
+ const topK = Number.isInteger(request.topK) && request.topK > 0 ? request.topK : resolveTopK();
209
209
  const seedsByType = {};
210
210
  for (const channel of CHANNELS) {
211
211
  seedsByType[channel.key] = await exhaustChannel({
@@ -245,7 +245,7 @@ const TOOLS = [
245
245
  properties: {
246
246
  element: { type: 'object' },
247
247
  view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
248
- 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.' },
248
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: find-or-create — attach an existing exact (type, name) match; a same-type semantic near-duplicate also blocks creation. allowDuplicate: create anyway (even if a duplicate exists), requires a justification.' },
249
249
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
250
250
  dryRun: { type: 'boolean', description: 'When true, validates and returns the result without writing to the graph. Default: false.' },
251
251
  architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
@@ -292,7 +292,7 @@ const TOOLS = [
292
292
  properties: {
293
293
  relationship: { type: 'object' },
294
294
  view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
295
- 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.' },
295
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: find-or-create — attach an existing exact (source, type, target, name) match. allowDuplicate: create anyway, requires a justification.' },
296
296
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
297
297
  dryRun: { type: 'boolean', description: 'When true, validates and returns the result without writing to the graph. Default: false.' },
298
298
  architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
@@ -338,7 +338,7 @@ const TOOLS = [
338
338
  required: ['view'],
339
339
  properties: {
340
340
  view: { type: 'object' },
341
- 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.' },
341
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy (default reuse). reuse: attach an existing exact (parent, view name) match. allowDuplicate: create anyway, requires a justification.' },
342
342
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate.' },
343
343
  dryRun: { type: 'boolean', description: 'When true, validates and returns the result without writing to the graph. Default: false.' },
344
344
  architecturePath: { type: 'string', description: `Default: ${DEFAULT_GRAPH_PATH}` },
@@ -480,7 +480,7 @@ function mutationInputSchema() {
480
480
  view_ids: { type: 'array', minItems: 1, items: { type: 'string' } },
481
481
  element_ids: { type: 'array', items: { type: 'string' } },
482
482
  relationship_ids: { type: 'array', items: { type: 'string' } },
483
- 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.' },
483
+ onConflict: { type: 'string', enum: ['reuse', 'allowDuplicate'], description: 'Dedup policy for add* mutations. reuse (default): find-or-create — attach an existing exact-natural-key match instead of creating a duplicate; a same-type semantic near-duplicate also blocks creation unless allowDuplicate is set. allowDuplicate: create a new object even if a duplicate exists, requires a non-empty justification.' },
484
484
  justification: { type: 'string', description: 'Required when onConflict is allowDuplicate; recorded as the reason a semantically-equal duplicate is intentionally created.' },
485
485
  },
486
486
  additionalProperties: false,
@@ -1169,7 +1169,7 @@ function mergeAttributesPatch(existing, patchEntries) {
1169
1169
  return result;
1170
1170
  }
1171
1171
 
1172
- const DUPLICATE_CONFLICT_POLICIES = new Set(['fail', 'reuse', 'allowDuplicate']);
1172
+ const DUPLICATE_CONFLICT_POLICIES = new Set(['reuse', 'allowDuplicate']);
1173
1173
 
1174
1174
  // L0 dedup gate: normalize a name to a stable natural-key component. NFKC folds
1175
1175
  // full-width forms, whitespace is collapsed, and case is folded, so " Widget ",
@@ -1221,27 +1221,19 @@ function findDuplicateViews(views, view) {
1221
1221
  }
1222
1222
 
1223
1223
  // Resolve an add against its exact natural-key candidates under the caller's
1224
- // onConflict policy: fail (default) rejects and returns candidates; reuse is
1225
- // find-or-create (attach the existing object); allowDuplicate creates only with
1226
- // a non-empty justification. Semantic similarity (L1) is never handled here.
1224
+ // onConflict policy: reuse (default) is find-or-create (attach the existing
1225
+ // object); allowDuplicate creates only with a non-empty justification. Semantic
1226
+ // similarity is handled separately (the semantic dedup gate), never here.
1227
1227
  function resolveDuplicateConflict(options, candidates) {
1228
1228
  const onConflict = options.onConflict === undefined || options.onConflict === null
1229
- ? 'fail'
1229
+ ? 'reuse'
1230
1230
  : options.onConflict;
1231
1231
  if (!DUPLICATE_CONFLICT_POLICIES.has(onConflict)) {
1232
- throw new Error(`onConflict must be one of fail, reuse, allowDuplicate (got '${onConflict}')`);
1232
+ throw new Error(`onConflict must be one of reuse, allowDuplicate (got '${onConflict}')`);
1233
1233
  }
1234
1234
  if (candidates.length === 0) {
1235
1235
  return { action: 'create' };
1236
1236
  }
1237
- if (onConflict === 'fail') {
1238
- const error = new Error(
1239
- `Duplicate ${options.label} already exists (${candidates.map(candidate => candidate.id).join(', ')}). ` +
1240
- 'Reuse it with onConflict:"reuse", or pass onConflict:"allowDuplicate" with a non-empty justification to create a new one.',
1241
- );
1242
- error.duplicateConflicts = candidates;
1243
- throw error;
1244
- }
1245
1237
  if (onConflict === 'reuse') {
1246
1238
  return { action: 'reuse', existing: candidates[0] };
1247
1239
  }
@@ -1812,16 +1804,33 @@ async function buildMutationResult(context, mutations, write, dependencies) {
1812
1804
  result.guidance = buildFailureGuidance(errors);
1813
1805
  }
1814
1806
 
1815
- // L1 advisory: semantic near-duplicate suggestions for element adds. Never
1816
- // blocks or fails the write; preview and apply both surface it.
1807
+ // Semantic dedup gate: a would-be element create is blocked when a same-type
1808
+ // semantic near-duplicate exists, unless the caller explicitly passed
1809
+ // onConflict:'allowDuplicate' (with justification). Never blocks on an
1810
+ // unavailable semantic backend. Both preview and apply surface the outcome.
1811
+ let semanticBlocked = false;
1817
1812
  if (errors.length === 0) {
1818
- const semanticDedup = await buildSemanticDedupAdvisory(context, mutations, dependencies);
1813
+ const createdAdds = selectCreatedElementAdds(mutations, mutationResult.mutationSummaries);
1814
+ const semanticDedup = await buildSemanticDedupAdvisory(context, createdAdds, dependencies);
1819
1815
  if (semanticDedup) {
1820
1816
  result.semanticDedup = semanticDedup;
1817
+ const gate = evaluateSemanticDedupGate(semanticDedup, createdAdds);
1818
+ if (gate.blocked) {
1819
+ semanticBlocked = true;
1820
+ result.status = 'failed';
1821
+ result.after = beforeSummary;
1822
+ result.semanticConflicts = gate.conflicts;
1823
+ result.errors = gate.conflicts.map(entry => (
1824
+ `Semantic duplicate for element (type '${entry.requested.type}', name '${entry.requested.name}'): `
1825
+ + `${entry.matches.map(match => `${match.id} (score ${match.score})`).join(', ')}. `
1826
+ + 'Reuse the existing element (re-call with that element id), or pass onConflict:"allowDuplicate" with a justification to create a new one.'
1827
+ ));
1828
+ result.guidance = addUnique(result.guidance || [], result.errors);
1829
+ }
1821
1830
  }
1822
1831
  }
1823
1832
 
1824
- if (errors.length > 0 || !write) {
1833
+ if (errors.length > 0 || semanticBlocked || !write) {
1825
1834
  return result;
1826
1835
  }
1827
1836
 
@@ -2449,6 +2458,9 @@ function compactMutationResponse(payload) {
2449
2458
  if (Array.isArray(payload.guidance)) {
2450
2459
  compact.guidance = payload.guidance;
2451
2460
  }
2461
+ if (Array.isArray(payload.semanticConflicts)) {
2462
+ compact.semanticConflicts = payload.semanticConflicts;
2463
+ }
2452
2464
  }
2453
2465
  return compact;
2454
2466
  }
@@ -2661,15 +2673,16 @@ function memoryHitCard(element, maxDescLen) {
2661
2673
  return card;
2662
2674
  }
2663
2675
 
2664
- // L1 semantic dedup advisory (GraphDeduplication item 3): for each element about
2665
- // to be created, return semantically near elements within a CONTROLLED scope
2666
- // (same ArchiMate type, and - when the add targets views - the union of those
2667
- // views' current members) scoring above a strict threshold. Advisory ONLY: it
2668
- // never rejects or blocks a write; a missing/unavailable semantic backend
2669
- // degrades to an explicit status, never an error.
2676
+ // Semantic dedup gate (GraphDeduplication): for each element that would be
2677
+ // CREATED, search the WHOLE graph for same-type semantically near elements (a
2678
+ // large candidate window, strict threshold). A hit blocks the default create
2679
+ // the caller must reuse an existing candidate or explicitly pass
2680
+ // onConflict:'allowDuplicate' with a justification. A missing/unavailable
2681
+ // semantic backend degrades to an explicit status, never an error.
2670
2682
  const DEFAULT_SEMANTIC_DEDUP_THRESHOLD = 0.85;
2671
2683
  const SEMANTIC_DEDUP_MAX_QUERIES = 3;
2672
- const SEMANTIC_DEDUP_MAX_MATCHES = 5;
2684
+ const SEMANTIC_DEDUP_MAX_MATCHES = 10;
2685
+ const SEMANTIC_DEDUP_TOP_K = 25;
2673
2686
 
2674
2687
  function semanticDedupThreshold() {
2675
2688
  const raw = process.env.ARGO_SEMANTIC_DEDUP_THRESHOLD
@@ -2695,6 +2708,47 @@ function collectViewMemberElementIds(document, viewIds) {
2695
2708
  return ids;
2696
2709
  }
2697
2710
 
2711
+ // Only element adds that the applied mutation actually CREATED are subject to
2712
+ // the L1 advisory. A reuse that found an exact natural-key match creates nothing
2713
+ // (created:false) and is skipped; a reuse that fell through to creation
2714
+ // (created:true, id === requested id) is advised like any other new element.
2715
+ function selectCreatedElementAdds(mutations, mutationSummaries) {
2716
+ const createdIds = new Set(
2717
+ (Array.isArray(mutationSummaries) ? mutationSummaries : [])
2718
+ .filter(summary => summary && summary.type === 'addElement' && summary.created === true)
2719
+ .map(summary => summary.id),
2720
+ );
2721
+ return (Array.isArray(mutations) ? mutations : []).filter(mutation => (
2722
+ mutation
2723
+ && mutation.type === 'addElement'
2724
+ && mutation.element
2725
+ && createdIds.has(mutation.element.id)
2726
+ ));
2727
+ }
2728
+
2729
+ // Decide whether the semantic candidates block the create. A create is blocked
2730
+ // unless the caller explicitly opted into allowDuplicate (with justification).
2731
+ function evaluateSemanticDedupGate(advisory, mutations) {
2732
+ if (!advisory || advisory.status !== 'passed' || !Array.isArray(advisory.candidates)) {
2733
+ return { blocked: false, conflicts: [] };
2734
+ }
2735
+ const byId = new Map((Array.isArray(mutations) ? mutations : [])
2736
+ .filter(mutation => mutation && mutation.type === 'addElement' && mutation.element)
2737
+ .map(mutation => [mutation.element.id, mutation]));
2738
+ const conflicts = [];
2739
+ for (const entry of advisory.candidates) {
2740
+ if (!entry || !Array.isArray(entry.matches) || entry.matches.length === 0) {
2741
+ continue;
2742
+ }
2743
+ const mutation = byId.get(entry.requested && entry.requested.id);
2744
+ const overridden = mutation && mutation.onConflict === 'allowDuplicate';
2745
+ if (!overridden) {
2746
+ conflicts.push(entry);
2747
+ }
2748
+ }
2749
+ return { blocked: conflicts.length > 0, conflicts };
2750
+ }
2751
+
2698
2752
  async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2699
2753
  if (process.env.ARGO_MCP_SEMANTIC_DEDUP === '0') {
2700
2754
  return undefined;
@@ -2705,7 +2759,6 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2705
2759
  && mutation.element
2706
2760
  && typeof mutation.element.name === 'string'
2707
2761
  && mutation.element.name.trim() !== ''
2708
- && mutation.onConflict !== 'reuse'
2709
2762
  ));
2710
2763
  if (addedElements.length === 0) {
2711
2764
  return undefined;
@@ -2734,7 +2787,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2734
2787
  const intent = [element.type, element.name, element.description]
2735
2788
  .filter(part => typeof part === 'string' && part.trim() !== '')
2736
2789
  .join(' ');
2737
- const retrieved = await journey.query({ purpose: 'general', intent });
2790
+ const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K });
2738
2791
  const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
2739
2792
  const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
2740
2793
  const elements = subset && subset.status === 'passed' && subset.document
@@ -2744,7 +2797,6 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2744
2797
  .filter(candidate => candidate && candidate.id !== element.id && typeof candidate.semanticScore === 'number')
2745
2798
  .filter(candidate => !element.type || candidate.type === element.type)
2746
2799
  .filter(candidate => candidate.semanticScore >= threshold)
2747
- .filter(candidate => targetMemberIds.size === 0 || targetMemberIds.has(candidate.id))
2748
2800
  .sort((left, right) => right.semanticScore - left.semanticScore)
2749
2801
  .slice(0, SEMANTIC_DEDUP_MAX_MATCHES)
2750
2802
  .map(candidate => ({
@@ -2768,7 +2820,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2768
2820
  status: 'passed',
2769
2821
  advisoryOnly: true,
2770
2822
  threshold,
2771
- scope: 'same type; when view_ids are given, restricted to those views\' current members',
2823
+ scope: 'whole graph, same type',
2772
2824
  candidates,
2773
2825
  has_suggestions: candidates.some(entry => entry.matches.length > 0),
2774
2826
  };
@@ -4106,6 +4158,8 @@ module.exports = {
4106
4158
  TOOLS,
4107
4159
  applyMutations,
4108
4160
  buildSemanticDedupAdvisory,
4161
+ selectCreatedElementAdds,
4162
+ evaluateSemanticDedupGate,
4109
4163
  callTool,
4110
4164
  compactMutationResponse,
4111
4165
  createDefaultCanonicalSemanticInitComposition,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.18.0",
3
+ "version": "0.19.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": {