capdag 1.226.626 → 1.240.679

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/capdag.js CHANGED
@@ -615,7 +615,7 @@ class CapUrn {
615
615
  }
616
616
  }
617
617
 
618
- // Output direction: provider output must conform to requested output.
618
+ // Output direction: candidate output must conform to requested output.
619
619
  // `media:` on the pattern side is wildcard top and skips the check.
620
620
  if (this.outSpec !== 'media:' && this.outSpec !== '*') {
621
621
  const capOut = MediaUrn.fromString(this.outSpec);
@@ -851,7 +851,7 @@ class CapUrn {
851
851
  }
852
852
 
853
853
  /**
854
- * Check if this provider can dispatch the given request.
854
+ * Check if this candidate can dispatch the given request.
855
855
  *
856
856
  * @param {CapUrn} request
857
857
  * @returns {boolean}
@@ -1338,7 +1338,7 @@ const MEDIA_EMBEDDING_VECTOR = 'media:embedding-vector;enc=utf-8;record';
1338
1338
  // and `file-type=txt` (binds the URN to the `.txt` extension).
1339
1339
  const MEDIA_IMAGE_DESCRIPTION = 'media:enc=utf-8;ext=txt;image-description;plain-text';
1340
1340
  // Media URN for finalised plain text — the canonical input/output of cap:save-as-txt.
1341
- // Producers of user-facing prose (LLM text-generation, OCR's extracted text,
1341
+ // Producers of user-facing prose (LLM text-generation, OCR's digitized text,
1342
1342
  // summarisation) declare this URN as their `out` so the planner restricts the .txt
1343
1343
  // persistence path to those caps. See fabric/media/plain-text.toml.
1344
1344
  const MEDIA_PLAIN_TEXT = 'media:enc=utf-8;ext=txt;plain-text';
@@ -2481,6 +2481,46 @@ class CapArg {
2481
2481
  return stdinSource ? stdinSource.stdin : null;
2482
2482
  }
2483
2483
 
2484
+ /**
2485
+ * The media URN the runtime demuxes this arg's input stream by: its
2486
+ * stdin source URN if it declares one, otherwise its declared slot
2487
+ * media URN. A cap need not declare any stdin source at all — a
2488
+ * producer-fed arg may be delivered by its declared URN — so this
2489
+ * never assumes a stdin source exists.
2490
+ * Mirrors Rust: CapArg::stream_urn()
2491
+ * @returns {string} The stream media URN
2492
+ */
2493
+ streamUrn() {
2494
+ const stdinSource = this.sources.find(s => s.stdin !== null);
2495
+ return stdinSource ? stdinSource.stdin : this.media_urn;
2496
+ }
2497
+
2498
+ /**
2499
+ * Whether this arg is the cap's MAIN input relative to `inSpec` (the cap
2500
+ * URN's `in=` value): it declares a stdin source whose URN is `in=`. The
2501
+ * main input is always the value piped in on stdin (like a Unix command's
2502
+ * stdin), so the main arg always declares a stdin source carrying `in=`.
2503
+ * Its DECLARED slot URN may differ from that stdin URN (e.g. a `file-path`
2504
+ * slot whose piped content is a `pdf-stream`) — the stdin URN, not the
2505
+ * slot URN, is `in=`. The main input may ALSO be delivered by
2506
+ * position/cli-flag, but stdin is the defining route. Compared by
2507
+ * tagged-URN equivalence, never as strings.
2508
+ * Mirrors Rust: CapArg::is_main_input(&in_spec)
2509
+ * @param {MediaUrn|string} inSpec - The cap URN's in= spec (parsed if given as string)
2510
+ * @returns {boolean} True if this arg is the cap's main input
2511
+ */
2512
+ isMainInput(inSpec) {
2513
+ const spec = inSpec instanceof MediaUrn ? inSpec : MediaUrn.fromString(inSpec);
2514
+ return this.sources.some(s => {
2515
+ if (s.stdin === null) return false;
2516
+ try {
2517
+ return MediaUrn.fromString(s.stdin).isEquivalent(spec);
2518
+ } catch (_) {
2519
+ return false;
2520
+ }
2521
+ });
2522
+ }
2523
+
2484
2524
  /**
2485
2525
  * Check if this argument has a position source
2486
2526
  * @returns {boolean} True if has position source
@@ -2530,21 +2570,22 @@ class Cap {
2530
2570
  * @param {Object|null} metadataJson - Optional arbitrary metadata as JSON object
2531
2571
  * @param {string|null} documentation - Optional long-form markdown documentation. Rendered in capability info panels, the cap navigator, capdag-dot-com, and anywhere else a rich-text explanation of the cap is useful.
2532
2572
  */
2533
- constructor(urn, title, command, capDescription = null, metadata = {}, metadataJson = null, documentation = null) {
2573
+ constructor(urn, title, aliases, capDescription = null, metadata = {}, metadataJson = null, documentation = null) {
2534
2574
  if (!(urn instanceof CapUrn)) {
2535
2575
  throw new Error('URN must be a CapUrn instance');
2536
2576
  }
2537
2577
  if (!title || typeof title !== 'string') {
2538
2578
  throw new Error('Title is required and must be a string');
2539
2579
  }
2540
- if (!command || typeof command !== 'string') {
2541
- throw new Error('Command is required and must be a string');
2580
+ if (!Array.isArray(aliases) || aliases.length === 0) {
2581
+ throw new Error('Aliases are required and must be a non-empty array');
2542
2582
  }
2543
2583
 
2544
2584
  this.urn = urn;
2545
2585
  this.version = 0;
2546
2586
  this.title = title;
2547
- this.command = command;
2587
+ this.aliases = aliases;
2588
+ this.is_abstract = false;
2548
2589
  this.cap_description = capDescription;
2549
2590
  this.documentation = documentation;
2550
2591
  this.metadata = metadata || {};
@@ -2752,7 +2793,8 @@ class Cap {
2752
2793
 
2753
2794
  return this.urn.equals(other.urn) &&
2754
2795
  this.title === other.title &&
2755
- this.command === other.command &&
2796
+ JSON.stringify([...this.aliases].sort()) === JSON.stringify([...other.aliases].sort()) &&
2797
+ this.is_abstract === other.is_abstract &&
2756
2798
  this.cap_description === other.cap_description &&
2757
2799
  this.documentation === other.documentation &&
2758
2800
  JSON.stringify(this.metadata) === JSON.stringify(other.metadata) &&
@@ -2772,7 +2814,7 @@ class Cap {
2772
2814
  const result = {
2773
2815
  urn: this.urn.toString(),
2774
2816
  title: this.title,
2775
- command: this.command,
2817
+ aliases: this.aliases,
2776
2818
  cap_description: this.cap_description,
2777
2819
  metadata: this.metadata,
2778
2820
  args: this.args.map(a => a.toJSON()),
@@ -2783,6 +2825,10 @@ class Cap {
2783
2825
  result.version = this.version;
2784
2826
  }
2785
2827
 
2828
+ if (this.is_abstract) {
2829
+ result.abstract = true;
2830
+ }
2831
+
2786
2832
  // Long-form markdown documentation. Only emitted when set, to match
2787
2833
  // the Rust serializer which skips this field when None.
2788
2834
  if (typeof this.documentation === 'string' && this.documentation.length > 0) {
@@ -2821,7 +2867,11 @@ class Cap {
2821
2867
  const documentation = (typeof json.documentation === 'string' && json.documentation.length > 0)
2822
2868
  ? json.documentation
2823
2869
  : null;
2824
- const cap = new Cap(urn, json.title, json.command, json.cap_description, json.metadata, json.metadata_json, documentation);
2870
+ if (!Array.isArray(json.aliases) || json.aliases.length === 0) {
2871
+ throw new Error(`cap '${json.urn}' must declare at least one alias (the 'aliases' field is required and non-empty)`);
2872
+ }
2873
+ const cap = new Cap(urn, json.title, json.aliases, json.cap_description, json.metadata, json.metadata_json, documentation);
2874
+ cap.is_abstract = json.abstract === true;
2825
2875
  cap.version = (typeof json.version === 'number' && json.version !== 0) ? json.version : 0;
2826
2876
  // Parse args (new format)
2827
2877
  if (json.args && Array.isArray(json.args)) {
@@ -2910,7 +2960,7 @@ class CapGroup {
2910
2960
  }
2911
2961
 
2912
2962
  /**
2913
- * Unified cap-based manifest for components (providers and cartridges).
2963
+ * Unified cap-based manifest for components (cartridges).
2914
2964
  *
2915
2965
  * `(registry_url, channel, name, version)` is the cartridge's full
2916
2966
  * identity — each (registry, channel) pair is an independent namespace.
@@ -3046,20 +3096,20 @@ class CapManifest {
3046
3096
  /**
3047
3097
  * Helper functions for creating capabilities
3048
3098
  */
3049
- function createCap(urn, title, command) {
3050
- return new Cap(urn, title, command);
3099
+ function createCap(urn, title, aliases) {
3100
+ return new Cap(urn, title, aliases);
3051
3101
  }
3052
3102
 
3053
- function createCapWithDescription(urn, title, command, description) {
3054
- return new Cap(urn, title, command, description);
3103
+ function createCapWithDescription(urn, title, aliases, description) {
3104
+ return new Cap(urn, title, aliases, description);
3055
3105
  }
3056
3106
 
3057
- function createCapWithMetadata(urn, title, command, metadata) {
3058
- return new Cap(urn, title, command, null, metadata);
3107
+ function createCapWithMetadata(urn, title, aliases, metadata) {
3108
+ return new Cap(urn, title, aliases, null, metadata);
3059
3109
  }
3060
3110
 
3061
- function createCapWithDescriptionAndMetadata(urn, title, command, description, metadata) {
3062
- return new Cap(urn, title, command, description, metadata);
3111
+ function createCapWithDescriptionAndMetadata(urn, title, aliases, description, metadata) {
3112
+ return new Cap(urn, title, aliases, description, metadata);
3063
3113
  }
3064
3114
 
3065
3115
  // ============================================================================
@@ -3747,9 +3797,9 @@ class CapValidator {
3747
3797
  });
3748
3798
  }
3749
3799
 
3750
- if (!cap.command || typeof cap.command !== 'string') {
3800
+ if (!Array.isArray(cap.aliases) || cap.aliases.length === 0) {
3751
3801
  throw new ValidationError('InvalidCapSchema', capUrn, {
3752
- issue: 'Cap must have a valid command'
3802
+ issue: 'Cap must have at least one alias'
3753
3803
  });
3754
3804
  }
3755
3805
 
@@ -4355,37 +4405,55 @@ class CartridgeCapGroup {
4355
4405
  // in synchronous contexts must await.
4356
4406
 
4357
4407
  const DEV_SLUG = "dev";
4358
- const SLUG_HEX_LEN = 16;
4359
4408
 
4360
4409
  /**
4361
- * Compute the on-disk slug for a registry URL.
4410
+ * The authority (host[:port]) of a registry URL: after `://` up to the next
4411
+ * `/`, `?`, or `#` (path/query/fragment discarded).
4412
+ */
4413
+ function authorityOfRegistryUrl(url) {
4414
+ const i = url.indexOf("://");
4415
+ const afterScheme = i >= 0 ? url.slice(i + 3) : url;
4416
+ const m = afterScheme.search(/[/?#]/);
4417
+ return m >= 0 ? afterScheme.slice(0, m) : afterScheme;
4418
+ }
4419
+
4420
+ /**
4421
+ * Compute the on-disk slug for a registry URL. Mirrors
4422
+ * capdag::cartridge_slug::slug_for byte-for-byte: `DEV_SLUG` for null/undefined,
4423
+ * otherwise a path-safe transform of the URL's authority (host[:port]) —
4424
+ * ASCII-lowercased, every character outside `[a-z0-9.-]` replaced by `-`.
4425
+ * Depends ONLY on the authority; path/version/query/trailing-slash/host-case
4426
+ * do not change it.
4362
4427
  *
4363
- * @param {string|null|undefined} registryUrl - The registry URL, or
4364
- * null/undefined for dev installs.
4365
- * @returns {Promise<string>} The slug — `DEV_SLUG` for null,
4366
- * otherwise the first 16 lowercase-hex characters of
4367
- * sha256(registryUrl).
4428
+ * @param {string|null|undefined} registryUrl
4429
+ * @returns {string}
4368
4430
  */
4369
- async function slugForRegistryUrl(registryUrl) {
4431
+ function slugForRegistryUrlSync(registryUrl) {
4370
4432
  if (registryUrl === null || registryUrl === undefined) {
4371
4433
  return DEV_SLUG;
4372
4434
  }
4373
- const bytes = new TextEncoder().encode(registryUrl);
4374
- // Web Crypto exposes SHA-256 via crypto.subtle. Node 16+ exposes
4375
- // it through globalThis.crypto.subtle.
4376
- const digestBuffer = await crypto.subtle.digest("SHA-256", bytes);
4377
- const digestBytes = new Uint8Array(digestBuffer);
4378
- let hex = "";
4379
- for (const b of digestBytes) {
4380
- hex += b.toString(16).padStart(2, "0");
4435
+ let out = "";
4436
+ for (const ch of authorityOfRegistryUrl(registryUrl)) {
4437
+ const lc = ch >= "A" && ch <= "Z" ? ch.toLowerCase() : ch;
4438
+ out += (lc >= "a" && lc <= "z") || (lc >= "0" && lc <= "9") || lc === "." || lc === "-" ? lc : "-";
4381
4439
  }
4382
- return hex.slice(0, SLUG_HEX_LEN);
4440
+ return out;
4441
+ }
4442
+
4443
+ /**
4444
+ * Async wrapper retained for callers that `await` the slug. The computation is
4445
+ * synchronous (no hashing) — the authority transform needs no crypto.
4446
+ * @returns {Promise<string>}
4447
+ */
4448
+ async function slugForRegistryUrl(registryUrl) {
4449
+ return slugForRegistryUrlSync(registryUrl);
4383
4450
  }
4384
4451
 
4385
4452
  function isRegistrySlug(s) {
4386
4453
  return typeof s === "string"
4387
- && s.length === SLUG_HEX_LEN
4388
- && /^[0-9a-f]+$/.test(s);
4454
+ && s.length > 0
4455
+ && s !== DEV_SLUG
4456
+ && /^[a-z0-9.-]+$/.test(s);
4389
4457
  }
4390
4458
 
4391
4459
  // =============================================================================
@@ -4744,6 +4812,18 @@ class CartridgeRepoClient {
4744
4812
  if (data.schemaVersion !== '5.0') {
4745
4813
  throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
4746
4814
  }
4815
+ // Cartridge registry regime version. v0 was the pre-versioning legacy at
4816
+ // the bare /manifest path; this speaks only v1 at /v1/manifest. A manifest
4817
+ // from a different regime version is rejected, not reinterpreted.
4818
+ if (data.registryVersion !== 1) {
4819
+ throw new Error(`Cartridge registry from ${repoUrl} has registryVersion '${data.registryVersion}'; this build speaks v1`);
4820
+ }
4821
+ // A cartridge registry must declare the fabric its caps resolve against. The
4822
+ // client cross-checks this equals its own fabric (so every registry it uses
4823
+ // shares one fabric); here we require it is at least present.
4824
+ if (typeof data.fabricRegistryUrl !== 'string' || data.fabricRegistryUrl.length === 0) {
4825
+ throw new Error(`Cartridge registry from ${repoUrl}: missing required 'fabricRegistryUrl' (the fabric its caps resolve against)`);
4826
+ }
4747
4827
  // Self-referential check: the manifest declares its own URL via
4748
4828
  // `registryUrl`. It must match the URL we just fetched from
4749
4829
  // byte-for-byte — a mismatch is a manifest-corruption signal
@@ -5021,6 +5101,12 @@ class CartridgeRepoServer {
5021
5101
  if (this.registry.schemaVersion !== '5.0') {
5022
5102
  throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
5023
5103
  }
5104
+ if (this.registry.registryVersion !== 1) {
5105
+ throw new Error(`Unsupported cartridge registry version: ${this.registry.registryVersion}. This build speaks v1.`);
5106
+ }
5107
+ if (typeof this.registry.fabricRegistryUrl !== 'string' || this.registry.fabricRegistryUrl.length === 0) {
5108
+ throw new Error('Registry must declare a non-empty `fabricRegistryUrl` (the fabric its caps resolve against)');
5109
+ }
5024
5110
  if (typeof this.registry.registryUrl !== 'string' || this.registry.registryUrl.length === 0) {
5025
5111
  throw new Error('Registry must have a non-empty top-level `registryUrl` field (self-referential URL)');
5026
5112
  }
@@ -5240,7 +5326,7 @@ const CartridgeAttachmentErrorKind = Object.freeze({
5240
5326
  QUARANTINED: 'quarantined',
5241
5327
  // On-disk install context disagrees with the cartridge.json the cartridge
5242
5328
  // declares — the slug folder doesn't match slug_for(registry_url), the
5243
- // channel folder doesn't match the manifest's channel, or a bundled-provider
5329
+ // channel folder doesn't match the manifest's channel, or a bundled-cartridge
5244
5330
  // integrity proof failed. Structurally well-formed but cannot be trusted
5245
5331
  // because its placement on disk does not match what it claims to be.
5246
5332
  BAD_INSTALLATION: 'bad_installation',
@@ -5450,24 +5536,15 @@ class InstalledCartridgeRecord {
5450
5536
  /**
5451
5537
  * Synchronous on-disk slug for a registry URL. Mirrors
5452
5538
  * capdag::bifaci::cartridge_slug::slug_for byte-for-byte: `null`/`undefined`
5453
- * (a dev cartridge) → the literal `DEV_SLUG`; otherwise the first
5454
- * SLUG_HEX_LEN lowercase-hex characters of sha256(url bytes).
5455
- *
5456
- * The async `slugForRegistryUrl` above uses Web Crypto for browser parity;
5457
- * discovery runs only under Node and needs a synchronous slug in tight
5458
- * per-file loops, so this uses Node's synchronous `crypto.createHash`. Both
5459
- * produce identical output.
5539
+ * (a dev cartridge) → the literal `DEV_SLUG`; otherwise a path-safe transform
5540
+ * of the URL's authority (host[:port]). Identical output to the async
5541
+ * `slugForRegistryUrl` above — no hashing, so both are trivially in sync.
5460
5542
  *
5461
5543
  * @param {string|null|undefined} registryUrl
5462
5544
  * @returns {string}
5463
5545
  */
5464
5546
  function slugForSync(registryUrl) {
5465
- if (registryUrl === null || registryUrl === undefined) {
5466
- return DEV_SLUG;
5467
- }
5468
- const crypto = require('crypto');
5469
- const hex = crypto.createHash('sha256').update(registryUrl, 'utf8').digest('hex');
5470
- return hex.slice(0, SLUG_HEX_LEN);
5547
+ return slugForRegistryUrlSync(registryUrl);
5471
5548
  }
5472
5549
 
5473
5550
  /**
@@ -5780,49 +5857,49 @@ function hashCartridgeDirectory(dir) {
5780
5857
  }
5781
5858
 
5782
5859
  // ---------------------------------------------------------------------------
5783
- // Bundled-provider integrity (non-macOS).
5860
+ // Bundled-cartridge integrity (non-macOS).
5784
5861
  // ---------------------------------------------------------------------------
5785
5862
  //
5786
- // Mirrors capdag::cartridge_discovery::verify_bundled_provider_hash and the
5787
- // BUNDLED_PROVIDER_HASHES const codegen'd by build.rs. Under a plain build —
5788
- // and in this mirror, which bundles no providers — the set is empty, so any
5863
+ // Mirrors capdag::cartridge_discovery::verify_bundled_cartridge_hash and the
5864
+ // BUNDLED_CARTRIDGE_HASHES const codegen'd by build.rs. Under a plain build —
5865
+ // and in this mirror, which bundles no cartridges — the set is empty, so any
5789
5866
  // `installed_from: bundle` cartridge has no baked hash and is rejected.
5790
- const BUNDLED_PROVIDER_HASHES = Object.freeze([]); // [name, version, hash] triples
5867
+ const BUNDLED_CARTRIDGE_HASHES = Object.freeze([]); // [name, version, hash] triples
5791
5868
 
5792
5869
  /**
5793
- * Look up the baked expected directory hash for a bundled provider, or null if
5870
+ * Look up the baked expected directory hash for a bundled cartridge, or null if
5794
5871
  * (name, version) was not recorded at build time.
5795
5872
  * @returns {string|null}
5796
5873
  */
5797
- function bundledProviderExpectedHash(name, version) {
5798
- for (const [n, v, h] of BUNDLED_PROVIDER_HASHES) {
5874
+ function bundledCartridgeExpectedHash(name, version) {
5875
+ for (const [n, v, h] of BUNDLED_CARTRIDGE_HASHES) {
5799
5876
  if (n === name && v === version) return h;
5800
5877
  }
5801
5878
  return null;
5802
5879
  }
5803
5880
 
5804
5881
  /**
5805
- * Verify a bundled provider's on-disk content against the hash baked into this
5882
+ * Verify a bundled cartridge's on-disk content against the hash baked into this
5806
5883
  * build. Returns null on success, or an error-reason string. Mirrors
5807
- * verify_bundled_provider_hash. Non-macOS only: macOS bundled-provider
5884
+ * verify_bundled_cartridge_hash. Non-macOS only: macOS bundled-cartridge
5808
5885
  * integrity is the OS code-signature, so the engine there neither bakes nor
5809
5886
  * checks these hashes.
5810
5887
  *
5811
5888
  * @returns {string|null} null on OK, else the failure reason.
5812
5889
  */
5813
- function verifyBundledProviderHash(name, version, versionDir) {
5814
- const expected = bundledProviderExpectedHash(name, version);
5890
+ function verifyBundledCartridgeHash(name, version, versionDir) {
5891
+ const expected = bundledCartridgeExpectedHash(name, version);
5815
5892
  if (expected === null) {
5816
- return `no baked hash for bundled provider ${name} ${version} — this build did not record it (MFR_BUNDLED_PROVIDER_HASHES)`;
5893
+ return `no baked hash for bundled cartridge ${name} ${version} — this build did not record it (MFR_BUNDLED_CARTRIDGE_HASHES)`;
5817
5894
  }
5818
5895
  let actual;
5819
5896
  try {
5820
5897
  actual = hashCartridgeDirectory(versionDir);
5821
5898
  } catch (e) {
5822
- return `failed to hash bundled provider directory: ${e.message}`;
5899
+ return `failed to hash bundled cartridge directory: ${e.message}`;
5823
5900
  }
5824
5901
  if (actual === expected) return null;
5825
- return `content hash mismatch — baked ${expected}, on-disk ${actual}; the shipped provider differs from what this build was compiled to ship`;
5902
+ return `content hash mismatch — baked ${expected}, on-disk ${actual}; the shipped cartridge differs from what this build was compiled to ship`;
5826
5903
  }
5827
5904
 
5828
5905
  /**
@@ -5838,14 +5915,21 @@ class DiscoveryIdentity {
5838
5915
  * @param {string|null} opts.registryUrl - Some(url) for release/nightly
5839
5916
  * hosts, null for dev hosts.
5840
5917
  * @param {number} opts.fabricManifestVersion
5918
+ * @param {number} opts.cartridgeRegistryVersion - The registry regime version
5919
+ * this host speaks; an on-disk PATH level ({slug}/v{N}/{channel}/…), pinned
5920
+ * like the channel so a v1 host never scans a v2 tree.
5841
5921
  */
5842
- constructor({ channel, registryUrl, fabricManifestVersion }) {
5922
+ constructor({ channel, registryUrl, fabricManifestVersion, cartridgeRegistryVersion }) {
5843
5923
  if (channel !== CartridgeChannel.Release && channel !== CartridgeChannel.Nightly) {
5844
5924
  throw new Error(`DiscoveryIdentity: invalid channel '${channel}'`);
5845
5925
  }
5926
+ if (!Number.isInteger(cartridgeRegistryVersion) || cartridgeRegistryVersion < 1) {
5927
+ throw new Error(`DiscoveryIdentity: cartridgeRegistryVersion must be an integer >= 1, got '${cartridgeRegistryVersion}'`);
5928
+ }
5846
5929
  this.channel = channel;
5847
5930
  this.registryUrl = registryUrl !== undefined ? registryUrl : null;
5848
5931
  this.fabricManifestVersion = fabricManifestVersion;
5932
+ this.cartridgeRegistryVersion = cartridgeRegistryVersion;
5849
5933
  }
5850
5934
 
5851
5935
  /** This host's own baked-registry slug (`dev` when registryUrl is null). */
@@ -5876,7 +5960,7 @@ function unixSecondsNow() {
5876
5960
  }
5877
5961
 
5878
5962
  // Default protocol limits / version, mirroring capdag::bifaci::frame.
5879
- const BIFACI_PROTOCOL_VERSION = 3;
5963
+ const BIFACI_PROTOCOL_VERSION = 4;
5880
5964
  const BIFACI_DEFAULT_MAX_FRAME = 3670016;
5881
5965
  const BIFACI_DEFAULT_MAX_CHUNK = 262144;
5882
5966
  const BIFACI_DEFAULT_MAX_REORDER_BUFFER = 64;
@@ -6011,6 +6095,7 @@ function encodeHelloFrame() {
6011
6095
  ['max_reorder_buffer', BIFACI_DEFAULT_MAX_REORDER_BUFFER],
6012
6096
  ['initial_credit', BIFACI_DEFAULT_INITIAL_CREDIT],
6013
6097
  ['version', BIFACI_PROTOCOL_VERSION],
6098
+ ['handler_capacity', 0],
6014
6099
  ],
6015
6100
  };
6016
6101
  const frame = {
@@ -6110,8 +6195,27 @@ function probeCartridgeCapGroups(entryPath) {
6110
6195
  finish(new Error(`cartridge ${entryPath} HELLO failed: expected HELLO, got frame_type ${frameType}`));
6111
6196
  return;
6112
6197
  }
6198
+ const version = decoded.get(BIFACI_KEY_VERSION);
6199
+ if (version !== BIFACI_PROTOCOL_VERSION) {
6200
+ finish(new Error(`cartridge ${entryPath} HELLO failed: protocol version ${version} does not match required version ${BIFACI_PROTOCOL_VERSION}`));
6201
+ return;
6202
+ }
6113
6203
  const meta = decoded.get(BIFACI_KEY_META);
6114
- const manifestVal = meta instanceof Map ? meta.get('manifest') : undefined;
6204
+ if (!(meta instanceof Map)) {
6205
+ finish(new Error(`cartridge ${entryPath} HELLO failed: HELLO meta must be a map`));
6206
+ return;
6207
+ }
6208
+ const metaVersion = meta.get('version');
6209
+ if (metaVersion !== BIFACI_PROTOCOL_VERSION) {
6210
+ finish(new Error(`cartridge ${entryPath} HELLO failed: meta.version ${metaVersion} does not match required version ${BIFACI_PROTOCOL_VERSION}`));
6211
+ return;
6212
+ }
6213
+ const handlerCapacity = meta.get('handler_capacity');
6214
+ if (!Number.isSafeInteger(handlerCapacity) || handlerCapacity < 0) {
6215
+ finish(new Error(`cartridge ${entryPath} HELLO failed: missing required non-negative handler_capacity`));
6216
+ return;
6217
+ }
6218
+ const manifestVal = meta.get('manifest');
6115
6219
  if (!manifestVal || manifestVal.__bytes === undefined) {
6116
6220
  finish(new Error(`cartridge ${entryPath} HELLO failed: Cartridge HELLO missing required manifest`));
6117
6221
  return;
@@ -6175,7 +6279,9 @@ async function discoverCartridges(cartridgesRoot, identity) {
6175
6279
  continue;
6176
6280
  }
6177
6281
  const expectedSlug = slugEntry.name;
6178
- const scanRoot = path.join(slugDir, identity.channel);
6282
+ // {slug}/v{cartridgeRegistryVersion}/{channel}/… — the registry regime
6283
+ // version is a path level pinned to the host's version (like the channel).
6284
+ const scanRoot = path.join(slugDir, `v${identity.cartridgeRegistryVersion}`, identity.channel);
6179
6285
  let scanStat;
6180
6286
  try { scanStat = fs.statSync(scanRoot); } catch (_e) { continue; }
6181
6287
  if (!scanStat.isDirectory()) continue;
@@ -6324,7 +6430,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6324
6430
  continue;
6325
6431
  }
6326
6432
 
6327
- // Bundled-provider integrity. A cartridge marked installed_from=bundle is
6433
+ // Bundled-cartridge integrity. A cartridge marked installed_from=bundle is
6328
6434
  // shipped INSIDE this build and has no upstream registry to verify against,
6329
6435
  // so it needs its own integrity proof. Platform-split: on macOS the OS
6330
6436
  // code-signature is the guard (baked-hash verification intentionally
@@ -6332,7 +6438,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6332
6438
  // value, else BadInstallation.
6333
6439
  if (cj.installedFrom === CartridgeInstallSource.BUNDLE) {
6334
6440
  if (process.platform !== 'darwin') {
6335
- const reason = verifyBundledProviderHash(cj.name, cj.version, versionDir);
6441
+ const reason = verifyBundledCartridgeHash(cj.name, cj.version, versionDir);
6336
6442
  if (reason !== null) {
6337
6443
  discovered.push(DiscoveredCartridge.incompatible({
6338
6444
  version_dir: versionDir,
@@ -6342,7 +6448,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6342
6448
  version: cj.version,
6343
6449
  error: new CartridgeAttachmentError(
6344
6450
  CartridgeAttachmentErrorKind.BAD_INSTALLATION,
6345
- `bundled provider integrity check failed: ${reason}`,
6451
+ `bundled cartridge integrity check failed: ${reason}`,
6346
6452
  detectedAt
6347
6453
  ),
6348
6454
  }));
@@ -7425,7 +7531,8 @@ class FabricRegistryEntry {
7425
7531
  constructor(data) {
7426
7532
  this.urn = data.urn;
7427
7533
  this.title = data.title || '';
7428
- this.command = data.command || '';
7534
+ this.aliases = data.aliases || [];
7535
+ this.isAbstract = data.abstract === true;
7429
7536
  this.description = data.cap_description || '';
7430
7537
  this.args = data.args || [];
7431
7538
  this.output = data.output || null;
@@ -7880,8 +7987,30 @@ class Manifest {
7880
7987
  }
7881
7988
  }
7882
7989
 
7990
+ // Unified configurable planner vocabulary (mirrors Rust plan_space.rs and the
7991
+ // PlanMachines/DiscoverConvergentTargets proto surface).
7992
+ const planner = require('./planner.js');
7993
+
7883
7994
  // Export for CommonJS
7884
7995
  module.exports = {
7996
+ // Planner plan-space vocabulary
7997
+ PlanStateError: planner.PlanStateError,
7998
+ ConvergencePresence: planner.ConvergencePresence,
7999
+ ConvergenceLocation: planner.ConvergenceLocation,
8000
+ ConvergenceMechanism: planner.ConvergenceMechanism,
8001
+ ConvergenceArity: planner.ConvergenceArity,
8002
+ DivergencePresence: planner.DivergencePresence,
8003
+ DivergenceLocation: planner.DivergenceLocation,
8004
+ RankPolicy: planner.RankPolicy,
8005
+ PlanMode: planner.PlanMode,
8006
+ PlanRequest: planner.PlanRequest,
8007
+ parsePlanApex: planner.parsePlanApex,
8008
+ parsePlanProfile: planner.parsePlanProfile,
8009
+ parsePlanCost: planner.parsePlanCost,
8010
+ parsePlanCandidate: planner.parsePlanCandidate,
8011
+ parsePlanCandidates: planner.parsePlanCandidates,
8012
+ parseConvergentTarget: planner.parseConvergentTarget,
8013
+ parseConvergentTargets: planner.parseConvergentTargets,
7885
8014
  ALIAS_TARGET_CAP,
7886
8015
  ALIAS_TARGET_MEDIA,
7887
8016
  tokenIsUrn,
@@ -8079,15 +8208,15 @@ module.exports = {
8079
8208
  CartridgeJsonError,
8080
8209
  CartridgeJson,
8081
8210
  hashCartridgeDirectory,
8082
- BUNDLED_PROVIDER_HASHES,
8083
- verifyBundledProviderHash,
8211
+ BUNDLED_CARTRIDGE_HASHES,
8212
+ verifyBundledCartridgeHash,
8084
8213
  DiscoveryIdentity,
8085
8214
  DiscoveredCartridge,
8086
8215
  probeCartridgeCapGroups,
8087
8216
  discoverCartridges,
8088
8217
  // Registry slug
8089
8218
  DEV_SLUG,
8090
- SLUG_HEX_LEN,
8219
+ slugForRegistryUrlSync,
8091
8220
  slugForRegistryUrl,
8092
8221
  isRegistrySlug,
8093
8222
  // Machine notation