capdag 1.255.27 → 1.266.66
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/build-browser.js +1 -1
- package/cap-fab-renderer.js +1 -1
- package/capdag.js +391 -10
- package/capdag.test.js +330 -15
- package/package.json +1 -1
- package/planner.js +2 -2
package/build-browser.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// The browser build exists because capdag-js source files are CJS:
|
|
8
8
|
// they use `require()` and `module.exports`, which a plain <script tag>
|
|
9
|
-
// cannot load. For browser consumers (capdag-dot-com,
|
|
9
|
+
// cannot load. For browser consumers (capdag-dot-com, macfloom
|
|
10
10
|
// WKWebViews) we strip those CJS hooks and wrap the result in an IIFE
|
|
11
11
|
// that assigns to window.*.
|
|
12
12
|
//
|
package/cap-fab-renderer.js
CHANGED
|
@@ -2447,7 +2447,7 @@ function buildRunGraphData(data) {
|
|
|
2447
2447
|
|
|
2448
2448
|
// Successful bodies merge into the collect target ONLY when a
|
|
2449
2449
|
// Collect closes the foreach body. Without a Collect (the
|
|
2450
|
-
// common "unclosed foreach" case in
|
|
2450
|
+
// common "unclosed foreach" case in floom-engine realize_strand),
|
|
2451
2451
|
// replicas terminate at their last body step — each body has
|
|
2452
2452
|
// its own separate output, no shared merge.
|
|
2453
2453
|
if (success && hasCollect && collectTargetId) {
|
package/capdag.js
CHANGED
|
@@ -2455,6 +2455,21 @@ class ArgSource {
|
|
|
2455
2455
|
}
|
|
2456
2456
|
}
|
|
2457
2457
|
|
|
2458
|
+
/** The definition's argument keys — what every capdag mirror's CapArg carries. */
|
|
2459
|
+
const CAP_ARG_KEYS = Object.freeze(['media_urn', 'required', 'sources', 'arg_description', 'default_value', 'metadata', 'is_sequence', 'streaming']);
|
|
2460
|
+
/** The definition's output keys — what every capdag mirror's CapOutput carries. */
|
|
2461
|
+
const CAP_OUTPUT_KEYS = Object.freeze(['media_urn', 'output_description', 'metadata', 'is_sequence', 'streaming']);
|
|
2462
|
+
|
|
2463
|
+
function refuseUnknownDefinitionKeys(json, known, what) {
|
|
2464
|
+
const unknown = Object.keys(json).filter(key => !known.includes(key)).sort();
|
|
2465
|
+
if (unknown.length > 0) {
|
|
2466
|
+
throw new Error(
|
|
2467
|
+
`${what} carries field(s) this capdag does not know: ${unknown.join(', ')} — ` +
|
|
2468
|
+
`the definition comes from a fabric newer than this capdag; the contract is ${known.join(', ')}`
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2458
2473
|
/**
|
|
2459
2474
|
* Cap argument definition - media_urn is the unique identifier
|
|
2460
2475
|
*/
|
|
@@ -2470,6 +2485,11 @@ class CapArg {
|
|
|
2470
2485
|
this.media_urn = mediaUrn;
|
|
2471
2486
|
this.required = required;
|
|
2472
2487
|
this.is_sequence = options.is_sequence || false;
|
|
2488
|
+
// Consumed WITHOUT a length promise (a feed): the cap processes items as
|
|
2489
|
+
// they arrive and may be wired live to an unbounded producer. Orthogonal
|
|
2490
|
+
// to is_sequence (cardinality), not part of the URN. Only the main input
|
|
2491
|
+
// may stream (RULE14).
|
|
2492
|
+
this.streaming = options.streaming || false;
|
|
2473
2493
|
this.sources = sources; // Array of ArgSource
|
|
2474
2494
|
this.arg_description = options.arg_description !== undefined ? options.arg_description : null;
|
|
2475
2495
|
this.default_value = options.default_value !== undefined ? options.default_value : null;
|
|
@@ -2482,6 +2502,12 @@ class CapArg {
|
|
|
2482
2502
|
* @returns {CapArg} The CapArg instance
|
|
2483
2503
|
*/
|
|
2484
2504
|
static fromJSON(json) {
|
|
2505
|
+
// A key this definition does not know is a fabric NEWER than this
|
|
2506
|
+
// capdag — a contract this build cannot honour. Dropping it would let a
|
|
2507
|
+
// cartridge advertise a contract the fabric never made (this is how a
|
|
2508
|
+
// `streaming` argument once went out as bounded); it is refused, named.
|
|
2509
|
+
// Mirrors Rust `deny_unknown_fields`.
|
|
2510
|
+
refuseUnknownDefinitionKeys(json, CAP_ARG_KEYS, 'cap argument');
|
|
2485
2511
|
const sources = (json.sources || []).map(s => ArgSource.fromJSON(s));
|
|
2486
2512
|
return new CapArg(
|
|
2487
2513
|
json.media_urn,
|
|
@@ -2489,6 +2515,7 @@ class CapArg {
|
|
|
2489
2515
|
sources,
|
|
2490
2516
|
{
|
|
2491
2517
|
is_sequence: json.is_sequence,
|
|
2518
|
+
streaming: json.streaming,
|
|
2492
2519
|
arg_description: json.arg_description,
|
|
2493
2520
|
default_value: json.default_value,
|
|
2494
2521
|
metadata: json.metadata
|
|
@@ -2507,6 +2534,7 @@ class CapArg {
|
|
|
2507
2534
|
sources: this.sources.map(s => s.toJSON())
|
|
2508
2535
|
};
|
|
2509
2536
|
if (this.is_sequence) result.is_sequence = true;
|
|
2537
|
+
if (this.streaming) result.streaming = true;
|
|
2510
2538
|
if (this.arg_description !== null && this.arg_description !== undefined) {
|
|
2511
2539
|
result.arg_description = this.arg_description;
|
|
2512
2540
|
}
|
|
@@ -2934,6 +2962,9 @@ class Cap {
|
|
|
2934
2962
|
} else {
|
|
2935
2963
|
cap.args = [];
|
|
2936
2964
|
}
|
|
2965
|
+
if (json.output !== undefined && json.output !== null) {
|
|
2966
|
+
refuseUnknownDefinitionKeys(json.output, CAP_OUTPUT_KEYS, 'cap output');
|
|
2967
|
+
}
|
|
2937
2968
|
cap.output = json.output;
|
|
2938
2969
|
cap.registered_by = json.registered_by ? RegisteredBy.fromJSON(json.registered_by) : null;
|
|
2939
2970
|
cap.supported_model_types = Array.isArray(json.supported_model_types) ? json.supported_model_types : [];
|
|
@@ -3333,6 +3364,19 @@ function validateCapArgs(cap) {
|
|
|
3333
3364
|
});
|
|
3334
3365
|
}
|
|
3335
3366
|
|
|
3367
|
+
// RULE14: Only the main input may stream. `streaming: true` declares that
|
|
3368
|
+
// an argument is consumed without a length promise — a feed. Side
|
|
3369
|
+
// arguments (options, model specs, prompts alongside the main input) are
|
|
3370
|
+
// values the runtime demuxes whole; a feed there has no meaning and would
|
|
3371
|
+
// let an unbounded stream reach a collector that must refuse it (L16).
|
|
3372
|
+
for (const arg of args) {
|
|
3373
|
+
if (arg.streaming && !arg.isMainInput(inMediaUrn)) {
|
|
3374
|
+
throw new ValidationError('InvalidCapSchema', capUrn, {
|
|
3375
|
+
issue: `RULE14: Argument '${arg.media_urn}' declares streaming=true but is not the main input (no stdin source equivalent to in='${cap.urn.getInSpec()}') — only the main input may be consumed without a length promise`
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3336
3380
|
// RULE5: No two args may have same position
|
|
3337
3381
|
const positionSet = new Set();
|
|
3338
3382
|
for (const { position, mediaUrn } of positions) {
|
|
@@ -5372,6 +5416,320 @@ const CartridgeLifecycle = Object.freeze({
|
|
|
5372
5416
|
OPERATIONAL: 'operational',
|
|
5373
5417
|
});
|
|
5374
5418
|
|
|
5419
|
+
/**
|
|
5420
|
+
* Format discriminator for release-key certificates.
|
|
5421
|
+
* Mirrors Rust `RELEASE_KEY_CERT_FORMAT` (bifaci/release_cert.rs).
|
|
5422
|
+
*
|
|
5423
|
+
* THE WIRE FORMAT IS NOT A PRODUCT NAME. Every verifier — this library, the
|
|
5424
|
+
* desktop clients' in-process verifiers, the publisher — must compare against
|
|
5425
|
+
* THIS constant. A client that keeps its own copy can be renamed away from the
|
|
5426
|
+
* protocol by a search-and-replace, verify nothing, and report the registry as
|
|
5427
|
+
* unreachable; that is exactly what happened, and it is why these live here.
|
|
5428
|
+
*/
|
|
5429
|
+
const RELEASE_KEY_CERT_FORMAT = 'machinefabric-release-key-cert/1';
|
|
5430
|
+
|
|
5431
|
+
/** Format discriminator for manifest signature envelopes.
|
|
5432
|
+
* Mirrors Rust `MANIFEST_SIG_FORMAT`. */
|
|
5433
|
+
const MANIFEST_SIG_FORMAT = 'machinefabric-manifest-sig/1';
|
|
5434
|
+
|
|
5435
|
+
/**
|
|
5436
|
+
* What a consumer concluded about a cartridge registry.
|
|
5437
|
+
* Mirrors Rust `RegistryVerdictState` (bifaci/registry_verdict.rs).
|
|
5438
|
+
*
|
|
5439
|
+
* A REGISTRY IS NOT A CARTRIDGE: this is one fact per registry URL, shared by
|
|
5440
|
+
* every cartridge that claims provenance from it. The vocabulary separates the
|
|
5441
|
+
* two things a consumer can conclude — that it could not get an answer
|
|
5442
|
+
* (OFFLINE, UNREACHABLE, HTTP_ERROR, MALFORMED) and that it got one and refused
|
|
5443
|
+
* it (UNSIGNED, UNTRUSTED, UNVERIFIABLE) — because those have opposite
|
|
5444
|
+
* remedies.
|
|
5445
|
+
*/
|
|
5446
|
+
const RegistryVerdictState = Object.freeze({
|
|
5447
|
+
// Fetched, chain-verified and parsed. The only state that lets a cartridge
|
|
5448
|
+
// from this registry attach.
|
|
5449
|
+
VERIFIED: 'verified',
|
|
5450
|
+
// No verdict yet — the first check has not run. NOT a failure.
|
|
5451
|
+
PENDING: 'pending',
|
|
5452
|
+
// The consumer's own network policy forbade the request. The remedy is a
|
|
5453
|
+
// setting, not the network.
|
|
5454
|
+
OFFLINE: 'offline',
|
|
5455
|
+
// DNS, refused, timeout, TLS. The only state where "check your connection"
|
|
5456
|
+
// is sound advice.
|
|
5457
|
+
UNREACHABLE: 'unreachable',
|
|
5458
|
+
// The registry answered with an HTTP error; the status travels with it.
|
|
5459
|
+
HTTP_ERROR: 'http_error',
|
|
5460
|
+
// The registry answered with a body this build cannot read as a manifest.
|
|
5461
|
+
MALFORMED: 'malformed',
|
|
5462
|
+
// No signature sidecar where one is required.
|
|
5463
|
+
UNSIGNED: 'unsigned',
|
|
5464
|
+
// The chain was evaluated and REJECTED — the registry's problem.
|
|
5465
|
+
UNTRUSTED: 'untrusted',
|
|
5466
|
+
// The chain could NOT be evaluated: a format this build does not implement.
|
|
5467
|
+
// Our problem, remedied by updating the client — never by distrusting the
|
|
5468
|
+
// registry, and never by checking the network.
|
|
5469
|
+
UNVERIFIABLE: 'unverifiable',
|
|
5470
|
+
// This build bakes no trust anchors, so there is no regime to verify against
|
|
5471
|
+
// and the manifest was accepted without proof. A development build, and only
|
|
5472
|
+
// ever that. It permits attachment — a dev build has to work — and is a
|
|
5473
|
+
// SEPARATE state rather than being reported as VERIFIED, because "we checked
|
|
5474
|
+
// and it passed" and "we did not check" are different facts.
|
|
5475
|
+
UNENFORCED: 'unenforced',
|
|
5476
|
+
});
|
|
5477
|
+
|
|
5478
|
+
/**
|
|
5479
|
+
* WHAT TO DO ABOUT A REGISTRY IN A GIVEN STATE. Mirrors Rust `RegistryRemedy`.
|
|
5480
|
+
*
|
|
5481
|
+
* The remedy follows from the state and nothing else. It used to be a sentence
|
|
5482
|
+
* glued onto the failure message at the point the record was built — "Check the
|
|
5483
|
+
* network connection and try again." — appended whatever the cause, so a
|
|
5484
|
+
* signature a build could not read sent operators to their router. A remedy
|
|
5485
|
+
* asserted as fact regardless of what failed is worse than none.
|
|
5486
|
+
*
|
|
5487
|
+
* This is the ACTION, not its wording: a CLI prints a line, a desktop client
|
|
5488
|
+
* offers a control. Both derive them from here.
|
|
5489
|
+
*/
|
|
5490
|
+
const RegistryRemedy = Object.freeze({
|
|
5491
|
+
// Nothing to do — the registry verified.
|
|
5492
|
+
NONE: 'none',
|
|
5493
|
+
// A check is in flight and will answer on its own.
|
|
5494
|
+
WAIT: 'wait',
|
|
5495
|
+
// The machine cannot reach the registry. Check the connection.
|
|
5496
|
+
CHECK_NETWORK: 'check_network',
|
|
5497
|
+
// This build was told not to go out. Change the network policy.
|
|
5498
|
+
CHANGE_NETWORK_POLICY: 'change_network_policy',
|
|
5499
|
+
// The registry answered badly; it is the registry's side to fix.
|
|
5500
|
+
RETRY_LATER: 'retry_later',
|
|
5501
|
+
// This build cannot read the registry's signature format. Update the client —
|
|
5502
|
+
// the registry is not at fault and the network is not involved.
|
|
5503
|
+
UPDATE_CLIENT: 'update_client',
|
|
5504
|
+
// The registry's answer was rejected. Do not proceed.
|
|
5505
|
+
DO_NOT_PROCEED: 'do_not_proceed',
|
|
5506
|
+
});
|
|
5507
|
+
|
|
5508
|
+
const _REGISTRY_REMEDY_BY_STATE = Object.freeze({
|
|
5509
|
+
verified: RegistryRemedy.NONE,
|
|
5510
|
+
pending: RegistryRemedy.WAIT,
|
|
5511
|
+
offline: RegistryRemedy.CHANGE_NETWORK_POLICY,
|
|
5512
|
+
unreachable: RegistryRemedy.CHECK_NETWORK,
|
|
5513
|
+
http_error: RegistryRemedy.RETRY_LATER,
|
|
5514
|
+
malformed: RegistryRemedy.RETRY_LATER,
|
|
5515
|
+
unsigned: RegistryRemedy.DO_NOT_PROCEED,
|
|
5516
|
+
untrusted: RegistryRemedy.DO_NOT_PROCEED,
|
|
5517
|
+
unverifiable: RegistryRemedy.UPDATE_CLIENT,
|
|
5518
|
+
unenforced: RegistryRemedy.NONE,
|
|
5519
|
+
});
|
|
5520
|
+
|
|
5521
|
+
/** The one thing to do about a registry in this state. */
|
|
5522
|
+
function registryVerdictRemedy(state) {
|
|
5523
|
+
const remedy = _REGISTRY_REMEDY_BY_STATE[state];
|
|
5524
|
+
if (remedy === undefined) {
|
|
5525
|
+
throw new Error(`unknown registry verdict state '${state}'`);
|
|
5526
|
+
}
|
|
5527
|
+
return remedy;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
/** Why a signature chain failed. Mirrors Rust `ChainFailureReason`. */
|
|
5531
|
+
const ChainFailureReason = Object.freeze({
|
|
5532
|
+
MALFORMED_ENVELOPE: 'malformed_envelope',
|
|
5533
|
+
UNSUPPORTED_ENVELOPE_FORMAT: 'unsupported_envelope_format',
|
|
5534
|
+
MALFORMED_CERTIFICATE: 'malformed_certificate',
|
|
5535
|
+
UNSUPPORTED_CERTIFICATE_FORMAT: 'unsupported_certificate_format',
|
|
5536
|
+
EMPTY_CERTIFICATE_LIST: 'empty_certificate_list',
|
|
5537
|
+
INSUFFICIENT_ROOT_SIGNATURES: 'insufficient_root_signatures',
|
|
5538
|
+
EXPIRED_CERTIFICATE: 'expired_certificate',
|
|
5539
|
+
NOT_YET_VALID_CERTIFICATE: 'not_yet_valid_certificate',
|
|
5540
|
+
ENVIRONMENT_MISMATCH: 'environment_mismatch',
|
|
5541
|
+
KEY_ID_MISMATCH: 'key_id_mismatch',
|
|
5542
|
+
NO_AUTHORIZING_CERTIFICATE: 'no_authorizing_certificate',
|
|
5543
|
+
MANIFEST_SIGNATURE_INVALID: 'manifest_signature_invalid',
|
|
5544
|
+
});
|
|
5545
|
+
|
|
5546
|
+
const REGISTRY_VERDICT_STATES = Object.freeze(Object.values(RegistryVerdictState));
|
|
5547
|
+
const CHAIN_FAILURE_REASONS = Object.freeze(Object.values(ChainFailureReason));
|
|
5548
|
+
|
|
5549
|
+
/** The reasons that mean the chain could not be EVALUATED, as opposed to being
|
|
5550
|
+
* evaluated and rejected. */
|
|
5551
|
+
const UNEVALUABLE_CHAIN_FAILURES = Object.freeze([
|
|
5552
|
+
ChainFailureReason.MALFORMED_ENVELOPE,
|
|
5553
|
+
ChainFailureReason.UNSUPPORTED_ENVELOPE_FORMAT,
|
|
5554
|
+
ChainFailureReason.MALFORMED_CERTIFICATE,
|
|
5555
|
+
ChainFailureReason.UNSUPPORTED_CERTIFICATE_FORMAT,
|
|
5556
|
+
ChainFailureReason.EMPTY_CERTIFICATE_LIST,
|
|
5557
|
+
]);
|
|
5558
|
+
|
|
5559
|
+
/**
|
|
5560
|
+
* The verdict a chain failure produces.
|
|
5561
|
+
*
|
|
5562
|
+
* Could the chain be evaluated at all? A format this build does not implement
|
|
5563
|
+
* means no judgement was reached (UNVERIFIABLE — update the client). Anything
|
|
5564
|
+
* else means the chain WAS judged and found wanting (UNTRUSTED — do not
|
|
5565
|
+
* proceed). Leaving this decision to each consumer is how one client reported
|
|
5566
|
+
* an unreadable signature format as a network outage.
|
|
5567
|
+
*/
|
|
5568
|
+
function registryVerdictStateForChainFailure(reason) {
|
|
5569
|
+
if (!CHAIN_FAILURE_REASONS.includes(reason)) {
|
|
5570
|
+
throw new Error(`unknown chain failure reason '${reason}'`);
|
|
5571
|
+
}
|
|
5572
|
+
return UNEVALUABLE_CHAIN_FAILURES.includes(reason)
|
|
5573
|
+
? RegistryVerdictState.UNVERIFIABLE
|
|
5574
|
+
: RegistryVerdictState.UNTRUSTED;
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
/** Whether a cartridge from a registry in this state may attach. */
|
|
5578
|
+
function registryVerdictPermitsAttachment(state) {
|
|
5579
|
+
if (!REGISTRY_VERDICT_STATES.includes(state)) {
|
|
5580
|
+
throw new Error(`unknown registry verdict state '${state}'`);
|
|
5581
|
+
}
|
|
5582
|
+
return state === RegistryVerdictState.VERIFIED || state === RegistryVerdictState.UNENFORCED;
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5585
|
+
/** Whether the state is a refusal of an answer we DID get. Never changes on retry. */
|
|
5586
|
+
function registryVerdictIsTrustFailure(state) {
|
|
5587
|
+
if (!REGISTRY_VERDICT_STATES.includes(state)) {
|
|
5588
|
+
throw new Error(`unknown registry verdict state '${state}'`);
|
|
5589
|
+
}
|
|
5590
|
+
return state === RegistryVerdictState.UNSIGNED
|
|
5591
|
+
|| state === RegistryVerdictState.UNTRUSTED
|
|
5592
|
+
|| state === RegistryVerdictState.UNVERIFIABLE;
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5595
|
+
/** Whether an unattended retry could plausibly reach a different verdict. */
|
|
5596
|
+
function registryVerdictIsTransient(state) {
|
|
5597
|
+
if (!REGISTRY_VERDICT_STATES.includes(state)) {
|
|
5598
|
+
throw new Error(`unknown registry verdict state '${state}'`);
|
|
5599
|
+
}
|
|
5600
|
+
return state === RegistryVerdictState.PENDING
|
|
5601
|
+
|| state === RegistryVerdictState.UNREACHABLE
|
|
5602
|
+
|| state === RegistryVerdictState.HTTP_ERROR
|
|
5603
|
+
|| state === RegistryVerdictState.MALFORMED;
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
/**
|
|
5607
|
+
* What a consumer concluded about one registry, and why.
|
|
5608
|
+
* Mirrors Rust `RegistryVerdict`.
|
|
5609
|
+
*
|
|
5610
|
+
* Illegal combinations are unrepresentable: the constructors take exactly what
|
|
5611
|
+
* their state requires and `validate()` refuses every contradiction, so a
|
|
5612
|
+
* verdict that says "http_error" without a status, or "verified" with a failure
|
|
5613
|
+
* detail, is refused at the boundary instead of rendered as a contradiction.
|
|
5614
|
+
*/
|
|
5615
|
+
class RegistryVerdict {
|
|
5616
|
+
constructor(registryUrl, state, detail, httpStatus, chainFailure, checkedAtUnixSeconds) {
|
|
5617
|
+
this.registry_url = registryUrl;
|
|
5618
|
+
this.state = state;
|
|
5619
|
+
this.detail = detail;
|
|
5620
|
+
this.http_status = httpStatus === undefined ? null : httpStatus;
|
|
5621
|
+
this.chain_failure = chainFailure === undefined ? null : chainFailure;
|
|
5622
|
+
this.checked_at_unix_seconds = checkedAtUnixSeconds;
|
|
5623
|
+
this.validate();
|
|
5624
|
+
}
|
|
5625
|
+
|
|
5626
|
+
static verified(registryUrl, checkedAtUnixSeconds) {
|
|
5627
|
+
return new RegistryVerdict(registryUrl, RegistryVerdictState.VERIFIED, '', null, null, checkedAtUnixSeconds);
|
|
5628
|
+
}
|
|
5629
|
+
|
|
5630
|
+
/** This build bakes no trust anchors: the manifest was accepted without
|
|
5631
|
+
* proof, and says so rather than claiming it verified. */
|
|
5632
|
+
static unenforced(registryUrl, checkedAtUnixSeconds) {
|
|
5633
|
+
return new RegistryVerdict(registryUrl, RegistryVerdictState.UNENFORCED, '', null, null, checkedAtUnixSeconds);
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
/** No verdict yet. Carries no time, because nothing has been checked. */
|
|
5637
|
+
static pending(registryUrl) {
|
|
5638
|
+
return new RegistryVerdict(registryUrl, RegistryVerdictState.PENDING, '', null, null, 0);
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
/** A state carrying only a detail line: OFFLINE, UNREACHABLE, MALFORMED, UNSIGNED. */
|
|
5642
|
+
static stated(registryUrl, state, detail, checkedAtUnixSeconds) {
|
|
5643
|
+
return new RegistryVerdict(registryUrl, state, detail, null, null, checkedAtUnixSeconds);
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
static httpError(registryUrl, status, detail, checkedAtUnixSeconds) {
|
|
5647
|
+
return new RegistryVerdict(registryUrl, RegistryVerdictState.HTTP_ERROR, detail, status, null, checkedAtUnixSeconds);
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
/** A signature chain that failed. The state FOLLOWS from the reason, so a
|
|
5651
|
+
* caller cannot file an unreadable format as a rejected key. */
|
|
5652
|
+
static chainFailed(registryUrl, reason, detail, checkedAtUnixSeconds) {
|
|
5653
|
+
return new RegistryVerdict(
|
|
5654
|
+
registryUrl,
|
|
5655
|
+
registryVerdictStateForChainFailure(reason),
|
|
5656
|
+
detail,
|
|
5657
|
+
null,
|
|
5658
|
+
reason,
|
|
5659
|
+
checkedAtUnixSeconds,
|
|
5660
|
+
);
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
validate() {
|
|
5664
|
+
if (typeof this.registry_url !== 'string' || this.registry_url.length === 0) {
|
|
5665
|
+
throw new Error('a registry verdict must name the registry it is about');
|
|
5666
|
+
}
|
|
5667
|
+
if (!REGISTRY_VERDICT_STATES.includes(this.state)) {
|
|
5668
|
+
throw new Error(`unknown registry verdict state '${this.state}'`);
|
|
5669
|
+
}
|
|
5670
|
+
if (typeof this.detail !== 'string') {
|
|
5671
|
+
throw new Error('a registry verdict detail must be a string');
|
|
5672
|
+
}
|
|
5673
|
+
const statesNoFailure = this.state === RegistryVerdictState.VERIFIED
|
|
5674
|
+
|| this.state === RegistryVerdictState.PENDING
|
|
5675
|
+
|| this.state === RegistryVerdictState.UNENFORCED;
|
|
5676
|
+
if (statesNoFailure && this.detail.length > 0) {
|
|
5677
|
+
throw new Error(`a '${this.state}' verdict states no failure, so it carries no detail (got ${JSON.stringify(this.detail)})`);
|
|
5678
|
+
}
|
|
5679
|
+
if (!statesNoFailure && this.detail.length === 0) {
|
|
5680
|
+
throw new Error(`a '${this.state}' verdict must carry the detail that explains it`);
|
|
5681
|
+
}
|
|
5682
|
+
if (this.state === RegistryVerdictState.HTTP_ERROR) {
|
|
5683
|
+
if (!Number.isInteger(this.http_status)) {
|
|
5684
|
+
throw new Error("an 'http_error' verdict must carry the status the registry answered with");
|
|
5685
|
+
}
|
|
5686
|
+
} else if (this.http_status !== null) {
|
|
5687
|
+
throw new Error(`only an 'http_error' verdict carries an HTTP status (got one on '${this.state}')`);
|
|
5688
|
+
}
|
|
5689
|
+
const chainStates = this.state === RegistryVerdictState.UNTRUSTED
|
|
5690
|
+
|| this.state === RegistryVerdictState.UNVERIFIABLE;
|
|
5691
|
+
if (chainStates) {
|
|
5692
|
+
if (this.chain_failure === null) {
|
|
5693
|
+
throw new Error(`a '${this.state}' verdict must carry the chain failure reason that produced it`);
|
|
5694
|
+
}
|
|
5695
|
+
if (registryVerdictStateForChainFailure(this.chain_failure) !== this.state) {
|
|
5696
|
+
throw new Error(`only a trust failure carries a chain failure reason (got one on '${this.state}')`);
|
|
5697
|
+
}
|
|
5698
|
+
} else if (this.chain_failure !== null) {
|
|
5699
|
+
throw new Error(`only a trust failure carries a chain failure reason (got one on '${this.state}')`);
|
|
5700
|
+
}
|
|
5701
|
+
if (!Number.isFinite(this.checked_at_unix_seconds)) {
|
|
5702
|
+
throw new Error('a registry verdict must say when it was reached');
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
|
|
5706
|
+
permitsAttachment() {
|
|
5707
|
+
return registryVerdictPermitsAttachment(this.state);
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
toJSON() {
|
|
5711
|
+
return {
|
|
5712
|
+
registry_url: this.registry_url,
|
|
5713
|
+
state: this.state,
|
|
5714
|
+
detail: this.detail,
|
|
5715
|
+
http_status: this.http_status,
|
|
5716
|
+
chain_failure: this.chain_failure,
|
|
5717
|
+
checked_at_unix_seconds: this.checked_at_unix_seconds,
|
|
5718
|
+
};
|
|
5719
|
+
}
|
|
5720
|
+
|
|
5721
|
+
static fromJSON(d) {
|
|
5722
|
+
return new RegistryVerdict(
|
|
5723
|
+
d.registry_url,
|
|
5724
|
+
d.state,
|
|
5725
|
+
d.detail,
|
|
5726
|
+
d.http_status === undefined ? null : d.http_status,
|
|
5727
|
+
d.chain_failure === undefined ? null : d.chain_failure,
|
|
5728
|
+
d.checked_at_unix_seconds,
|
|
5729
|
+
);
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5375
5733
|
const CartridgeAttachmentErrorKind = Object.freeze({
|
|
5376
5734
|
INCOMPATIBLE: 'incompatible',
|
|
5377
5735
|
MANIFEST_INVALID: 'manifest_invalid',
|
|
@@ -5384,12 +5742,20 @@ const CartridgeAttachmentErrorKind = Object.freeze({
|
|
|
5384
5742
|
// channel folder doesn't match the manifest's channel, or a bundled-cartridge
|
|
5385
5743
|
// integrity proof failed. Structurally well-formed but cannot be trusted
|
|
5386
5744
|
// because its placement on disk does not match what it claims to be.
|
|
5387
|
-
|
|
5745
|
+
// Recovery is "reinstall".
|
|
5746
|
+
MISPLACED: 'misplaced',
|
|
5747
|
+
// The cartridge's registry verified, and does not list this
|
|
5748
|
+
// (channel, id, version): the artefact says it came from somewhere that has
|
|
5749
|
+
// never heard of it. Recovery is "wait for the publish, or rebuild as dev".
|
|
5750
|
+
NOT_LISTED: 'not_listed',
|
|
5388
5751
|
// Operator explicitly disabled this cartridge through the host UI.
|
|
5389
5752
|
DISABLED: 'disabled',
|
|
5390
|
-
// The cartridge declares a non-null registry_url
|
|
5391
|
-
//
|
|
5392
|
-
|
|
5753
|
+
// The cartridge declares a non-null registry_url and that registry's verdict
|
|
5754
|
+
// is not `verified`, so its provenance claim is unconfirmed. WHY belongs to
|
|
5755
|
+
// the registry, not the cartridge — consumers join on registry_url and read
|
|
5756
|
+
// the RegistryVerdict, which is one fact per registry rather than one per
|
|
5757
|
+
// cartridge that claims it.
|
|
5758
|
+
REGISTRY_UNVERIFIED: 'registry_unverified',
|
|
5393
5759
|
// The cartridge was built against a different fabric registry manifest
|
|
5394
5760
|
// version than this host is pinned to.
|
|
5395
5761
|
FABRIC_MANIFEST_VERSION_MISMATCH: 'fabric_manifest_version_mismatch',
|
|
@@ -5507,7 +5873,7 @@ class InstalledCartridgeRecord {
|
|
|
5507
5873
|
* @param {Array<Object>} [opts.capGroups=[]] - Cartridge's manifest cap_groups; each element is `{name, caps, adapter_urns}`.
|
|
5508
5874
|
* @param {CartridgeAttachmentError|null} [opts.attachmentError=null]
|
|
5509
5875
|
* @param {CartridgeRuntimeStats|null} [opts.runtimeStats=null]
|
|
5510
|
-
* @param {string} [opts.lifecycle='discovered'] - One of `discovered` | `inspecting` | `verifying` | `operational`. Mutually exclusive with attachmentError; see `
|
|
5876
|
+
* @param {string} [opts.lifecycle='discovered'] - One of `discovered` | `inspecting` | `verifying` | `operational`. Mutually exclusive with attachmentError; see `macfloom/docs/cartridge state machine.md`.
|
|
5511
5877
|
*/
|
|
5512
5878
|
constructor({ registryUrl = null, channel, id, version, sha256, capGroups = [], attachmentError = null, runtimeStats = null, lifecycle = 'discovered' }) {
|
|
5513
5879
|
this.registry_url = registryUrl;
|
|
@@ -5664,7 +6030,7 @@ function validateRegistryUrlScheme(url, devMode) {
|
|
|
5664
6030
|
/**
|
|
5665
6031
|
* Error kinds for CartridgeJson reading/validation. Mirrors the variants of
|
|
5666
6032
|
* Rust CartridgeJsonError. `RegistrySlugMismatch` is the three-place
|
|
5667
|
-
* consistency failure that discovery maps to
|
|
6033
|
+
* consistency failure that discovery maps to MISPLACED.
|
|
5668
6034
|
*/
|
|
5669
6035
|
const CartridgeJsonErrorKind = Object.freeze({
|
|
5670
6036
|
NOT_FOUND: 'not_found',
|
|
@@ -5678,7 +6044,7 @@ const CartridgeJsonErrorKind = Object.freeze({
|
|
|
5678
6044
|
|
|
5679
6045
|
/**
|
|
5680
6046
|
* Error thrown by CartridgeJson.readFromDir. Carries a structured `kind` so
|
|
5681
|
-
* discovery can map a slug mismatch to
|
|
6047
|
+
* discovery can map a slug mismatch to MISPLACED and everything else
|
|
5682
6048
|
* to MANIFEST_INVALID. Mirrors Rust CartridgeJsonError.
|
|
5683
6049
|
*/
|
|
5684
6050
|
class CartridgeJsonError extends Error {
|
|
@@ -6416,7 +6782,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6416
6782
|
cj = CartridgeJson.readFromDir(versionDir, expectedSlug);
|
|
6417
6783
|
} catch (e) {
|
|
6418
6784
|
const kind = (e instanceof CartridgeJsonError && e.kind === CartridgeJsonErrorKind.REGISTRY_SLUG_MISMATCH)
|
|
6419
|
-
? CartridgeAttachmentErrorKind.
|
|
6785
|
+
? CartridgeAttachmentErrorKind.MISPLACED
|
|
6420
6786
|
: CartridgeAttachmentErrorKind.MANIFEST_INVALID;
|
|
6421
6787
|
discovered.push(DiscoveredCartridge.incompatible({
|
|
6422
6788
|
version_dir: versionDir,
|
|
@@ -6441,7 +6807,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6441
6807
|
registry_url: cj.registryUrl,
|
|
6442
6808
|
version: cj.version,
|
|
6443
6809
|
error: new CartridgeAttachmentError(
|
|
6444
|
-
CartridgeAttachmentErrorKind.
|
|
6810
|
+
CartridgeAttachmentErrorKind.MISPLACED,
|
|
6445
6811
|
`Channel mismatch: cartridge declares '${cj.channel}' but host is pinned to '${identity.channel}'. Release and nightly artefacts must not mix.`,
|
|
6446
6812
|
detectedAt
|
|
6447
6813
|
),
|
|
@@ -6518,7 +6884,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
|
|
|
6518
6884
|
registry_url: cj.registryUrl,
|
|
6519
6885
|
version: cj.version,
|
|
6520
6886
|
error: new CartridgeAttachmentError(
|
|
6521
|
-
CartridgeAttachmentErrorKind.
|
|
6887
|
+
CartridgeAttachmentErrorKind.MISPLACED,
|
|
6522
6888
|
`bundled cartridge integrity check failed: ${reason}`,
|
|
6523
6889
|
detectedAt
|
|
6524
6890
|
),
|
|
@@ -8064,6 +8430,21 @@ const planner = require('./planner.js');
|
|
|
8064
8430
|
|
|
8065
8431
|
// Export for CommonJS
|
|
8066
8432
|
module.exports = {
|
|
8433
|
+
// Registry trust vocabulary — one verdict per registry, shared by every
|
|
8434
|
+
// cartridge that claims provenance from it.
|
|
8435
|
+
RegistryVerdict,
|
|
8436
|
+
RegistryVerdictState,
|
|
8437
|
+
ChainFailureReason,
|
|
8438
|
+
REGISTRY_VERDICT_STATES,
|
|
8439
|
+
CHAIN_FAILURE_REASONS,
|
|
8440
|
+
registryVerdictStateForChainFailure,
|
|
8441
|
+
registryVerdictPermitsAttachment,
|
|
8442
|
+
registryVerdictIsTrustFailure,
|
|
8443
|
+
registryVerdictIsTransient,
|
|
8444
|
+
RegistryRemedy,
|
|
8445
|
+
registryVerdictRemedy,
|
|
8446
|
+
MANIFEST_SIG_FORMAT,
|
|
8447
|
+
RELEASE_KEY_CERT_FORMAT,
|
|
8067
8448
|
// Planner plan-space vocabulary
|
|
8068
8449
|
PlanStateError: planner.PlanStateError,
|
|
8069
8450
|
ConvergencePresence: planner.ConvergencePresence,
|
package/capdag.test.js
CHANGED
|
@@ -14,6 +14,12 @@ const {
|
|
|
14
14
|
CartridgeJson, CartridgeJsonError, CartridgeJsonErrorKind, hashCartridgeDirectory,
|
|
15
15
|
DiscoveryIdentity, DiscoveredCartridge, discoverCartridges,
|
|
16
16
|
CartridgeAttachmentErrorKind, CartridgeChannel,
|
|
17
|
+
RegistryVerdict, RegistryVerdictState, ChainFailureReason,
|
|
18
|
+
REGISTRY_VERDICT_STATES, CHAIN_FAILURE_REASONS,
|
|
19
|
+
registryVerdictStateForChainFailure, registryVerdictPermitsAttachment,
|
|
20
|
+
registryVerdictIsTrustFailure, registryVerdictIsTransient,
|
|
21
|
+
RegistryRemedy, registryVerdictRemedy,
|
|
22
|
+
MANIFEST_SIG_FORMAT, RELEASE_KEY_CERT_FORMAT,
|
|
17
23
|
CapFabEdge, CapFabStats, CapFab,
|
|
18
24
|
StdinSource, StdinSourceKind,
|
|
19
25
|
validateNoMediaDefRedefinitionSync,
|
|
@@ -74,6 +80,27 @@ function assertThrows(fn, expectedErrorCode, message) {
|
|
|
74
80
|
}
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
/**
|
|
84
|
+
* A refusal: the call must throw, and its message must match. Distinct from
|
|
85
|
+
* `assertThrows`, which is specific to `CapUrnError` codes — asserting only
|
|
86
|
+
* "it threw" would pass on a typo in the very message the refusal exists to
|
|
87
|
+
* state.
|
|
88
|
+
*/
|
|
89
|
+
function assertRefuses(fn, pattern, message) {
|
|
90
|
+
let thrown = null;
|
|
91
|
+
try {
|
|
92
|
+
fn();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
thrown = error;
|
|
95
|
+
}
|
|
96
|
+
if (thrown === null) {
|
|
97
|
+
throw new Error(`Expected a refusal but the call succeeded: ${message || pattern}`);
|
|
98
|
+
}
|
|
99
|
+
if (!pattern.test(thrown.message)) {
|
|
100
|
+
throw new Error(`Expected a refusal matching ${pattern} but got: ${thrown.message}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
77
104
|
function assertThrowsMediaUrn(fn, expectedErrorCode, message) {
|
|
78
105
|
try {
|
|
79
106
|
fn();
|
|
@@ -1705,7 +1732,7 @@ function test6236_JS_mediaUrnsForExtension() {
|
|
|
1705
1732
|
assert(thrownError instanceof MediaDefError, 'Should throw MediaDefError for unknown ext');
|
|
1706
1733
|
}
|
|
1707
1734
|
|
|
1708
|
-
//
|
|
1735
|
+
// TEST6240: J s get extension mappings
|
|
1709
1736
|
function test6240_JS_getExtensionMappings() {
|
|
1710
1737
|
const mediaDefs = [
|
|
1711
1738
|
{ urn: 'media:ext=pdf', media_type: 'application/pdf', extensions: ['pdf'] },
|
|
@@ -1716,7 +1743,7 @@ function test6240_JS_getExtensionMappings() {
|
|
|
1716
1743
|
assertEqual(mappings.length, 3, 'Should have 3 mappings');
|
|
1717
1744
|
}
|
|
1718
1745
|
|
|
1719
|
-
//
|
|
1746
|
+
// TEST6242: J s resolve media urn from specs
|
|
1720
1747
|
function test6242_JS_resolveMediaUrnFromSpecs() {
|
|
1721
1748
|
const mediaDefs = [
|
|
1722
1749
|
{ urn: MEDIA_STRING, media_type: 'text/plain', title: 'String', profile_uri: 'https://capdag.com/schema/str' },
|
|
@@ -2693,7 +2720,7 @@ async function test1877_registryCartridgeUnderWrongSlugIsBadInstall() {
|
|
|
2693
2720
|
const json = discoveryRegistryCartridgeJson(url, 'nightly', 1);
|
|
2694
2721
|
discoveryInstallFixture(root, wrongSlug, 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2695
2722
|
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2696
|
-
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.
|
|
2723
|
+
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.MISPLACED);
|
|
2697
2724
|
} finally {
|
|
2698
2725
|
discoveryRemoveRoot(root);
|
|
2699
2726
|
}
|
|
@@ -2721,7 +2748,7 @@ async function test1878_bundledCartridgeWithoutBakedHashIsRejected() {
|
|
|
2721
2748
|
});
|
|
2722
2749
|
discoveryInstallFixture(root, 'dev', 'nightly', 'cart', '1.0.0', json, 'cart');
|
|
2723
2750
|
const out = await discoverCartridges(root, new DiscoveryIdentity({ channel: 'nightly', registryUrl: null, fabricManifestVersion: 1, cartridgeRegistryVersion: 1 }));
|
|
2724
|
-
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.
|
|
2751
|
+
discoveryExpectIncompatible(out, CartridgeAttachmentErrorKind.MISPLACED);
|
|
2725
2752
|
assert(out[0].error.message.includes('bundled cartridge integrity'),
|
|
2726
2753
|
`message should name the bundled-integrity failure: ${out[0].error.message}`);
|
|
2727
2754
|
} finally {
|
|
@@ -3018,6 +3045,54 @@ function test1296_rule11VoidInputCliFlagOnly() {
|
|
|
3018
3045
|
validateCapArgs(cap);
|
|
3019
3046
|
}
|
|
3020
3047
|
|
|
3048
|
+
// TEST1953: RULE14 — `streaming: true` is accepted on the main input (the
|
|
3049
|
+
// stdin arg equivalent to `in=`), survives the JSON round-trip, and is refused
|
|
3050
|
+
// on any other argument: a side option has no wire stream, so it has nothing
|
|
3051
|
+
// to consume incrementally, and the rule keeps the executor's hop rule
|
|
3052
|
+
// one-dimensional.
|
|
3053
|
+
function test1953_rule14StreamingOnlyOnMainInput() {
|
|
3054
|
+
const urn = CapUrn.fromString('cap:in="media:string";test;out="media:string"');
|
|
3055
|
+
const ok = new Cap(urn, 'Test', ['test-cmd']);
|
|
3056
|
+
const main = new CapArg('media:string', true, [ArgSource.fromJSON({ stdin: 'media:string' })], { streaming: true });
|
|
3057
|
+
ok.args = [main];
|
|
3058
|
+
validateCapArgs(ok);
|
|
3059
|
+
assert(CapArg.fromJSON(main.toJSON()).streaming === true, 'streaming survives the JSON round-trip');
|
|
3060
|
+
assert(!('streaming' in new CapArg('media:string', true, []).toJSON()), 'streaming is omitted when false, like is_sequence');
|
|
3061
|
+
|
|
3062
|
+
const bad = new Cap(CapUrn.fromString('cap:in="media:string";test;out="media:string"'), 'Test', ['test-cmd']);
|
|
3063
|
+
bad.args = [
|
|
3064
|
+
new CapArg('media:string', true, [ArgSource.fromJSON({ stdin: 'media:string' })]),
|
|
3065
|
+
new CapArg('media:integer;numeric', false, [ArgSource.fromJSON({ cli_flag: '--count' })], { streaming: true }),
|
|
3066
|
+
];
|
|
3067
|
+
try {
|
|
3068
|
+
validateCapArgs(bad);
|
|
3069
|
+
assert(false, 'Should have thrown RULE14 for a streaming side option');
|
|
3070
|
+
} catch (e) {
|
|
3071
|
+
assert(e instanceof ValidationError, 'Should be ValidationError');
|
|
3072
|
+
assert(e.message.includes('RULE14'), 'Should mention RULE14: ' + e.message);
|
|
3073
|
+
assert(e.message.includes('media:integer;numeric'), 'Should name the offending arg: ' + e.message);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// TEST1964: a definition field this capdag does not know is a NEWER fabric,
|
|
3078
|
+
// not noise — parsing refuses it, naming the key, for arguments and outputs
|
|
3079
|
+
// alike. The field being dropped is how a cartridge built on an older capdag
|
|
3080
|
+
// once advertised a `streaming` input as bounded.
|
|
3081
|
+
function test1964_unknownDefinitionFieldIsRefused() {
|
|
3082
|
+
let threw = null;
|
|
3083
|
+
try {
|
|
3084
|
+
CapArg.fromJSON({ media_urn: 'media:string', required: true, sources: [{ stdin: 'media:string' }], chunking: 'adaptive' });
|
|
3085
|
+
} catch (e) { threw = e; }
|
|
3086
|
+
assert(threw !== null && threw.message.includes('chunking'), 'an unknown arg field must be refused by name: ' + (threw && threw.message));
|
|
3087
|
+
assert(CapArg.fromJSON({ media_urn: 'media:string', required: true, sources: [{ stdin: 'media:string' }], streaming: true }).streaming === true);
|
|
3088
|
+
|
|
3089
|
+
const capJson = new Cap(CapUrn.fromString('cap:in="media:string";test;out="media:string"'), 'Test', ['test-cmd']).toJSON();
|
|
3090
|
+
capJson.output = { media_urn: 'media:string', output_description: 'x', windowed: true };
|
|
3091
|
+
threw = null;
|
|
3092
|
+
try { Cap.fromJSON(capJson); } catch (e) { threw = e; }
|
|
3093
|
+
assert(threw !== null && threw.message.includes('windowed'), 'an unknown output field must be refused by name: ' + (threw && threw.message));
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3021
3096
|
// TEST1297: RULE11 - non-void-input cap with stdin source passes
|
|
3022
3097
|
function test1297_rule11NonVoidInputWithStdin() {
|
|
3023
3098
|
const urn = CapUrn.fromString('cap:in="media:string";test;out="media:string"');
|
|
@@ -3309,12 +3384,12 @@ function test6275_Machine_emptyInput() {
|
|
|
3309
3384
|
assertThrowsWithCode(() => parseMachine(''), MachineSyntaxErrorCodes.EMPTY);
|
|
3310
3385
|
}
|
|
3311
3386
|
|
|
3312
|
-
//
|
|
3387
|
+
// TEST6277: Machine whitespace only
|
|
3313
3388
|
function test6277_Machine_whitespaceOnly() {
|
|
3314
3389
|
assertThrowsWithCode(() => parseMachine(' \n \t '), MachineSyntaxErrorCodes.EMPTY);
|
|
3315
3390
|
}
|
|
3316
3391
|
|
|
3317
|
-
//
|
|
3392
|
+
// TEST6279: Machine header only no wirings
|
|
3318
3393
|
function test6279_Machine_headerOnlyNoWirings() {
|
|
3319
3394
|
assertThrowsWithCode(
|
|
3320
3395
|
() => Machine.fromString('[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]'),
|
|
@@ -3322,7 +3397,7 @@ function test6279_Machine_headerOnlyNoWirings() {
|
|
|
3322
3397
|
);
|
|
3323
3398
|
}
|
|
3324
3399
|
|
|
3325
|
-
//
|
|
3400
|
+
// TEST6280: Machine duplicate alias
|
|
3326
3401
|
function test6280_Machine_duplicateAlias() {
|
|
3327
3402
|
assertThrowsWithCode(
|
|
3328
3403
|
() => Machine.fromString(
|
|
@@ -3334,7 +3409,7 @@ function test6280_Machine_duplicateAlias() {
|
|
|
3334
3409
|
);
|
|
3335
3410
|
}
|
|
3336
3411
|
|
|
3337
|
-
//
|
|
3412
|
+
// TEST6286: Machine simple linear chain
|
|
3338
3413
|
function test6286_Machine_simpleLinearChain() {
|
|
3339
3414
|
const g = Machine.fromString(
|
|
3340
3415
|
'[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
|
|
@@ -3350,7 +3425,7 @@ function test6286_Machine_simpleLinearChain() {
|
|
|
3350
3425
|
assertEqual(edge.isLoop, false);
|
|
3351
3426
|
}
|
|
3352
3427
|
|
|
3353
|
-
//
|
|
3428
|
+
// TEST6288: Machine two step chain
|
|
3354
3429
|
function test6288_Machine_twoStepChain() {
|
|
3355
3430
|
const g = Machine.fromString(
|
|
3356
3431
|
'[extract cap:in="media:ext=pdf";extract;out="media:enc=utf-8;ext=txt"]' +
|
|
@@ -3365,7 +3440,7 @@ function test6288_Machine_twoStepChain() {
|
|
|
3365
3440
|
'Second edge target should be media:embedding-vector;enc=utf-8;record');
|
|
3366
3441
|
}
|
|
3367
3442
|
|
|
3368
|
-
//
|
|
3443
|
+
// TEST6290: Machine fan out
|
|
3369
3444
|
function test6290_Machine_fanOut() {
|
|
3370
3445
|
const g = Machine.fromString(
|
|
3371
3446
|
'[meta cap:in="media:ext=pdf";extract-metadata;out="media:enc=utf-8;file-metadata;record"]' +
|
|
@@ -3383,7 +3458,7 @@ function test6290_Machine_fanOut() {
|
|
|
3383
3458
|
}
|
|
3384
3459
|
}
|
|
3385
3460
|
|
|
3386
|
-
//
|
|
3461
|
+
// TEST6292: Machine fan in secondary assigned by prior wiring
|
|
3387
3462
|
function test6292_Machine_fanInSecondaryAssignedByPriorWiring() {
|
|
3388
3463
|
const g = Machine.fromString(
|
|
3389
3464
|
'[thumb cap:in="media:ext=pdf";generate-thumbnail;out="media:ext=png;image;thumbnail"]' +
|
|
@@ -3397,7 +3472,7 @@ function test6292_Machine_fanInSecondaryAssignedByPriorWiring() {
|
|
|
3397
3472
|
assertEqual(g.edges()[2].sources.length, 2);
|
|
3398
3473
|
}
|
|
3399
3474
|
|
|
3400
|
-
//
|
|
3475
|
+
// TEST6294: Machine fan in secondary unassigned gets wildcard
|
|
3401
3476
|
function test6294_Machine_fanInSecondaryUnassignedGetsWildcard() {
|
|
3402
3477
|
const g = Machine.fromString(
|
|
3403
3478
|
'[describe cap:in="media:ext=png;image";describe-image;out="media:enc=utf-8;image-description"]\n' +
|
|
@@ -5779,7 +5854,7 @@ function test6508_Renderer_buildRunGraphData_allFailedDropsTargetPlaceholder() {
|
|
|
5779
5854
|
// TEST6509: Renderer build run graph data unclosed foreach success no merge
|
|
5780
5855
|
function test6509_Renderer_buildRunGraphData_unclosedForeachSuccessNoMerge() {
|
|
5781
5856
|
// Strand without a Collect: [Disbind, ForEach, make_decision].
|
|
5782
|
-
// Under the
|
|
5857
|
+
// Under the floom-engine realize_strand semantics there's no Collect,
|
|
5783
5858
|
// so each body produces its OWN terminal output. Successful
|
|
5784
5859
|
// replicas do NOT merge into a shared target — each body has
|
|
5785
5860
|
// its own separate decision.
|
|
@@ -6963,8 +7038,6 @@ function assertThrowsPlanState(fn, message) {
|
|
|
6963
7038
|
throw new Error(`Expected PlanStateError but function succeeded: ${message}`);
|
|
6964
7039
|
}
|
|
6965
7040
|
|
|
6966
|
-
// TEST1450: a defaults-only request is AUTO with every knob at its default,
|
|
6967
|
-
// and the proto JSON carries the documented wire shape.
|
|
6968
7041
|
// TEST1514: the provenance vocabulary grows with installers. A workspace
|
|
6969
7042
|
// build install parses to its named value; a spelling this build does not
|
|
6970
7043
|
// know parses, is preserved VERBATIM, round-trips, and is not BUNDLE (the
|
|
@@ -6999,6 +7072,8 @@ function test1514_installSourceVocabularyTolerance() {
|
|
|
6999
7072
|
'unknown spellings round-trip');
|
|
7000
7073
|
}
|
|
7001
7074
|
|
|
7075
|
+
// TEST1450: a defaults-only request is AUTO with every knob at its default,
|
|
7076
|
+
// and the proto JSON carries the documented wire shape.
|
|
7002
7077
|
function test1450_planRequestDefaults() {
|
|
7003
7078
|
const request = new plannerNS.PlanRequest({
|
|
7004
7079
|
sources: [{ mediaUrn: 'media:ext=pdf', isSequence: false }],
|
|
@@ -7367,6 +7442,8 @@ async function runTests() {
|
|
|
7367
7442
|
runTest('TEST1295: rule11_non_void_input_without_stdin_rejected', test1295_rule11NonVoidInputWithoutStdinRejected);
|
|
7368
7443
|
runTest('TEST1296: rule11_void_input_cli_flag_only', test1296_rule11VoidInputCliFlagOnly);
|
|
7369
7444
|
runTest('TEST1297: rule11_non_void_input_with_stdin', test1297_rule11NonVoidInputWithStdin);
|
|
7445
|
+
runTest('TEST1953: rule14_streaming_only_on_main_input', test1953_rule14StreamingOnlyOnMainInput);
|
|
7446
|
+
runTest('TEST1964: unknown_definition_field_is_refused', test1964_unknownDefinitionFieldIsRefused);
|
|
7370
7447
|
|
|
7371
7448
|
// cap_urn.rs: TEST639-TEST653 (Cap URN wildcard tests)
|
|
7372
7449
|
console.log('\n--- cap_urn.rs (wildcard tests) ---');
|
|
@@ -7389,6 +7466,15 @@ async function runTests() {
|
|
|
7389
7466
|
runTest('TEST655: effect_declared_uses_declared_output', test126_effectDeclaredUsesDeclaredOutput);
|
|
7390
7467
|
runTest('TEST656: invalid_effect_none_fails_hard', test127_invalidEffectNoneFailsHard);
|
|
7391
7468
|
runTest('TEST657: effect_dispatch_requires_explicit_wildcard', test128_effectDispatchRequiresExplicitWildcard);
|
|
7469
|
+
runTest('TEST8150: registry_verdict_states_closed', test8150_registryVerdictStatesAreTheClosedWireVocabulary);
|
|
7470
|
+
runTest('TEST8151: chain_failure_classification', test8151_unreadableFormatIsUnverifiableAndRejectedKeyIsUntrusted);
|
|
7471
|
+
runTest('TEST8152: only_verified_permits_attachment', test8152_onlyVerifiedPermitsAttachment);
|
|
7472
|
+
runTest('TEST8153: trust_failures_never_transient', test8153_trustFailuresAreNeverTransient);
|
|
7473
|
+
runTest('TEST8154: contradictory_verdicts_refused', test8154_contradictoryVerdictsAreRefused);
|
|
7474
|
+
runTest('TEST8159: remedy_follows_from_state', test8159_theRemedyFollowsFromTheState);
|
|
7475
|
+
runTest('TEST8155: registry_verdict_wire_round_trip', test8155_registryVerdictWireRoundTrip);
|
|
7476
|
+
runTest('TEST8157: signature_formats_from_library', test8157_signatureFormatDiscriminatorsComeFromTheLibrary);
|
|
7477
|
+
runTest('TEST8158: attachment_kinds_separate_situations', test8158_attachmentKindsSeparateTheirSituations);
|
|
7392
7478
|
runTest('TEST8121: effect_conformance_declared_asymmetry', test8121_effectConformanceDeclaredAsymmetry);
|
|
7393
7479
|
runTest('TEST8122: effect_conformance_none_requires_equivalence', test8122_effectConformanceNoneRequiresEquivalence);
|
|
7394
7480
|
runTest('TEST8123: effect_conformance_patch_requires_patched_input', test8123_effectConformancePatchRequiresPatchedInput);
|
|
@@ -7639,6 +7725,235 @@ async function runTests() {
|
|
|
7639
7725
|
}
|
|
7640
7726
|
}
|
|
7641
7727
|
|
|
7728
|
+
|
|
7729
|
+
// ============================================================================
|
|
7730
|
+
// Registry verdicts — the vocabulary a consumer concludes about a REGISTRY
|
|
7731
|
+
// ============================================================================
|
|
7732
|
+
|
|
7733
|
+
/** TEST8150: the wire vocabulary is closed and matches the Rust mirror. */
|
|
7734
|
+
function test8150_registryVerdictStatesAreTheClosedWireVocabulary() {
|
|
7735
|
+
const expected = [
|
|
7736
|
+
'verified', 'pending', 'offline', 'unreachable',
|
|
7737
|
+
'http_error', 'malformed', 'unsigned', 'untrusted', 'unverifiable', 'unenforced',
|
|
7738
|
+
];
|
|
7739
|
+
assertEqual(REGISTRY_VERDICT_STATES.length, expected.length, 'state count matches the Rust mirror');
|
|
7740
|
+
for (const state of expected) {
|
|
7741
|
+
assert(REGISTRY_VERDICT_STATES.includes(state), `wire vocabulary carries '${state}'`);
|
|
7742
|
+
}
|
|
7743
|
+
const reasons = [
|
|
7744
|
+
'malformed_envelope', 'unsupported_envelope_format', 'malformed_certificate',
|
|
7745
|
+
'unsupported_certificate_format', 'empty_certificate_list', 'insufficient_root_signatures',
|
|
7746
|
+
'expired_certificate', 'not_yet_valid_certificate', 'environment_mismatch',
|
|
7747
|
+
'key_id_mismatch', 'no_authorizing_certificate', 'manifest_signature_invalid',
|
|
7748
|
+
];
|
|
7749
|
+
assertEqual(CHAIN_FAILURE_REASONS.length, reasons.length, 'reason count matches the Rust mirror');
|
|
7750
|
+
for (const reason of reasons) {
|
|
7751
|
+
assert(CHAIN_FAILURE_REASONS.includes(reason), `wire vocabulary carries '${reason}'`);
|
|
7752
|
+
}
|
|
7753
|
+
assertRefuses(() => registryVerdictPermitsAttachment('network_error'), /unknown registry verdict state/);
|
|
7754
|
+
}
|
|
7755
|
+
|
|
7756
|
+
/** TEST8151: a format this build cannot read is OUR limitation — never the
|
|
7757
|
+
* registry being untrustworthy, and never a network problem. */
|
|
7758
|
+
function test8151_unreadableFormatIsUnverifiableAndRejectedKeyIsUntrusted() {
|
|
7759
|
+
for (const unevaluable of [
|
|
7760
|
+
ChainFailureReason.MALFORMED_ENVELOPE,
|
|
7761
|
+
ChainFailureReason.UNSUPPORTED_ENVELOPE_FORMAT,
|
|
7762
|
+
ChainFailureReason.MALFORMED_CERTIFICATE,
|
|
7763
|
+
ChainFailureReason.UNSUPPORTED_CERTIFICATE_FORMAT,
|
|
7764
|
+
ChainFailureReason.EMPTY_CERTIFICATE_LIST,
|
|
7765
|
+
]) {
|
|
7766
|
+
assertEqual(
|
|
7767
|
+
registryVerdictStateForChainFailure(unevaluable),
|
|
7768
|
+
RegistryVerdictState.UNVERIFIABLE,
|
|
7769
|
+
`${unevaluable} could not be judged at all`,
|
|
7770
|
+
);
|
|
7771
|
+
}
|
|
7772
|
+
for (const judged of [
|
|
7773
|
+
ChainFailureReason.INSUFFICIENT_ROOT_SIGNATURES,
|
|
7774
|
+
ChainFailureReason.EXPIRED_CERTIFICATE,
|
|
7775
|
+
ChainFailureReason.NOT_YET_VALID_CERTIFICATE,
|
|
7776
|
+
ChainFailureReason.ENVIRONMENT_MISMATCH,
|
|
7777
|
+
ChainFailureReason.KEY_ID_MISMATCH,
|
|
7778
|
+
ChainFailureReason.NO_AUTHORIZING_CERTIFICATE,
|
|
7779
|
+
ChainFailureReason.MANIFEST_SIGNATURE_INVALID,
|
|
7780
|
+
]) {
|
|
7781
|
+
assertEqual(
|
|
7782
|
+
registryVerdictStateForChainFailure(judged),
|
|
7783
|
+
RegistryVerdictState.UNTRUSTED,
|
|
7784
|
+
`${judged} is a judgement that WAS reached`,
|
|
7785
|
+
);
|
|
7786
|
+
}
|
|
7787
|
+
assertRefuses(() => registryVerdictStateForChainFailure('bad_signature'), /unknown chain failure reason/);
|
|
7788
|
+
}
|
|
7789
|
+
|
|
7790
|
+
/** TEST8152: only a verified registry lets a cartridge attach — `pending`
|
|
7791
|
+
* included, which must never read as permission. */
|
|
7792
|
+
function test8152_onlyVerifiedPermitsAttachment() {
|
|
7793
|
+
for (const state of REGISTRY_VERDICT_STATES) {
|
|
7794
|
+
assertEqual(
|
|
7795
|
+
registryVerdictPermitsAttachment(state),
|
|
7796
|
+
state === RegistryVerdictState.VERIFIED || state === RegistryVerdictState.UNENFORCED,
|
|
7797
|
+
`permits_attachment('${state}')`,
|
|
7798
|
+
);
|
|
7799
|
+
}
|
|
7800
|
+
// A DEV BUILD HAS TO WORK, and it says which of the two it is: "we checked
|
|
7801
|
+
// and it passed" and "we did not check" are different facts.
|
|
7802
|
+
assert(registryVerdictPermitsAttachment(RegistryVerdictState.UNENFORCED));
|
|
7803
|
+
assert(!registryVerdictIsTrustFailure(RegistryVerdictState.UNENFORCED));
|
|
7804
|
+
assert(!registryVerdictIsTransient(RegistryVerdictState.UNENFORCED));
|
|
7805
|
+
assertEqual(registryVerdictRemedy(RegistryVerdictState.UNENFORCED), RegistryRemedy.NONE);
|
|
7806
|
+
}
|
|
7807
|
+
|
|
7808
|
+
/** TEST8153: a refusal never resolves itself, so nothing may present it as
|
|
7809
|
+
* worth retrying. */
|
|
7810
|
+
function test8153_trustFailuresAreNeverTransient() {
|
|
7811
|
+
for (const state of REGISTRY_VERDICT_STATES) {
|
|
7812
|
+
assert(
|
|
7813
|
+
!(registryVerdictIsTrustFailure(state) && registryVerdictIsTransient(state)),
|
|
7814
|
+
`'${state}' cannot be both a refusal and something a retry could fix`,
|
|
7815
|
+
);
|
|
7816
|
+
}
|
|
7817
|
+
assert(registryVerdictIsTrustFailure(RegistryVerdictState.UNVERIFIABLE));
|
|
7818
|
+
assert(registryVerdictIsTrustFailure(RegistryVerdictState.UNTRUSTED));
|
|
7819
|
+
assert(registryVerdictIsTrustFailure(RegistryVerdictState.UNSIGNED));
|
|
7820
|
+
assert(registryVerdictIsTransient(RegistryVerdictState.UNREACHABLE));
|
|
7821
|
+
assert(registryVerdictIsTransient(RegistryVerdictState.PENDING));
|
|
7822
|
+
// Policy is not transient: it holds until an operator changes it.
|
|
7823
|
+
assert(!registryVerdictIsTransient(RegistryVerdictState.OFFLINE));
|
|
7824
|
+
assert(!registryVerdictIsTrustFailure(RegistryVerdictState.OFFLINE));
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
/** TEST8159: the remedy follows from the state, and "check the network" is
|
|
7828
|
+
* reachable from exactly one state. That sentence used to be appended to every
|
|
7829
|
+
* held-cartridge message whatever the cause, which is how a signature format a
|
|
7830
|
+
* build could not read sent operators to their router. */
|
|
7831
|
+
function test8159_theRemedyFollowsFromTheState() {
|
|
7832
|
+
const network = REGISTRY_VERDICT_STATES.filter(
|
|
7833
|
+
(state) => registryVerdictRemedy(state) === RegistryRemedy.CHECK_NETWORK,
|
|
7834
|
+
);
|
|
7835
|
+
assertEqual(network.length, 1, 'exactly one state is a network problem');
|
|
7836
|
+
assertEqual(network[0], RegistryVerdictState.UNREACHABLE);
|
|
7837
|
+
for (const state of REGISTRY_VERDICT_STATES) {
|
|
7838
|
+
if (!registryVerdictIsTrustFailure(state)) continue;
|
|
7839
|
+
const remedy = registryVerdictRemedy(state);
|
|
7840
|
+
assert(
|
|
7841
|
+
remedy === RegistryRemedy.DO_NOT_PROCEED || remedy === RegistryRemedy.UPDATE_CLIENT,
|
|
7842
|
+
`'${state}' is a refusal; its remedy must not be a retry (got '${remedy}')`,
|
|
7843
|
+
);
|
|
7844
|
+
}
|
|
7845
|
+
// The one that was misclassified: our limitation, so update the client —
|
|
7846
|
+
// never distrust the registry, never touch the network.
|
|
7847
|
+
assertEqual(registryVerdictRemedy(RegistryVerdictState.UNVERIFIABLE), RegistryRemedy.UPDATE_CLIENT);
|
|
7848
|
+
assertEqual(registryVerdictRemedy(RegistryVerdictState.UNTRUSTED), RegistryRemedy.DO_NOT_PROCEED);
|
|
7849
|
+
assertEqual(registryVerdictRemedy(RegistryVerdictState.VERIFIED), RegistryRemedy.NONE);
|
|
7850
|
+
assertEqual(registryVerdictRemedy(RegistryVerdictState.PENDING), RegistryRemedy.WAIT);
|
|
7851
|
+
// Policy is the operator's setting, not their router.
|
|
7852
|
+
assertEqual(registryVerdictRemedy(RegistryVerdictState.OFFLINE), RegistryRemedy.CHANGE_NETWORK_POLICY);
|
|
7853
|
+
assertRefuses(() => registryVerdictRemedy('flaky'), /unknown registry verdict state/);
|
|
7854
|
+
}
|
|
7855
|
+
|
|
7856
|
+
/** TEST8154: illegal states are unrepresentable — every contradiction is
|
|
7857
|
+
* refused at construction. */
|
|
7858
|
+
function test8154_contradictoryVerdictsAreRefused() {
|
|
7859
|
+
const URL = 'https://cartridges.example/v1/manifest';
|
|
7860
|
+
const now = 1700000000;
|
|
7861
|
+
assertRefuses(
|
|
7862
|
+
() => RegistryVerdict.stated(URL, RegistryVerdictState.UNREACHABLE, '', now),
|
|
7863
|
+
/must carry the detail/,
|
|
7864
|
+
);
|
|
7865
|
+
assertRefuses(
|
|
7866
|
+
() => RegistryVerdict.stated(URL, RegistryVerdictState.VERIFIED, 'all good', now),
|
|
7867
|
+
/states no failure/,
|
|
7868
|
+
);
|
|
7869
|
+
assertRefuses(
|
|
7870
|
+
() => RegistryVerdict.stated(URL, RegistryVerdictState.HTTP_ERROR, '500', now),
|
|
7871
|
+
/must carry the status/,
|
|
7872
|
+
);
|
|
7873
|
+
assertRefuses(
|
|
7874
|
+
() => RegistryVerdict.stated(URL, RegistryVerdictState.UNTRUSTED, 'nope', now),
|
|
7875
|
+
/must carry the chain failure reason/,
|
|
7876
|
+
);
|
|
7877
|
+
assertRefuses(
|
|
7878
|
+
() => RegistryVerdict.stated('', RegistryVerdictState.UNREACHABLE, 'timeout', now),
|
|
7879
|
+
/must name the registry/,
|
|
7880
|
+
);
|
|
7881
|
+
// A reason that contradicts the state it is filed under.
|
|
7882
|
+
assertRefuses(
|
|
7883
|
+
() => new RegistryVerdict(URL, RegistryVerdictState.UNTRUSTED, 'x', null,
|
|
7884
|
+
ChainFailureReason.UNSUPPORTED_ENVELOPE_FORMAT, now),
|
|
7885
|
+
/chain failure reason/,
|
|
7886
|
+
);
|
|
7887
|
+
// A status on a state that never answered.
|
|
7888
|
+
assertRefuses(
|
|
7889
|
+
() => new RegistryVerdict(URL, RegistryVerdictState.UNREACHABLE, 'timeout', 404, null, now),
|
|
7890
|
+
/only an 'http_error' verdict carries an HTTP status/,
|
|
7891
|
+
);
|
|
7892
|
+
}
|
|
7893
|
+
|
|
7894
|
+
/** TEST8155: the wire form round-trips with its invariants intact. */
|
|
7895
|
+
function test8155_registryVerdictWireRoundTrip() {
|
|
7896
|
+
const URL = 'https://cartridges.example/v1/manifest';
|
|
7897
|
+
const now = 1700000000;
|
|
7898
|
+
const verdict = RegistryVerdict.chainFailed(
|
|
7899
|
+
URL,
|
|
7900
|
+
ChainFailureReason.UNSUPPORTED_ENVELOPE_FORMAT,
|
|
7901
|
+
"envelope format 'other/1' is not implemented by this build",
|
|
7902
|
+
now,
|
|
7903
|
+
);
|
|
7904
|
+
assertEqual(verdict.state, RegistryVerdictState.UNVERIFIABLE);
|
|
7905
|
+
const decoded = RegistryVerdict.fromJSON(JSON.parse(JSON.stringify(verdict.toJSON())));
|
|
7906
|
+
assertEqual(decoded.state, verdict.state);
|
|
7907
|
+
assertEqual(decoded.chain_failure, verdict.chain_failure);
|
|
7908
|
+
assertEqual(decoded.detail, verdict.detail);
|
|
7909
|
+
assertEqual(decoded.registry_url, verdict.registry_url);
|
|
7910
|
+
assertEqual(decoded.permitsAttachment(), false);
|
|
7911
|
+
|
|
7912
|
+
const http = RegistryVerdict.httpError(URL, 404, 'registry answered HTTP 404', now);
|
|
7913
|
+
assertEqual(http.http_status, 404);
|
|
7914
|
+
assertEqual(RegistryVerdict.fromJSON(http.toJSON()).http_status, 404);
|
|
7915
|
+
|
|
7916
|
+
assertRefuses(
|
|
7917
|
+
() => RegistryVerdict.fromJSON({
|
|
7918
|
+
registry_url: URL, state: 'http_error', detail: 'answered badly',
|
|
7919
|
+
http_status: null, chain_failure: null, checked_at_unix_seconds: now,
|
|
7920
|
+
}),
|
|
7921
|
+
/must carry the status/,
|
|
7922
|
+
);
|
|
7923
|
+
assertRefuses(
|
|
7924
|
+
() => RegistryVerdict.fromJSON({
|
|
7925
|
+
registry_url: URL, state: 'flaky', detail: 'hm',
|
|
7926
|
+
http_status: null, chain_failure: null, checked_at_unix_seconds: now,
|
|
7927
|
+
}),
|
|
7928
|
+
/unknown registry verdict state/,
|
|
7929
|
+
);
|
|
7930
|
+
}
|
|
7931
|
+
|
|
7932
|
+
/** TEST8157: the format discriminators are the library's, so no consumer can
|
|
7933
|
+
* hold a divergent copy. A product rename that edits a client's private
|
|
7934
|
+
* constant makes that client verify nothing while every other implementation
|
|
7935
|
+
* keeps working — which is precisely what happened. */
|
|
7936
|
+
function test8157_signatureFormatDiscriminatorsComeFromTheLibrary() {
|
|
7937
|
+
assertEqual(MANIFEST_SIG_FORMAT, 'machinefabric-manifest-sig/1');
|
|
7938
|
+
assertEqual(RELEASE_KEY_CERT_FORMAT, 'machinefabric-release-key-cert/1');
|
|
7939
|
+
}
|
|
7940
|
+
|
|
7941
|
+
/** TEST8158: the attachment vocabulary names the situations apart. */
|
|
7942
|
+
function test8158_attachmentKindsSeparateTheirSituations() {
|
|
7943
|
+
assertEqual(CartridgeAttachmentErrorKind.MISPLACED, 'misplaced');
|
|
7944
|
+
assertEqual(CartridgeAttachmentErrorKind.NOT_LISTED, 'not_listed');
|
|
7945
|
+
assertEqual(CartridgeAttachmentErrorKind.REGISTRY_UNVERIFIED, 'registry_unverified');
|
|
7946
|
+
assert(
|
|
7947
|
+
CartridgeAttachmentErrorKind.BAD_INSTALLATION === undefined,
|
|
7948
|
+
'bad_installation meant two things with different remedies and is gone',
|
|
7949
|
+
);
|
|
7950
|
+
assert(
|
|
7951
|
+
CartridgeAttachmentErrorKind.REGISTRY_UNREACHABLE === undefined,
|
|
7952
|
+
'registry_unreachable claimed a cause it could not know and is gone',
|
|
7953
|
+
);
|
|
7954
|
+
}
|
|
7955
|
+
|
|
7956
|
+
|
|
7642
7957
|
// Run the tests
|
|
7643
7958
|
if (require.main === module) {
|
|
7644
7959
|
runTests()
|
package/package.json
CHANGED
package/planner.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Unified configurable planner — the plan-space vocabulary for JS clients.
|
|
2
2
|
//
|
|
3
3
|
// Mirrors core Rust `capdag/src/planner/plan_space.rs` and the proto surface
|
|
4
|
-
// (`
|
|
4
|
+
// (`floom-engine/proto/cap.proto` PlanMachines/DiscoverConvergentTargets): the knob
|
|
5
5
|
// enums, the request shape with the same validation the engine applies, and
|
|
6
6
|
// parsers for the proto-JSON response shapes (ts-proto `snakeToCamel=false`,
|
|
7
|
-
// so wire field names are snake_case). This is what lets
|
|
7
|
+
// so wire field names are snake_case). This is what lets floom-uikit represent
|
|
8
8
|
// and manipulate the FULL planner state — knobs, ranked candidates, apexes —
|
|
9
9
|
// both standalone (fixtures) and fed over gRPC by the desktop clients.
|
|
10
10
|
//
|