archgraph-argo 0.18.1 → 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,20 +1804,33 @@ async function buildMutationResult(context, mutations, write, dependencies) {
|
|
|
1812
1804
|
result.guidance = buildFailureGuidance(errors);
|
|
1813
1805
|
}
|
|
1814
1806
|
|
|
1815
|
-
//
|
|
1816
|
-
//
|
|
1817
|
-
//
|
|
1818
|
-
//
|
|
1819
|
-
|
|
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;
|
|
1820
1812
|
if (errors.length === 0) {
|
|
1821
1813
|
const createdAdds = selectCreatedElementAdds(mutations, mutationResult.mutationSummaries);
|
|
1822
1814
|
const semanticDedup = await buildSemanticDedupAdvisory(context, createdAdds, dependencies);
|
|
1823
1815
|
if (semanticDedup) {
|
|
1824
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
|
+
}
|
|
1825
1830
|
}
|
|
1826
1831
|
}
|
|
1827
1832
|
|
|
1828
|
-
if (errors.length > 0 || !write) {
|
|
1833
|
+
if (errors.length > 0 || semanticBlocked || !write) {
|
|
1829
1834
|
return result;
|
|
1830
1835
|
}
|
|
1831
1836
|
|
|
@@ -2453,6 +2458,9 @@ function compactMutationResponse(payload) {
|
|
|
2453
2458
|
if (Array.isArray(payload.guidance)) {
|
|
2454
2459
|
compact.guidance = payload.guidance;
|
|
2455
2460
|
}
|
|
2461
|
+
if (Array.isArray(payload.semanticConflicts)) {
|
|
2462
|
+
compact.semanticConflicts = payload.semanticConflicts;
|
|
2463
|
+
}
|
|
2456
2464
|
}
|
|
2457
2465
|
return compact;
|
|
2458
2466
|
}
|
|
@@ -2665,15 +2673,16 @@ function memoryHitCard(element, maxDescLen) {
|
|
|
2665
2673
|
return card;
|
|
2666
2674
|
}
|
|
2667
2675
|
|
|
2668
|
-
//
|
|
2669
|
-
//
|
|
2670
|
-
//
|
|
2671
|
-
//
|
|
2672
|
-
//
|
|
2673
|
-
// 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.
|
|
2674
2682
|
const DEFAULT_SEMANTIC_DEDUP_THRESHOLD = 0.85;
|
|
2675
2683
|
const SEMANTIC_DEDUP_MAX_QUERIES = 3;
|
|
2676
|
-
const SEMANTIC_DEDUP_MAX_MATCHES =
|
|
2684
|
+
const SEMANTIC_DEDUP_MAX_MATCHES = 10;
|
|
2685
|
+
const SEMANTIC_DEDUP_TOP_K = 25;
|
|
2677
2686
|
|
|
2678
2687
|
function semanticDedupThreshold() {
|
|
2679
2688
|
const raw = process.env.ARGO_SEMANTIC_DEDUP_THRESHOLD
|
|
@@ -2717,6 +2726,29 @@ function selectCreatedElementAdds(mutations, mutationSummaries) {
|
|
|
2717
2726
|
));
|
|
2718
2727
|
}
|
|
2719
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
|
+
|
|
2720
2752
|
async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
2721
2753
|
if (process.env.ARGO_MCP_SEMANTIC_DEDUP === '0') {
|
|
2722
2754
|
return undefined;
|
|
@@ -2755,7 +2787,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
|
2755
2787
|
const intent = [element.type, element.name, element.description]
|
|
2756
2788
|
.filter(part => typeof part === 'string' && part.trim() !== '')
|
|
2757
2789
|
.join(' ');
|
|
2758
|
-
const retrieved = await journey.query({ purpose: 'general', intent });
|
|
2790
|
+
const retrieved = await journey.query({ purpose: 'general', intent, topK: SEMANTIC_DEDUP_TOP_K });
|
|
2759
2791
|
const source = retrieved && (retrieved.result || retrieved.document) || retrieved;
|
|
2760
2792
|
const subset = buildCanonicalSemanticDocumentSubset(source, context.document);
|
|
2761
2793
|
const elements = subset && subset.status === 'passed' && subset.document
|
|
@@ -2765,7 +2797,6 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
|
2765
2797
|
.filter(candidate => candidate && candidate.id !== element.id && typeof candidate.semanticScore === 'number')
|
|
2766
2798
|
.filter(candidate => !element.type || candidate.type === element.type)
|
|
2767
2799
|
.filter(candidate => candidate.semanticScore >= threshold)
|
|
2768
|
-
.filter(candidate => targetMemberIds.size === 0 || targetMemberIds.has(candidate.id))
|
|
2769
2800
|
.sort((left, right) => right.semanticScore - left.semanticScore)
|
|
2770
2801
|
.slice(0, SEMANTIC_DEDUP_MAX_MATCHES)
|
|
2771
2802
|
.map(candidate => ({
|
|
@@ -2789,7 +2820,7 @@ async function buildSemanticDedupAdvisory(context, mutations, dependencies) {
|
|
|
2789
2820
|
status: 'passed',
|
|
2790
2821
|
advisoryOnly: true,
|
|
2791
2822
|
threshold,
|
|
2792
|
-
scope: '
|
|
2823
|
+
scope: 'whole graph, same type',
|
|
2793
2824
|
candidates,
|
|
2794
2825
|
has_suggestions: candidates.some(entry => entry.matches.length > 0),
|
|
2795
2826
|
};
|
|
@@ -4128,6 +4159,7 @@ module.exports = {
|
|
|
4128
4159
|
applyMutations,
|
|
4129
4160
|
buildSemanticDedupAdvisory,
|
|
4130
4161
|
selectCreatedElementAdds,
|
|
4162
|
+
evaluateSemanticDedupGate,
|
|
4131
4163
|
callTool,
|
|
4132
4164
|
compactMutationResponse,
|
|
4133
4165
|
createDefaultCanonicalSemanticInitComposition,
|
package/package.json
CHANGED