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
|
|
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
|
|
58
|
-
1.
|
|
59
|
-
2.
|
|
60
|
-
3.
|
|
61
|
-
4.
|
|
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 reused — the 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: ['
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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: ['
|
|
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(['
|
|
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:
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
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
|
-
? '
|
|
1229
|
+
? 'reuse'
|
|
1230
1230
|
: options.onConflict;
|
|
1231
1231
|
if (!DUPLICATE_CONFLICT_POLICIES.has(onConflict)) {
|
|
1232
|
-
throw new Error(`onConflict must be one of
|
|
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
|
-
//
|
|
1816
|
-
//
|
|
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
|
|
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
|
-
//
|
|
2665
|
-
//
|
|
2666
|
-
//
|
|
2667
|
-
//
|
|
2668
|
-
//
|
|
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 =
|
|
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: '
|
|
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