capdag 1.219.594 → 1.222.603

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.
Files changed (3) hide show
  1. package/capdag.js +196 -1
  2. package/capdag.test.js +108 -0
  3. package/package.json +1 -1
package/capdag.js CHANGED
@@ -5876,10 +5876,11 @@ function unixSecondsNow() {
5876
5876
  }
5877
5877
 
5878
5878
  // Default protocol limits / version, mirroring capdag::bifaci::frame.
5879
- const BIFACI_PROTOCOL_VERSION = 2;
5879
+ const BIFACI_PROTOCOL_VERSION = 3;
5880
5880
  const BIFACI_DEFAULT_MAX_FRAME = 3670016;
5881
5881
  const BIFACI_DEFAULT_MAX_CHUNK = 262144;
5882
5882
  const BIFACI_DEFAULT_MAX_REORDER_BUFFER = 64;
5883
+ const BIFACI_DEFAULT_INITIAL_CREDIT = 32;
5883
5884
  const BIFACI_FRAME_TYPE_HELLO = 0;
5884
5885
  // Frame CBOR integer keys (capdag::bifaci::frame::keys).
5885
5886
  const BIFACI_KEY_VERSION = 0;
@@ -6008,6 +6009,7 @@ function encodeHelloFrame() {
6008
6009
  ['max_frame', BIFACI_DEFAULT_MAX_FRAME],
6009
6010
  ['max_chunk', BIFACI_DEFAULT_MAX_CHUNK],
6010
6011
  ['max_reorder_buffer', BIFACI_DEFAULT_MAX_REORDER_BUFFER],
6012
+ ['initial_credit', BIFACI_DEFAULT_INITIAL_CREDIT],
6011
6013
  ['version', BIFACI_PROTOCOL_VERSION],
6012
6014
  ],
6013
6015
  };
@@ -6825,6 +6827,88 @@ class Machine {
6825
6827
  return bracketed ? lines.join('') : lines.join('\n');
6826
6828
  }
6827
6829
 
6830
+ /**
6831
+ * Serialize to machine notation rendering each cap by its registered display
6832
+ * alias (shortest name, ties alphabetical) when one exists, falling back to
6833
+ * the canonical-URN `edge_N` header form otherwise. This is the "store
6834
+ * aliased" form: generated and persisted machines use it so the saved
6835
+ * notation reads in terms of aliases.
6836
+ *
6837
+ * Semantics (mirrors Rust Machine::to_machine_notation_aliased):
6838
+ * - A cap WITH a display alias is referenced DIRECTLY in the wiring's
6839
+ * cap-position by its alias name, with NO `[edge_N cap:...]` header (the
6840
+ * grammar forbids an alias in a header's cap position anyway, and a header
6841
+ * would be redundant).
6842
+ * - A cap WITHOUT an alias keeps the synthetic `edge_N` wiring token plus its
6843
+ * `[edge_N cap:...]` header binding it to the canonical cap URN.
6844
+ *
6845
+ * The canonical `edge_N` numbering and node naming are inherited unchanged
6846
+ * from the canonical serializer, so an un-aliased machine produces byte-
6847
+ * identical output to toMachineNotationFormatted.
6848
+ *
6849
+ * @param {FabricRegistryClient} registry - resolves URN → display alias
6850
+ * @param {'bracketed' | 'line-based'} format
6851
+ * @returns {string}
6852
+ */
6853
+ toMachineNotationAliased(registry, format) {
6854
+ if (this._edges.length === 0) {
6855
+ return '';
6856
+ }
6857
+
6858
+ const { aliases, nodeNames, edgeOrder } = this._buildSerializationMaps();
6859
+ const bracketed = format === 'bracketed';
6860
+ const open = bracketed ? '[' : '';
6861
+ const close = bracketed ? ']' : '';
6862
+
6863
+ // Resolve, per edge index, the cap-position token and whether it needs a
6864
+ // header. An aliased cap uses its display alias directly (no header); an
6865
+ // un-aliased cap keeps its synthetic `edge_N` token (with a header).
6866
+ const edgeToken = new Map(); // edgeIdx → token used in the wiring
6867
+ const edgeNeedsHeader = new Map(); // edgeIdx → bool
6868
+ for (const [edgeAlias, info] of aliases) {
6869
+ const edge = this._edges[info.edgeIdx];
6870
+ const displayAlias = registry.displayAliasForUrn(edge.capUrn.toString());
6871
+ if (displayAlias !== null && displayAlias !== undefined) {
6872
+ edgeToken.set(info.edgeIdx, displayAlias);
6873
+ edgeNeedsHeader.set(info.edgeIdx, false);
6874
+ } else {
6875
+ edgeToken.set(info.edgeIdx, edgeAlias);
6876
+ edgeNeedsHeader.set(info.edgeIdx, true);
6877
+ }
6878
+ }
6879
+
6880
+ const lines = [];
6881
+
6882
+ // Headers only for edges that kept a synthetic `edge_N` token (un-aliased
6883
+ // caps). Emit in alias-sorted order to match the canonical serializer.
6884
+ const sortedAliases = Array.from(aliases.entries()).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
6885
+ for (const [, info] of sortedAliases) {
6886
+ if (edgeNeedsHeader.get(info.edgeIdx)) {
6887
+ const edge = this._edges[info.edgeIdx];
6888
+ lines.push(`${open}${edgeToken.get(info.edgeIdx)} ${edge.capUrn}${close}`);
6889
+ }
6890
+ }
6891
+
6892
+ // Wirings in canonical edge order, using the resolved cap-position token.
6893
+ for (const edgeIdx of edgeOrder) {
6894
+ const edge = this._edges[edgeIdx];
6895
+ const token = edgeToken.get(edgeIdx);
6896
+
6897
+ const sources = edge.sources.map(s => nodeNames.get(s.toString()));
6898
+ const targetName = nodeNames.get(edge.target.toString());
6899
+ const loopPrefix = edge.isLoop ? 'LOOP ' : '';
6900
+
6901
+ if (sources.length === 1) {
6902
+ lines.push(`${open}${sources[0]} -> ${loopPrefix}${token} -> ${targetName}${close}`);
6903
+ } else {
6904
+ const group = sources.join(', ');
6905
+ lines.push(`${open}(${group}) -> ${loopPrefix}${token} -> ${targetName}${close}`);
6906
+ }
6907
+ }
6908
+
6909
+ return bracketed ? lines.join('') : lines.join('\n');
6910
+ }
6911
+
6828
6912
  /**
6829
6913
  * Build the alias map, node name map, and edge ordering for serialization.
6830
6914
  *
@@ -7413,6 +7497,12 @@ class FabricRegistryClient {
7413
7497
  // canonical key only needs to be unique per equivalence class; we
7414
7498
  // store one entry per equivalence class.
7415
7499
  this._mediaCache = new Map();
7500
+ // Normalized alias name → StoredAlias. Mirrors Rust
7501
+ // FabricRegistry::cached_aliases. The display/serialization surfaces
7502
+ // read this synchronously; it is warmed by insertCachedAliasForTest (and,
7503
+ // in the heavier mirrors, a background prefetch this lightweight client
7504
+ // does not implement).
7505
+ this._cachedAliases = new Map();
7416
7506
  }
7417
7507
 
7418
7508
  /**
@@ -7542,12 +7632,88 @@ class FabricRegistryClient {
7542
7632
  return Array.from(ops).sort();
7543
7633
  }
7544
7634
 
7635
+ /**
7636
+ * Insert an alias directly into the in-memory cache, keyed by its
7637
+ * normalized name. Mirrors Rust
7638
+ * FabricRegistry::insert_cached_alias_for_test — it is the warm-cache seam
7639
+ * the display/serialization primitives read from. The lightweight JS client
7640
+ * has no background alias prefetch, so callers seed the cache through here.
7641
+ *
7642
+ * @param {StoredAlias} alias
7643
+ */
7644
+ insertCachedAliasForTest(alias) {
7645
+ const normalized = normalizeAliasName(alias.name);
7646
+ this._cachedAliases.set(normalized, alias);
7647
+ }
7648
+
7649
+ /**
7650
+ * Reverse lookup: the display alias for a `cap:`/`media:` URN, or null if no
7651
+ * cached alias points at it. This is the canonical primitive every UI surface
7652
+ * and notation generator uses to render an aliased name in place of a raw URN.
7653
+ *
7654
+ * The query URN is canonicalised through its own parser (cap vs media by
7655
+ * prefix) before matching, because alias targets are stored canonically — a
7656
+ * non-canonical query (different tag order, redundant whitespace) would
7657
+ * otherwise miss. A URN that is neither a cap nor a media URN, or that fails
7658
+ * to parse, returns null (it cannot have an alias).
7659
+ *
7660
+ * When multiple aliases target the same URN, the winner is the SHORTEST name,
7661
+ * ties broken alphabetically (see selectDisplayAlias). This is deterministic
7662
+ * and stable across processes for a given alias set.
7663
+ *
7664
+ * Mirrors Rust FabricRegistry::display_alias_for_urn.
7665
+ * @param {string} urn
7666
+ * @returns {string|null}
7667
+ */
7668
+ displayAliasForUrn(urn) {
7669
+ // Canonicalise by kind. classifyAliasTarget keys off the parser (cap vs
7670
+ // media), the same classifier the alias publisher uses for targets, so a
7671
+ // query and a stored target canonicalise identically.
7672
+ const kind = classifyAliasTarget(urn);
7673
+ let canonical;
7674
+ if (kind === ALIAS_TARGET_CAP) {
7675
+ canonical = CapUrn.fromString(urn).toString();
7676
+ } else if (kind === ALIAS_TARGET_MEDIA) {
7677
+ canonical = MediaUrn.fromString(urn).toString();
7678
+ } else {
7679
+ return null;
7680
+ }
7681
+
7682
+ const names = [];
7683
+ for (const alias of this._cachedAliases.values()) {
7684
+ if (alias.target === canonical) {
7685
+ names.push(alias.name);
7686
+ }
7687
+ }
7688
+ return selectDisplayAlias(names);
7689
+ }
7690
+
7691
+ /**
7692
+ * All cached aliases whose target is a CAP URN, as [name, capUrn] pairs.
7693
+ * Used by the notation editor to offer registered cap aliases as wiring
7694
+ * completions. Order is unspecified (the caller sorts/filters). Synchronous,
7695
+ * cache-only — relies on the alias cache having been warmed.
7696
+ *
7697
+ * Mirrors Rust FabricRegistry::cached_cap_aliases.
7698
+ * @returns {Array<[string, string]>}
7699
+ */
7700
+ cachedCapAliases() {
7701
+ const pairs = [];
7702
+ for (const alias of this._cachedAliases.values()) {
7703
+ if (classifyAliasTarget(alias.target) === ALIAS_TARGET_CAP) {
7704
+ pairs.push([alias.name, alias.target]);
7705
+ }
7706
+ }
7707
+ return pairs;
7708
+ }
7709
+
7545
7710
  /**
7546
7711
  * Invalidate all caches. Next call to any method fetches fresh data.
7547
7712
  */
7548
7713
  invalidate() {
7549
7714
  this._capCache = null;
7550
7715
  this._mediaCache.clear();
7716
+ this._cachedAliases.clear();
7551
7717
  }
7552
7718
  }
7553
7719
 
@@ -7620,6 +7786,34 @@ function classifyAliasTarget(target) {
7620
7786
  return null;
7621
7787
  }
7622
7788
 
7789
+ /**
7790
+ * Pick the display alias from a set of alias names that all target the same
7791
+ * URN: the SHORTEST name, ties broken alphabetically. Returns null for an
7792
+ * empty set.
7793
+ *
7794
+ * The ordering is total and deterministic: (length, name) lexicographic. So
7795
+ * `png` beats `png-image` (shorter), and between equal-length `a16` / `a09`
7796
+ * the alphabetical-smaller `a09` wins. Stable across processes for a given
7797
+ * alias set, which is what makes aliased UI/notation reproducible.
7798
+ *
7799
+ * Mirrors Rust select_display_alias.
7800
+ * @param {Iterable<string>} names
7801
+ * @returns {string|null}
7802
+ */
7803
+ function selectDisplayAlias(names) {
7804
+ let best = null;
7805
+ for (const name of names) {
7806
+ if (
7807
+ best === null ||
7808
+ name.length < best.length ||
7809
+ (name.length === best.length && name < best)
7810
+ ) {
7811
+ best = name;
7812
+ }
7813
+ }
7814
+ return best;
7815
+ }
7816
+
7623
7817
  /**
7624
7818
  * The published wire/cache shape of a single fabric alias. Mirrors
7625
7819
  * fabric/alias.schema.json: { name, target, version }.
@@ -7682,6 +7876,7 @@ module.exports = {
7682
7876
  isAliasToken,
7683
7877
  normalizeAliasName,
7684
7878
  classifyAliasTarget,
7879
+ selectDisplayAlias,
7685
7880
  StoredAlias,
7686
7881
  Manifest,
7687
7882
  CapUrn,
package/capdag.test.js CHANGED
@@ -36,6 +36,7 @@ const {
36
36
  CAP_IDENTITY,
37
37
  ALIAS_TARGET_CAP, ALIAS_TARGET_MEDIA,
38
38
  tokenIsUrn, isAliasToken, normalizeAliasName, classifyAliasTarget,
39
+ selectDisplayAlias,
39
40
  StoredAlias, Manifest
40
41
  } = require('./capdag.js');
41
42
 
@@ -6383,6 +6384,107 @@ function test1887_manifestSerdeRoundTripsAliases() {
6383
6384
  assertEqual(JSON.stringify(a.toJSON()), '{"name":"pdf2text","target":"cap:effect=none","version":3}', 'alias wire shape');
6384
6385
  }
6385
6386
 
6387
+ // TEST1894: selectDisplayAlias picks the SHORTEST name, ties broken
6388
+ // alphabetically. This is the deterministic ordering every aliased-display
6389
+ // surface relies on; a regression here silently changes which alias the whole
6390
+ // UI renders.
6391
+ function test1894_selectDisplayAliasOrdering() {
6392
+ // Shorter wins over longer regardless of alphabetical order.
6393
+ assertEqual(selectDisplayAlias(['png-image', 'png', 'image-png']), 'png', 'shortest wins');
6394
+ // Equal length → alphabetical (a09 < a16).
6395
+ assertEqual(selectDisplayAlias(['a16', 'a09', 'a12']), 'a09', 'tie → alphabetical');
6396
+ // Single candidate returns itself.
6397
+ assertEqual(selectDisplayAlias(['solo']), 'solo', 'single candidate');
6398
+ // Empty set → null.
6399
+ assertEqual(selectDisplayAlias([]), null, 'empty → null');
6400
+ }
6401
+
6402
+ // TEST1895: displayAliasForUrn reverse-resolves a URN to its display alias.
6403
+ // Proves: (1) the shortest-then-alphabetical winner among multiple aliases on
6404
+ // the same target, (2) a NON-canonical query URN (different tag order) still
6405
+ // resolves because the query is canonicalised before matching, (3) a URN with
6406
+ // no alias returns null, (4) a non-URN string returns null.
6407
+ function test1895_displayAliasForUrn() {
6408
+ const registry = new FabricRegistryClient();
6409
+ const capTarget = CapUrn.fromString('cap:coerce;in="media:integer;numeric";out="media:enc=utf-8"').toString();
6410
+ // Two aliases on the same cap target; "i2s" is shorter than "int2str".
6411
+ registry.insertCachedAliasForTest(new StoredAlias('int2str', capTarget, 1));
6412
+ registry.insertCachedAliasForTest(new StoredAlias('i2s', capTarget, 1));
6413
+ // A media alias too. Stored canonically.
6414
+ const jsonTarget = MediaUrn.fromString('media:fmt=json;record').toString();
6415
+ registry.insertCachedAliasForTest(new StoredAlias('json', jsonTarget, 1));
6416
+
6417
+ // Canonical query → shortest alias wins.
6418
+ assertEqual(
6419
+ registry.displayAliasForUrn('cap:coerce;in="media:integer;numeric";out="media:enc=utf-8"'),
6420
+ 'i2s', 'shortest cap alias wins');
6421
+ // NON-canonical query (media tags reordered) must still resolve via
6422
+ // canonicalisation.
6423
+ assertEqual(registry.displayAliasForUrn('media:record;fmt=json'), 'json',
6424
+ 'non-canonical media query canonicalises');
6425
+ // A real URN with no alias → null.
6426
+ assertEqual(registry.displayAliasForUrn('media:enc=utf-8;ext=pdf'), null, 'no alias → null');
6427
+ // A non-URN (no cap:/media: prefix) → null, never a throw.
6428
+ assertEqual(registry.displayAliasForUrn('int2str'), null, 'non-URN → null');
6429
+ }
6430
+
6431
+ // TEST1896: cachedCapAliases returns only CAP-targeted aliases as [name,
6432
+ // target] pairs — media aliases are excluded. Drives the notation editor's
6433
+ // registered-alias completions.
6434
+ function test1896_cachedCapAliasesFiltersToCapTargets() {
6435
+ const registry = new FabricRegistryClient();
6436
+ const capTarget = CapUrn.fromString('cap:coerce;in="media:integer;numeric";out="media:enc=utf-8"').toString();
6437
+ registry.insertCachedAliasForTest(new StoredAlias('int2str', capTarget, 1));
6438
+ registry.insertCachedAliasForTest(new StoredAlias('json', MediaUrn.fromString('media:fmt=json;record').toString(), 1));
6439
+ const capAliases = registry.cachedCapAliases();
6440
+ assertEqual(capAliases.length, 1, 'only the cap alias is returned');
6441
+ assertEqual(capAliases[0][0], 'int2str', 'pair name');
6442
+ assertEqual(capAliases[0][1], capTarget, 'pair target');
6443
+ }
6444
+
6445
+ // TEST1196: toMachineNotationAliased references an aliased cap DIRECTLY in the
6446
+ // wiring by its display alias (shortest, then alphabetical) with NO header, and
6447
+ // keeps the synthetic `edge_N` token + header for a cap that has no alias.
6448
+ function test1196_aliasedSerializationUsesAliasAndDropsHeader() {
6449
+ const registry = new FabricRegistryClient();
6450
+ const m = Machine.fromString(
6451
+ '[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
6452
+ '[embed cap:in="media:enc=utf-8;ext=txt";embed;out="media:embedding-vector;enc=utf-8;record"]' +
6453
+ '[doc -> extract -> text]' +
6454
+ '[text -> embed -> vectors]'
6455
+ );
6456
+ // Canonical target strings for the two caps, exactly as the serializer keys.
6457
+ const extractTarget = m.edges().find(e => e.capUrn.toString().includes('extract')).capUrn.toString();
6458
+ // Two aliases on the extract cap; "ex" is shorter than "extract-pdf".
6459
+ registry.insertCachedAliasForTest(new StoredAlias('extract-pdf', extractTarget, 1));
6460
+ registry.insertCachedAliasForTest(new StoredAlias('ex', extractTarget, 1));
6461
+ // No alias for the embed cap → it must stay a raw URN with a header.
6462
+
6463
+ const aliased = m.toMachineNotationAliased(registry, 'bracketed');
6464
+
6465
+ // The extract cap is aliased: referenced directly in the wiring by its
6466
+ // SHORTER alias `ex`, with NO header, and its URN must not appear.
6467
+ assert(aliased.includes('-> ex ->'),
6468
+ `extract cap must be referenced by shortest alias 'ex', got: ${aliased}`);
6469
+ assert(!aliased.includes('extract;out'),
6470
+ `the aliased extract cap URN must not appear, got: ${aliased}`);
6471
+ // The longer alias must not be chosen.
6472
+ assert(!aliased.includes('extract-pdf'),
6473
+ `the longer alias must not be used, got: ${aliased}`);
6474
+ // The un-aliased embed cap keeps its synthetic header binding `edge_N` to the
6475
+ // canonical embed URN.
6476
+ assert(/\[edge_\d+ cap:.*embed/.test(aliased),
6477
+ `the un-aliased embed cap must keep its header URN, got: ${aliased}`);
6478
+
6479
+ // An un-aliased machine (empty registry) is byte-identical to the canonical
6480
+ // formatted form — the aliased path adds nothing when no alias exists.
6481
+ const empty = new FabricRegistryClient();
6482
+ assertEqual(
6483
+ m.toMachineNotationAliased(empty, 'bracketed'),
6484
+ m.toMachineNotationFormatted('bracketed'),
6485
+ 'no aliases → identical to canonical bracketed form');
6486
+ }
6487
+
6386
6488
  // ============================================================================
6387
6489
  // Test runner
6388
6490
  // ============================================================================
@@ -6849,6 +6951,12 @@ async function runTests() {
6849
6951
  runTest('TEST1882: classify_alias_target_by_prefix', test1882_classifyAliasTargetByPrefix);
6850
6952
  runTest('TEST1887: manifest_serde_round_trips_aliases', test1887_manifestSerdeRoundTripsAliases);
6851
6953
 
6954
+ console.log('\n--- URN→alias display + aliased serialization (test1894–test1896, test1196) ---');
6955
+ runTest('TEST1894: select_display_alias_ordering', test1894_selectDisplayAliasOrdering);
6956
+ runTest('TEST1895: display_alias_for_urn', test1895_displayAliasForUrn);
6957
+ runTest('TEST1896: cached_cap_aliases_filters_to_cap', test1896_cachedCapAliasesFiltersToCapTargets);
6958
+ runTest('TEST1196: aliased_serialization_uses_alias', test1196_aliasedSerializationUsesAliasAndDropsHeader);
6959
+
6852
6960
  // Summary
6853
6961
  console.log(`\n${passCount + failCount} tests: ${passCount} passed, ${failCount} failed`);
6854
6962
  if (failCount > 0) {
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": "1.219.594"
43
+ "version": "1.222.603"
44
44
  }