capdag 0.146.323 → 0.149.334

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.
@@ -158,10 +158,10 @@ function layoutForMode(mode) {
158
158
  algorithm: 'layered',
159
159
  'elk.direction': 'RIGHT',
160
160
  'elk.edgeRouting': 'POLYLINE',
161
- 'elk.layered.spacing.edgeEdgeBetweenLayers': 20,
162
- 'elk.layered.spacing.edgeNodeBetweenLayers': 30,
163
- 'elk.spacing.edgeEdge': 15,
164
- 'elk.spacing.edgeNode': 25,
161
+ 'elk.layered.spacing.edgeEdgeBetweenLayers': 30,
162
+ 'elk.layered.spacing.edgeNodeBetweenLayers': 40,
163
+ 'elk.spacing.edgeEdge': 25,
164
+ 'elk.spacing.edgeNode': 35,
165
165
  'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
166
166
  'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
167
167
  'elk.layered.considerModelOrder.strategy': 'NODES_AND_EDGES',
@@ -169,8 +169,8 @@ function layoutForMode(mode) {
169
169
  };
170
170
  if (mode === 'browse') {
171
171
  return Object.assign({}, base, {
172
- 'elk.layered.spacing.nodeNodeBetweenLayers': 150,
173
- 'elk.spacing.nodeNode': 50,
172
+ 'elk.layered.spacing.nodeNodeBetweenLayers': 180,
173
+ 'elk.spacing.nodeNode': 90,
174
174
  });
175
175
  }
176
176
  if (mode === 'strand') {
package/capdag.js CHANGED
@@ -3596,6 +3596,23 @@ class CartridgeCapSummary {
3596
3596
  }
3597
3597
  }
3598
3598
 
3599
+ /**
3600
+ * Cartridge cap group from registry
3601
+ */
3602
+ class CartridgeCapGroup {
3603
+ constructor(data) {
3604
+ if (!data.name) throw new Error('CapGroup missing name');
3605
+ this.name = data.name;
3606
+ this.caps = (data.caps || []).map(c => new CartridgeCapSummary(c.urn, c.title, c.description || ''));
3607
+ this.adapter_urns = data.adapter_urns || [];
3608
+ }
3609
+
3610
+ /** Flat caps in this group */
3611
+ allCaps() {
3612
+ return this.caps;
3613
+ }
3614
+ }
3615
+
3599
3616
  /**
3600
3617
  * Cartridge information from registry
3601
3618
  */
@@ -3610,7 +3627,8 @@ class CartridgeInfo {
3610
3627
  this.teamId = data.teamId || '';
3611
3628
  this.signedAt = data.signedAt || '';
3612
3629
  this.minAppVersion = data.minAppVersion || '';
3613
- this.caps = (data.caps || []).map(c => new CartridgeCapSummary(c.urn, c.title, c.description || ''));
3630
+ if (!Array.isArray(data.cap_groups)) throw new Error(`CartridgeInfo ${data.id || '?'}: missing cap_groups array`);
3631
+ this.cap_groups = data.cap_groups.map(g => new CartridgeCapGroup(g));
3614
3632
  this.categories = data.categories || [];
3615
3633
  this.tags = data.tags || [];
3616
3634
  // Versions with platform-specific builds
@@ -3618,6 +3636,21 @@ class CartridgeInfo {
3618
3636
  this.availableVersions = data.availableVersions || [];
3619
3637
  }
3620
3638
 
3639
+ /** All caps flattened across all cap_groups, deduplicated by URN */
3640
+ allCaps() {
3641
+ const seen = new Set();
3642
+ const result = [];
3643
+ for (const group of this.cap_groups) {
3644
+ for (const cap of group.caps) {
3645
+ if (!seen.has(cap.urn)) {
3646
+ seen.add(cap.urn);
3647
+ result.push(cap);
3648
+ }
3649
+ }
3650
+ }
3651
+ return result;
3652
+ }
3653
+
3621
3654
  /**
3622
3655
  * Check if cartridge is signed (has team_id and signed_at)
3623
3656
  */
@@ -3697,15 +3730,28 @@ class CartridgeRepoClient {
3697
3730
 
3698
3731
  const data = await response.json();
3699
3732
 
3700
- if (!data.cartridges || !Array.isArray(data.cartridges)) {
3701
- throw new Error(`Invalid cartridge registry response from ${repoUrl}: missing cartridges array`);
3733
+ if (!data.cartridges || typeof data.cartridges !== 'object') {
3734
+ throw new Error(`Invalid cartridge registry response from ${repoUrl}: missing cartridges object`);
3702
3735
  }
3703
3736
 
3704
- return data.cartridges.map(p => new CartridgeInfo(p));
3737
+ // Registry stores cartridges as an object keyed by id; normalize to array.
3738
+ return Object.entries(data.cartridges).map(([id, c]) => new CartridgeInfo({
3739
+ ...c,
3740
+ id,
3741
+ version: c.latestVersion
3742
+ }));
3705
3743
  }
3706
3744
 
3707
3745
  /**
3708
- * Update cache from registry data
3746
+ * Update cache from registry data.
3747
+ *
3748
+ * The cap-to-cartridges index keys on the *normalized* tagged-URN form
3749
+ * of each cap URN (parse via CapUrn.fromString, then take toString()).
3750
+ * This collapses textually different but canonically identical URNs
3751
+ * (different declared tag order) into the same bucket so that lookups
3752
+ * resolve regardless of how the requester phrased the URN. A cap URN
3753
+ * that fails to parse is a registry corruption: we throw rather than
3754
+ * silently keep the malformed string in the index.
3709
3755
  */
3710
3756
  updateCache(repoUrl, cartridges) {
3711
3757
  const cache = new CartridgeRepoCache(repoUrl);
@@ -3713,11 +3759,12 @@ class CartridgeRepoClient {
3713
3759
  for (const cartridge of cartridges) {
3714
3760
  cache.cartridges.set(cartridge.id, cartridge);
3715
3761
 
3716
- for (const cap of cartridge.caps) {
3717
- if (!cache.capToCartridges.has(cap.urn)) {
3718
- cache.capToCartridges.set(cap.urn, []);
3762
+ for (const cap of cartridge.allCaps()) {
3763
+ const normalized = CapUrn.fromString(cap.urn).toString();
3764
+ if (!cache.capToCartridges.has(normalized)) {
3765
+ cache.capToCartridges.set(normalized, []);
3719
3766
  }
3720
- cache.capToCartridges.get(cap.urn).push(cartridge.id);
3767
+ cache.capToCartridges.get(normalized).push(cartridge.id);
3721
3768
  }
3722
3769
  }
3723
3770
 
@@ -3760,20 +3807,36 @@ class CartridgeRepoClient {
3760
3807
  }
3761
3808
 
3762
3809
  /**
3763
- * Get cartridge suggestions for a cap URN
3810
+ * Get cartridge suggestions for a cap URN.
3811
+ *
3812
+ * `capUrn` is parsed via CapUrn.fromString; the parsed-and-
3813
+ * re-serialized form is the canonical key into the cap-to-cartridges
3814
+ * index. Inside each candidate cartridge we walk its caps via
3815
+ * `allCaps()` and match each one with `isEquivalent`, never with
3816
+ * string equality.
3764
3817
  */
3765
3818
  getSuggestionsForCap(capUrn) {
3819
+ const requested = CapUrn.fromString(capUrn);
3820
+ const normalized = requested.toString();
3766
3821
  const suggestions = [];
3767
3822
 
3768
3823
  for (const cache of this.caches.values()) {
3769
- const cartridgeIds = cache.capToCartridges.get(capUrn);
3824
+ const cartridgeIds = cache.capToCartridges.get(normalized);
3770
3825
  if (!cartridgeIds) continue;
3771
3826
 
3772
3827
  for (const cartridgeId of cartridgeIds) {
3773
3828
  const cartridge = cache.cartridges.get(cartridgeId);
3774
3829
  if (!cartridge) continue;
3775
3830
 
3776
- const capInfo = cartridge.caps.find(c => c.urn === capUrn);
3831
+ const capInfo = cartridge.allCaps().find(c => {
3832
+ let parsed;
3833
+ try {
3834
+ parsed = CapUrn.fromString(c.urn);
3835
+ } catch (_e) {
3836
+ return false;
3837
+ }
3838
+ return parsed.isEquivalent(requested);
3839
+ });
3777
3840
  if (!capInfo) continue;
3778
3841
 
3779
3842
  const pageUrl = cartridge.pageUrl || cache.repoUrl;
@@ -3782,7 +3845,7 @@ class CartridgeRepoClient {
3782
3845
  cartridgeId: cartridge.id,
3783
3846
  cartridgeName: cartridge.name,
3784
3847
  cartridgeDescription: cartridge.description,
3785
- capUrn: capUrn,
3848
+ capUrn: normalized,
3786
3849
  capTitle: capInfo.title,
3787
3850
  latestVersion: cartridge.version,
3788
3851
  repoUrl: cache.repoUrl,
@@ -3889,6 +3952,9 @@ class CartridgeRepoServer {
3889
3952
  if (!build.package || !build.package.name) {
3890
3953
  throw new Error(`Cartridge ${id} v${version}: build[${i}] (${build.platform}) missing package.name`);
3891
3954
  }
3955
+ if (!build.package.url) {
3956
+ throw new Error(`Cartridge ${id} v${version}: build[${i}] (${build.platform}) missing package.url`);
3957
+ }
3892
3958
  }
3893
3959
  }
3894
3960
 
@@ -3942,7 +4008,10 @@ class CartridgeRepoServer {
3942
4008
  teamId: cartridge.teamId,
3943
4009
  signedAt: versionData.releaseDate,
3944
4010
  minAppVersion: versionData.minAppVersion || cartridge.minAppVersion,
3945
- caps: cartridge.caps || [],
4011
+ cap_groups: (() => {
4012
+ if (!Array.isArray(cartridge.cap_groups)) throw new Error(`Cartridge ${id}: missing cap_groups array`);
4013
+ return cartridge.cap_groups;
4014
+ })(),
3946
4015
  categories: cartridge.categories,
3947
4016
  tags: cartridge.tags,
3948
4017
  versions: cartridge.versions,
@@ -3971,18 +4040,25 @@ class CartridgeRepoServer {
3971
4040
  }
3972
4041
 
3973
4042
  /**
3974
- * Search cartridges by query
4043
+ * Search cartridges by free-text query.
4044
+ *
4045
+ * Matches against cartridge name, description, tags, and cap titles.
4046
+ * Cap URN strings are not substring-matched: a cap URN is a tagged
4047
+ * identifier and substring matching against it is a category error.
4048
+ * Use `getCartridgesByCap` to look up cartridges that provide a
4049
+ * specific cap.
3975
4050
  */
3976
4051
  searchCartridges(query) {
3977
4052
  const cartridges = this.transformToCartridgeArray();
3978
4053
  const lowerQuery = query.toLowerCase();
3979
4054
 
3980
- return cartridges.filter(p =>
3981
- p.name.toLowerCase().includes(lowerQuery) ||
3982
- p.description.toLowerCase().includes(lowerQuery) ||
3983
- p.tags.some(t => t.toLowerCase().includes(lowerQuery)) ||
3984
- p.caps.some(c => c.urn.toLowerCase().includes(lowerQuery) || c.title.toLowerCase().includes(lowerQuery))
3985
- );
4055
+ return cartridges.filter(p => {
4056
+ const allCaps = (p.cap_groups || []).flatMap(g => g.caps || []);
4057
+ return p.name.toLowerCase().includes(lowerQuery) ||
4058
+ p.description.toLowerCase().includes(lowerQuery) ||
4059
+ p.tags.some(t => t.toLowerCase().includes(lowerQuery)) ||
4060
+ allCaps.some(c => c.title.toLowerCase().includes(lowerQuery));
4061
+ });
3986
4062
  }
3987
4063
 
3988
4064
  /**
@@ -3994,11 +4070,29 @@ class CartridgeRepoServer {
3994
4070
  }
3995
4071
 
3996
4072
  /**
3997
- * Get cartridges that provide a specific cap
4073
+ * Get cartridges that provide a specific cap.
4074
+ *
4075
+ * Both the request URN and each candidate cap URN are parsed via
4076
+ * CapUrn.fromString and matched with `isEquivalent` so caps declared
4077
+ * in any tag order resolve. A malformed input URN throws — there is
4078
+ * no fallback that compares the raw strings.
3998
4079
  */
3999
4080
  getCartridgesByCap(capUrn) {
4081
+ const requested = CapUrn.fromString(capUrn);
4000
4082
  const cartridges = this.transformToCartridgeArray();
4001
- return cartridges.filter(p => p.caps.some(c => c.urn === capUrn));
4083
+ return cartridges.filter(p =>
4084
+ (p.cap_groups || []).some(g =>
4085
+ (g.caps || []).some(c => {
4086
+ let parsed;
4087
+ try {
4088
+ parsed = CapUrn.fromString(c.urn);
4089
+ } catch (_e) {
4090
+ return false;
4091
+ }
4092
+ return parsed.isEquivalent(requested);
4093
+ })
4094
+ )
4095
+ );
4002
4096
  }
4003
4097
  }
4004
4098
 
package/capdag.test.js CHANGED
@@ -1651,16 +1651,22 @@ const sampleRegistry = {
1651
1651
  minAppVersion: '1.0.0',
1652
1652
  categories: ['document'],
1653
1653
  tags: ['pdf', 'extractor'],
1654
- caps: [
1654
+ cap_groups: [
1655
1655
  {
1656
- urn: 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"',
1657
- title: 'Disbind PDF',
1658
- description: 'Extract pages'
1659
- },
1660
- {
1661
- urn: 'cap:in="media:pdf";op=extract_metadata;out="media:file-metadata;textable;record"',
1662
- title: 'Extract Metadata',
1663
- description: 'Get PDF metadata'
1656
+ name: 'pdf-processing',
1657
+ adapter_urns: ['media:pdf'],
1658
+ caps: [
1659
+ {
1660
+ urn: 'cap:in="media:pdf";op=disbind;out="media:disbound-page;textable;list"',
1661
+ title: 'Disbind PDF',
1662
+ description: 'Extract pages'
1663
+ },
1664
+ {
1665
+ urn: 'cap:in="media:pdf";op=extract_metadata;out="media:file-metadata;textable;record"',
1666
+ title: 'Extract Metadata',
1667
+ description: 'Get PDF metadata'
1668
+ }
1669
+ ]
1664
1670
  }
1665
1671
  ],
1666
1672
  latestVersion: '0.81.5325',
@@ -1673,6 +1679,7 @@ const sampleRegistry = {
1673
1679
  platform: 'darwin-arm64',
1674
1680
  package: {
1675
1681
  name: 'pdfcartridge-0.81.5325.pkg',
1682
+ url: 'https://cartridges.machinefabric.com/pdfcartridge/0.81.5325/pdfcartridge-0.81.5325.pkg',
1676
1683
  sha256: '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0',
1677
1684
  size: 5187485
1678
1685
  }
@@ -1689,11 +1696,17 @@ const sampleRegistry = {
1689
1696
  minAppVersion: '1.0.0',
1690
1697
  categories: ['text'],
1691
1698
  tags: ['txt', 'text'],
1692
- caps: [
1699
+ cap_groups: [
1693
1700
  {
1694
- urn: 'cap:in="media:txt;textable";op=disbind;out="media:disbound-page;textable;list"',
1695
- title: 'Disbind Text',
1696
- description: 'Extract text pages'
1701
+ name: 'text-processing',
1702
+ adapter_urns: ['media:txt;textable'],
1703
+ caps: [
1704
+ {
1705
+ urn: 'cap:in="media:txt;textable";op=disbind;out="media:disbound-page;textable;list"',
1706
+ title: 'Disbind Text',
1707
+ description: 'Extract text pages'
1708
+ }
1709
+ ]
1697
1710
  }
1698
1711
  ],
1699
1712
  latestVersion: '0.54.6408',
@@ -1706,6 +1719,7 @@ const sampleRegistry = {
1706
1719
  platform: 'darwin-arm64',
1707
1720
  package: {
1708
1721
  name: 'txtcartridge-0.54.6408.pkg',
1722
+ url: 'https://cartridges.machinefabric.com/txtcartridge/0.54.6408/txtcartridge-0.54.6408.pkg',
1709
1723
  sha256: 'abc123',
1710
1724
  size: 821000
1711
1725
  }
@@ -1725,13 +1739,17 @@ function test320_cartridgeInfoConstruction() {
1725
1739
  description: 'A test',
1726
1740
  teamId: 'TEAM123',
1727
1741
  signedAt: '2026-01-01',
1728
- caps: [{urn: 'cap:in="media:void";op=test;out="media:void"', title: 'Test', description: ''}],
1742
+ cap_groups: [{
1743
+ name: 'test-group',
1744
+ adapter_urns: ['media:void'],
1745
+ caps: [{urn: 'cap:in="media:void";op=test;out="media:void"', title: 'Test', description: ''}]
1746
+ }],
1729
1747
  versions: {
1730
1748
  '1.0.0': {
1731
1749
  releaseDate: '2026-01-01',
1732
1750
  changelog: ['Initial'],
1733
1751
  minAppVersion: '1.0.0',
1734
- builds: [{platform: 'darwin-arm64', package: {name: 'test-1.0.0.pkg', sha256: 'abc123', size: 100}}]
1752
+ builds: [{platform: 'darwin-arm64', package: {name: 'test-1.0.0.pkg', url: 'https://cartridges.machinefabric.com/testcartridge/1.0.0/test-1.0.0.pkg', sha256: 'abc123', size: 100}}]
1735
1753
  }
1736
1754
  },
1737
1755
  availableVersions: ['1.0.0']
@@ -1739,31 +1757,32 @@ function test320_cartridgeInfoConstruction() {
1739
1757
  const cartridge = new CartridgeInfo(data);
1740
1758
  assert(cartridge.id === 'testcartridge', 'ID should match');
1741
1759
  assert(cartridge.teamId === 'TEAM123', 'Team ID should match');
1742
- assert(cartridge.caps.length === 1, 'Should have 1 cap');
1743
- assert(cartridge.caps[0].urn === 'cap:in="media:void";op=test;out="media:void"', 'Cap URN should match');
1760
+ assert(cartridge.cap_groups.length === 1, 'Should have 1 cap_group');
1761
+ assert(cartridge.cap_groups[0].caps[0].urn === 'cap:in="media:void";op=test;out="media:void"', 'Cap URN should match');
1762
+ assert(cartridge.allCaps().length === 1, 'allCaps() should return 1 cap');
1744
1763
  }
1745
1764
 
1746
1765
  // TEST321: CartridgeInfo.is_signed() returns true when signature is present
1747
1766
  function test321_cartridgeInfoIsSigned() {
1748
- const signed = new CartridgeInfo({id: 'test', teamId: 'TEAM', signedAt: '2026-01-01', caps: []});
1767
+ const signed = new CartridgeInfo({id: 'test', teamId: 'TEAM', signedAt: '2026-01-01', cap_groups: []});
1749
1768
  assert(signed.isSigned() === true, 'Cartridge with teamId and signedAt should be signed');
1750
1769
 
1751
- const unsigned1 = new CartridgeInfo({id: 'test', teamId: '', signedAt: '2026-01-01', caps: []});
1770
+ const unsigned1 = new CartridgeInfo({id: 'test', teamId: '', signedAt: '2026-01-01', cap_groups: []});
1752
1771
  assert(unsigned1.isSigned() === false, 'Cartridge without teamId should not be signed');
1753
1772
 
1754
- const unsigned2 = new CartridgeInfo({id: 'test', teamId: 'TEAM', signedAt: '', caps: []});
1773
+ const unsigned2 = new CartridgeInfo({id: 'test', teamId: 'TEAM', signedAt: '', cap_groups: []});
1755
1774
  assert(unsigned2.isSigned() === false, 'Cartridge without signedAt should not be signed');
1756
1775
  }
1757
1776
 
1758
1777
  // TEST322: CartridgeInfo.build_for_platform() returns the build matching the current platform
1759
1778
  function test322_cartridgeInfoBuildForPlatform() {
1760
1779
  const withBuilds = new CartridgeInfo({
1761
- id: 'test', version: '1.0.0', caps: [],
1780
+ id: 'test', version: '1.0.0', cap_groups: [],
1762
1781
  versions: {
1763
1782
  '1.0.0': {
1764
1783
  builds: [
1765
- {platform: 'darwin-arm64', package: {name: 'test-darwin.pkg', sha256: 'abc', size: 100}},
1766
- {platform: 'linux-x86_64', package: {name: 'test-linux.pkg', sha256: 'def', size: 200}}
1784
+ {platform: 'darwin-arm64', package: {name: 'test-darwin.pkg', url: 'https://cartridges.machinefabric.com/test/1.0.0/test-darwin.pkg', sha256: 'abc', size: 100}},
1785
+ {platform: 'linux-x86_64', package: {name: 'test-linux.pkg', url: 'https://cartridges.machinefabric.com/test/1.0.0/test-linux.pkg', sha256: 'def', size: 200}}
1767
1786
  ]
1768
1787
  }
1769
1788
  },
@@ -1784,7 +1803,7 @@ function test322_cartridgeInfoBuildForPlatform() {
1784
1803
  assert(platforms.includes('darwin-arm64'), 'Should include darwin-arm64');
1785
1804
  assert(platforms.includes('linux-x86_64'), 'Should include linux-x86_64');
1786
1805
 
1787
- const noBuilds = new CartridgeInfo({id: 'test', version: '1.0.0', caps: [], versions: {}, availableVersions: []});
1806
+ const noBuilds = new CartridgeInfo({id: 'test', version: '1.0.0', cap_groups: [], versions: {}, availableVersions: []});
1788
1807
  assert(noBuilds.buildForPlatform('darwin-arm64') === null, 'Should return null when no versions');
1789
1808
  assert(noBuilds.availablePlatforms().length === 0, 'Should have no platforms');
1790
1809
  }
@@ -1837,8 +1856,9 @@ function test324_cartridgeRepoServerTransformToArray() {
1837
1856
  assert(pdf.versions['0.81.5325'].builds[0].package.sha256 === '9b68724eb9220ecf01e8ed4f5f80c594fbac2239bc5bf675005ec882ecc5eba0', 'Should have package SHA256');
1838
1857
  assert(Array.isArray(pdf.availableVersions), 'Should have availableVersions array');
1839
1858
  assert(pdf.availableVersions.includes('0.81.5325'), 'Should include latest version');
1840
- assert(Array.isArray(pdf.caps), 'Should have caps array');
1841
- assert(pdf.caps.length === 2, 'Should have 2 caps');
1859
+ assert(Array.isArray(pdf.cap_groups), 'Should have cap_groups array');
1860
+ assert(pdf.cap_groups.length === 1, 'Should have 1 cap_group');
1861
+ assert(pdf.cap_groups[0].caps.length === 2, 'Should have 2 caps in the group');
1842
1862
  }
1843
1863
 
1844
1864
  // TEST325: CartridgeRepoServer.get_cartridges() returns all parsed cartridges
@@ -1933,7 +1953,12 @@ function test331_cartridgeRepoClientGetSuggestions() {
1933
1953
 
1934
1954
  assert(suggestions.length === 1, 'Should find 1 suggestion');
1935
1955
  assert(suggestions[0].cartridgeId === 'pdfcartridge', 'Should suggest pdfcartridge');
1936
- assert(suggestions[0].capUrn === disbindCap, 'Should have correct cap URN');
1956
+ // The returned capUrn is the canonical (normalized) form. Compare via
1957
+ // tagged-URN equivalence rather than string equality so a tag-order
1958
+ // difference between the request and the canonical form is tolerated.
1959
+ const requested = CapUrn.fromString(disbindCap);
1960
+ const returned = CapUrn.fromString(suggestions[0].capUrn);
1961
+ assert(returned.isEquivalent(requested), 'Should have equivalent cap URN');
1937
1962
  assert(suggestions[0].capTitle === 'Disbind PDF', 'Should have cap title');
1938
1963
  }
1939
1964
 
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "pretest": "npm run build:parser",
41
41
  "test": "node capdag.test.js"
42
42
  },
43
- "version": "0.146.323"
43
+ "version": "0.149.334"
44
44
  }