archgraph-argo 0.18.1 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  },
@@ -698,6 +698,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
698
698
  RectRight: 40 + col * 260 + 180, RectBottom: 40 + row * 160 + 90,
699
699
  Sequence: seq,
700
700
  });
701
+ placedObjs.add(Number(oid));
701
702
  seq++;
702
703
  }
703
704
  if (newObjs.length > 0 && !o.dryRun) {
@@ -731,6 +732,7 @@ function syncGraphToQea(graph, qeaPath, opts) {
731
732
  if (cid === undefined || cid < 0) { continue; }
732
733
  if (placedLinks.has(Number(cid))) { continue; }
733
734
  newLinks.push({ DiagramID: diagramId, ConnectorID: Number(cid), Style: '', Geometry: '' });
735
+ placedLinks.add(Number(cid));
734
736
  }
735
737
  if (newLinks.length > 0 && !o.dryRun) {
736
738
  insertMany(db, 't_diagramlinks', ['DiagramID', 'ConnectorID', 'Style', 'Geometry'], newLinks);
@@ -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({
@@ -116,12 +116,19 @@ function validateGraphSemantics(document, errors) {
116
116
  }
117
117
  }
118
118
  const includedElementIds = new Set(view.included_elements || []);
119
+ if (includedElementIds.size !== (view.included_elements || []).length) {
120
+ errors.push(`views '${view.view_id}' must not contain duplicate included_elements`);
121
+ }
119
122
  for (const elementId of view.included_elements || []) {
120
123
  elementIdsIncludedInViews.add(elementId);
121
124
  if (!elementById.has(elementId)) {
122
125
  errors.push(`views '${view.view_id}' references missing included element '${elementId}'`);
123
126
  }
124
127
  }
128
+ const includedRelationshipIds = new Set(view.included_relationships || []);
129
+ if (includedRelationshipIds.size !== (view.included_relationships || []).length) {
130
+ errors.push(`views '${view.view_id}' must not contain duplicate included_relationships`);
131
+ }
125
132
  for (const relationshipId of view.included_relationships || []) {
126
133
  relationshipIdsIncludedInViews.add(relationshipId);
127
134
  const rel = relationshipById.get(relationshipId);
@@ -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
  }
@@ -1527,11 +1519,18 @@ function applyMutations(document, mutations) {
1527
1519
  requestedId: mutation.view.view_id,
1528
1520
  });
1529
1521
  } else {
1530
- nextDocument.views.push(clone(mutation.view));
1531
- upsertSubdiagramViewIntoElement(nextDocument, mutation.view.parent_element_id, mutation.view);
1532
- touchedViewIds.add(mutation.view.view_id);
1533
- viewLimitCheckIds.add(mutation.view.view_id);
1534
- mutationSummaries.push({ type: mutation.type, id: mutation.view.view_id });
1522
+ const newView = clone(mutation.view);
1523
+ if (Array.isArray(newView.included_elements)) {
1524
+ newView.included_elements = addUnique([], newView.included_elements);
1525
+ }
1526
+ if (Array.isArray(newView.included_relationships)) {
1527
+ newView.included_relationships = addUnique([], newView.included_relationships);
1528
+ }
1529
+ nextDocument.views.push(newView);
1530
+ upsertSubdiagramViewIntoElement(nextDocument, newView.parent_element_id, newView);
1531
+ touchedViewIds.add(newView.view_id);
1532
+ viewLimitCheckIds.add(newView.view_id);
1533
+ mutationSummaries.push({ type: mutation.type, id: newView.view_id });
1535
1534
  }
1536
1535
  continue;
1537
1536
  }
@@ -1546,7 +1545,14 @@ function applyMutations(document, mutations) {
1546
1545
  }
1547
1546
  const oldParentId = view.parent_element_id;
1548
1547
  const oldViewId = view.view_id;
1549
- Object.assign(view, clone(mutation.patch));
1548
+ const patch = clone(mutation.patch);
1549
+ if (Array.isArray(patch.included_elements)) {
1550
+ patch.included_elements = addUnique([], patch.included_elements);
1551
+ }
1552
+ if (Array.isArray(patch.included_relationships)) {
1553
+ patch.included_relationships = addUnique([], patch.included_relationships);
1554
+ }
1555
+ Object.assign(view, patch);
1550
1556
  if (oldParentId !== view.parent_element_id) {
1551
1557
  removeSubdiagramViewFromElement(nextDocument, oldParentId, oldViewId);
1552
1558
  }
@@ -1812,20 +1818,33 @@ async function buildMutationResult(context, mutations, write, dependencies) {
1812
1818
  result.guidance = buildFailureGuidance(errors);
1813
1819
  }
1814
1820
 
1815
- // L1 advisory: semantic near-duplicate suggestions for element adds. Never
1816
- // blocks or fails the write; preview and apply both surface it. Only adds the
1817
- // applied mutation actually CREATED are advised — a reuse that found an exact
1818
- // natural-key match creates nothing and is skipped, while a reuse that fell
1819
- // through to creation is advised like any other new element.
1821
+ // Semantic dedup gate: a would-be element create is blocked when a same-type
1822
+ // semantic near-duplicate exists, unless the caller explicitly passed
1823
+ // onConflict:'allowDuplicate' (with justification). Never blocks on an
1824
+ // unavailable semantic backend. Both preview and apply surface the outcome.
1825
+ let semanticBlocked = false;
1820
1826
  if (errors.length === 0) {
1821
1827
  const createdAdds = selectCreatedElementAdds(mutations, mutationResult.mutationSummaries);
1822
1828
  const semanticDedup = await buildSemanticDedupAdvisory(context, createdAdds, dependencies);
1823
1829
  if (semanticDedup) {
1824
1830
  result.semanticDedup = semanticDedup;
1831
+ const gate = evaluateSemanticDedupGate(semanticDedup, createdAdds);
1832
+ if (gate.blocked) {
1833
+ semanticBlocked = true;
1834
+ result.status = 'failed';
1835
+ result.after = beforeSummary;
1836
+ result.semanticConflicts = gate.conflicts;
1837
+ result.errors = gate.conflicts.map(entry => (
1838
+ `Semantic duplicate for element (type '${entry.requested.type}', name '${entry.requested.name}'): `
1839
+ + `${entry.matches.map(match => `${match.id} (score ${match.score})`).join(', ')}. `
1840
+ + 'Reuse the existing element (re-call with that element id), or pass onConflict:"allowDuplicate" with a justification to create a new one.'
1841
+ ));
1842
+ result.guidance = addUnique(result.guidance || [], result.errors);
1843
+ }
1825
1844
  }
1826
1845
  }
1827
1846
 
1828
- if (errors.length > 0 || !write) {
1847
+ if (errors.length > 0 || semanticBlocked || !write) {
1829
1848
  return result;
1830
1849
  }
1831
1850
 
@@ -2453,6 +2472,9 @@ function compactMutationResponse(payload) {
2453
2472
  if (Array.isArray(payload.guidance)) {
2454
2473
  compact.guidance = payload.guidance;
2455
2474
  }
2475
+ if (Array.isArray(payload.semanticConflicts)) {
2476
+ compact.semanticConflicts = payload.semanticConflicts;
2477
+ }
2456
2478
  }
2457
2479
  return compact;
2458
2480
  }
@@ -2665,15 +2687,16 @@ function memoryHitCard(element, maxDescLen) {
2665
2687
  return card;
2666
2688
  }
2667
2689
 
2668
- // L1 semantic dedup advisory (GraphDeduplication item 3): for each element about
2669
- // to be created, return semantically near elements within a CONTROLLED scope
2670
- // (same ArchiMate type, and - when the add targets views - the union of those
2671
- // views' current members) scoring above a strict threshold. Advisory ONLY: it
2672
- // never rejects or blocks a write; a missing/unavailable semantic backend
2673
- // degrades to an explicit status, never an error.
2690
+ // Semantic dedup gate (GraphDeduplication): for each element that would be
2691
+ // CREATED, search the WHOLE graph for same-type semantically near elements (a
2692
+ // large candidate window, strict threshold). A hit blocks the default create
2693
+ // the caller must reuse an existing candidate or explicitly pass
2694
+ // onConflict:'allowDuplicate' with a justification. A missing/unavailable
2695
+ // semantic backend degrades to an explicit status, never an error.
2674
2696
  const DEFAULT_SEMANTIC_DEDUP_THRESHOLD = 0.85;
2675
2697
  const SEMANTIC_DEDUP_MAX_QUERIES = 3;
2676
- const SEMANTIC_DEDUP_MAX_MATCHES = 5;
2698
+ const SEMANTIC_DEDUP_MAX_MATCHES = 10;
2699
+ const SEMANTIC_DEDUP_TOP_K = 25;
2677
2700
 
2678
2701
  function semanticDedupThreshold() {
2679
2702
  const raw = process.env.ARGO_SEMANTIC_DEDUP_THRESHOLD
@@ -2717,6 +2740,29 @@ function selectCreatedElementAdds(mutations, mutationSummaries) {
2717
2740
  ));
2718
2741
  }
2719
2742
 
2743
+ // Decide whether the semantic candidates block the create. A create is blocked
2744
+ // unless the caller explicitly opted into allowDuplicate (with justification).
2745
+ function evaluateSemanticDedupGate(advisory, mutations) {
2746
+ if (!advisory || advisory.status !== 'passed' || !Array.isArray(advisory.candidates)) {
2747
+ return { blocked: false, conflicts: [] };
2748
+ }
2749
+ const byId = new Map((Array.isArray(mutations) ? mutations : [])
2750
+ .filter(mutation => mutation && mutation.type === 'addElement' && mutation.element)
2751
+ .map(mutation => [mutation.element.id, mutation]));
2752
+ const conflicts = [];
2753
+ for (const entry of advisory.candidates) {
2754
+ if (!entry || !Array.isArray(entry.matches) || entry.matches.length === 0) {
2755
+ continue;
2756
+ }
2757
+ const mutation = byId.get(entry.requested && entry.requested.id);
2758
+ const overridden = mutation && mutation.onConflict === 'allowDuplicate';
2759
+ if (!overridden) {
2760
+ conflicts.push(entry);
2761
+ }
2762
+ }
2763
+ return { blocked: conflicts.length > 0, conflicts };
2764
+ }
2765
+
2720
2766
  async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2721
2767
  if (process.env.ARGO_MCP_SEMANTIC_DEDUP === '0') {
2722
2768
  return undefined;
@@ -2755,7 +2801,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2755
2801
  const intent = [element.type, element.name, element.description]
2756
2802
  .filter(part => typeof part === 'string' && part.trim() !== '')
2757
2803
  .join(' ');
2758
- const retrieved = await journey.query({ purpose: 'general', intent });
2804
+ const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K });
2759
2805
  const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
2760
2806
  const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
2761
2807
  const elements = subset && subset.status === 'passed' && subset.document
@@ -2765,7 +2811,6 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2765
2811
  .filter(candidate => candidate && candidate.id !== element.id && typeof candidate.semanticScore === 'number')
2766
2812
  .filter(candidate => !element.type || candidate.type === element.type)
2767
2813
  .filter(candidate => candidate.semanticScore >= threshold)
2768
- .filter(candidate => targetMemberIds.size === 0 || targetMemberIds.has(candidate.id))
2769
2814
  .sort((left, right) => right.semanticScore - left.semanticScore)
2770
2815
  .slice(0, SEMANTIC_DEDUP_MAX_MATCHES)
2771
2816
  .map(candidate => ({
@@ -2789,7 +2834,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
2789
2834
  status: 'passed',
2790
2835
  advisoryOnly: true,
2791
2836
  threshold,
2792
- scope: 'same type; when view_ids are given, restricted to those views\' current members',
2837
+ scope: 'whole graph, same type',
2793
2838
  candidates,
2794
2839
  has_suggestions: candidates.some(entry => entry.matches.length > 0),
2795
2840
  };
@@ -4128,6 +4173,7 @@ module.exports = {
4128
4173
  applyMutations,
4129
4174
  buildSemanticDedupAdvisory,
4130
4175
  selectCreatedElementAdds,
4176
+ evaluateSemanticDedupGate,
4131
4177
  callTool,
4132
4178
  compactMutationResponse,
4133
4179
  createDefaultCanonicalSemanticInitComposition,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.18.1",
3
+ "version": "0.19.1",
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": {