archgraph-argo 0.10.40 → 0.10.41

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.
@@ -155,7 +155,7 @@ const TOOLS = [
155
155
  },
156
156
  {
157
157
  name: 'updateArchitectureElement',
158
- description: 'Use for one global element metadata patch. Does not change view membership. Element id and type are immutable; remove and re-add to change them. patch.attributes is a targeted merge by attribute name: each entry {name, value?, description?} upserts that attribute, {name, op:"remove"} deletes it, and attributes NOT mentioned are preserved — updating one attribute never wipes out the others.',
158
+ description: 'Use for one global element metadata patch. Does not change view membership. Element id and type are immutable; remove and re-add to change them. patch.attributes is a targeted merge: single-valued attributes upsert by name; multi-valued ledgers (e.g. commit) upsert by name+value — a new value appends a new entry, re-registering the same value updates its description; {name, op:"remove"} (optionally with value) deletes the matching entries; attributes NOT mentioned are preserved — updating one attribute never wipes out the others.',
159
159
  inputSchema: {
160
160
  type: 'object',
161
161
  required: ['id', 'patch'],
@@ -248,7 +248,7 @@ const TOOLS = [
248
248
  },
249
249
  {
250
250
  name: 'updateArchitectureElement',
251
- description: 'Use for one global element metadata patch. Does not change view membership. Element id and type are immutable; remove and re-add to change them. patch.attributes is a targeted merge by attribute name: each entry {name, value?, description?} upserts that attribute, {name, op:"remove"} deletes it, and attributes NOT mentioned are preserved — updating one attribute never wipes out the others. Set dryRun to preview without writing.',
251
+ description: 'Use for one global element metadata patch. Does not change view membership. Element id and type are immutable; remove and re-add to change them. patch.attributes is a targeted merge: single-valued attributes upsert by name; multi-valued ledgers (e.g. commit) upsert by name+value — a new value appends a new entry, re-registering the same value updates its description; {name, op:"remove"} (optionally with value) deletes the matching entries; attributes NOT mentioned are preserved — updating one attribute never wipes out the others. Set dryRun to preview without writing.',
252
252
  inputSchema: {
253
253
  type: 'object',
254
254
  required: ['id', 'patch'],
@@ -1031,17 +1031,33 @@ function buildNativeSubgraph(document, includedElementIds, includedRelationshipI
1031
1031
  return { elements, relationships, views };
1032
1032
  }
1033
1033
 
1034
+ /**
1035
+ * Attribute names that are MULTI-VALUED ledgers (many attributes share the same
1036
+ * `name` but carry distinct `value`s). The canonical example is the `commit`
1037
+ * ledger: an element accumulates one `{ name: 'commit', value: <sha> }` per
1038
+ * commit. For these, a patch entry upserts by (name, value) — a new value
1039
+ * APPENDS a new entry, re-registering the same value updates its description.
1040
+ */
1041
+ const MULTI_VALUE_ATTRIBUTE_NAMES = new Set(['commit']);
1042
+
1034
1043
  /**
1035
1044
  * Merge a targeted attribute patch into the element's existing attributes.
1036
1045
  *
1037
1046
  * Each patch entry is an object with a non-empty `name`:
1038
- * - `{ name, op: 'remove' }` -> remove that attribute;
1039
- * - `{ name, value }` / `{ name, value, description }` -> upsert (add or update in place).
1047
+ * - `{ name, op: 'remove' }` / `{ name, value, op: 'remove' }`
1048
+ * -> remove matching attribute(s); with `value`, only the exact entry.
1049
+ * - `{ name, value }` / `{ name, value, description }` -> upsert.
1050
+ *
1051
+ * Upsert semantics:
1052
+ * - Multi-valued ledgers (e.g. `commit`): match by (name, value). Same value
1053
+ * -> update description in place; new value -> APPEND a new entry.
1054
+ * - Single-valued attributes (e.g. `deliveryStatus`): match by name and
1055
+ * update value/description in place; absent -> append.
1040
1056
  *
1041
1057
  * Attributes NOT mentioned in the patch are preserved. This avoids the
1042
1058
  * full-array replacement footgun where updating one attribute would wipe out the
1043
1059
  * rest (e.g. a stale deliveryStatus being re-written when registering a commit
1044
- * attribute).
1060
+ * attribute), while still letting the commit ledger accumulate entries.
1045
1061
  */
1046
1062
  function mergeAttributesPatch(existing, patchEntries) {
1047
1063
  const result = Array.isArray(existing)
@@ -1055,21 +1071,37 @@ function mergeAttributesPatch(existing, patchEntries) {
1055
1071
  throw new Error('patch.attributes entries must have a non-empty string name');
1056
1072
  }
1057
1073
  if (entry.op === 'remove') {
1058
- const index = result.findIndex(attr => attr.name === entry.name);
1059
- if (index >= 0) {
1060
- result.splice(index, 1);
1074
+ // Remove ALL matching entries; when a value is given, restrict to that
1075
+ // exact (name, value) entry so a single ledger line can be removed.
1076
+ for (let i = result.length - 1; i >= 0; i -= 1) {
1077
+ const attr = result[i];
1078
+ const nameMatches = attr.name === entry.name;
1079
+ const valueMatches = !Object.prototype.hasOwnProperty.call(entry, 'value') || attr.value === entry.value;
1080
+ if (nameMatches && valueMatches) result.splice(i, 1);
1061
1081
  }
1062
1082
  continue;
1063
1083
  }
1064
- const index = result.findIndex(attr => attr.name === entry.name);
1065
- if (index >= 0) {
1066
- if (Object.prototype.hasOwnProperty.call(entry, 'value')) result[index].value = entry.value;
1067
- if (Object.prototype.hasOwnProperty.call(entry, 'description')) result[index].description = entry.description;
1084
+ if (MULTI_VALUE_ATTRIBUTE_NAMES.has(entry.name)) {
1085
+ const index = result.findIndex(attr => attr.name === entry.name && attr.value === entry.value);
1086
+ if (index >= 0) {
1087
+ if (Object.prototype.hasOwnProperty.call(entry, 'description')) result[index].description = entry.description;
1088
+ } else {
1089
+ const newAttribute = { name: entry.name };
1090
+ if (Object.prototype.hasOwnProperty.call(entry, 'value')) newAttribute.value = entry.value;
1091
+ if (Object.prototype.hasOwnProperty.call(entry, 'description')) newAttribute.description = entry.description;
1092
+ result.push(newAttribute);
1093
+ }
1068
1094
  } else {
1069
- const newAttribute = { name: entry.name };
1070
- if (Object.prototype.hasOwnProperty.call(entry, 'value')) newAttribute.value = entry.value;
1071
- if (Object.prototype.hasOwnProperty.call(entry, 'description')) newAttribute.description = entry.description;
1072
- result.push(newAttribute);
1095
+ const index = result.findIndex(attr => attr.name === entry.name);
1096
+ if (index >= 0) {
1097
+ if (Object.prototype.hasOwnProperty.call(entry, 'value')) result[index].value = entry.value;
1098
+ if (Object.prototype.hasOwnProperty.call(entry, 'description')) result[index].description = entry.description;
1099
+ } else {
1100
+ const newAttribute = { name: entry.name };
1101
+ if (Object.prototype.hasOwnProperty.call(entry, 'value')) newAttribute.value = entry.value;
1102
+ if (Object.prototype.hasOwnProperty.call(entry, 'description')) newAttribute.description = entry.description;
1103
+ result.push(newAttribute);
1104
+ }
1073
1105
  }
1074
1106
  }
1075
1107
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgraph-argo",
3
- "version": "0.10.40",
3
+ "version": "0.10.41",
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": {