tagged-urn 0.40.107 → 0.41.110

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.
package/package.json CHANGED
@@ -28,5 +28,5 @@
28
28
  "scripts": {
29
29
  "test": "node tagged-urn.test.js"
30
30
  },
31
- "version": "0.40.107"
31
+ "version": "0.41.110"
32
32
  }
package/tagged-urn.js CHANGED
@@ -110,6 +110,25 @@ const Form = {
110
110
  MUST_NOT_HAVE: 6, // "!"
111
111
  };
112
112
 
113
+ const TaggedUrnRelationKind = Object.freeze({
114
+ EQUIVALENT: 'equivalent',
115
+ COMPARABLE: 'comparable',
116
+ INCOMPARABLE: 'incomparable',
117
+ });
118
+
119
+ class TaggedUrnCoordinateDelta {
120
+ constructor(prefix, removed = {}, added = {}, relationKind = TaggedUrnRelationKind.EQUIVALENT) {
121
+ this.prefix = prefix.toLowerCase();
122
+ this.removed = { ...removed };
123
+ this.added = { ...added };
124
+ this.relationKind = relationKind;
125
+ }
126
+
127
+ isEmpty() {
128
+ return Object.keys(this.removed).length === 0 && Object.keys(this.added).length === 0;
129
+ }
130
+ }
131
+
113
132
  // Classify a stored value. Returns { kind, raw } — raw is the inner
114
133
  // v for ?=v and !=v, the literal value for exact, and '' otherwise.
115
134
  function classifyForm(value) {
@@ -828,6 +847,94 @@ class TaggedUrn {
828
847
  return this.isComparable(other);
829
848
  }
830
849
 
850
+ /**
851
+ * Compute the coordinate-space delta from `base` to `this`.
852
+ *
853
+ * Delta is defined over explicit canonical coordinates, not semantic
854
+ * equivalence classes. Equivalent URNs may still yield a non-empty delta if
855
+ * one side explicitly authors no-op coordinates.
856
+ *
857
+ * @param {TaggedUrn} base
858
+ * @returns {TaggedUrnCoordinateDelta}
859
+ * @throws {TaggedUrnError} If prefixes do not match
860
+ */
861
+ deltaFrom(base) {
862
+ if (!base) {
863
+ throw new TaggedUrnError(ErrorCodes.INVALID_FORMAT, 'cannot derive delta from null URN');
864
+ }
865
+ if (this.prefix !== base.prefix) {
866
+ throw new TaggedUrnError(
867
+ ErrorCodes.PREFIX_MISMATCH,
868
+ `Cannot compare URNs with different prefixes: '${base.prefix}' vs '${this.prefix}'`
869
+ );
870
+ }
871
+
872
+ const relationKind = this.isEquivalent(base)
873
+ ? TaggedUrnRelationKind.EQUIVALENT
874
+ : (this.isComparable(base)
875
+ ? TaggedUrnRelationKind.COMPARABLE
876
+ : TaggedUrnRelationKind.INCOMPARABLE);
877
+
878
+ const removed = {};
879
+ const added = {};
880
+ const allKeys = new Set([
881
+ ...Object.keys(base.tags),
882
+ ...Object.keys(this.tags),
883
+ ]);
884
+
885
+ for (const key of allKeys) {
886
+ const baseValue = Object.prototype.hasOwnProperty.call(base.tags, key)
887
+ ? base.tags[key]
888
+ : undefined;
889
+ const targetValue = Object.prototype.hasOwnProperty.call(this.tags, key)
890
+ ? this.tags[key]
891
+ : undefined;
892
+ if (baseValue === targetValue) {
893
+ continue;
894
+ }
895
+ if (baseValue !== undefined) {
896
+ removed[key] = baseValue;
897
+ }
898
+ if (targetValue !== undefined) {
899
+ added[key] = targetValue;
900
+ }
901
+ }
902
+
903
+ return new TaggedUrnCoordinateDelta(this.prefix, removed, added, relationKind);
904
+ }
905
+
906
+ /**
907
+ * Apply a coordinate-space delta to this tagged URN.
908
+ *
909
+ * Keys named in `removed` are deleted regardless of their current value,
910
+ * then keys named in `added` are inserted. Unrelated coordinates are
911
+ * preserved.
912
+ *
913
+ * @param {TaggedUrnCoordinateDelta} delta
914
+ * @returns {TaggedUrn}
915
+ * @throws {TaggedUrnError} If prefixes do not match
916
+ */
917
+ applyDelta(delta) {
918
+ if (!delta) {
919
+ throw new TaggedUrnError(ErrorCodes.INVALID_FORMAT, 'cannot apply null delta');
920
+ }
921
+ if (this.prefix !== delta.prefix) {
922
+ throw new TaggedUrnError(
923
+ ErrorCodes.PREFIX_MISMATCH,
924
+ `Cannot apply delta with different prefix: '${delta.prefix}' vs '${this.prefix}'`
925
+ );
926
+ }
927
+
928
+ const nextTags = { ...this.tags };
929
+ for (const key of Object.keys(delta.removed)) {
930
+ delete nextTags[key];
931
+ }
932
+ for (const [key, value] of Object.entries(delta.added)) {
933
+ nextTags[key] = value;
934
+ }
935
+ return new TaggedUrn(this.prefix, nextTags, true);
936
+ }
937
+
831
938
  /**
832
939
  * Create a new URN with a specific tag set to wildcard
833
940
  *
@@ -1075,6 +1182,8 @@ class UrnMatcher {
1075
1182
  // Export for CommonJS
1076
1183
  module.exports = {
1077
1184
  TaggedUrn,
1185
+ TaggedUrnCoordinateDelta,
1186
+ TaggedUrnRelationKind,
1078
1187
  TaggedUrnBuilder,
1079
1188
  UrnMatcher,
1080
1189
  TaggedUrnError,
@@ -4,6 +4,7 @@
4
4
 
5
5
  const {
6
6
  TaggedUrn,
7
+ TaggedUrnRelationKind,
7
8
  TaggedUrnBuilder,
8
9
  UrnMatcher,
9
10
  TaggedUrnError,
@@ -1495,4 +1496,35 @@ module.exports = { runTests };
1495
1496
  );
1496
1497
  })();
1497
1498
 
1499
+ // TEST596: Empty delta preserves a more specific concrete instance.
1500
+ (() => {
1501
+ const base = TaggedUrn.fromString('media:');
1502
+ const target = TaggedUrn.fromString('media:');
1503
+ const runtime = TaggedUrn.fromString('media:image;png');
1504
+ const delta = target.deltaFrom(base);
1505
+ assertEqual(delta.relationKind, TaggedUrnRelationKind.EQUIVALENT, 'TEST596: empty delta should be equivalent');
1506
+ assert(delta.isEmpty(), 'TEST596: empty delta should have no edits');
1507
+ assertEqual(runtime.applyDelta(delta).toString(), 'media:image;png', 'TEST596: empty delta should preserve runtime refinement');
1508
+ })();
1509
+
1510
+ // TEST597: Incomparable replacement rewrites only declared coordinates.
1511
+ (() => {
1512
+ const base = TaggedUrn.fromString('media:markdown;textable');
1513
+ const target = TaggedUrn.fromString('media:html;textable');
1514
+ const runtime = TaggedUrn.fromString('media:lang=en;markdown;textable');
1515
+ const delta = target.deltaFrom(base);
1516
+ assertEqual(delta.relationKind, TaggedUrnRelationKind.INCOMPARABLE, 'TEST597: markdown -> html should be incomparable');
1517
+ assertEqual(runtime.applyDelta(delta).toString(), 'media:html;lang=en;textable', 'TEST597: unrelated runtime refinements must be preserved');
1518
+ })();
1519
+
1520
+ // TEST598: Comparable removal drops only the declared key.
1521
+ (() => {
1522
+ const base = TaggedUrn.fromString('media:image;png');
1523
+ const target = TaggedUrn.fromString('media:image');
1524
+ const runtime = TaggedUrn.fromString('media:author=me;image;png');
1525
+ const delta = target.deltaFrom(base);
1526
+ assertEqual(delta.relationKind, TaggedUrnRelationKind.COMPARABLE, 'TEST598: image;png -> image should be comparable');
1527
+ assertEqual(runtime.applyDelta(delta).toString(), 'media:author=me;image', 'TEST598: only the declared key should be removed');
1528
+ })();
1529
+
1498
1530
  console.log('All tests passed!');