capdag 1.249.12 → 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/README.md CHANGED
@@ -1,131 +1,64 @@
1
- # Cap URN - JavaScript Implementation
1
+ # CapDAG for JavaScript
2
2
 
3
- JavaScript implementation of Cap URN (Capability Uniform Resource Names), built on [Tagged URN](https://github.com/machinefabric/tagged-urn-js).
3
+ This public package is CapDAG's JavaScript planning and notation mirror. Use it
4
+ in browsers or Node.js for Tagged, Media, and Cap URNs, capability definitions,
5
+ dispatch, Machine Notation, planning, and graph rendering.
4
6
 
5
- ## Features
7
+ JavaScript intentionally does not implement the cartridge runtime, host, relay,
8
+ or Bifaci process surface. That boundary is part of the package design, not an
9
+ unreported parity gap. Rust remains the behavioral reference for shared
10
+ features.
6
11
 
7
- - **Required Direction Specifiers** - `in`/`out` tags for input/output media types
8
- - **Media URN Validation** - Validates direction spec values are valid Media URNs
9
- - **Special Pattern Values** - `*` (must-have-any), `?` (unspecified), `!` (must-not-have)
10
- - **Graded Specificity** - Exact values score higher than wildcards
11
- - **Cross-Language Compatible** - Identical behavior to Rust, Go, and Objective-C implementations
12
- - **Production Ready** - No fallbacks, fails hard on invalid input
13
-
14
- ## Installation
12
+ ## Install the package
15
13
 
16
14
  ```bash
17
15
  npm install capdag
18
16
  ```
19
17
 
20
- ## Quick Start
18
+ Node.js 14 or newer is required by the package manifest.
21
19
 
22
- ```javascript
23
- const { CapUrn, CapUrnBuilder, CapMatcher } = require('capdag');
20
+ ## Parse and build Cap URNs
24
21
 
25
- // Create from string (with required direction specifiers)
26
- const cap = CapUrn.fromString('cap:in="media:binary";extract;out="media:object"');
27
- console.log(cap.toString());
22
+ ```javascript
23
+ const { CapUrn, CapUrnBuilder } = require("capdag");
28
24
 
29
- // Use builder pattern
25
+ const parsed = CapUrn.fromString(
26
+ 'cap:disbind;in="media:ext=pdf";out="media:enc=utf-8;page"'
27
+ );
30
28
  const built = new CapUrnBuilder()
31
- .inSpec('media:void')
32
- .outSpec('media:object')
33
- .tag('op', 'generate')
34
- .tag('target', 'thumbnail')
35
- .build();
36
-
37
- // Matching
38
- const request = CapUrn.fromString('cap:in="media:binary";extract;out="media:object"');
39
- console.log(cap.accepts(request)); // true
40
-
41
- // Find best match by specificity
42
- const caps = [
43
- CapUrn.fromString('cap:in=*;extract;out=*'),
44
- CapUrn.fromString('cap:in="media:binary";extract;out="media:object"'),
45
- CapUrn.fromString('cap:ext=pdf;in="media:binary";extract;out="media:object"')
46
- ];
47
- const best = CapMatcher.findBestMatch(caps, request);
48
- console.log(best.toString()); // Most specific match
49
- ```
50
-
51
- ## API Reference
52
-
53
- ### CapUrn Class
54
-
55
- #### Static Methods
56
- - `CapUrn.fromString(s)` - Parse Cap URN from string
57
- - Throws `CapUrnError` on invalid format or missing direction specifiers
58
-
59
- #### Instance Methods
60
- - `toString()` - Get canonical string representation
61
- - `getTag(key)` - Get tag value (case-insensitive)
62
- - `getInSpec()` - Get input media type
63
- - `getOutSpec()` - Get output media type
64
- - `hasTag(key, value)` - Check if tag exists with value
65
- - `withTag(key, value)` - Add/update tag (returns new instance)
66
- - `withoutTag(key)` - Remove tag (returns new instance)
67
- - `accepts(request)` - Check if this cap (as pattern) accepts a request
68
- - `specificity()` - Get specificity score for matching
69
- - `isMoreSpecificThan(other)` - Compare specificity
70
- - `equals(other)` - Check equality
71
-
72
- ### CapUrnBuilder Class
73
-
74
- Fluent builder for constructing Cap URNs:
75
-
76
- ```javascript
77
- const cap = new CapUrnBuilder()
78
- .inSpec('media:binary')
79
- .outSpec('media:object')
80
- .tag('op', 'extract')
81
- .tag('target', 'metadata')
29
+ .inSpec("media:ext=pdf")
30
+ .outSpec("media:enc=utf-8;page")
31
+ .marker("disbind")
82
32
  .build();
83
- ```
84
33
 
85
- ### CapMatcher Class
86
-
87
- Utility for matching sets of caps:
88
-
89
- - `CapMatcher.findBestMatch(caps, request)` - Find most specific match
90
- - `CapMatcher.findAllMatches(caps, request)` - Find all matches (sorted by specificity)
91
-
92
- ### Error Handling
93
-
94
- ```javascript
95
- const { CapUrnError, ErrorCodes } = require('capdag');
96
-
97
- try {
98
- const cap = CapUrn.fromString('cap:extract;in=media:;out=media:'); // Missing in/out
99
- } catch (error) {
100
- if (error instanceof CapUrnError) {
101
- console.log(`Error code: ${error.code}`); // MISSING_IN_SPEC
102
- }
103
- }
34
+ console.log(parsed.toString() === built.toString());
104
35
  ```
105
36
 
106
- Cap-specific error codes:
107
- - `ErrorCodes.MISSING_IN_SPEC` - Missing required `in` tag
108
- - `ErrorCodes.MISSING_OUT_SPEC` - Missing required `out` tag
109
- - `ErrorCodes.INVALID_MEDIA_URN` - Invalid Media URN in direction spec
37
+ Treat URNs as opaque parsed values. Use the package's predicates for
38
+ equivalence, conformance, dispatch, and ranking instead of string surgery.
110
39
 
111
- For base Tagged URN error codes, see [Tagged URN documentation](https://github.com/machinefabric/tagged-urn-js).
40
+ ## Find the relevant API
112
41
 
113
- ## Documentation
42
+ - `capdag.js` provides URNs, definitions, dispatch, and matching.
43
+ - `machine-parser.js` is generated from `machine.pegjs`.
44
+ - `planner.js` provides planning and Machine Notation structures.
45
+ - `cap-fab-renderer.js` and the browser build support graph presentation.
46
+ - [`RULES.md`](RULES.md) records package-specific construction rules.
114
47
 
115
- - [RULES.md](./RULES.md) - Cap-specific rules
116
- - [Tagged URN RULES.md](https://github.com/machinefabric/tagged-urn-js/blob/main/RULES.md) - Base format rules (case, quoting, wildcards, etc.)
48
+ The normative semantics and terminology live in the
49
+ [CapDAG specification](https://github.com/machinefabric/capdag/blob/main/docs/01-overview.md). Source comments and
50
+ exports are the JavaScript API reference.
117
51
 
118
- ## Testing
52
+ ## Build and verify changes
119
53
 
120
54
  ```bash
55
+ npm run build:parser
121
56
  npm test
122
57
  ```
123
58
 
124
- ## Cross-Language Compatibility
59
+ `npm test` runs the parser build first. Shared behavior changes require the
60
+ applicable reference test with the same substantive number and assertions.
125
61
 
126
- This JavaScript implementation produces identical results to:
127
- - [Rust reference implementation](https://github.com/machinefabric/capdag)
128
- - [Go implementation](https://github.com/machinefabric/capdag-go)
129
- - [Objective-C implementation](https://github.com/machinefabric/capdag-objc)
62
+ ## License
130
63
 
131
- All implementations pass the same test cases and follow identical rules.
64
+ MIT
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, machfab-mac
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
  //
@@ -2234,8 +2234,7 @@ function buildRunGraphData(data) {
2234
2234
  };
2235
2235
  }
2236
2236
 
2237
- // Expanding. The plan mirrors the working
2238
- // `machfab-mac/.scrap/working_graph/.../RunGraphViewer.html`:
2237
+ // Expanding. The plan uses the renderer's established fan-out layout:
2239
2238
  //
2240
2239
  // pre-foreach backbone ... → [anchor]
2241
2240
  // │
@@ -2448,7 +2447,7 @@ function buildRunGraphData(data) {
2448
2447
 
2449
2448
  // Successful bodies merge into the collect target ONLY when a
2450
2449
  // Collect closes the foreach body. Without a Collect (the
2451
- // common "unclosed foreach" case in machfab realize_strand),
2450
+ // common "unclosed foreach" case in floom-engine realize_strand),
2452
2451
  // replicas terminate at their last body step — each body has
2453
2452
  // its own separate output, no shared merge.
2454
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
- BAD_INSTALLATION: 'bad_installation',
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 but the host could not
5391
- // reach that registry to verify the cartridge is listed.
5392
- REGISTRY_UNREACHABLE: 'registry_unreachable',
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 `machfab-mac/docs/cartridge state machine.md`.
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 BAD_INSTALLATION.
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 BAD_INSTALLATION and everything else
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 {
@@ -6155,7 +6521,6 @@ function encodeHelloFrame() {
6155
6521
  ['max_reorder_buffer', BIFACI_DEFAULT_MAX_REORDER_BUFFER],
6156
6522
  ['initial_credit', BIFACI_DEFAULT_INITIAL_CREDIT],
6157
6523
  ['version', BIFACI_PROTOCOL_VERSION],
6158
- ['handler_capacity', 0],
6159
6524
  ],
6160
6525
  };
6161
6526
  const frame = {
@@ -6270,9 +6635,21 @@ function probeCartridgeCapGroups(entryPath) {
6270
6635
  finish(new Error(`cartridge ${entryPath} HELLO failed: meta.version ${metaVersion} does not match required version ${BIFACI_PROTOCOL_VERSION}`));
6271
6636
  return;
6272
6637
  }
6273
- const handlerCapacity = meta.get('handler_capacity');
6274
- if (!Number.isSafeInteger(handlerCapacity) || handlerCapacity < 0) {
6275
- finish(new Error(`cartridge ${entryPath} HELLO failed: missing required non-negative handler_capacity`));
6638
+ // The concurrency-pool state map is MANDATORY on a cartridge HELLO
6639
+ // (bifaci pools, protocol v4): JSON bytes under meta key "pools". A
6640
+ // missing or malformed map is a protocol violation, never a default.
6641
+ const poolsVal = meta.get('pools');
6642
+ if (!poolsVal || poolsVal.__bytes === undefined) {
6643
+ finish(new Error(`cartridge ${entryPath} HELLO failed: missing required concurrency-pool state map`));
6644
+ return;
6645
+ }
6646
+ try {
6647
+ const poolStates = JSON.parse(Buffer.from(poolsVal.__bytes).toString('utf8'));
6648
+ if (poolStates === null || typeof poolStates !== 'object' || Array.isArray(poolStates)) {
6649
+ throw new Error('pool-state map must be a JSON object');
6650
+ }
6651
+ } catch (e) {
6652
+ finish(new Error(`cartridge ${entryPath} HELLO failed: malformed pool-state map: ${e.message}`));
6276
6653
  return;
6277
6654
  }
6278
6655
  const manifestVal = meta.get('manifest');
@@ -6405,7 +6782,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6405
6782
  cj = CartridgeJson.readFromDir(versionDir, expectedSlug);
6406
6783
  } catch (e) {
6407
6784
  const kind = (e instanceof CartridgeJsonError && e.kind === CartridgeJsonErrorKind.REGISTRY_SLUG_MISMATCH)
6408
- ? CartridgeAttachmentErrorKind.BAD_INSTALLATION
6785
+ ? CartridgeAttachmentErrorKind.MISPLACED
6409
6786
  : CartridgeAttachmentErrorKind.MANIFEST_INVALID;
6410
6787
  discovered.push(DiscoveredCartridge.incompatible({
6411
6788
  version_dir: versionDir,
@@ -6430,7 +6807,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6430
6807
  registry_url: cj.registryUrl,
6431
6808
  version: cj.version,
6432
6809
  error: new CartridgeAttachmentError(
6433
- CartridgeAttachmentErrorKind.BAD_INSTALLATION,
6810
+ CartridgeAttachmentErrorKind.MISPLACED,
6434
6811
  `Channel mismatch: cartridge declares '${cj.channel}' but host is pinned to '${identity.channel}'. Release and nightly artefacts must not mix.`,
6435
6812
  detectedAt
6436
6813
  ),
@@ -6507,7 +6884,7 @@ async function scanChannelRoot(scanRoot, expectedSlug, identity, discovered) {
6507
6884
  registry_url: cj.registryUrl,
6508
6885
  version: cj.version,
6509
6886
  error: new CartridgeAttachmentError(
6510
- CartridgeAttachmentErrorKind.BAD_INSTALLATION,
6887
+ CartridgeAttachmentErrorKind.MISPLACED,
6511
6888
  `bundled cartridge integrity check failed: ${reason}`,
6512
6889
  detectedAt
6513
6890
  ),
@@ -8053,6 +8430,21 @@ const planner = require('./planner.js');
8053
8430
 
8054
8431
  // Export for CommonJS
8055
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,
8056
8448
  // Planner plan-space vocabulary
8057
8449
  PlanStateError: planner.PlanStateError,
8058
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
- // TEST0070: J s get extension mappings
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
- // TEST0073: J s resolve media urn from specs
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.BAD_INSTALLATION);
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.BAD_INSTALLATION);
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
- // TEST0088: Machine whitespace only
3387
+ // TEST6277: Machine whitespace only
3313
3388
  function test6277_Machine_whitespaceOnly() {
3314
3389
  assertThrowsWithCode(() => parseMachine(' \n \t '), MachineSyntaxErrorCodes.EMPTY);
3315
3390
  }
3316
3391
 
3317
- // TEST0089: Machine header only no wirings
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
- // TEST0090: Machine duplicate alias
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
- // TEST0094: Machine simple linear chain
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
- // TEST0095: Machine two step chain
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
- // TEST0096: Machine fan out
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
- // TEST0097: Machine fan in secondary assigned by prior wiring
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
- // TEST0098: Machine fan in secondary unassigned gets wildcard
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 machfab realize_strand semantics there's no Collect,
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
@@ -41,5 +41,5 @@
41
41
  "pretest": "npm run build:parser",
42
42
  "test": "node capdag.test.js"
43
43
  },
44
- "version": "1.249.12"
44
+ "version": "1.266.66"
45
45
  }
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
- // (`machfab/proto/cap.proto` PlanMachines/DiscoverConvergentTargets): the knob
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 fabricloom represent
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
  //