capdag 1.226.626 → 1.233.649
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/cap-fab-renderer.js +1 -1
- package/capdag.js +122 -53
- package/capdag.test.js +192 -20
- package/package.json +1 -1
package/cap-fab-renderer.js
CHANGED
|
@@ -1125,7 +1125,7 @@ function buildBrowseGraphData(capabilities) {
|
|
|
1125
1125
|
// Parsing the URN canonicalizes it and validates it — fail hard on
|
|
1126
1126
|
// malformed registry data.
|
|
1127
1127
|
const parsedUrn = CapUrn.fromString(capData.urn);
|
|
1128
|
-
const cap = createCap(parsedUrn, title, capData.
|
|
1128
|
+
const cap = createCap(parsedUrn, title, capData.aliases || []);
|
|
1129
1129
|
const capFabEdgeIndex = capFab.edges.length;
|
|
1130
1130
|
capFab.addCap(cap, 'registry');
|
|
1131
1131
|
|
package/capdag.js
CHANGED
|
@@ -2530,21 +2530,22 @@ class Cap {
|
|
|
2530
2530
|
* @param {Object|null} metadataJson - Optional arbitrary metadata as JSON object
|
|
2531
2531
|
* @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
2532
|
*/
|
|
2533
|
-
constructor(urn, title,
|
|
2533
|
+
constructor(urn, title, aliases, capDescription = null, metadata = {}, metadataJson = null, documentation = null) {
|
|
2534
2534
|
if (!(urn instanceof CapUrn)) {
|
|
2535
2535
|
throw new Error('URN must be a CapUrn instance');
|
|
2536
2536
|
}
|
|
2537
2537
|
if (!title || typeof title !== 'string') {
|
|
2538
2538
|
throw new Error('Title is required and must be a string');
|
|
2539
2539
|
}
|
|
2540
|
-
if (!
|
|
2541
|
-
throw new Error('
|
|
2540
|
+
if (!Array.isArray(aliases) || aliases.length === 0) {
|
|
2541
|
+
throw new Error('Aliases are required and must be a non-empty array');
|
|
2542
2542
|
}
|
|
2543
2543
|
|
|
2544
2544
|
this.urn = urn;
|
|
2545
2545
|
this.version = 0;
|
|
2546
2546
|
this.title = title;
|
|
2547
|
-
this.
|
|
2547
|
+
this.aliases = aliases;
|
|
2548
|
+
this.is_abstract = false;
|
|
2548
2549
|
this.cap_description = capDescription;
|
|
2549
2550
|
this.documentation = documentation;
|
|
2550
2551
|
this.metadata = metadata || {};
|
|
@@ -2752,7 +2753,8 @@ class Cap {
|
|
|
2752
2753
|
|
|
2753
2754
|
return this.urn.equals(other.urn) &&
|
|
2754
2755
|
this.title === other.title &&
|
|
2755
|
-
this.
|
|
2756
|
+
JSON.stringify([...this.aliases].sort()) === JSON.stringify([...other.aliases].sort()) &&
|
|
2757
|
+
this.is_abstract === other.is_abstract &&
|
|
2756
2758
|
this.cap_description === other.cap_description &&
|
|
2757
2759
|
this.documentation === other.documentation &&
|
|
2758
2760
|
JSON.stringify(this.metadata) === JSON.stringify(other.metadata) &&
|
|
@@ -2772,7 +2774,7 @@ class Cap {
|
|
|
2772
2774
|
const result = {
|
|
2773
2775
|
urn: this.urn.toString(),
|
|
2774
2776
|
title: this.title,
|
|
2775
|
-
|
|
2777
|
+
aliases: this.aliases,
|
|
2776
2778
|
cap_description: this.cap_description,
|
|
2777
2779
|
metadata: this.metadata,
|
|
2778
2780
|
args: this.args.map(a => a.toJSON()),
|
|
@@ -2783,6 +2785,10 @@ class Cap {
|
|
|
2783
2785
|
result.version = this.version;
|
|
2784
2786
|
}
|
|
2785
2787
|
|
|
2788
|
+
if (this.is_abstract) {
|
|
2789
|
+
result.abstract = true;
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2786
2792
|
// Long-form markdown documentation. Only emitted when set, to match
|
|
2787
2793
|
// the Rust serializer which skips this field when None.
|
|
2788
2794
|
if (typeof this.documentation === 'string' && this.documentation.length > 0) {
|
|
@@ -2821,7 +2827,11 @@ class Cap {
|
|
|
2821
2827
|
const documentation = (typeof json.documentation === 'string' && json.documentation.length > 0)
|
|
2822
2828
|
? json.documentation
|
|
2823
2829
|
: null;
|
|
2824
|
-
|
|
2830
|
+
if (!Array.isArray(json.aliases) || json.aliases.length === 0) {
|
|
2831
|
+
throw new Error(`cap '${json.urn}' must declare at least one alias (the 'aliases' field is required and non-empty)`);
|
|
2832
|
+
}
|
|
2833
|
+
const cap = new Cap(urn, json.title, json.aliases, json.cap_description, json.metadata, json.metadata_json, documentation);
|
|
2834
|
+
cap.is_abstract = json.abstract === true;
|
|
2825
2835
|
cap.version = (typeof json.version === 'number' && json.version !== 0) ? json.version : 0;
|
|
2826
2836
|
// Parse args (new format)
|
|
2827
2837
|
if (json.args && Array.isArray(json.args)) {
|
|
@@ -3046,20 +3056,20 @@ class CapManifest {
|
|
|
3046
3056
|
/**
|
|
3047
3057
|
* Helper functions for creating capabilities
|
|
3048
3058
|
*/
|
|
3049
|
-
function createCap(urn, title,
|
|
3050
|
-
return new Cap(urn, title,
|
|
3059
|
+
function createCap(urn, title, aliases) {
|
|
3060
|
+
return new Cap(urn, title, aliases);
|
|
3051
3061
|
}
|
|
3052
3062
|
|
|
3053
|
-
function createCapWithDescription(urn, title,
|
|
3054
|
-
return new Cap(urn, title,
|
|
3063
|
+
function createCapWithDescription(urn, title, aliases, description) {
|
|
3064
|
+
return new Cap(urn, title, aliases, description);
|
|
3055
3065
|
}
|
|
3056
3066
|
|
|
3057
|
-
function createCapWithMetadata(urn, title,
|
|
3058
|
-
return new Cap(urn, title,
|
|
3067
|
+
function createCapWithMetadata(urn, title, aliases, metadata) {
|
|
3068
|
+
return new Cap(urn, title, aliases, null, metadata);
|
|
3059
3069
|
}
|
|
3060
3070
|
|
|
3061
|
-
function createCapWithDescriptionAndMetadata(urn, title,
|
|
3062
|
-
return new Cap(urn, title,
|
|
3071
|
+
function createCapWithDescriptionAndMetadata(urn, title, aliases, description, metadata) {
|
|
3072
|
+
return new Cap(urn, title, aliases, description, metadata);
|
|
3063
3073
|
}
|
|
3064
3074
|
|
|
3065
3075
|
// ============================================================================
|
|
@@ -3747,9 +3757,9 @@ class CapValidator {
|
|
|
3747
3757
|
});
|
|
3748
3758
|
}
|
|
3749
3759
|
|
|
3750
|
-
if (!cap.
|
|
3760
|
+
if (!Array.isArray(cap.aliases) || cap.aliases.length === 0) {
|
|
3751
3761
|
throw new ValidationError('InvalidCapSchema', capUrn, {
|
|
3752
|
-
issue: 'Cap must have
|
|
3762
|
+
issue: 'Cap must have at least one alias'
|
|
3753
3763
|
});
|
|
3754
3764
|
}
|
|
3755
3765
|
|
|
@@ -4355,37 +4365,55 @@ class CartridgeCapGroup {
|
|
|
4355
4365
|
// in synchronous contexts must await.
|
|
4356
4366
|
|
|
4357
4367
|
const DEV_SLUG = "dev";
|
|
4358
|
-
const SLUG_HEX_LEN = 16;
|
|
4359
4368
|
|
|
4360
4369
|
/**
|
|
4361
|
-
*
|
|
4370
|
+
* The authority (host[:port]) of a registry URL: after `://` up to the next
|
|
4371
|
+
* `/`, `?`, or `#` (path/query/fragment discarded).
|
|
4372
|
+
*/
|
|
4373
|
+
function authorityOfRegistryUrl(url) {
|
|
4374
|
+
const i = url.indexOf("://");
|
|
4375
|
+
const afterScheme = i >= 0 ? url.slice(i + 3) : url;
|
|
4376
|
+
const m = afterScheme.search(/[/?#]/);
|
|
4377
|
+
return m >= 0 ? afterScheme.slice(0, m) : afterScheme;
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
/**
|
|
4381
|
+
* Compute the on-disk slug for a registry URL. Mirrors
|
|
4382
|
+
* capdag::cartridge_slug::slug_for byte-for-byte: `DEV_SLUG` for null/undefined,
|
|
4383
|
+
* otherwise a path-safe transform of the URL's authority (host[:port]) —
|
|
4384
|
+
* ASCII-lowercased, every character outside `[a-z0-9.-]` replaced by `-`.
|
|
4385
|
+
* Depends ONLY on the authority; path/version/query/trailing-slash/host-case
|
|
4386
|
+
* do not change it.
|
|
4362
4387
|
*
|
|
4363
|
-
* @param {string|null|undefined} registryUrl
|
|
4364
|
-
*
|
|
4365
|
-
* @returns {Promise<string>} The slug — `DEV_SLUG` for null,
|
|
4366
|
-
* otherwise the first 16 lowercase-hex characters of
|
|
4367
|
-
* sha256(registryUrl).
|
|
4388
|
+
* @param {string|null|undefined} registryUrl
|
|
4389
|
+
* @returns {string}
|
|
4368
4390
|
*/
|
|
4369
|
-
|
|
4391
|
+
function slugForRegistryUrlSync(registryUrl) {
|
|
4370
4392
|
if (registryUrl === null || registryUrl === undefined) {
|
|
4371
4393
|
return DEV_SLUG;
|
|
4372
4394
|
}
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
const digestBytes = new Uint8Array(digestBuffer);
|
|
4378
|
-
let hex = "";
|
|
4379
|
-
for (const b of digestBytes) {
|
|
4380
|
-
hex += b.toString(16).padStart(2, "0");
|
|
4395
|
+
let out = "";
|
|
4396
|
+
for (const ch of authorityOfRegistryUrl(registryUrl)) {
|
|
4397
|
+
const lc = ch >= "A" && ch <= "Z" ? ch.toLowerCase() : ch;
|
|
4398
|
+
out += (lc >= "a" && lc <= "z") || (lc >= "0" && lc <= "9") || lc === "." || lc === "-" ? lc : "-";
|
|
4381
4399
|
}
|
|
4382
|
-
return
|
|
4400
|
+
return out;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
/**
|
|
4404
|
+
* Async wrapper retained for callers that `await` the slug. The computation is
|
|
4405
|
+
* synchronous (no hashing) — the authority transform needs no crypto.
|
|
4406
|
+
* @returns {Promise<string>}
|
|
4407
|
+
*/
|
|
4408
|
+
async function slugForRegistryUrl(registryUrl) {
|
|
4409
|
+
return slugForRegistryUrlSync(registryUrl);
|
|
4383
4410
|
}
|
|
4384
4411
|
|
|
4385
4412
|
function isRegistrySlug(s) {
|
|
4386
4413
|
return typeof s === "string"
|
|
4387
|
-
&& s.length
|
|
4388
|
-
&&
|
|
4414
|
+
&& s.length > 0
|
|
4415
|
+
&& s !== DEV_SLUG
|
|
4416
|
+
&& /^[a-z0-9.-]+$/.test(s);
|
|
4389
4417
|
}
|
|
4390
4418
|
|
|
4391
4419
|
// =============================================================================
|
|
@@ -4744,6 +4772,18 @@ class CartridgeRepoClient {
|
|
|
4744
4772
|
if (data.schemaVersion !== '5.0') {
|
|
4745
4773
|
throw new Error(`Cartridge registry from ${repoUrl} has schemaVersion '${data.schemaVersion}'; required: 5.0`);
|
|
4746
4774
|
}
|
|
4775
|
+
// Cartridge registry regime version. v0 was the pre-versioning legacy at
|
|
4776
|
+
// the bare /manifest path; this speaks only v1 at /v1/manifest. A manifest
|
|
4777
|
+
// from a different regime version is rejected, not reinterpreted.
|
|
4778
|
+
if (data.registryVersion !== 1) {
|
|
4779
|
+
throw new Error(`Cartridge registry from ${repoUrl} has registryVersion '${data.registryVersion}'; this build speaks v1`);
|
|
4780
|
+
}
|
|
4781
|
+
// A cartridge registry must declare the fabric its caps resolve against. The
|
|
4782
|
+
// client cross-checks this equals its own fabric (so every registry it uses
|
|
4783
|
+
// shares one fabric); here we require it is at least present.
|
|
4784
|
+
if (typeof data.fabricRegistryUrl !== 'string' || data.fabricRegistryUrl.length === 0) {
|
|
4785
|
+
throw new Error(`Cartridge registry from ${repoUrl}: missing required 'fabricRegistryUrl' (the fabric its caps resolve against)`);
|
|
4786
|
+
}
|
|
4747
4787
|
// Self-referential check: the manifest declares its own URL via
|
|
4748
4788
|
// `registryUrl`. It must match the URL we just fetched from
|
|
4749
4789
|
// byte-for-byte — a mismatch is a manifest-corruption signal
|
|
@@ -5021,6 +5061,12 @@ class CartridgeRepoServer {
|
|
|
5021
5061
|
if (this.registry.schemaVersion !== '5.0') {
|
|
5022
5062
|
throw new Error(`Unsupported registry schema version: ${this.registry.schemaVersion}. Required: 5.0`);
|
|
5023
5063
|
}
|
|
5064
|
+
if (this.registry.registryVersion !== 1) {
|
|
5065
|
+
throw new Error(`Unsupported cartridge registry version: ${this.registry.registryVersion}. This build speaks v1.`);
|
|
5066
|
+
}
|
|
5067
|
+
if (typeof this.registry.fabricRegistryUrl !== 'string' || this.registry.fabricRegistryUrl.length === 0) {
|
|
5068
|
+
throw new Error('Registry must declare a non-empty `fabricRegistryUrl` (the fabric its caps resolve against)');
|
|
5069
|
+
}
|
|
5024
5070
|
if (typeof this.registry.registryUrl !== 'string' || this.registry.registryUrl.length === 0) {
|
|
5025
5071
|
throw new Error('Registry must have a non-empty top-level `registryUrl` field (self-referential URL)');
|
|
5026
5072
|
}
|
|
@@ -5450,24 +5496,15 @@ class InstalledCartridgeRecord {
|
|
|
5450
5496
|
/**
|
|
5451
5497
|
* Synchronous on-disk slug for a registry URL. Mirrors
|
|
5452
5498
|
* capdag::bifaci::cartridge_slug::slug_for byte-for-byte: `null`/`undefined`
|
|
5453
|
-
* (a dev cartridge) → the literal `DEV_SLUG`; otherwise
|
|
5454
|
-
*
|
|
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.
|
|
5499
|
+
* (a dev cartridge) → the literal `DEV_SLUG`; otherwise a path-safe transform
|
|
5500
|
+
* of the URL's authority (host[:port]). Identical output to the async
|
|
5501
|
+
* `slugForRegistryUrl` above — no hashing, so both are trivially in sync.
|
|
5460
5502
|
*
|
|
5461
5503
|
* @param {string|null|undefined} registryUrl
|
|
5462
5504
|
* @returns {string}
|
|
5463
5505
|
*/
|
|
5464
5506
|
function slugForSync(registryUrl) {
|
|
5465
|
-
|
|
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);
|
|
5507
|
+
return slugForRegistryUrlSync(registryUrl);
|
|
5471
5508
|
}
|
|
5472
5509
|
|
|
5473
5510
|
/**
|
|
@@ -5838,14 +5875,21 @@ class DiscoveryIdentity {
|
|
|
5838
5875
|
* @param {string|null} opts.registryUrl - Some(url) for release/nightly
|
|
5839
5876
|
* hosts, null for dev hosts.
|
|
5840
5877
|
* @param {number} opts.fabricManifestVersion
|
|
5878
|
+
* @param {number} opts.cartridgeRegistryVersion - The registry regime version
|
|
5879
|
+
* this host speaks; an on-disk PATH level ({slug}/v{N}/{channel}/…), pinned
|
|
5880
|
+
* like the channel so a v1 host never scans a v2 tree.
|
|
5841
5881
|
*/
|
|
5842
|
-
constructor({ channel, registryUrl, fabricManifestVersion }) {
|
|
5882
|
+
constructor({ channel, registryUrl, fabricManifestVersion, cartridgeRegistryVersion }) {
|
|
5843
5883
|
if (channel !== CartridgeChannel.Release && channel !== CartridgeChannel.Nightly) {
|
|
5844
5884
|
throw new Error(`DiscoveryIdentity: invalid channel '${channel}'`);
|
|
5845
5885
|
}
|
|
5886
|
+
if (!Number.isInteger(cartridgeRegistryVersion) || cartridgeRegistryVersion < 1) {
|
|
5887
|
+
throw new Error(`DiscoveryIdentity: cartridgeRegistryVersion must be an integer >= 1, got '${cartridgeRegistryVersion}'`);
|
|
5888
|
+
}
|
|
5846
5889
|
this.channel = channel;
|
|
5847
5890
|
this.registryUrl = registryUrl !== undefined ? registryUrl : null;
|
|
5848
5891
|
this.fabricManifestVersion = fabricManifestVersion;
|
|
5892
|
+
this.cartridgeRegistryVersion = cartridgeRegistryVersion;
|
|
5849
5893
|
}
|
|
5850
5894
|
|
|
5851
5895
|
/** This host's own baked-registry slug (`dev` when registryUrl is null). */
|
|
@@ -6175,7 +6219,9 @@ async function discoverCartridges(cartridgesRoot, identity) {
|
|
|
6175
6219
|
continue;
|
|
6176
6220
|
}
|
|
6177
6221
|
const expectedSlug = slugEntry.name;
|
|
6178
|
-
|
|
6222
|
+
// {slug}/v{cartridgeRegistryVersion}/{channel}/… — the registry regime
|
|
6223
|
+
// version is a path level pinned to the host's version (like the channel).
|
|
6224
|
+
const scanRoot = path.join(slugDir, `v${identity.cartridgeRegistryVersion}`, identity.channel);
|
|
6179
6225
|
let scanStat;
|
|
6180
6226
|
try { scanStat = fs.statSync(scanRoot); } catch (_e) { continue; }
|
|
6181
6227
|
if (!scanStat.isDirectory()) continue;
|
|
@@ -7425,7 +7471,8 @@ class FabricRegistryEntry {
|
|
|
7425
7471
|
constructor(data) {
|
|
7426
7472
|
this.urn = data.urn;
|
|
7427
7473
|
this.title = data.title || '';
|
|
7428
|
-
this.
|
|
7474
|
+
this.aliases = data.aliases || [];
|
|
7475
|
+
this.isAbstract = data.abstract === true;
|
|
7429
7476
|
this.description = data.cap_description || '';
|
|
7430
7477
|
this.args = data.args || [];
|
|
7431
7478
|
this.output = data.output || null;
|
|
@@ -7880,8 +7927,30 @@ class Manifest {
|
|
|
7880
7927
|
}
|
|
7881
7928
|
}
|
|
7882
7929
|
|
|
7930
|
+
// Unified configurable planner vocabulary (mirrors Rust plan_space.rs and the
|
|
7931
|
+
// PlanMachines/DiscoverConvergentTargets proto surface).
|
|
7932
|
+
const planner = require('./planner.js');
|
|
7933
|
+
|
|
7883
7934
|
// Export for CommonJS
|
|
7884
7935
|
module.exports = {
|
|
7936
|
+
// Planner plan-space vocabulary
|
|
7937
|
+
PlanStateError: planner.PlanStateError,
|
|
7938
|
+
ConvergencePresence: planner.ConvergencePresence,
|
|
7939
|
+
ConvergenceLocation: planner.ConvergenceLocation,
|
|
7940
|
+
ConvergenceMechanism: planner.ConvergenceMechanism,
|
|
7941
|
+
ConvergenceArity: planner.ConvergenceArity,
|
|
7942
|
+
DivergencePresence: planner.DivergencePresence,
|
|
7943
|
+
DivergenceLocation: planner.DivergenceLocation,
|
|
7944
|
+
RankPolicy: planner.RankPolicy,
|
|
7945
|
+
PlanMode: planner.PlanMode,
|
|
7946
|
+
PlanRequest: planner.PlanRequest,
|
|
7947
|
+
parsePlanApex: planner.parsePlanApex,
|
|
7948
|
+
parsePlanProfile: planner.parsePlanProfile,
|
|
7949
|
+
parsePlanCost: planner.parsePlanCost,
|
|
7950
|
+
parsePlanCandidate: planner.parsePlanCandidate,
|
|
7951
|
+
parsePlanCandidates: planner.parsePlanCandidates,
|
|
7952
|
+
parseConvergentTarget: planner.parseConvergentTarget,
|
|
7953
|
+
parseConvergentTargets: planner.parseConvergentTargets,
|
|
7885
7954
|
ALIAS_TARGET_CAP,
|
|
7886
7955
|
ALIAS_TARGET_MEDIA,
|
|
7887
7956
|
tokenIsUrn,
|
|
@@ -8087,7 +8156,7 @@ module.exports = {
|
|
|
8087
8156
|
discoverCartridges,
|
|
8088
8157
|
// Registry slug
|
|
8089
8158
|
DEV_SLUG,
|
|
8090
|
-
|
|
8159
|
+
slugForRegistryUrlSync,
|
|
8091
8160
|
slugForRegistryUrl,
|
|
8092
8161
|
isRegistrySlug,
|
|
8093
8162
|
// Machine notation
|
package/capdag.test.js
CHANGED
|
@@ -126,14 +126,14 @@ function test6204_Urn(tags) {
|
|
|
126
126
|
// Helper to create a Cap for testing
|
|
127
127
|
function makeCap(urnString, title) {
|
|
128
128
|
const capUrn = CapUrn.fromString(urnString);
|
|
129
|
-
return new Cap(capUrn, title, 'test', title);
|
|
129
|
+
return new Cap(capUrn, title, ['test'], title);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// Helper to create caps with specific in/out media URNs for graph testing
|
|
133
133
|
function makeGraphCap(inUrn, outUrn, title) {
|
|
134
134
|
const urnString = `cap:in="${inUrn}";convert;out="${outUrn}"`;
|
|
135
135
|
const capUrn = CapUrn.fromString(urnString);
|
|
136
|
-
return new Cap(capUrn, title, 'convert', title);
|
|
136
|
+
return new Cap(capUrn, title, ['convert'], title);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// ============================================================================
|
|
@@ -1271,7 +1271,7 @@ function test116_capArgConstructors() {
|
|
|
1271
1271
|
// TEST150: JSON roundtrip
|
|
1272
1272
|
function test150_capManifestJsonSerialization() {
|
|
1273
1273
|
const capUrn = CapUrn.fromString(test6204_Urn('extract;target=metadata'));
|
|
1274
|
-
const cap = new Cap(capUrn, 'Extract Metadata', 'extract-metadata');
|
|
1274
|
+
const cap = new Cap(capUrn, 'Extract Metadata', ['extract-metadata']);
|
|
1275
1275
|
cap.addArg(new CapArg('media:ext=pdf', true, [new ArgSource({ stdin: 'media:ext=pdf' })]));
|
|
1276
1276
|
cap.addArg(new CapArg(
|
|
1277
1277
|
'media:chunk-size;enc=utf-8;numeric',
|
|
@@ -1719,7 +1719,7 @@ function test6242_JS_resolveMediaUrnFromSpecs() {
|
|
|
1719
1719
|
// TEST6246: J s cap j s o n serialization
|
|
1720
1720
|
function test6246_JS_capJSONSerialization() {
|
|
1721
1721
|
const urn = CapUrn.fromString(test6204_Urn('test'));
|
|
1722
|
-
const cap = new Cap(urn, 'Test Cap', 'test_command');
|
|
1722
|
+
const cap = new Cap(urn, 'Test Cap', ['test_command']);
|
|
1723
1723
|
cap.arguments = {
|
|
1724
1724
|
required: [{ name: 'input', media_urn: MEDIA_STRING }],
|
|
1725
1725
|
optional: []
|
|
@@ -1742,7 +1742,7 @@ function test6246_JS_capJSONSerialization() {
|
|
|
1742
1742
|
// surface as failures here.
|
|
1743
1743
|
function test6249_JS_capDocumentationRoundTrip() {
|
|
1744
1744
|
const urn = CapUrn.fromString(test6204_Urn('documented'));
|
|
1745
|
-
const cap = new Cap(urn, 'Documented Cap', 'documented');
|
|
1745
|
+
const cap = new Cap(urn, 'Documented Cap', ['documented']);
|
|
1746
1746
|
const body = '# Documented Cap\r\n\nDoes the thing.\n\n```bash\necho "hi"\n```\n\nSee also: \u2605\n';
|
|
1747
1747
|
cap.setDocumentation(body);
|
|
1748
1748
|
assertEqual(cap.getDocumentation(), body, 'Setter must store the body verbatim');
|
|
@@ -1764,7 +1764,7 @@ function test6249_JS_capDocumentationRoundTrip() {
|
|
|
1764
1764
|
// (which has no null sentinel) and pollute generated JSON.
|
|
1765
1765
|
function test6253_JS_capDocumentationOmittedWhenNull() {
|
|
1766
1766
|
const urn = CapUrn.fromString(test6204_Urn('undocumented'));
|
|
1767
|
-
const cap = new Cap(urn, 'Undocumented Cap', 'undocumented');
|
|
1767
|
+
const cap = new Cap(urn, 'Undocumented Cap', ['undocumented']);
|
|
1768
1768
|
assertEqual(cap.getDocumentation(), null, 'Default documentation must be null');
|
|
1769
1769
|
|
|
1770
1770
|
const json = cap.toJSON();
|
|
@@ -1854,8 +1854,10 @@ function test6269_JS_mediaDefConstruction() {
|
|
|
1854
1854
|
// to assert on. Tests that need an empty channel use literals inline.
|
|
1855
1855
|
const sampleRegistry = {
|
|
1856
1856
|
schemaVersion: '5.0',
|
|
1857
|
+
registryVersion: 1,
|
|
1857
1858
|
lastUpdated: '2026-02-07T16:48:28Z',
|
|
1858
1859
|
registryUrl: 'https://test.example/manifest',
|
|
1860
|
+
fabricRegistryUrl: 'https://fabric.test',
|
|
1859
1861
|
channels: {
|
|
1860
1862
|
release: { cartridges: {
|
|
1861
1863
|
pdfcartridge: {
|
|
@@ -2089,7 +2091,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
|
|
|
2089
2091
|
// Missing channels object
|
|
2090
2092
|
threw = false;
|
|
2091
2093
|
try {
|
|
2092
|
-
new CartridgeRepoServer({schemaVersion: '5.0', registryUrl: 'https://test.example/manifest'});
|
|
2094
|
+
new CartridgeRepoServer({schemaVersion: '5.0', registryVersion: 1, fabricRegistryUrl: 'https://fabric.test', registryUrl: 'https://test.example/manifest'});
|
|
2093
2095
|
} catch (e) {
|
|
2094
2096
|
threw = true;
|
|
2095
2097
|
assert(e.message.includes('channels'), 'Should reject missing channels');
|
|
@@ -2099,7 +2101,7 @@ function test323_cartridgeRepoServerValidateRegistry() {
|
|
|
2099
2101
|
// Missing one of the two required channels
|
|
2100
2102
|
threw = false;
|
|
2101
2103
|
try {
|
|
2102
|
-
new CartridgeRepoServer({schemaVersion: '5.0', registryUrl: 'https://test.example/manifest', channels: {release: {cartridges: {}}}});
|
|
2104
|
+
new CartridgeRepoServer({schemaVersion: '5.0', registryVersion: 1, fabricRegistryUrl: 'https://fabric.test', registryUrl: 'https://test.example/manifest', channels: {release: {cartridges: {}}}});
|
|
2103
2105
|
} catch (e) {
|
|
2104
2106
|
threw = true;
|
|
2105
2107
|
assert(e.message.includes('nightly'), 'Should require nightly channel');
|
|
@@ -2581,7 +2583,9 @@ function test1874_registryUrlFromBuildEnvRejectsEmptyString() {
|
|
|
2581
2583
|
function discoveryInstallFixture(root, slug, channelFolder, name, version, cartridgeJson, entry) {
|
|
2582
2584
|
const fs = require('fs');
|
|
2583
2585
|
const path = require('path');
|
|
2584
|
-
|
|
2586
|
+
// {slug}/v1/{channel}/{name}/{version} — the version level pins to the host
|
|
2587
|
+
// registry version (the tests construct identities at cartridgeRegistryVersion 1).
|
|
2588
|
+
const dir = path.join(root, slug, 'v1', channelFolder, name, version);
|
|
2585
2589
|
fs.mkdirSync(dir, { recursive: true });
|
|
2586
2590
|
if (cartridgeJson !== null && cartridgeJson !== undefined) {
|
|
2587
2591
|
fs.writeFileSync(path.join(dir, 'cartridge.json'), cartridgeJson);
|
|
@@ -2634,7 +2638,7 @@ async function test1875_scanAllReachesBothDevAndRegistrySlugs() {
|
|
|
2634
2638
|
const url = 'https://cartridges.example.com/manifest';
|
|
2635
2639
|
const rslug = slugForSync(url);
|
|
2636
2640
|
// Host baked for a DIFFERENT registry than the on-disk registry cartridge.
|
|
2637
|
-
const host = new DiscoveryIdentity({ channel: 'nightly', registryUrl: 'https://other.example.com/manifest', fabricManifestVersion: 1 });
|
|
2641
|
+
const host = new DiscoveryIdentity({ channel: 'nightly', registryUrl: 'https://other.example.com/manifest', fabricManifestVersion: 1, cartridgeRegistryVersion: 1 });
|
|
2638
2642
|
discoveryInstallFixture(root, 'dev', 'nightly', 'devcart', '1.0.0', discoveryDevCartridgeJson('nightly', 1), 'cart');
|
|
2639
2643
|
discoveryInstallFixture(root, rslug, 'nightly', 'regcart', '1.0.0', discoveryRegistryCartridgeJson(url, 'nightly', 1), 'cart');
|
|
2640
2644
|
const out = await discoverCartridges(root, host);
|
|
@@ -2658,7 +2662,7 @@ async function test1876_otherChannelSubtreeIsSkipped() {
|
|
|
2658
2662
|
const url = 'https://cartridges.example.com/manifest';
|
|
2659
2663
|
const rslug = slugForSync(url);
|
|
2660
2664
|
discoveryInstallFixture(root, rslug, 'release', 'regcart', '1.0.0', discoveryRegistryCartridgeJson(url, 'release', 1), 'cart');
|
|
2661
|
-
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
|
|
2665
|
+
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2662
2666
|
assertEqual(out.length, 0, 'a release-only slug must be invisible to a nightly host');
|
|
2663
2667
|
} finally {
|
|
2664
2668
|
discoveryRemoveRoot(root);
|
|
@@ -2675,7 +2679,7 @@ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
|
|
|
2675
2679
|
const wrongSlug = slugForSync('https://somewhere-else.example.com/manifest');
|
|
2676
2680
|
const json = discoveryRegistryCartridgeJson(url, 'nightly', 1);
|
|
2677
2681
|
discoveryInstallFixture(root, wrongSlug, 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2678
|
-
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
|
|
2682
|
+
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2679
2683
|
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
|
|
2680
2684
|
} finally {
|
|
2681
2685
|
discoveryRemoveRoot(root);
|
|
@@ -2703,7 +2707,7 @@ async function test1878_bundledProviderWithoutBakedHashIsRejected() {
|
|
|
2703
2707
|
installed_at: '2024-01-01T00:00:00Z', installed_from: 'bundle', fabric_manifest_version: 1,
|
|
2704
2708
|
});
|
|
2705
2709
|
discoveryInstallFixture(root, 'dev', 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2706
|
-
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1 }));
|
|
2710
|
+
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2707
2711
|
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.BAD_INSTALLATION);
|
|
2708
2712
|
assert(out[0].error.message.includes('bundled provider integrity'),
|
|
2709
2713
|
`message should name the bundled-integrity failure: ${out[0].error.message}`);
|
|
@@ -2954,7 +2958,7 @@ function test6544_builderRejectsStructuralKeys() {
|
|
|
2954
2958
|
// TEST1294: RULE11 - void-input cap with stdin source rejected
|
|
2955
2959
|
function test1294_rule11VoidInputWithStdinRejected() {
|
|
2956
2960
|
const urn = CapUrn.fromString('cap:in="media:void";test;out="media:string"');
|
|
2957
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
2961
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2958
2962
|
const stdinSource = ArgSource.fromJSON({ stdin: 'media:string' });
|
|
2959
2963
|
cap.args = [new CapArg('media:string', true, [stdinSource])];
|
|
2960
2964
|
try {
|
|
@@ -2969,7 +2973,7 @@ function test1294_rule11VoidInputWithStdinRejected() {
|
|
|
2969
2973
|
// TEST1295: RULE11 - non-void-input cap without stdin source rejected
|
|
2970
2974
|
function test1295_rule11NonVoidInputWithoutStdinRejected() {
|
|
2971
2975
|
const urn = CapUrn.fromString('cap:in="media:string";test;out="media:string"');
|
|
2972
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
2976
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2973
2977
|
const posSource = ArgSource.fromJSON({ cli_flag: '--name' });
|
|
2974
2978
|
cap.args = [new CapArg('media:string', true, [posSource])];
|
|
2975
2979
|
try {
|
|
@@ -2984,7 +2988,7 @@ function test1295_rule11NonVoidInputWithoutStdinRejected() {
|
|
|
2984
2988
|
// TEST1296: RULE11 - void-input cap with only cli_flag sources passes
|
|
2985
2989
|
function test1296_rule11VoidInputCliFlagOnly() {
|
|
2986
2990
|
const urn = CapUrn.fromString('cap:in="media:void";test;out="media:string"');
|
|
2987
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
2991
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2988
2992
|
const flagSource = ArgSource.fromJSON({ cli_flag: '--name' });
|
|
2989
2993
|
cap.args = [new CapArg('media:string', true, [flagSource])];
|
|
2990
2994
|
// Should not throw
|
|
@@ -2994,7 +2998,7 @@ function test1296_rule11VoidInputCliFlagOnly() {
|
|
|
2994
2998
|
// TEST1297: RULE11 - non-void-input cap with stdin source passes
|
|
2995
2999
|
function test1297_rule11NonVoidInputWithStdin() {
|
|
2996
3000
|
const urn = CapUrn.fromString('cap:in="media:string";test;out="media:string"');
|
|
2997
|
-
const cap = new Cap(urn, 'Test', 'test-cmd');
|
|
3001
|
+
const cap = new Cap(urn, 'Test', ['test-cmd']);
|
|
2998
3002
|
const stdinSource = ArgSource.fromJSON({ stdin: 'media:string' });
|
|
2999
3003
|
cap.args = [new CapArg('media:string', true, [stdinSource])];
|
|
3000
3004
|
// Should not throw
|
|
@@ -4337,7 +4341,7 @@ function test6470_Machine_capRegistryEntry_defaults() {
|
|
|
4337
4341
|
assertEqual(entry.urn, 'cap:in=media:;test;out=media:', 'URN should match');
|
|
4338
4342
|
assertEqual(entry.title, '', 'Title should default to empty');
|
|
4339
4343
|
assertEqual(entry.description, '', 'Description should default to empty');
|
|
4340
|
-
|
|
4344
|
+
assert(Array.isArray(entry.aliases) && entry.aliases.length === 0, 'Aliases should default to an empty array');
|
|
4341
4345
|
assert(Array.isArray(entry.args), 'Args should default to array');
|
|
4342
4346
|
assertEqual(entry.args.length, 0, 'Args should be empty');
|
|
4343
4347
|
}
|
|
@@ -6337,7 +6341,7 @@ function test6736_axisWeightingDecodedLayout() {
|
|
|
6337
6341
|
// TEST6737: Cap with version=0 round-trips with no `version` key on wire
|
|
6338
6342
|
function test6737_capVersionZeroOmittedOnWire() {
|
|
6339
6343
|
const urn = CapUrn.fromString('cap:in="media:void";test-op;out="media:enc=utf-8;record"');
|
|
6340
|
-
const cap = new Cap(urn, 'Test Cap', 'test-op');
|
|
6344
|
+
const cap = new Cap(urn, 'Test Cap', ['test-op']);
|
|
6341
6345
|
// version defaults to 0
|
|
6342
6346
|
assertEqual(cap.version, 0, 'Default version should be 0');
|
|
6343
6347
|
const json = cap.toJSON();
|
|
@@ -6349,7 +6353,7 @@ function test6737_capVersionZeroOmittedOnWire() {
|
|
|
6349
6353
|
// TEST1848: Cap with version=N round-trips with `version: N` on wire
|
|
6350
6354
|
function test1848_capVersionNonZeroOnWire() {
|
|
6351
6355
|
const urn = CapUrn.fromString('cap:in="media:void";versioned-op;out="media:enc=utf-8;record"');
|
|
6352
|
-
const cap = new Cap(urn, 'Versioned Cap', 'versioned-op');
|
|
6356
|
+
const cap = new Cap(urn, 'Versioned Cap', ['versioned-op']);
|
|
6353
6357
|
cap.version = 42;
|
|
6354
6358
|
const json = cap.toJSON();
|
|
6355
6359
|
assert('version' in json, 'version!=0 must appear on wire');
|
|
@@ -6519,6 +6523,167 @@ function test1196_aliasedSerializationUsesAliasAndDropsHeader() {
|
|
|
6519
6523
|
// Test runner
|
|
6520
6524
|
// ============================================================================
|
|
6521
6525
|
|
|
6526
|
+
|
|
6527
|
+
// ============================================================================
|
|
6528
|
+
// Unified planner plan-space (planner.js) — TEST1450+
|
|
6529
|
+
// ============================================================================
|
|
6530
|
+
|
|
6531
|
+
const plannerNS = require('./planner.js');
|
|
6532
|
+
|
|
6533
|
+
/** Planner-specific throw assertion: any PlanStateError counts. */
|
|
6534
|
+
function assertThrowsPlanState(fn, message) {
|
|
6535
|
+
try {
|
|
6536
|
+
fn();
|
|
6537
|
+
} catch (error) {
|
|
6538
|
+
if (error instanceof plannerNS.PlanStateError) return;
|
|
6539
|
+
throw new Error(`Expected PlanStateError but got ${error.name}: ${error.message} (${message})`);
|
|
6540
|
+
}
|
|
6541
|
+
throw new Error(`Expected PlanStateError but function succeeded: ${message}`);
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
// TEST1450: a defaults-only request is AUTO with every knob at its default,
|
|
6545
|
+
// and the proto JSON carries the documented wire shape.
|
|
6546
|
+
function test1450_planRequestDefaults() {
|
|
6547
|
+
const request = new plannerNS.PlanRequest({
|
|
6548
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6549
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6550
|
+
});
|
|
6551
|
+
assertEqual(request.isConfigured, false, 'defaults are not configured');
|
|
6552
|
+
const wire = request.toProtoJSON();
|
|
6553
|
+
assertEqual(wire.mode, 0, 'AUTO mode');
|
|
6554
|
+
assertEqual(wire.ranking, 0, 'intent ranking');
|
|
6555
|
+
assertEqual(wire.convergence.presence, 0, 'auto presence');
|
|
6556
|
+
assertEqual(wire.convergence.mechanism, 0, 'any mechanism');
|
|
6557
|
+
assertEqual(wire.sources[0].media_urn, 'media:ext=pdf', 'snake_case source field');
|
|
6558
|
+
assertEqual(wire.sources[0].is_sequence, false, 'snake_case sequence field');
|
|
6559
|
+
assertEqual(wire.max_depth, 0, '0 = engine default depth');
|
|
6560
|
+
}
|
|
6561
|
+
|
|
6562
|
+
// TEST1451: at-depth REQUIRES a positive depth; a depth anywhere else is
|
|
6563
|
+
// invalid; ranking never flips the request to CONFIGURED but any
|
|
6564
|
+
// space-constraining knob does (mode is forced to CONFIGURED on the wire).
|
|
6565
|
+
function test1451_planRequestKnobValidation() {
|
|
6566
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6567
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6568
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6569
|
+
convergenceLocation: 'at-depth',
|
|
6570
|
+
}), 'at-depth without a depth must throw');
|
|
6571
|
+
|
|
6572
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6573
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6574
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6575
|
+
convergenceAtDepth: 2,
|
|
6576
|
+
}), 'a depth without at-depth must throw');
|
|
6577
|
+
|
|
6578
|
+
const ranked = new plannerNS.PlanRequest({
|
|
6579
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6580
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6581
|
+
ranking: 'shortest',
|
|
6582
|
+
});
|
|
6583
|
+
assertEqual(ranked.isConfigured, false, 'ranking alone never configures');
|
|
6584
|
+
assertEqual(ranked.toProtoJSON().mode, 0, 'ranking alone stays AUTO');
|
|
6585
|
+
|
|
6586
|
+
const constrained = new plannerNS.PlanRequest({
|
|
6587
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6588
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6589
|
+
convergenceMechanism: 'collect',
|
|
6590
|
+
});
|
|
6591
|
+
assertEqual(constrained.isConfigured, true, 'a mechanism constrains');
|
|
6592
|
+
assertEqual(constrained.toProtoJSON().mode, 1, 'constrained ⇒ CONFIGURED on the wire');
|
|
6593
|
+
|
|
6594
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6595
|
+
sources: [],
|
|
6596
|
+
exactTargets: ['media:enc=utf-8;ext=txt'],
|
|
6597
|
+
}), 'no sources must throw');
|
|
6598
|
+
assertThrowsPlanState(() => new plannerNS.PlanRequest({
|
|
6599
|
+
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
6600
|
+
exactTargets: [],
|
|
6601
|
+
}), 'no targets must throw');
|
|
6602
|
+
}
|
|
6603
|
+
|
|
6604
|
+
// TEST1452: every knob round-trips value → proto number → value, and an
|
|
6605
|
+
// unknown value/number fails hard in both directions.
|
|
6606
|
+
function test1452_knobProtoRoundTrip() {
|
|
6607
|
+
const knobs = [
|
|
6608
|
+
plannerNS.ConvergencePresence, plannerNS.ConvergenceLocation,
|
|
6609
|
+
plannerNS.ConvergenceMechanism, plannerNS.ConvergenceArity,
|
|
6610
|
+
plannerNS.DivergencePresence, plannerNS.DivergenceLocation,
|
|
6611
|
+
plannerNS.RankPolicy, plannerNS.PlanMode,
|
|
6612
|
+
];
|
|
6613
|
+
for (const knob of knobs) {
|
|
6614
|
+
for (const value of knob.values) {
|
|
6615
|
+
assertEqual(knob.fromProto(knob.toProto(value)), value, `${knob.name} round-trips ${value}`);
|
|
6616
|
+
}
|
|
6617
|
+
assertThrowsPlanState(() => knob.toProto('no-such-value'), `${knob.name} rejects unknown value`);
|
|
6618
|
+
assertThrowsPlanState(() => knob.fromProto(999), `${knob.name} rejects unknown number`);
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
|
|
6622
|
+
// TEST1453: candidate/target parsers accept realistic proto-JSON and sort
|
|
6623
|
+
// candidates by rank; malformed payloads (missing field, wrong type) fail
|
|
6624
|
+
// hard with the offending path in the message.
|
|
6625
|
+
function test1453_planResponseParsers() {
|
|
6626
|
+
const candidates = plannerNS.parsePlanCandidates({
|
|
6627
|
+
candidates: [
|
|
6628
|
+
{
|
|
6629
|
+
notation: '[n0 -> edge_1 -> n1]',
|
|
6630
|
+
label: 'Convert each',
|
|
6631
|
+
rank: 1,
|
|
6632
|
+
profile: { source_media: ['media:ext=pdf'], target_media: ['media:enc=utf-8;ext=txt'], apexes: [], converged: false, diverged: false },
|
|
6633
|
+
cost: { cap_steps: 2, total_steps: 2, max_leg_depth: 0, intent_score: 0.5 },
|
|
6634
|
+
},
|
|
6635
|
+
{
|
|
6636
|
+
notation: '[(n0, n1) -> edge_0 -> n2]',
|
|
6637
|
+
label: 'Combine via text',
|
|
6638
|
+
rank: 0,
|
|
6639
|
+
profile: {
|
|
6640
|
+
source_media: ['media:ext=pdf', 'media:ext=md'],
|
|
6641
|
+
target_media: ['media:enc=utf-8;ext=txt'],
|
|
6642
|
+
apexes: [{ media_urn: 'media:enc=utf-8;page', mechanism: 2, depth: 1 }],
|
|
6643
|
+
converged: true,
|
|
6644
|
+
diverged: false,
|
|
6645
|
+
},
|
|
6646
|
+
cost: { cap_steps: 3, total_steps: 4, max_leg_depth: 1, intent_score: 0.8 },
|
|
6647
|
+
},
|
|
6648
|
+
],
|
|
6649
|
+
});
|
|
6650
|
+
assertEqual(candidates.length, 2, 'two candidates');
|
|
6651
|
+
assertEqual(candidates[0].rank, 0, 'sorted by rank');
|
|
6652
|
+
assertEqual(candidates[0].profile.apexes[0].mechanism, 'collect', 'proto mechanism decoded to knob value');
|
|
6653
|
+
assertEqual(candidates[0].profile.converged, true, 'converged carried');
|
|
6654
|
+
|
|
6655
|
+
assertThrowsPlanState(
|
|
6656
|
+
() => plannerNS.parsePlanCandidates({ candidates: [{ label: 'missing notation', rank: 0 }] }),
|
|
6657
|
+
'a candidate without notation must fail hard'
|
|
6658
|
+
);
|
|
6659
|
+
|
|
6660
|
+
const targets = plannerNS.parseConvergentTargets({
|
|
6661
|
+
targets: [
|
|
6662
|
+
{
|
|
6663
|
+
media_def: 'media:enc=utf-8;ext=txt',
|
|
6664
|
+
display_name: 'Plain Text',
|
|
6665
|
+
min_total_steps: 3,
|
|
6666
|
+
apex: { media_urn: 'media:enc=utf-8;page', mechanism: 2, depth: 1 },
|
|
6667
|
+
convergent: true,
|
|
6668
|
+
file_extension: 'txt',
|
|
6669
|
+
},
|
|
6670
|
+
{
|
|
6671
|
+
media_def: 'media:ext=png;image',
|
|
6672
|
+
display_name: 'PNG Image',
|
|
6673
|
+
min_total_steps: 2,
|
|
6674
|
+
convergent: false,
|
|
6675
|
+
file_extension: 'png',
|
|
6676
|
+
},
|
|
6677
|
+
],
|
|
6678
|
+
});
|
|
6679
|
+
assertEqual(targets[0].apex.mediaUrn, 'media:enc=utf-8;page', 'apex carried');
|
|
6680
|
+
assertEqual(targets[1].apex, null, 'independent target has no apex');
|
|
6681
|
+
assertThrowsPlanState(
|
|
6682
|
+
() => plannerNS.parseConvergentTargets({ targets: [{ display_name: 'no media_def' }] }),
|
|
6683
|
+
'a target without media_def must fail hard'
|
|
6684
|
+
);
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6522
6687
|
async function runTests() {
|
|
6523
6688
|
console.log('Running capdag-js tests...\n');
|
|
6524
6689
|
|
|
@@ -6987,6 +7152,13 @@ async function runTests() {
|
|
|
6987
7152
|
runTest('TEST1896: cached_cap_aliases_filters_to_cap', test1896_cachedCapAliasesFiltersToCapTargets);
|
|
6988
7153
|
runTest('TEST1196: aliased_serialization_uses_alias', test1196_aliasedSerializationUsesAliasAndDropsHeader);
|
|
6989
7154
|
|
|
7155
|
+
// planner.js: unified plan-space vocabulary
|
|
7156
|
+
console.log('--- planner.js ---');
|
|
7157
|
+
runTest('TEST1450: plan_request_defaults', test1450_planRequestDefaults);
|
|
7158
|
+
runTest('TEST1451: plan_request_knob_validation', test1451_planRequestKnobValidation);
|
|
7159
|
+
runTest('TEST1452: knob_proto_round_trip', test1452_knobProtoRoundTrip);
|
|
7160
|
+
runTest('TEST1453: plan_response_parsers', test1453_planResponseParsers);
|
|
7161
|
+
|
|
6990
7162
|
// Summary
|
|
6991
7163
|
console.log(`\n${passCount + failCount} tests: ${passCount} passed, ${failCount} failed`);
|
|
6992
7164
|
if (failCount > 0) {
|
package/package.json
CHANGED