@byre/vellum 1.0.0

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/dist/spec.md ADDED
@@ -0,0 +1,1292 @@
1
+ # Vellum Specification v2.0.0 RC3
2
+
3
+ **Version**: 1.0.0
4
+ **Date**: January 30, 2026
5
+ **Status**: Release Candidate 4
6
+
7
+ ## 1. Purpose and Scope
8
+
9
+ ### 1.1 What This Specification Defines
10
+
11
+ Vellum is a specification for mod loaders in web applications. It defines:
12
+
13
+ - Host and mod custom element interfaces
14
+ - Mod discovery and loading behavior
15
+ - Dependency resolution
16
+ - Lifecycle events
17
+
18
+ A conforming implementation provides a mod loader that adheres to this specification.
19
+
20
+ ### 1.2 What This Specification Does Not Define
21
+
22
+ This specification does not define:
23
+
24
+ - Action/event systems (ecosystem mods MAY provide these)
25
+ - Rendering or layout management
26
+ - State management
27
+ - Logging or debugging utilities
28
+ - Any application-level functionality
29
+
30
+ These capabilities MAY be provided by mods and are not part of the kernel.
31
+
32
+ ### 1.3 Conformance
33
+
34
+ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
35
+ "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
36
+ document are to be interpreted as described in RFC 2119.
37
+
38
+ ### 1.4 Design Philosophy
39
+
40
+ Vellum is a kernel, not a framework. It loads mods and coordinates their
41
+ initialization. Everything else is delegated to mods.
42
+
43
+ **The DOM is the source of truth.** Wherever possible, Vellum state is
44
+ expressed through DOM structure and element attributes, not internal
45
+ JavaScript state. For example:
46
+
47
+ - **Add a mod element to the DOM** → mod loads
48
+ - **Remove mod element from DOM** → mod unloads
49
+ - **`disabled` attribute** → mod skipped/unloaded
50
+ - **`src` attribute changes** → mod reloads with new source
51
+ - **`loading` attribute** → mod is loading
52
+ - **`loaded` attribute** → mod loaded successfully
53
+ - **`failed` attribute** → mod failed to load
54
+
55
+ Inspecting the document reveals application structure without tracing
56
+ JavaScript state. DevTools, CSS selectors, and mutation observers can all
57
+ interact with mod lifecycle.
58
+
59
+ This declarative approach has practical benefits:
60
+
61
+ - Server-rendered HTML can include mod elements
62
+ - CSS can target mod states (`my-mod[loading] { opacity: 0.5; }`)
63
+ - Tests can assert on DOM attributes
64
+ - Debugging requires only element inspection
65
+
66
+ ### 1.5 Architecture
67
+
68
+ Vellum provides two custom elements:
69
+
70
+ - **`<vellum-app>`** provides a host element for mod discovery and loading.
71
+ - **`<vellum-mod>`** is a mod element, providing a source and dependencies.
72
+
73
+ The tag names are configurable.
74
+
75
+ ## 2. Definitions
76
+
77
+ - **Dependency** A mod that must be loaded before another mod can load.
78
+ - **Host element** The custom element that manages mod elements (e.g., `<vellum-app>`)
79
+ - **Lifecycle event** A CustomEvent dispatched by the host element to signal state changes.
80
+ - **Mod element** A custom element representing a module. Describes source and dependencies.
81
+ - **Mod module** The imported module object returned by `import()`
82
+ - **Mod script** The ES module file referenced by `src`
83
+ - **Priority** A number used to determine load order among peer mods. Lower values load first.
84
+
85
+ ## 3. Host Element
86
+
87
+ ### 3.1 Class Export
88
+
89
+ A conforming implementation MUST export a host element class that extends `HTMLElement`.
90
+
91
+ The implementation MUST NOT call `customElements.define()`. Tag name registration is the consumer's responsibility.
92
+
93
+ **Example Invocation**
94
+
95
+ _In your JavaScript_
96
+ ```javascript
97
+ import { VellumApp } from '@byre/vellum';
98
+ customElements.define('vellum-app', VellumApp);
99
+ ```
100
+
101
+ _In your HTML_
102
+ ```html
103
+ <vellum-app></vellum-app>
104
+ ```
105
+
106
+ ### 3.2 Attributes
107
+
108
+ | Attribute | Required | Default | Description |
109
+ |-----------|----------|--------------|----------------------------------------|
110
+ | `mod-tag` | No | `vellum-mod` | Tag name used to discover mod elements |
111
+ | `debug` | No | — | If present, implementation MAY emit debug events |
112
+
113
+ ### 3.3 Behavior
114
+
115
+ When connected to the DOM, the host element MUST:
116
+
117
+ 1. Wait for descendant elements to be parsed
118
+ 2. Discover all descendant mod elements matching `mod-tag`, excluding:
119
+ - Elements with `disabled` attribute
120
+ - Elements that have a closer ancestor host element (see Section 3.6)
121
+ 3. Resolve dependencies and determine load order (see Section 5)
122
+ 4. Load each mod element in resolved order (see Section 3.5)
123
+ - Mod elements with all dependencies resolved MAY be loaded in parallel
124
+ 5. Begin observing for mod element additions, removals, and attribute changes
125
+
126
+ When a mod element is added to the host's subtree after initial load:
127
+
128
+ 1. The host MUST detect the addition via MutationObserver
129
+ 2. If the mod element has `disabled` attribute, the host MUST skip it
130
+ 3. If the mod element has a closer ancestor host, the host MUST skip it
131
+ 4. The host MUST resolve dependencies against already-loaded mod elements
132
+ 5. The host MUST load the new mod element
133
+ 6. If the mod element's hard dependencies cannot be satisfied, the host MUST
134
+ set the `failed` attribute and dispatch `vellum:mod-failed` with a
135
+ `VellumDependencyError`
136
+
137
+ When a mod element is removed from the host's subtree:
138
+
139
+ 1. The host MUST detect the removal via MutationObserver
140
+ 2. If the mod element has `loaded` attribute, the host MUST unload it (see Section 3.5)
141
+
142
+ When a mod element's attributes change:
143
+
144
+ 1. The host MUST observe attribute changes via MutationObserver for at least
145
+ `src` and `disabled` attributes
146
+ 2. If `src` changes on a mod element with `loaded` attribute:
147
+ - The host MUST unload the current module
148
+ - The host MUST load from the new source
149
+ 3. If `src` changes on a mod element with `loading` attribute:
150
+ - The host MUST abort the current load (via AbortSignal)
151
+ - The host MUST load from the new source
152
+ 4. If `disabled` is added to a mod element with `loaded` attribute:
153
+ - The host MUST unload the module
154
+ 5. If `disabled` is removed from a mod element without `loaded` or `loading`:
155
+ - The host MUST initiate loading (respecting dependencies)
156
+
157
+ When disconnected from the DOM, the host element MUST:
158
+
159
+ 1. Stop observing for mod changes
160
+ 2. Dispatch `vellum:teardown` event
161
+
162
+ ### 3.4 Events Dispatched
163
+
164
+ | Event | Detail | When |
165
+ |---------------------------|--------------------------------------|--------------------------|
166
+ | `vellum:loading-started` | `{ total: number }` | Before first mod loads |
167
+ | `vellum:loading-complete` | `{ loaded: number, failed: number }` | After all mods processed |
168
+ | `vellum:teardown` | `{}` | On disconnect |
169
+ | `vellum:debug` | Implementation-defined | MAY be emitted if `debug` attribute present |
170
+
171
+ Events MUST bubble and MUST be composed (cross shadow DOM boundaries).
172
+
173
+ ### 3.5 Loading and Unloading
174
+
175
+ The host element is solely responsible for loading and unloading mod elements.
176
+ Mod elements MUST NOT load themselves.
177
+
178
+ **Loading a mod element:**
179
+
180
+ 1. If `loading`, `loaded`, `failed`, or `disabled` attribute is present, return
181
+ 2. Validate attributes:
182
+ - If both `src` and `ref` are present, set `failed` attribute, dispatch
183
+ `vellum:mod-failed` with `VellumAttributeError`, and stop
184
+ - If neither `src` nor `ref` is present, set `failed` attribute, dispatch
185
+ `vellum:mod-failed` with `VellumAttributeError`, and stop
186
+ 3. Set `loading` attribute
187
+ 4. Obtain the mod definition:
188
+ - If `src` is present: Import the mod script via dynamic `import()`
189
+ - If `ref` is present: Resolve the key to a mod definition (resolution
190
+ mechanism is implementation-defined)
191
+ 5. If obtaining the mod definition fails:
192
+ - Remove `loading` attribute
193
+ - Set `failed` attribute
194
+ - Dispatch `vellum:mod-failed` with `VellumLoadError` (for `src`) or
195
+ `VellumReferenceError` (for `ref`)
196
+ - Stop
197
+ 6. If the mod definition exports a `mount` function:
198
+ - Create an AbortController
199
+ - Call `mount({ element, host, signal })` where:
200
+ - `element` is the mod element
201
+ - `host` is the host element
202
+ - `signal` is the AbortController's signal
203
+ - If `mount` throws or rejects:
204
+ - Remove `loading` attribute
205
+ - Set `failed` attribute
206
+ - Dispatch `vellum:mod-failed` with `VellumMountError`
207
+ - Stop
208
+ 7. Remove `loading` attribute
209
+ 8. Set `loaded` attribute
210
+ 9. Dispatch `vellum:mod-loaded` with `{ element, module }`
211
+
212
+ **Unloading a mod element:**
213
+
214
+ 1. If the `loading` attribute is present, abort the current load then continue.
215
+ 2. If the `unloading` attribute is present, return
216
+ 3. If neither the `loaded` nor `failed` attributes are present, return
217
+ 4. If `loaded` attribute is present:
218
+ - Set `unloading` attribute
219
+ - If the mod definition exported an `unmount` function during load:
220
+ - Call `unmount({ element, host })`
221
+ - Remove `unloading` attribute
222
+ 5. Remove `loading`, `loaded`, and `failed` attributes
223
+ 6. Dispatch `vellum:mod-unloaded` with `{ element, module, wasLoaded }` where:
224
+ - `module` is the mod definition (or `null` if load failed before resolution)
225
+ - `wasLoaded` is `true` if `loaded` attribute was present, `false` otherwise
226
+ 7. Release any references to the mod definition.
227
+
228
+ ### 3.6 Nested Hosts
229
+
230
+ Host elements MAY be nested within other host elements.
231
+
232
+ A mod element belongs to its **closest ancestor host element**. When
233
+ discovering mod elements, a host MUST NOT process mod elements that have a
234
+ closer ancestor host.
235
+
236
+ ```html
237
+ <outer-app mod-tag="my-mod">
238
+ <my-mod src="./a.mjs" name="a"></my-mod> <!-- belongs to outer-app -->
239
+ <inner-app mod-tag="my-mod">
240
+ <my-mod src="./b.mjs" name="b"></my-mod> <!-- belongs to inner-app -->
241
+ </inner-app>
242
+ <my-mod src="./c.mjs" name="c"></my-mod> <!-- belongs to outer-app -->
243
+ </outer-app>
244
+ ```
245
+
246
+ Each host element manages only its direct mod elements. Events from inner
247
+ hosts bubble normally and MAY be observed by outer hosts.
248
+
249
+ Host elements MAY catch events from within their own subtree and prevent
250
+ them from propagating.
251
+
252
+ A host MUST NOT wait for nested hosts to complete before dispatching its
253
+ own `vellum:loading-complete` event.
254
+
255
+ ### 3.7 Registration Requirements
256
+
257
+ Consumers MUST register both the host element class and the mod element class
258
+ as custom elements before connecting the host element to the DOM.
259
+
260
+ The tag name used for `customElements.define()` on the mod element class MUST
261
+ match the host element's `mod-tag` attribute (or the default `vellum-mod`).
262
+
263
+ If the host encounters elements matching `mod-tag` that are not registered
264
+ custom elements, the host SHOULD log a warning and skip those elements.
265
+
266
+ ## 4. Mod Element
267
+
268
+ ### 4.1 Purpose
269
+
270
+ The mod element is a declarative marker representing a loadable module. It
271
+ carries configuration (source URL, dependencies, priority) but does not
272
+ perform loading itself.
273
+
274
+ **Loading is the sole responsibility of the host element.**
275
+
276
+ The mod element is inert until processed by a host.
277
+
278
+ ### 4.2 Class Export
279
+
280
+ A conforming implementation MUST export a mod element class that extends
281
+ `HTMLElement`.
282
+
283
+ The implementation MUST NOT call `customElements.define()`. Tag name
284
+ registration is the consumer's responsibility.
285
+
286
+
287
+ ### 4.3 Attributes
288
+
289
+ | Attribute | Type | Required | Default | Description |
290
+ |------------|--------|----------|----------------|-----------------------------------------------|
291
+ | `src` | Input | No* | — | URL of the mod script |
292
+ | `ref` | Input | No* | — | Key identifying a pre-registered mod definition |
293
+ | `name` | Input | No | Value of `src` or `ref` | Unique identifier for dependency resolution |
294
+ | `require` | Input | No | — | Comma-separated list of hard dependencies |
295
+ | `after` | Input | No | — | Comma-separated list of soft dependencies |
296
+ | `priority` | Input | No | — | Integer; lower values load first among peers |
297
+ | `disabled` | Input | No | — | If present, mod will not load |
298
+ | `loading` | Output | — | — | Set by host while load is in progress |
299
+ | `loaded` | Output | — | — | Set by host on successful load |
300
+ | `failed` | Output | — | — | Set by host on failed load |
301
+ | `unloading`| Output | — | — | Set by host while unload is in progress |
302
+
303
+ *Exactly one of `src` or `ref` MUST be present. If both are present or neither
304
+ is present, the host MUST set the `failed` attribute and dispatch
305
+ `vellum:mod-failed` with a `VellumAttributeError`.
306
+
307
+ The `loading`, `loaded`, `failed`, and `unloading` attributes are reserved
308
+ and output-only. They MUST NOT be set by consumers. They are managed
309
+ exclusively by the host element.
310
+
311
+ **Hard vs Soft Dependencies:**
312
+
313
+ - `require`: The mod element MUST NOT load until all required mod elements have
314
+ loaded successfully. If a required mod element is missing or fails, this mod
315
+ element MUST fail.
316
+
317
+ - `after`: The mod element SHOULD load after these mod elements if they exist
318
+ and are loading/loaded. If an `after` target is missing, this mod element
319
+ loads anyway. If an `after` target fails, this mod element still attempts
320
+ to load.
321
+
322
+ **Priority:**
323
+
324
+ The `priority` attribute serves as a tiebreaker when dependency order does
325
+ not determine load sequence:
326
+
327
+ 1. For mod elements with equivalent dependency status, lower `priority` values
328
+ load first
329
+ 2. If priorities are equal, DOM order determines sequence
330
+
331
+ ### 4.4 Behavior
332
+
333
+ The mod element has no autonomous behavior. It MUST NOT initiate loading,
334
+ unloading, or any lifecycle operations.
335
+
336
+ When connected to the DOM, the mod element remains inert until the host
337
+ element processes it.
338
+
339
+ When disconnected from the DOM, the host element is responsible for detecting
340
+ the removal and performing cleanup (see Section 3.3).
341
+
342
+ The mod element MAY provide convenience getters for attribute access:
343
+
344
+ ```javascript
345
+ get src() { return this.getAttribute('src'); }
346
+ get name() { return this.getAttribute('name') || this.src; }
347
+ get dependencies() {
348
+ const req = this.getAttribute('require');
349
+ return req ? req.split(',').map(s => s.trim()).filter(Boolean) : [];
350
+ }
351
+ get softDependencies() {
352
+ const after = this.getAttribute('after');
353
+ return after ? after.split(',').map(s => s.trim()).filter(Boolean) : [];
354
+ }
355
+ ```
356
+
357
+ ### 4.5 Child Content
358
+
359
+ Mod elements MAY contain child elements. This specification does not define
360
+ semantics for child content.
361
+
362
+ Mod scripts MAY access children via standard DOM APIs on the `element`
363
+ parameter passed to `mount()`:
364
+
365
+ ```javascript
366
+ export function mount({ element, host, signal }) {
367
+ const template = element.querySelector('template');
368
+ const config = element.querySelector('script[type="application/json"]');
369
+ // ...
370
+ }
371
+ ```
372
+
373
+ ### 4.6 Events
374
+
375
+ All lifecycle events are dispatched by the host element, using the mod
376
+ element as the event target:
377
+
378
+ | Event | Detail | When |
379
+ |-----------------------|-------------------------------------------------|---------------------------------|
380
+ | `vellum:mod-loaded` | `{ element: HTMLElement, module: object }` | After successful load and mount |
381
+ | `vellum:mod-failed` | `{ element: HTMLElement, error: Error }` | After load or mount failure |
382
+ | `vellum:mod-unloaded` | `{ element: HTMLElement, module: object, wasLoaded: boolean }` | After unload |
383
+
384
+ Events MUST bubble and MUST be composed.
385
+
386
+ ### 4.7 State Machine
387
+
388
+ The mod element's state is represented by output attributes. The following
389
+ state transitions are valid:
390
+
391
+ ```
392
+ ┌─────────────────────────────────┐
393
+ │ │
394
+ ▼ │
395
+ ┌─────────┐ host triggers ┌───────────┐ import+mount ┌────────┐ │
396
+ │ Initial │ ───────────────►│ Loading │ ───────────────►│ Loaded │ │
397
+ └─────────┘ └───────────┘ └────────┘ │
398
+ ▲ │ │ │
399
+ │ │ import or │ │
400
+ │ │ mount fails │ │
401
+ │ ▼ │ │
402
+ │ ┌────────┐ │ │
403
+ │ DOM removal │ Failed │ │ │
404
+ └───────────────────────┴────────┘ │ │
405
+ ▲ │ │
406
+ │ ┌───────────┐ │ │
407
+ │ unmount completes │ Unloading │◄────────────────────┘ │
408
+ └───────────────────────┴───────────┘ DOM removal / │
409
+ disabled / │
410
+ src change │
411
+
412
+ ▲ │
413
+ │ disabled present (no-op) │
414
+ └────────────────────────────────────────────────────────────────┘
415
+ ```
416
+
417
+ **State-to-attribute mapping:**
418
+
419
+ | State | Attributes Present |
420
+ |-----------|--------------------|
421
+ | Initial | (none) |
422
+ | Loading | `loading` |
423
+ | Loaded | `loaded` |
424
+ | Failed | `failed` |
425
+ | Unloading | `unloading` |
426
+
427
+ A mod element MUST have at most one of these output attributes present.
428
+
429
+ ## 5. Dependency Resolution
430
+
431
+ ### 5.1 Resolution Algorithm
432
+
433
+ The host MUST resolve mod load order using:
434
+
435
+ 1. **Hard dependencies (`require`)**: Mod elements MUST load after all required
436
+ mod elements have successfully loaded
437
+ 2. **Soft dependencies (`after`)**: Mod elements SHOULD load after listed mod
438
+ elements if those mod elements exist and haven't failed
439
+ 3. **Priority**: Among mod elements with satisfied dependencies, lower
440
+ `priority` values load first
441
+ 4. **DOM order**: Among mod elements with equal priority, earlier DOM position
442
+ loads first
443
+
444
+ ### 5.2 Hard Dependency Failures
445
+
446
+ If a mod element's required dependency (via `require`) is missing or has failed:
447
+
448
+ - The mod element MUST NOT attempt to load
449
+ - The host MUST set the `failed` attribute on the mod element
450
+ - The host MUST dispatch `vellum:mod-failed` with a `VellumDependencyError`
451
+
452
+ ### 5.3 Soft Dependency Handling
453
+
454
+ If a mod element's `after` dependency is missing:
455
+
456
+ - The mod element MUST proceed as if the dependency was satisfied
457
+ - The mod element loads in its normal priority/DOM order position
458
+
459
+ If a mod element's `after` dependency has failed:
460
+
461
+ - The mod element MUST still attempt to load
462
+ - The mod script's `mount` function MAY check for the failed dependency if needed
463
+
464
+ Mod scripts using `after` dependencies SHOULD check for dependent mod state
465
+ if they require conditional behavior based on whether the soft dependency
466
+ loaded successfully.
467
+
468
+ ### 5.4 Circular Dependencies
469
+
470
+ Circular dependencies in `require` attributes produce a fatal error.
471
+
472
+ If circular hard dependencies are detected:
473
+
474
+ - The host MUST NOT attempt to load any mod element in the cycle
475
+ - The host MUST set the `failed` attribute on all mod elements in the cycle
476
+ - The host MUST dispatch `vellum:mod-failed` for each with a
477
+ `VellumCircularDependencyError`
478
+ - The error message MUST include the cycle path (e.g., "a → b → c → a")
479
+
480
+ Circular dependencies in `after` attributes are permitted and do not cause
481
+ errors, since `after` is non-blocking.
482
+
483
+ ### 5.5 Dynamic Mod Elements
484
+
485
+ When a mod element is added after initial load:
486
+
487
+ - Its `require` dependencies MUST already be loaded (or the mod element fails
488
+ with `VellumDependencyError`)
489
+ - Its `after` dependencies refer to currently loaded or loading mod elements;
490
+ missing targets are treated as satisfied
491
+ - Already-loaded mod elements MUST NOT be reloaded
492
+ - The new mod element MUST wait for any pending mod elements it depends on
493
+
494
+ ## 6. Mod Script Interface
495
+
496
+ ### 6.1 Module Format
497
+
498
+ Mod scripts MUST be ES modules.
499
+
500
+ All valid ES module specifiers are permitted for the `src` attribute,
501
+ including:
502
+
503
+ - Relative URLs (resolved relative to the document)
504
+ - Absolute URLs
505
+ - Data URLs
506
+ - Blob URLs
507
+
508
+ Cross-origin mod scripts are subject to CORS restrictions per the ES module
509
+ specification.
510
+
511
+ ### 6.2 Exports
512
+
513
+ #### 6.2.1 `mount` function
514
+
515
+ Mod scripts MAY export a `mount` function. The `mount` function MAY be async.
516
+
517
+ ```typescript
518
+ mount(context: {
519
+ element: HTMLElement,
520
+ host: HTMLElement,
521
+ signal: AbortSignal
522
+ }): void | Promise<void>
523
+ ```
524
+
525
+ Parameters (passed as a single object):
526
+
527
+ - `element`: The mod element that initiated the load
528
+ - `host`: The host element coordinating the load
529
+ - `signal`: An AbortSignal that is aborted if the load is cancelled (e.g.,
530
+ due to `src` attribute change or element removal during load)
531
+
532
+ If `mount` is not exported, the mod script runs its top-level code only.
533
+
534
+ The `mount` function SHOULD:
535
+
536
+ - Store the `host` reference if needed for later access
537
+ - Attach an abort listener to `signal` if cleanup is needed on cancellation
538
+ - Return or resolve when initialization is complete
539
+
540
+ #### 6.2.2 `unmount` function
541
+
542
+ Mod scripts MAY export an `unmount` function for cleanup:
543
+
544
+ ```typescript
545
+ unmount(context: {
546
+ element: HTMLElement,
547
+ host: HTMLElement
548
+ }): void | Promise<void>
549
+ ```
550
+
551
+ Parameters (passed as a single object):
552
+
553
+ - `element`: The mod element being unloaded
554
+ - `host`: The host element coordinating the unload
555
+
556
+ The `unmount` function is called when:
557
+
558
+ - The mod element is removed from the DOM
559
+ - The `disabled` attribute is added
560
+ - The `src` attribute changes
561
+ - (There is no public `unload()` method; these are the only triggers)
562
+
563
+ The `unmount` function SHOULD:
564
+
565
+ - Remove event listeners
566
+ - Cancel pending operations
567
+ - Release resources
568
+ - Clean up any DOM modifications
569
+
570
+ If `unmount` is not exported, no cleanup is performed.
571
+
572
+ #### 6.2.3 Other exports
573
+
574
+ A mod script MAY export any other functions or variables. The host MUST NOT
575
+ store or access exports other than `mount` and `unmount`.
576
+
577
+ ### 6.3 Dispatching Events
578
+
579
+ Mod scripts communicate via native DOM events:
580
+
581
+ ```javascript
582
+ export async function mount({ element, host, signal }) {
583
+ element.dispatchEvent(new CustomEvent('my:event', {
584
+ bubbles: true,
585
+ composed: true,
586
+ detail: { /* ... */ }
587
+ }));
588
+ }
589
+ ```
590
+
591
+ No toolkit or wrapper is required.
592
+
593
+ ## 7. Event Namespacing
594
+
595
+ All events defined by this specification use the `vellum:` prefix.
596
+
597
+ | Event | Source | Target | Purpose |
598
+ |---------------------------|--------|-------------|-------------------------|
599
+ | `vellum:loading-started` | Host | Host | Loading phase beginning |
600
+ | `vellum:loading-complete` | Host | Host | Loading phase complete |
601
+ | `vellum:teardown` | Host | Host | Host disconnecting |
602
+ | `vellum:debug` | Host | Host | Debug information |
603
+ | `vellum:mod-loaded` | Host | Mod element | Mod successfully loaded |
604
+ | `vellum:mod-failed` | Host | Mod element | Mod failed to load |
605
+ | `vellum:mod-unloaded` | Host | Mod element | Mod unloaded |
606
+
607
+ Implementations MUST use these exact event names.
608
+
609
+ Ecosystem mods MAY define additional events with their own namespaces.
610
+
611
+ ### 7.1 Reserved Namespaces
612
+
613
+ The following attribute and event prefixes are reserved:
614
+
615
+ - `vellum-*` attributes: Reserved for future specification use
616
+ - `vellum:*` events: Reserved for specification-defined events
617
+
618
+ Consumers SHOULD use `data-*` attributes for custom metadata on mod elements.
619
+ Consumers MAY use any valid, HTML-compliant attributes on mod elements.
620
+
621
+ Ecosystem mods SHOULD use their own event namespace (e.g., `mymod:event`).
622
+
623
+ ## 8. Error Types
624
+
625
+ Implementations MUST define and throw the following error types:
626
+
627
+ ### 8.1 VellumError
628
+
629
+ Base class for all Vellum errors.
630
+
631
+ ```typescript
632
+ class VellumError extends Error {
633
+ name: 'VellumError';
634
+ cause?: Error;
635
+ }
636
+ ```
637
+
638
+ ### 8.2 VellumDependencyError
639
+
640
+ Thrown when a mod element's hard dependencies cannot be satisfied.
641
+
642
+ ```typescript
643
+ class VellumDependencyError extends VellumError {
644
+ name: 'VellumDependencyError';
645
+ modName: string;
646
+ missingDependencies: string[];
647
+ }
648
+ ```
649
+
650
+ Message format: `Mod "{modName}" has unmet dependencies: {deps.join(', ')}`
651
+
652
+ ### 8.3 VellumCircularDependencyError
653
+
654
+ Thrown when circular hard dependencies are detected.
655
+
656
+ ```typescript
657
+ class VellumCircularDependencyError extends VellumError {
658
+ name: 'VellumCircularDependencyError';
659
+ cycle: string[];
660
+ }
661
+ ```
662
+
663
+ Message format: `Circular dependency detected: {cycle.join(' → ')}`
664
+
665
+ ### 8.4 VellumLoadError
666
+
667
+ Thrown when a mod script fails to import.
668
+
669
+ ```typescript
670
+ class VellumLoadError extends VellumError {
671
+ name: 'VellumLoadError';
672
+ src: string;
673
+ }
674
+ ```
675
+
676
+ Message format: `Failed to load mod from "{src}"`
677
+
678
+ ### 8.5 VellumMountError
679
+
680
+ Thrown when a mod script's `mount` function fails.
681
+
682
+ ```typescript
683
+ class VellumMountError extends VellumError {
684
+ name: 'VellumMountError';
685
+ modName: string;
686
+ }
687
+ ```
688
+
689
+ Message format: `Mount failed for mod "{modName}"`
690
+
691
+ ### 8.6 VellumAttributeError
692
+
693
+ Thrown when a mod element has invalid attribute configuration.
694
+ ```typescript
695
+ class VellumAttributeError extends VellumError {
696
+ name: 'VellumAttributeError';
697
+ modName: string;
698
+ }
699
+ ```
700
+
701
+ Message format (both present): `Mod "{modName}" has both src and ref attributes; exactly one is required`
702
+
703
+ Message format (neither present): `Mod "{modName}" has neither src nor ref attribute; exactly one is required`
704
+
705
+ ### 8.7 VellumReferenceError
706
+
707
+ Thrown when a mod element's `ref` attribute cannot be resolved.
708
+ ```typescript
709
+ class VellumReferenceError extends VellumError {
710
+ name: 'VellumReferenceError';
711
+ ref: string;
712
+ }
713
+ ```
714
+
715
+ Message format: `Could not resolve mod reference "{ref}"`
716
+
717
+ ## 9. Security Considerations
718
+
719
+ ### 9.1 Content Security Policy
720
+
721
+ Mod scripts are loaded via dynamic `import()`. For this to succeed, the mod
722
+ script's origin MUST be permitted by the document's `script-src` CSP directive.
723
+
724
+ Implementations MUST NOT bypass or weaken CSP restrictions.
725
+
726
+ Example CSP header allowing mod scripts from same origin and a trusted CDN:
727
+
728
+ ```
729
+ Content-Security-Policy: script-src 'self' https://cdn.example.com
730
+ ```
731
+
732
+ ### 9.2 Subresource Integrity
733
+
734
+ This specification does not require SRI support for mod scripts.
735
+
736
+ Implementations MAY support an `integrity` attribute on mod elements:
737
+
738
+ ```html
739
+ <my-mod src="./x.mjs" integrity="sha384-..."></my-mod>
740
+ ```
741
+
742
+ If supported, implementations MUST reject modules that fail integrity checks
743
+ and MUST set the `failed` attribute with a `VellumLoadError`.
744
+
745
+ ### 9.3 Cross-Origin Loading
746
+
747
+ Mod scripts loaded from different origins are subject to CORS restrictions
748
+ per the ES module specification.
749
+
750
+ Cross-origin scripts MUST be served with appropriate headers:
751
+
752
+ - `Access-Control-Allow-Origin` matching the document origin or `*`
753
+
754
+ If CORS checks fail, the host MUST set the `failed` attribute and dispatch
755
+ `vellum:mod-failed` with a `VellumLoadError`.
756
+
757
+ ### 9.4 Module Isolation
758
+
759
+ Implementations SHOULD NOT store arbitrary properties from loaded mod modules.
760
+ Only the `mount` and `unmount` exports (if present) should be retained and
761
+ invoked.
762
+
763
+ This mitigates prototype pollution risks from malicious mod scripts.
764
+
765
+ ## 10. Conformance
766
+
767
+ ### 10.1 Implementation Conformance
768
+
769
+ A conforming Vellum implementation MUST:
770
+
771
+ 1. Export a host element class meeting Section 3 requirements
772
+ 2. Export a mod element class meeting Section 4 requirements
773
+ 3. Implement dependency resolution per Section 5
774
+ 4. Dispatch all required events per Section 7
775
+ 5. Define all error types per Section 8
776
+
777
+ A conforming implementation MUST NOT:
778
+
779
+ 1. Call `customElements.define()` for either element class
780
+ 2. Expose a `load()` method on mod elements
781
+ 3. Allow mod elements to initiate their own loading
782
+
783
+ ### 10.2 Consumer Conformance
784
+
785
+ A conforming Vellum consumer MUST:
786
+
787
+ 1. Register both host and mod element classes as custom elements
788
+ 2. Use matching tag names (host's `mod-tag` matches mod element registration)
789
+ 3. Register both element classes before connecting the host to the DOM
790
+
791
+ ### 10.3 Mod Script Conformance
792
+
793
+ Mod scripts have no conformance requirements. They MAY export `mount`
794
+ and/or `unmount`. They MAY dispatch events.
795
+
796
+ Mod scripts MAY provide user interface elements.
797
+
798
+ ## 11. Versioning
799
+
800
+ ### 11.1 Specification Versioning
801
+
802
+ This specification follows Semantic Versioning 2.0.0:
803
+
804
+ - **MAJOR**: Breaking changes to element interfaces or required behaviors
805
+ - **MINOR**: New optional features, attributes, or recommendations
806
+ - **PATCH**: Clarifications and corrections
807
+
808
+ ### 11.2 Implementation Versioning
809
+
810
+ Implementations SHOULD indicate which specification version they conform to.
811
+
812
+ ---
813
+
814
+ ## Appendix A: Glossary
815
+
816
+ **Host Element**: The coordinating custom element that manages mod loading.
817
+
818
+ **Mod Element**: A custom element representing a single loadable module. Inert
819
+ until processed by a host.
820
+
821
+ **Mod Script**: The ES module file loaded by a mod element.
822
+
823
+ **Mod Module**: The JavaScript module object returned by `import()`.
824
+
825
+ **Kernel**: The core Vellum implementation (host + mod elements). Provides
826
+ loading only.
827
+
828
+ **Ecosystem Mod**: A mod script providing reusable functionality (e.g.,
829
+ actions, routing, rendering).
830
+
831
+ ### Mod Element
832
+ ```
833
+ Attributes (input):
834
+ src Module URL (required if ref not present)
835
+ ref Registry key (required if src not present)
836
+ name Identifier for dependencies
837
+ require Comma-separated hard dependency names
838
+ after Comma-separated soft dependency names
839
+ priority Load order tiebreaker (lower = earlier)
840
+ disabled If present, mod will not load
841
+
842
+ Attributes (output, managed by host):
843
+ loading Present while load is in progress
844
+ loaded Present after successful load
845
+ failed Present after failed load
846
+ unloading Present while unload is in progress
847
+
848
+ Methods:
849
+ (none - loading is host's responsibility)
850
+
851
+ Events (dispatched by host on this element):
852
+ vellum:mod-loaded { element, module }
853
+ vellum:mod-failed { element, error }
854
+ vellum:mod-unloaded { element, module, wasLoaded }
855
+
856
+ Behavior:
857
+ - Inert until processed by host
858
+ - No autonomous lifecycle operations
859
+ - Exactly one of src or ref must be present
860
+ - Child content accessible to mod script via element parameter
861
+ ```
862
+
863
+ ## Appendix C: Example Usages
864
+
865
+ ### Basic Setup
866
+
867
+ ```javascript
868
+ // app.js
869
+ import { VellumHost, VellumMod } from '@byre/vellum';
870
+
871
+ customElements.define('my-app', VellumHost);
872
+ customElements.define('my-mod', VellumMod);
873
+ ```
874
+
875
+ ```html
876
+ <my-app mod-tag="my-mod">
877
+ <my-mod src="./layout.mjs" name="layout"></my-mod>
878
+ <my-mod src="./header.mjs" name="header" require="layout"></my-mod>
879
+ <my-mod src="./analytics.mjs" name="analytics" after="layout"></my-mod>
880
+ <my-mod src="./content.mjs" require="layout" after="analytics"></my-mod>
881
+ <my-mod src="./experimental.mjs" disabled></my-mod>
882
+ </my-app>
883
+ ```
884
+
885
+ In this example:
886
+ - `header` requires `layout` (fails if layout fails)
887
+ - `analytics` loads after `layout` if present (but would work without it)
888
+ - `content` requires `layout`, and loads after `analytics` if present
889
+ - `experimental` is present but won't load until `disabled` is removed
890
+
891
+ ### Mod Script with Mount and Unmount
892
+
893
+ ```javascript
894
+ // layout.mjs
895
+ let container;
896
+ let hostRef;
897
+
898
+ export function mount({ element, host, signal }) {
899
+ hostRef = host;
900
+
901
+ // Handle cancellation
902
+ signal.addEventListener('abort', () => {
903
+ container?.remove();
904
+ container = null;
905
+ });
906
+
907
+ container = document.createElement('div');
908
+ container.id = 'layout-root';
909
+ host.appendChild(container);
910
+
911
+ element.dispatchEvent(new CustomEvent('layout:ready', {
912
+ bubbles: true,
913
+ composed: true,
914
+ detail: { container }
915
+ }));
916
+ }
917
+
918
+ export function unmount({ element, host }) {
919
+ container?.remove();
920
+ container = null;
921
+ hostRef = null;
922
+ }
923
+ ```
924
+
925
+ ### Mod Script Listening to Another Mod
926
+
927
+ ```javascript
928
+ // header.mjs
929
+ let headerElement;
930
+
931
+ export function mount({ element, host, signal }) {
932
+ // Wait for layout to be ready
933
+ host.addEventListener('layout:ready', (e) => {
934
+ headerElement = document.createElement('header');
935
+ headerElement.textContent = 'My Application';
936
+ e.detail.container.prepend(headerElement);
937
+ }, { once: true });
938
+ }
939
+
940
+ export function unmount({ element, host }) {
941
+ headerElement?.remove();
942
+ headerElement = null;
943
+ }
944
+ ```
945
+
946
+ ### Bundle Registration
947
+ ```javascript
948
+ // mybundle.mjs - a mod that registers other mods
949
+ export function mount({ element, host, signal }) {
950
+ // Registration mechanism is implementation-defined
951
+ // This example assumes host.register() exists
952
+ host.register('mybundle.mod-a', {
953
+ mount({ element, host, signal }) {
954
+ console.log('mod-a mounted');
955
+ },
956
+ unmount({ element, host }) {
957
+ console.log('mod-a unmounted');
958
+ }
959
+ });
960
+
961
+ host.register('mybundle.mod-b', {
962
+ mount({ element, host, signal }) {
963
+ console.log('mod-b mounted');
964
+ }
965
+ });
966
+ }
967
+ ```
968
+
969
+ ```html
970
+ <!-- Load the bundle first -->
971
+ <my-mod src="mybundle.mjs" name="mybundle"></my-mod>
972
+
973
+ <!-- These resolve from the bundle's registrations -->
974
+ <my-mod ref="mybundle.mod-a" require="mybundle"></my-mod>
975
+ <my-mod ref="mybundle.mod-b" require="mybundle"></my-mod>
976
+ ```
977
+
978
+ In this example:
979
+ - `mybundle` loads via `src` and registers two mod definitions during mount
980
+ - `mybundle.mod-a` and `mybundle.mod-b` use `ref` to resolve from registrations
981
+ - `require="mybundle"` ensures the bundle mounts before resolution is attempted
982
+ - The registration mechanism (`host.register()`) is implementation-defined
983
+
984
+ ### Using Child Content
985
+
986
+ ```html
987
+ <my-mod src="./configurable.mjs" name="configurable">
988
+ <script type="application/json">
989
+ { "theme": "dark", "language": "en" }
990
+ </script>
991
+ <template>
992
+ <div class="custom-content">
993
+ <slot></slot>
994
+ </div>
995
+ </template>
996
+ </my-mod>
997
+ ```
998
+
999
+ ```javascript
1000
+ // configurable.mjs
1001
+ export function mount({ element, host, signal }) {
1002
+ const configScript = element.querySelector('script[type="application/json"]');
1003
+ const config = configScript ? JSON.parse(configScript.textContent) : {};
1004
+
1005
+ const template = element.querySelector('template');
1006
+ if (template) {
1007
+ const content = template.content.cloneNode(true);
1008
+ // Use content...
1009
+ }
1010
+
1011
+ console.log('Config:', config);
1012
+ }
1013
+ ```
1014
+
1015
+ ### Using Default Tag Names
1016
+
1017
+ ```javascript
1018
+ // For users who want vellum-app / vellum-mod
1019
+ import '@byre/vellum/register';
1020
+ ```
1021
+
1022
+ ```html
1023
+ <vellum-app>
1024
+ <vellum-mod src="./my-mod.mjs"></vellum-mod>
1025
+ </vellum-app>
1026
+ ```
1027
+
1028
+ ### DOM-Driven Lifecycle
1029
+
1030
+ ```javascript
1031
+ const host = document.querySelector('my-app');
1032
+
1033
+ // Load a mod: add to DOM
1034
+ const mod = document.createElement('my-mod');
1035
+ mod.setAttribute('src', './new-feature.mjs');
1036
+ host.appendChild(mod);
1037
+
1038
+ // Unload a mod: remove from DOM
1039
+ mod.remove();
1040
+
1041
+ // Reload a mod: remove and re-add
1042
+ const parent = mod.parentElement;
1043
+ const next = mod.nextSibling;
1044
+ mod.remove();
1045
+ parent.insertBefore(mod, next);
1046
+
1047
+ // Disable a mod (unloads if loaded)
1048
+ mod.setAttribute('disabled', '');
1049
+
1050
+ // Enable a mod (loads if dependencies satisfied)
1051
+ mod.removeAttribute('disabled');
1052
+
1053
+ // Change source (unloads old, loads new)
1054
+ mod.setAttribute('src', './new-feature-v2.mjs');
1055
+ ```
1056
+
1057
+ ### CSS Targeting Mod State
1058
+
1059
+ ```css
1060
+ /* Style based on load state */
1061
+ my-mod[loading] {
1062
+ opacity: 0.5;
1063
+ }
1064
+
1065
+ my-mod[loaded] {
1066
+ /* loaded indicator */
1067
+ }
1068
+
1069
+ my-mod[failed] {
1070
+ outline: 2px solid red;
1071
+ }
1072
+
1073
+ my-mod[failed]::after {
1074
+ content: ' (failed)';
1075
+ color: red;
1076
+ }
1077
+
1078
+ my-mod[disabled] {
1079
+ display: none;
1080
+ }
1081
+
1082
+ my-mod[unloading] {
1083
+ opacity: 0.3;
1084
+ pointer-events: none;
1085
+ }
1086
+ ```
1087
+
1088
+ ### Nested Hosts
1089
+
1090
+ ```html
1091
+ <my-app mod-tag="my-mod">
1092
+ <my-mod src="./shell.mjs" name="shell"></my-mod>
1093
+
1094
+ <!-- Inner application with its own mods -->
1095
+ <my-app mod-tag="my-mod" id="inner-app">
1096
+ <my-mod src="./inner-feature.mjs" name="inner-feature"></my-mod>
1097
+ </my-app>
1098
+
1099
+ <my-mod src="./outer-feature.mjs" name="outer-feature"></my-mod>
1100
+ </my-app>
1101
+ ```
1102
+
1103
+ ```javascript
1104
+ // Outer host can listen to inner host events
1105
+ document.querySelector('my-app').addEventListener('vellum:mod-loaded', (e) => {
1106
+ console.log('A mod loaded somewhere:', e.detail.element.getAttribute('name'));
1107
+ });
1108
+ ```
1109
+
1110
+ ## Appendix D: Migration from v1.x
1111
+
1112
+ ### Removed Features
1113
+
1114
+ | v1.x Feature | v2.0 Equivalent |
1115
+ |--------------------------|----------------------------------|
1116
+ | Built-in action registry | Use `@byre/vellum-actions` mod |
1117
+ | Toolkit object | Use native DOM APIs |
1118
+ | `dispatchAction()` | Use `element.dispatchEvent()` |
1119
+ | Built-in rendering | Use rendering mod |
1120
+ | Logging utilities | Use debug mod or browser console |
1121
+ | `mod.load()` | (removed - host loads mods) |
1122
+ | `mod.reload()` | Remove and re-add element |
1123
+ | `mod.unload()` | Remove element from DOM |
1124
+
1125
+ ### Changed Behaviors
1126
+
1127
+ | Behavior | v1.x | v2.0 |
1128
+ |-------------------|--------------------------|-----------------------------------------------------|
1129
+ | Mod element | Inert DOM element | Inert custom element (still inert, but typed) |
1130
+ | Mod state | Internal | Visible via `loading`/`loaded`/`failed`/`unloading` |
1131
+ | Event prefix | `harness:` / none | `vellum:` |
1132
+ | Setup function | `init(toolkit)` | `mount({ element, host, signal })` |
1133
+ | Cleanup function | Not supported | `unmount({ element, host })` |
1134
+ | Load trigger | Public `load()` on mod | Host-only (no public method) |
1135
+ | Unload trigger | Not supported | DOM removal or `disabled` attribute |
1136
+ | Soft dependencies | Not supported | Use `after` attribute |
1137
+ | Disable mod | Not supported | Use `disabled` attribute |
1138
+ | Circular deps | Resolved via priority | Fatal error (`VellumCircularDependencyError`) |
1139
+
1140
+ ### Migration Steps
1141
+
1142
+ 1. Register both `VellumHost` and `VellumMod` as custom elements
1143
+ 2. Add `@byre/vellum-actions` mod if using action system
1144
+ 3. Rename `init(toolkit)` to `mount({ element, host, signal })`
1145
+ 4. Add `unmount({ element, host })` for any cleanup logic
1146
+ 5. Replace `toolkit.dispatchAction()` with native `dispatchEvent()`
1147
+ 6. Update event listeners from `harness:action` to mod-specific events
1148
+ 7. Change optional dependencies from `require` to `after`
1149
+ 8. Use `disabled` attribute instead of conditional loading logic
1150
+ 9. Remove any direct calls to `mod.load()` (loading is automatic)
1151
+ 10. Replace `mod.reload()` with remove-and-re-add pattern
1152
+ 11. Replace `mod.unload()` with `mod.remove()`
1153
+ 12. Handle `VellumCircularDependencyError` if you relied on cycle resolution
1154
+
1155
+ ## Appendix E: Implementation Notes
1156
+
1157
+ This appendix provides non-normative guidance for implementers.
1158
+
1159
+ ### E.1 MutationObserver Configuration
1160
+
1161
+ The host element should observe its subtree for:
1162
+
1163
+ - Child list changes (mod additions and removals)
1164
+ - Attribute changes on mod elements (`src`, `disabled` at minimum)
1165
+
1166
+ Recommended configuration:
1167
+
1168
+ ```javascript
1169
+ const observer = new MutationObserver(callback);
1170
+ observer.observe(hostElement, {
1171
+ childList: true,
1172
+ subtree: true,
1173
+ attributes: true,
1174
+ attributeFilter: ['src', 'disabled'],
1175
+ attributeOldValue: true
1176
+ });
1177
+ ```
1178
+
1179
+ Note: Observing `subtree: true` with `attributes: true` can be expensive for
1180
+ large DOM trees. Implementations MAY use separate observers for child list
1181
+ and attributes, or MAY track mod elements and observe them individually.
1182
+
1183
+ ### E.2 Handling Shadow DOM Boundaries
1184
+
1185
+ If mod elements are placed inside shadow roots, the host's `querySelectorAll`
1186
+ will not find them by default.
1187
+
1188
+ Implementations MAY:
1189
+
1190
+ - Document that mod elements must be in the light DOM
1191
+ - Use `querySelectorAll` with `{ root: document }` (where supported)
1192
+ - Provide a mechanism for shadow roots to register with the host
1193
+
1194
+ The simplest approach is to require mod elements in light DOM.
1195
+
1196
+ ### E.3 Performance Considerations for Large Mod Counts
1197
+
1198
+ For applications with many mod elements (50+):
1199
+
1200
+ - **Batch processing**: Process mod additions/removals in batches using
1201
+ `requestAnimationFrame` or `queueMicrotask`
1202
+ - **Parallel loading**: Load mod elements with satisfied dependencies in
1203
+ parallel, respecting any implementation-defined concurrency limit
1204
+ - **Lazy attribute observation**: Only observe attributes on mod elements
1205
+ that have been loaded (not disabled/failed)
1206
+
1207
+ ### E.4 AbortSignal Usage
1208
+
1209
+ The `signal` passed to `mount()` enables cancellation. Common patterns:
1210
+
1211
+ ```javascript
1212
+ export async function mount({ element, host, signal }) {
1213
+ // Check if already aborted
1214
+ if (signal.aborted) return;
1215
+
1216
+ // Cancel fetch requests
1217
+ const response = await fetch(url, { signal });
1218
+
1219
+ // Cancel timers
1220
+ const timeoutId = setTimeout(doSomething, 1000);
1221
+ signal.addEventListener('abort', () => clearTimeout(timeoutId));
1222
+
1223
+ // Cancel event listeners
1224
+ const handler = (e) => { /* ... */ };
1225
+ document.addEventListener('click', handler);
1226
+ signal.addEventListener('abort', () => {
1227
+ document.removeEventListener('click', handler);
1228
+ });
1229
+ }
1230
+ ```
1231
+
1232
+ ### E.5 Testing Recommendations
1233
+
1234
+ Implementations should test:
1235
+
1236
+ - All state transitions in the state machine
1237
+ - Dependency resolution with complex graphs
1238
+ - Circular dependency detection
1239
+ - Dynamic mod addition/removal
1240
+ - Attribute change handling during various states
1241
+ - Nested host behavior
1242
+ - Error propagation and error types
1243
+ - AbortSignal cancellation during load
1244
+
1245
+ ## Appendix F: Related Work
1246
+
1247
+ Vellum draws inspiration from and differs from several existing systems:
1248
+
1249
+ ### Module Loaders
1250
+
1251
+ **SystemJS / RequireJS**: Full module loaders with resolution, bundling
1252
+ concerns, and format transformation. Vellum delegates to native ES modules
1253
+ and focuses only on lifecycle coordination.
1254
+
1255
+ **ES Module Shims**: Polyfills for import maps and other module features.
1256
+ Vellum works alongside such tools but doesn't replace them.
1257
+
1258
+ ### Web Component Libraries
1259
+
1260
+ **Lit / Stencil / etc.**: Component authoring libraries. Vellum doesn't
1261
+ provide component authoring—it loads arbitrary modules that MAY use any
1262
+ component library.
1263
+
1264
+ ### Micro-Frontend Frameworks
1265
+
1266
+ **single-spa / Module Federation**: Orchestrate multiple applications.
1267
+ Vellum is lower-level—it loads modules, not applications. A micro-frontend
1268
+ framework could be built as a Vellum mod.
1269
+
1270
+ ### Key Differentiators
1271
+
1272
+ 1. **DOM-centric**: State visible in DOM, not JavaScript
1273
+ 2. **Kernel philosophy**: Minimal core, everything else is a mod
1274
+ 3. **No opinions**: No rendering, routing, state management built-in
1275
+ 4. **Native ES modules**: Uses `import()`, no custom loader
1276
+
1277
+ ---
1278
+
1279
+ ## References
1280
+
1281
+ **Normative:**
1282
+ - [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119): Key words for requirements
1283
+ - [Custom Elements](https://html.spec.whatwg.org/multipage/custom-elements.html): WHATWG specification
1284
+ - [ES Modules](https://tc39.es/ecma262/#sec-modules): ECMAScript specification
1285
+ - [Semantic Versioning 2.0.0](https://semver.org/)
1286
+
1287
+ **Informative:**
1288
+ - [MutationObserver](https://dom.spec.whatwg.org/#mutationobserver): DOM observation
1289
+ - [CustomEvent](https://dom.spec.whatwg.org/#customevent): Event interface
1290
+ - [AbortController](https://dom.spec.whatwg.org/#abortcontroller): Cancellation API
1291
+ - [Content Security Policy](https://www.w3.org/TR/CSP3/): CSP Level 3
1292
+ - [Subresource Integrity](https://www.w3.org/TR/SRI/): SRI specification