capdag 1.222.603 → 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 +101 -2
- package/capdag.js +156 -75
- package/capdag.test.js +251 -49
- package/machine-parser.js +73 -120
- package/machine.pegjs +6 -7
- package/package.json +1 -1
package/cap-fab-renderer.js
CHANGED
|
@@ -590,6 +590,18 @@ function buildStylesheet() {
|
|
|
590
590
|
'target-arrow-shape': 'tee',
|
|
591
591
|
},
|
|
592
592
|
},
|
|
593
|
+
{
|
|
594
|
+
// A convergence (fan-in) edge: a second producer feeding a cap's
|
|
595
|
+
// non-main argument. Dotted + diamond-tail to read as a merging
|
|
596
|
+
// side-input distinct from the solid main backbone edge.
|
|
597
|
+
selector: 'edge.strand-convergence',
|
|
598
|
+
style: {
|
|
599
|
+
'line-style': 'dotted',
|
|
600
|
+
'width': 2,
|
|
601
|
+
'source-arrow-shape': 'diamond',
|
|
602
|
+
'source-arrow-color': machineEdgeColor || 'data(color)',
|
|
603
|
+
},
|
|
604
|
+
},
|
|
593
605
|
{
|
|
594
606
|
selector: 'edge.faded',
|
|
595
607
|
style: { 'opacity': fadedEdgeOpacity },
|
|
@@ -762,6 +774,28 @@ function validateStrandStep(step, path) {
|
|
|
762
774
|
if (typeof body.output_is_sequence !== 'boolean') {
|
|
763
775
|
throw new Error(`CapFabRenderer: ${path}.step_type.Cap.output_is_sequence must be a boolean`);
|
|
764
776
|
}
|
|
777
|
+
// `inputs` (capdag CapInput list) carries the data-flow graph: the main
|
|
778
|
+
// input plus any convergence inputs. Optional on legacy payloads, but
|
|
779
|
+
// when present it must be well-formed — the fan-in edges are drawn from
|
|
780
|
+
// it, so a malformed entry is a hard error, not a silent skip.
|
|
781
|
+
if (body.inputs !== undefined) {
|
|
782
|
+
assertArray(body.inputs, `${path}.step_type.Cap.inputs`);
|
|
783
|
+
body.inputs.forEach((input, inputIdx) => {
|
|
784
|
+
const inputPath = `${path}.step_type.Cap.inputs[${inputIdx}]`;
|
|
785
|
+
assertObject(input, inputPath);
|
|
786
|
+
assertString(input.arg_urn, `${inputPath}.arg_urn`);
|
|
787
|
+
// serde: unit variant → the string "StrandInput"; struct variant →
|
|
788
|
+
// { Step: { token_id } }. Anything else is malformed.
|
|
789
|
+
if (input.source === 'StrandInput') return;
|
|
790
|
+
if (input.source && typeof input.source === 'object' && input.source.Step) {
|
|
791
|
+
assertString(input.source.Step.token_id, `${inputPath}.source.Step.token_id`);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
throw new Error(
|
|
795
|
+
`CapFabRenderer: ${inputPath}.source must be "StrandInput" or { Step: { token_id } }`
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
765
799
|
} else {
|
|
766
800
|
assertString(body.media_def, `${path}.step_type.${variant}.media_def`);
|
|
767
801
|
}
|
|
@@ -1091,7 +1125,7 @@ function buildBrowseGraphData(capabilities) {
|
|
|
1091
1125
|
// Parsing the URN canonicalizes it and validates it — fail hard on
|
|
1092
1126
|
// malformed registry data.
|
|
1093
1127
|
const parsedUrn = CapUrn.fromString(capData.urn);
|
|
1094
|
-
const cap = createCap(parsedUrn, title, capData.
|
|
1128
|
+
const cap = createCap(parsedUrn, title, capData.aliases || []);
|
|
1095
1129
|
const capFabEdgeIndex = capFab.edges.length;
|
|
1096
1130
|
capFab.addCap(cap, 'registry');
|
|
1097
1131
|
|
|
@@ -1309,6 +1343,17 @@ function buildStrandGraphData(data) {
|
|
|
1309
1343
|
return outerExit;
|
|
1310
1344
|
}
|
|
1311
1345
|
|
|
1346
|
+
// Map every step's stable token_id to its output node id. A cap
|
|
1347
|
+
// step's node (`step_${i}`) IS its `to_spec` output, so a convergence
|
|
1348
|
+
// input naming a producer by token resolves to that producer's output
|
|
1349
|
+
// node — the value flowing into the fan-in arg.
|
|
1350
|
+
const tokenToNodeId = new Map();
|
|
1351
|
+
data.steps.forEach((step, i) => {
|
|
1352
|
+
if (step && typeof step.token_id === 'string' && step.token_id.length > 0) {
|
|
1353
|
+
tokenToNodeId.set(step.token_id, `step_${i}`);
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1312
1357
|
data.steps.forEach((step, i) => {
|
|
1313
1358
|
const variant = Object.keys(step.step_type)[0];
|
|
1314
1359
|
const nodeId = `step_${i}`;
|
|
@@ -1330,8 +1375,43 @@ function buildStrandGraphData(data) {
|
|
|
1330
1375
|
if (cardinality !== '1\u21921') {
|
|
1331
1376
|
label = `${label} (${cardinality})`;
|
|
1332
1377
|
}
|
|
1378
|
+
// Resolve this cap's actual data-flow producers from `body.inputs`
|
|
1379
|
+
// (capdag CapInput serde shape): each `source` is the string
|
|
1380
|
+
// "StrandInput" (fed by the strand's input anchor → the input slot
|
|
1381
|
+
// node) or `{ Step: { token_id } }` (fed by a producing cap → that
|
|
1382
|
+
// step's output node). This is the authoritative topology; step
|
|
1383
|
+
// ORDER is not — the realizer emits edges greedily in dependency
|
|
1384
|
+
// order, so `prevNodeId` (the previously-emitted step) need not be
|
|
1385
|
+
// one of this cap's inputs at all.
|
|
1386
|
+
const capInputs = Array.isArray(body.inputs) ? body.inputs : [];
|
|
1387
|
+
const producerNodeIds = [];
|
|
1388
|
+
for (const input of capInputs) {
|
|
1389
|
+
const src = input && input.source;
|
|
1390
|
+
if (src === 'StrandInput') {
|
|
1391
|
+
producerNodeIds.push(inputSlotId);
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
const producerToken = src && typeof src === 'object' && src.Step && src.Step.token_id;
|
|
1395
|
+
if (typeof producerToken !== 'string' || producerToken.length === 0) continue;
|
|
1396
|
+
const producerNodeId = tokenToNodeId.get(producerToken);
|
|
1397
|
+
if (producerNodeId) producerNodeIds.push(producerNodeId);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Pick the source of the single labeled "backbone" edge (the one
|
|
1401
|
+
// carrying the cap title + cardinality). Inside a ForEach body — or
|
|
1402
|
+
// when the cap declares no inputs — keep the linear `prevNodeId`
|
|
1403
|
+
// thread so the ForEach boundary handling and its (1→n) entry marker
|
|
1404
|
+
// are preserved exactly. Otherwise anchor the backbone on a REAL
|
|
1405
|
+
// input: `prevNodeId` if it is genuinely one, else the first actual
|
|
1406
|
+
// producer — so a non-input step ordered just before this cap can
|
|
1407
|
+
// never become a spurious backbone edge.
|
|
1408
|
+
const isForeachContext = isForeachEntry || insideForEachBody !== null;
|
|
1409
|
+
let backboneSource = prevNodeId;
|
|
1410
|
+
if (!isForeachContext && producerNodeIds.length > 0 && !producerNodeIds.includes(prevNodeId)) {
|
|
1411
|
+
backboneSource = producerNodeIds[0];
|
|
1412
|
+
}
|
|
1333
1413
|
addEdge(
|
|
1334
|
-
|
|
1414
|
+
backboneSource,
|
|
1335
1415
|
nodeId,
|
|
1336
1416
|
label,
|
|
1337
1417
|
body.title,
|
|
@@ -1340,6 +1420,25 @@ function buildStrandGraphData(data) {
|
|
|
1340
1420
|
{ foreachEntry: isForeachEntry }
|
|
1341
1421
|
);
|
|
1342
1422
|
|
|
1423
|
+
// Convergence (fan-in): draw an edge from every other producer that
|
|
1424
|
+
// isn't the backbone source, so a cap fed by several producers renders
|
|
1425
|
+
// as a DAG, not a chain. Deduplicated against the backbone edge and
|
|
1426
|
+
// against a self-loop.
|
|
1427
|
+
const drawnSources = new Set([backboneSource, nodeId]);
|
|
1428
|
+
for (const producerNodeId of producerNodeIds) {
|
|
1429
|
+
if (drawnSources.has(producerNodeId)) continue;
|
|
1430
|
+
drawnSources.add(producerNodeId);
|
|
1431
|
+
addEdge(
|
|
1432
|
+
producerNodeId,
|
|
1433
|
+
nodeId,
|
|
1434
|
+
'',
|
|
1435
|
+
body.title,
|
|
1436
|
+
body.cap_urn,
|
|
1437
|
+
'strand-convergence',
|
|
1438
|
+
{}
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1343
1442
|
if (insideForEachBody !== null) {
|
|
1344
1443
|
if (bodyEntry === null) bodyEntry = nodeId;
|
|
1345
1444
|
bodyExit = nodeId;
|
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;
|
|
@@ -6388,7 +6434,9 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6388
6434
|
// [alias cap:in="...";op=...;out="..."] — header (defines a cap with alias)
|
|
6389
6435
|
// [src -> alias -> dst] — wiring (connects nodes via cap)
|
|
6390
6436
|
// [(a, b) -> alias -> dst] — fan-in wiring
|
|
6391
|
-
//
|
|
6437
|
+
//
|
|
6438
|
+
// Per-item map (`is_loop`) is a derived cardinality property, never authored
|
|
6439
|
+
// syntax — there is no LOOP marker in the grammar.
|
|
6392
6440
|
// ============================================================================
|
|
6393
6441
|
|
|
6394
6442
|
/**
|
|
@@ -6504,7 +6552,9 @@ class MachineEdge {
|
|
|
6504
6552
|
*/
|
|
6505
6553
|
toString() {
|
|
6506
6554
|
const sources = this.sources.map(s => s.toString()).join(', ');
|
|
6507
|
-
|
|
6555
|
+
// Debug-only marker for a per-item map edge. Not notation syntax (the
|
|
6556
|
+
// `LOOP` keyword is retired); `is_loop` is a derived cardinality property.
|
|
6557
|
+
const loopPrefix = this.isLoop ? 'map ' : '';
|
|
6508
6558
|
return `(${sources}) -${loopPrefix}${this.capUrn}-> ${this.target}`;
|
|
6509
6559
|
}
|
|
6510
6560
|
}
|
|
@@ -6703,13 +6753,13 @@ class Machine {
|
|
|
6703
6753
|
const targetKey = edge.target.toString();
|
|
6704
6754
|
const targetName = nodeNames.get(targetKey);
|
|
6705
6755
|
|
|
6706
|
-
|
|
6707
|
-
|
|
6756
|
+
// `is_loop` is NOT emitted into notation text: it is a derived cardinality
|
|
6757
|
+
// property, not authored syntax (the `LOOP` keyword is retired).
|
|
6708
6758
|
if (sources.length === 1) {
|
|
6709
|
-
output += `[${sources[0]} -> ${
|
|
6759
|
+
output += `[${sources[0]} -> ${alias} -> ${targetName}]`;
|
|
6710
6760
|
} else {
|
|
6711
6761
|
const group = sources.join(', ');
|
|
6712
|
-
output += `[(${group}) -> ${
|
|
6762
|
+
output += `[(${group}) -> ${alias} -> ${targetName}]`;
|
|
6713
6763
|
}
|
|
6714
6764
|
}
|
|
6715
6765
|
|
|
@@ -6756,13 +6806,13 @@ class Machine {
|
|
|
6756
6806
|
const targetKey = edge.target.toString();
|
|
6757
6807
|
const targetName = nodeNames.get(targetKey);
|
|
6758
6808
|
|
|
6759
|
-
|
|
6760
|
-
|
|
6809
|
+
// `is_loop` is NOT emitted into notation text: it is a derived cardinality
|
|
6810
|
+
// property, not authored syntax (the `LOOP` keyword is retired).
|
|
6761
6811
|
if (sources.length === 1) {
|
|
6762
|
-
lines.push(`[${sources[0]} -> ${
|
|
6812
|
+
lines.push(`[${sources[0]} -> ${alias} -> ${targetName}]`);
|
|
6763
6813
|
} else {
|
|
6764
6814
|
const group = sources.join(', ');
|
|
6765
|
-
lines.push(`[(${group}) -> ${
|
|
6815
|
+
lines.push(`[(${group}) -> ${alias} -> ${targetName}]`);
|
|
6766
6816
|
}
|
|
6767
6817
|
}
|
|
6768
6818
|
|
|
@@ -6814,13 +6864,13 @@ class Machine {
|
|
|
6814
6864
|
const targetKey = edge.target.toString();
|
|
6815
6865
|
const targetName = nodeNames.get(targetKey);
|
|
6816
6866
|
|
|
6817
|
-
|
|
6818
|
-
|
|
6867
|
+
// `is_loop` is NOT emitted into notation text: it is a derived cardinality
|
|
6868
|
+
// property, not authored syntax (the `LOOP` keyword is retired).
|
|
6819
6869
|
if (sources.length === 1) {
|
|
6820
|
-
lines.push(`${open}${sources[0]} -> ${
|
|
6870
|
+
lines.push(`${open}${sources[0]} -> ${alias} -> ${targetName}${close}`);
|
|
6821
6871
|
} else {
|
|
6822
6872
|
const group = sources.join(', ');
|
|
6823
|
-
lines.push(`${open}(${group}) -> ${
|
|
6873
|
+
lines.push(`${open}(${group}) -> ${alias} -> ${targetName}${close}`);
|
|
6824
6874
|
}
|
|
6825
6875
|
}
|
|
6826
6876
|
|
|
@@ -6896,13 +6946,14 @@ class Machine {
|
|
|
6896
6946
|
|
|
6897
6947
|
const sources = edge.sources.map(s => nodeNames.get(s.toString()));
|
|
6898
6948
|
const targetName = nodeNames.get(edge.target.toString());
|
|
6899
|
-
const loopPrefix = edge.isLoop ? 'LOOP ' : '';
|
|
6900
6949
|
|
|
6950
|
+
// `is_loop` is NOT emitted into notation text: it is a derived cardinality
|
|
6951
|
+
// property, not authored syntax (the `LOOP` keyword is retired).
|
|
6901
6952
|
if (sources.length === 1) {
|
|
6902
|
-
lines.push(`${open}${sources[0]} -> ${
|
|
6953
|
+
lines.push(`${open}${sources[0]} -> ${token} -> ${targetName}${close}`);
|
|
6903
6954
|
} else {
|
|
6904
6955
|
const group = sources.join(', ');
|
|
6905
|
-
lines.push(`${open}(${group}) -> ${
|
|
6956
|
+
lines.push(`${open}(${group}) -> ${token} -> ${targetName}${close}`);
|
|
6906
6957
|
}
|
|
6907
6958
|
}
|
|
6908
6959
|
|
|
@@ -6990,7 +7041,8 @@ class Machine {
|
|
|
6990
7041
|
* - Leaf targets: stadium-shaped nodes (rounded)
|
|
6991
7042
|
* - Intermediate nodes: rectangular
|
|
6992
7043
|
* - Edge labels: op= tag value (or full cap URN if no op)
|
|
6993
|
-
* -
|
|
7044
|
+
* - Per-item map edges (`is_loop`): dotted line style — `is_loop` is a
|
|
7045
|
+
* derived render property, never the retired `LOOP` keyword
|
|
6994
7046
|
* - Node labels: derived MediaUrn type
|
|
6995
7047
|
*
|
|
6996
7048
|
* @returns {string} Mermaid flowchart definition
|
|
@@ -7045,8 +7097,9 @@ class Machine {
|
|
|
7045
7097
|
const srcName = nodeNames.get(srcKey);
|
|
7046
7098
|
|
|
7047
7099
|
if (edge.isLoop) {
|
|
7048
|
-
// Dotted line
|
|
7049
|
-
|
|
7100
|
+
// Dotted line renders the derived per-item map (`is_loop`) property.
|
|
7101
|
+
// No "LOOP" text — that keyword is retired.
|
|
7102
|
+
lines.push(` ${srcName} -. "${label}" .-> ${targetName}`);
|
|
7050
7103
|
} else {
|
|
7051
7104
|
lines.push(` ${srcName} -- "${label}" --> ${targetName}`);
|
|
7052
7105
|
}
|
|
@@ -7164,7 +7217,6 @@ function _parseMachineInternal(input) {
|
|
|
7164
7217
|
sources: stmt.sources,
|
|
7165
7218
|
capAlias: stmt.capAlias,
|
|
7166
7219
|
target: stmt.target,
|
|
7167
|
-
isLoop: stmt.isLoop,
|
|
7168
7220
|
position: i,
|
|
7169
7221
|
location: stmt.location,
|
|
7170
7222
|
sourceLocations: stmt.sourceLocations,
|
|
@@ -7286,7 +7338,13 @@ function _parseMachineInternal(input) {
|
|
|
7286
7338
|
assignOrCheckNode(wiring.target, capOutMedia, nodeMedia, wiring.position,
|
|
7287
7339
|
wiring.targetLocation || wiring.location);
|
|
7288
7340
|
|
|
7289
|
-
|
|
7341
|
+
// `is_loop` is a derived cardinality property, not authored syntax (the
|
|
7342
|
+
// `LOOP` keyword is retired). The pure-JS parse path has no cap definitions
|
|
7343
|
+
// — it derives media only from the cap URN's in=/out= specs — so it cannot
|
|
7344
|
+
// evaluate the ForEach rule (a sequence source feeding a scalar-input cap).
|
|
7345
|
+
// Editors obtain the derived `is_loop` from the engine server's resolved
|
|
7346
|
+
// graph; here it defaults to false.
|
|
7347
|
+
edges.push(new MachineEdge(sourceUrns, capUrn, capOutMedia, false));
|
|
7290
7348
|
}
|
|
7291
7349
|
|
|
7292
7350
|
return {
|
|
@@ -7413,7 +7471,8 @@ class FabricRegistryEntry {
|
|
|
7413
7471
|
constructor(data) {
|
|
7414
7472
|
this.urn = data.urn;
|
|
7415
7473
|
this.title = data.title || '';
|
|
7416
|
-
this.
|
|
7474
|
+
this.aliases = data.aliases || [];
|
|
7475
|
+
this.isAbstract = data.abstract === true;
|
|
7417
7476
|
this.description = data.cap_description || '';
|
|
7418
7477
|
this.args = data.args || [];
|
|
7419
7478
|
this.output = data.output || null;
|
|
@@ -7868,8 +7927,30 @@ class Manifest {
|
|
|
7868
7927
|
}
|
|
7869
7928
|
}
|
|
7870
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
|
+
|
|
7871
7934
|
// Export for CommonJS
|
|
7872
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,
|
|
7873
7954
|
ALIAS_TARGET_CAP,
|
|
7874
7955
|
ALIAS_TARGET_MEDIA,
|
|
7875
7956
|
tokenIsUrn,
|
|
@@ -8075,7 +8156,7 @@ module.exports = {
|
|
|
8075
8156
|
discoverCartridges,
|
|
8076
8157
|
// Registry slug
|
|
8077
8158
|
DEV_SLUG,
|
|
8078
|
-
|
|
8159
|
+
slugForRegistryUrlSync,
|
|
8079
8160
|
slugForRegistryUrl,
|
|
8080
8161
|
isRegistrySlug,
|
|
8081
8162
|
// Machine notation
|