@cldmv/slothlet-types 3.16.2 → 3.17.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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Instance-wide event manager (#407).
3
+ * @extends ComponentBase
4
+ * @public
5
+ */
6
+ export class EventManager extends ComponentBase {
7
+ /**
8
+ * Where this component mounts on the Slothlet instance (`slothlet.handlers.eventManager`).
9
+ * @type {string}
10
+ */
11
+ static slothletProperty: string;
12
+ /**
13
+ * @param {object} slothlet - Slothlet instance.
14
+ */
15
+ constructor(slothlet: object);
16
+ /**
17
+ * Subscribe a listener to an event. The subscriber's granted delivery level is resolved from the
18
+ * event-rule pool against its own identity and returned so the caller knows whether it will
19
+ * receive payloads.
20
+ *
21
+ * @param {string} event - Event name to subscribe to.
22
+ * @param {Function} listener - Listener, called `(payload, meta)` on emit. At `notify`, `payload`
23
+ * is `undefined`; `meta` is `{ event, at, instanceID }`.
24
+ * @param {object} [options={}] - Options.
25
+ * @param {boolean} [options.once=false] - Remove the subscription after its first delivery.
26
+ * @returns {{ level: "deny"|"notify"|"allow", off: Function }} The granted level and an unsubscribe
27
+ * function. At `deny` the listener is not registered and `off` is a no-op.
28
+ * @throws {SlothletError} INVALID_ARGUMENT for a non-string event or non-function listener.
29
+ * @public
30
+ */
31
+ public on(event: string, listener: Function, options?: {
32
+ once?: boolean | undefined;
33
+ }): {
34
+ level: "deny" | "notify" | "allow";
35
+ off: Function;
36
+ };
37
+ /**
38
+ * Subscribe for a single delivery, then auto-unsubscribe. Shorthand for `on(event, listener, { once: true })`.
39
+ * @param {string} event - Event name.
40
+ * @param {Function} listener - Listener.
41
+ * @param {object} [options={}] - Options (merged with `once: true`).
42
+ * @returns {{ level: "deny"|"notify"|"allow", off: Function }} The granted level and unsubscribe.
43
+ * @public
44
+ */
45
+ public once(event: string, listener: Function, options?: object): {
46
+ level: "deny" | "notify" | "allow";
47
+ off: Function;
48
+ };
49
+ /**
50
+ * Remove a specific listener from an event.
51
+ * @param {string} event - Event name.
52
+ * @param {Function} listener - The listener reference passed to `on`/`once`.
53
+ * @returns {boolean} True if a matching subscription was removed.
54
+ * @public
55
+ */
56
+ public off(event: string, listener: Function): boolean;
57
+ /**
58
+ * Emit an event to its subscribers. NOT gated — any caller may emit; the three-level policy is
59
+ * enforced per subscriber at delivery. Async, fire-and-forget, per-listener error isolation:
60
+ * a throwing listener surfaces a `SlothletWarning` and never affects the others or the emitter.
61
+ *
62
+ * @param {string} event - Event name.
63
+ * @param {*} [payload] - Domain payload, delivered only to `allow`-level subscribers.
64
+ * @returns {Promise<void>} Resolves once all listeners (including async) have settled.
65
+ * @throws {SlothletError} INVALID_ARGUMENT for a non-string event.
66
+ * @public
67
+ */
68
+ public emit(event: string, payload?: any): Promise<void>;
69
+ /**
70
+ * Tear down all subscriptions. Called from `Slothlet.shutdown()`.
71
+ * @returns {void}
72
+ * @public
73
+ */
74
+ public shutdown(): void;
75
+ #private;
76
+ }
77
+ import { ComponentBase } from "#factories/component-base";
@@ -14,6 +14,7 @@ export class Lifecycle extends ComponentBase {
14
14
  */
15
15
  constructor(slothlet: object);
16
16
  subscribers: Map<any, any>;
17
+ internalSubscribers: Map<any, any>;
17
18
  eventLog: any[];
18
19
  maxLogSize: number;
19
20
  /**
@@ -45,6 +46,18 @@ export class Lifecycle extends ComponentBase {
45
46
  * });
46
47
  */
47
48
  public on(event: string, handler: Function): Function;
49
+ /**
50
+ * Subscribe to the INTERNAL lifecycle tier (#398) — the framework's own systems (metadata,
51
+ * routine manager, ownership) use this for the construction/contribution stream, which fires
52
+ * per contribution BEFORE collision resolution decides placement and carries the raw callable.
53
+ * Public consumers never reach this tier; they use {@link Lifecycle#subscribe} / `on`, which
54
+ * receives the sanitized post-placement PUBLIC events emitted via {@link Lifecycle#emit}.
55
+ * @param {string} event - Event name (e.g. `"impl:created"`, `"impl:changed"`).
56
+ * @param {Function} handler - Event handler function(eventData, token).
57
+ * @returns {Function} Unsubscribe function.
58
+ * @internal
59
+ */
60
+ subscribeInternal(event: string, handler: Function): Function;
48
61
  /**
49
62
  * Unsubscribe from lifecycle event - standard EventEmitter pattern
50
63
  * @param {string} event - Event name
@@ -89,5 +102,17 @@ export class Lifecycle extends ComponentBase {
89
102
  * });
90
103
  */
91
104
  private emit;
105
+ /**
106
+ * Emit an INTERNAL lifecycle event (#398) — delivered ONLY to {@link Lifecycle#subscribeInternal}
107
+ * subscribers (the framework's own metadata/routine/ownership systems), never to public
108
+ * consumers. Used for the construction/contribution stream (`impl:created` / `impl:changed`
109
+ * emitted per contribution, pre-placement, carrying the raw callable the internal systems need).
110
+ * @param {string} event - Event name.
111
+ * @param {object} data - Event data.
112
+ * @returns {Promise<void>}
113
+ * @internal
114
+ */
115
+ emitInternal(event: string, data: object): Promise<void>;
116
+ #private;
92
117
  }
93
118
  import { ComponentBase } from "#factories/component-base";
@@ -39,7 +39,7 @@ export class PermissionManager extends ComponentBase {
39
39
  caller: string;
40
40
  target: string;
41
41
  effect: string;
42
- }, ownerModuleID?: string | null, ruleId?: string | null): string;
42
+ }, ownerModuleID?: string | null, ruleId?: string | null, layer?: null): string;
43
43
  /**
44
44
  * Remove a permission rule by ID.
45
45
  * A module cannot remove rules it owns (immutability).
@@ -52,6 +52,65 @@ export class PermissionManager extends ComponentBase {
52
52
  * pm.removeRule("perm-3", "mod_other");
53
53
  */
54
54
  removeRule(ruleId: string, callerModuleID?: string | null): boolean;
55
+ /**
56
+ * Add an event rule (#407). Separate three-level construct from {@link addRule}: `effect` is
57
+ * "deny" | "notify" | "allow". Matched most-specific-wins with the same layered tiebreak; the
58
+ * EventManager uses it to resolve each subscriber's delivery level.
59
+ *
60
+ * @param {object} rule - The event-rule definition.
61
+ * @param {string} rule.caller - Glob matching the SUBSCRIBER's api path.
62
+ * @param {string} rule.event - Glob matching the event name.
63
+ * @param {"deny"|"notify"|"allow"} rule.effect - Delivery level.
64
+ * @param {object|Function|Array<object|Function>} [rule.condition] - Optional condition(s).
65
+ * @param {string|null} [ownerModuleID=null] - Owning module id ("__builtin__" for framework).
66
+ * @param {string|null} [ruleId=null] - Optional rule id to reuse (for reload replay).
67
+ * @param {string|null} [layer=null] - Explicit precedence layer; derived from ownerModuleID when null.
68
+ * @returns {string} The rule id.
69
+ * @throws {SlothletError} INVALID_PERMISSION_RULE if the rule is malformed.
70
+ * @example
71
+ * pm.addEventRule({ caller: "orders.**", event: "orders.*", effect: "allow" }, "mod_orders", null, "manifest");
72
+ */
73
+ addEventRule(rule: {
74
+ caller: string;
75
+ event: string;
76
+ effect: "deny" | "notify" | "allow";
77
+ condition?: object | Function | (object | Function)[] | undefined;
78
+ }, ownerModuleID?: string | null, ruleId?: string | null, layer?: string | null): string;
79
+ /**
80
+ * Remove an event rule by id. A module cannot remove an event rule it owns (immutability),
81
+ * mirroring {@link removeRule}.
82
+ *
83
+ * @param {string} ruleId - The event-rule id.
84
+ * @param {string|null} [callerModuleID=null] - Module id attempting removal.
85
+ * @returns {boolean} True if a rule was removed.
86
+ * @throws {SlothletError} PERMISSION_SELF_MODIFY if the caller owns the rule.
87
+ */
88
+ removeEventRule(ruleId: string, callerModuleID?: string | null): boolean;
89
+ /**
90
+ * Resolve the delivery level for a subscriber/event pair (#407): "deny" | "notify" | "allow".
91
+ * Most-specific-wins with the layered tiebreak (see {@link RULE_LAYER_RANK}); falls back to the
92
+ * base default (`permissions.events.default`, built-in "notify") when no rule matches. A host
93
+ * subscription (no module caller) is trusted like a host-initiated call and always resolves "allow".
94
+ *
95
+ * @param {string|null} subscriberPath - The subscribing module's api path, or null for the host.
96
+ * @param {string} eventName - The event name being subscribed to / emitted.
97
+ * @param {object|null} [runtimeContext=null] - Per-request ALS context for condition evaluation.
98
+ * @returns {"deny"|"notify"|"allow"} The resolved delivery level.
99
+ */
100
+ resolveEventLevel(subscriberPath: string | null, eventName: string, runtimeContext?: object | null): "deny" | "notify" | "allow";
101
+ /**
102
+ * Monotonic epoch that changes on every event-rule mutation. The EventManager caches resolved
103
+ * levels against it and re-resolves only when it changes.
104
+ * @returns {number} The current event-rules epoch.
105
+ */
106
+ get eventRulesEpoch(): number;
107
+ /**
108
+ * Whether any event rule carries a condition. When false, a resolved subscriber level depends
109
+ * only on the rule set and can be safely cached against {@link eventRulesEpoch}; when true, the
110
+ * level can vary with the per-request context and must be re-resolved on each emit.
111
+ * @returns {boolean} True if at least one event rule has a condition.
112
+ */
113
+ get hasConditionalEventRules(): boolean;
55
114
  /**
56
115
  * Silent query: check whether a caller path is allowed to access a target path.
57
116
  * Never emits lifecycle or debug events — use {@link enforceAccess} at actual enforcement points.
@@ -80,12 +80,12 @@ export class RoutineManager extends ComponentBase {
80
80
  * rebuild/cascade time (see the class-level description for why).
81
81
  *
82
82
  * @description
83
- * Reads `data.wrapper.__impl` (present on every such event, in both eager and lazy mode) rather
84
- * than `data.impl` — `impl:created` fires twice per leaf construction (once with `impl` set to
85
- * the wrapper itself, once with the raw value for eager-known impls), and `wrapper.__impl` is
86
- * the one consistent field across every variant. Fires BEFORE collision resolution decides
87
- * which contributor's value survives onto the composed tree, so every contributor is captured —
88
- * not just the merge winner.
83
+ * Subscribed to the INTERNAL contribution stream (#398), `emitInternal("impl:created"/"impl:changed")`,
84
+ * which fires once per contribution BEFORE collision resolution decides which contributor's value
85
+ * survives onto the composed tree — so every contributor is captured, not just the merge winner.
86
+ * Reads the leaf's callable from `data.wrapper.__impl` (present on every such event, eager and
87
+ * lazy); the raw `data.impl` field no longer exists (that was the enforcement-bypassing leak #398
88
+ * removed). The real `UnifiedWrapper` instance, when there is one, arrives on `data.__wrapperRef`.
89
89
  *
90
90
  * Also subscribed to `impl:changed` so a LATE, direct reassignment (`self.auth.shutdown = fn`,
91
91
  * done after the module that owns `auth` finished loading) is captured too, not just the
@@ -317,6 +317,37 @@ export class RoutineManager extends ComponentBase {
317
317
  * @public
318
318
  */
319
319
  public runPath(apiPath: string, args?: any[], routine?: object): Promise<any>;
320
+ /**
321
+ * Run ONE contributor at an exact api path, selected by its `moduleID` (#400 — the mechanism
322
+ * behind `api.<path>.<name>.for(key)`). Unlike {@link runPath}, this deliberately does NOT apply
323
+ * the `stackRoutines` owner-filter: selecting a specific co-owner by key is the whole point, so a
324
+ * contributor that would lose the shared-path collision still runs when addressed directly. The
325
+ * contributor runs in its own extent + identity via {@link #runEntries}, exactly as a normal
326
+ * stacked/cascade run does — so `self.*` and permission checks resolve against that contributor.
327
+ * @param {string} apiPath - Exact composed api path.
328
+ * @param {string} key - The contributor's `moduleID`.
329
+ * @param {Array} [args] - Arguments forwarded to the selected contributor.
330
+ * @param {object} [routine] - The routine config the selector was built for; entries are filtered
331
+ * by {@link #matches} so a different routine sharing the same exact apiPath isn't selected.
332
+ * @returns {Promise<*>} The selected contributor's return value.
333
+ * @throws {SlothletError} `INVALID_ARGUMENT` when no contributor with `moduleID === key` matches
334
+ * this routine at this path; `ROUTINE_FAILED` when the selected contributor throws.
335
+ * @public
336
+ */
337
+ public runPathFor(apiPath: string, key: string, args?: any[], routine?: object): Promise<any>;
338
+ /**
339
+ * List the `moduleID`s of every contributor to a routine at an exact api path, in registration
340
+ * order (#400 — the mechanism behind `api.<path>.<name>.contributors`). Symmetry with
341
+ * `versioning.list(path)`: it lets a host discover which co-owners it can address via
342
+ * {@link runPathFor} / `.for(key)`. Enumerates every contribution regardless of the
343
+ * `stackRoutines` owner-filter, since `.for(key)` can address any of them.
344
+ * @param {string} apiPath - Exact composed api path.
345
+ * @param {object} [routine] - The routine config; entries are filtered by {@link #matches} so a
346
+ * different routine sharing the same exact apiPath isn't counted.
347
+ * @returns {string[]} The contributors' `moduleID`s at this path, in registration order.
348
+ * @public
349
+ */
350
+ public contributorsAt(apiPath: string, routine?: object): string[];
320
351
  /**
321
352
  * Run the root cascade for a routine: every matching contribution anywhere, grouped by exact
322
353
  * api path — with `stackRoutines: true`, contributors colliding at the same path all run
@@ -339,7 +370,7 @@ export class RoutineManager extends ComponentBase {
339
370
  * use only, for a caller (`#runModeRoutines`) that already force-materialized this exact
340
371
  * routine immediately beforehand and would otherwise re-walk the same tree for no new
341
372
  * information. Always leave this `false` for any externally-triggered cascade (the installed
342
- * `api[name]()` / `api.slothlet[name]()` callables never pass it), since those calls have no
373
+ * `api[name]()` callable never passes it), since those calls have no
343
374
  * such prior guarantee.
344
375
  * @returns {Promise<*>} The sole involved path's result, an ordered array of every involved
345
376
  * path's result when there are two or more, `[]` when the routine has no contributors
@@ -396,10 +427,11 @@ export class RoutineManager extends ComponentBase {
396
427
  public runStartupModeRoutines(): Promise<void>;
397
428
  /**
398
429
  * Overwrite every currently-known matching api path's slot on the live api tree with its
399
- * stacked callable, and (re)attach every configured routine's root cascade at the api root and
400
- * under `api.slothlet` (using the routine's `name` verbatim as the property key — a dotted or
401
- * `^`-prefixed name is reachable via bracket notation, e.g. `api.slothlet["admin.initialize"]`,
402
- * `api["^ext.*.initialize"]`; only a bare name gets clean dot-notation access).
430
+ * stacked callable, and (re)attach every configured routine's root cascade at the api root
431
+ * (using the routine's `name` verbatim as the property key — a dotted or `^`-prefixed name is
432
+ * reachable via bracket notation, e.g. `api["^ext.*.initialize"]`; only a bare name gets clean
433
+ * dot-notation access). A routine configured with `cascade: false` (#400) gets no root cascade.
434
+ * The cascade lives only at the root `api.<name>`, never mirrored onto `api.slothlet.*` (#399).
403
435
  *
404
436
  * @description
405
437
  * Safe to call repeatedly — at the end of initial `load()`, again after every
@@ -242,12 +242,13 @@ export class Config extends ComponentBase {
242
242
  * a root cascade runs every matching contribution anywhere, ordered per the entry's `order`.
243
243
  * See `docs/LIFECYCLE.md` ("Routines") for the full contract.
244
244
  *
245
- * Each entry normalizes to `{ name, mode, recursive, order }` — `recursive` and `order` are
246
- * always present on the normalized output, even when the raw entry omitted them:
247
- * - `"name"` (string, no `:`) → `{ name, mode: "manual", recursive: false, order: "mount" }`.
248
- * - `"name:mode"` (string, split once on the first `:`) → `{ name, mode, recursive: false, order: <mode-defaulted> }`.
249
- * - `{ name, mode?, recursive?, order? }` (object) → `mode` defaults to `"manual"`, `recursive` to
250
- * `false`, and `order` to {@link DEFAULT_ROUTINE_ORDER_BY_MODE}`[mode]` when each is omitted.
245
+ * Each entry normalizes to `{ name, mode, recursive, order, cascade }` — `recursive`, `order` and
246
+ * `cascade` are always present on the normalized output, even when the raw entry omitted them:
247
+ * - `"name"` (string, no `:`) → `{ name, mode: "manual", recursive: false, order: "mount", cascade: true }`.
248
+ * - `"name:mode"` (string, split once on the first `:`) → `{ name, mode, recursive: false, order: <mode-defaulted>, cascade: true }`.
249
+ * - `{ name, mode?, recursive?, order?, cascade? }` (object) → `mode` defaults to `"manual"`, `recursive`
250
+ * to `false`, `order` to {@link DEFAULT_ROUTINE_ORDER_BY_MODE}`[mode]`, and `cascade` to `true` when each
251
+ * is omitted. `cascade: false` (#400) suppresses the root `api.<name>()` run-all cascade for that routine.
251
252
  *
252
253
  * Providing `routines` at all REPLACES {@link DEFAULT_ROUTINES} — that is the off-switch
253
254
  * (`routines: []` disables every routine). Omitting the option keeps the built-in defaults.
@@ -258,8 +259,8 @@ export class Config extends ComponentBase {
258
259
  * normalizes to an equivalent list — same values, always freshly-built objects (never the same
259
260
  * references) — so `reload()` can safely re-feed it.
260
261
  *
261
- * @param {undefined|null|Array<string|{name: string, mode?: string, recursive?: boolean, order?: string}>} routines - Raw `routines` option.
262
- * @returns {Array<{name: string, mode: "manual"|"startup"|"shutdown"|"destroy", recursive: boolean, order: "mount"|"depth"}>} Normalized routines list.
262
+ * @param {undefined|null|Array<string|{name: string, mode?: string, recursive?: boolean, order?: string, cascade?: boolean}>} routines - Raw `routines` option.
263
+ * @returns {Array<{name: string, mode: "manual"|"startup"|"shutdown"|"destroy", recursive: boolean, order: "mount"|"depth", cascade: boolean}>} Normalized routines list.
263
264
  * @throws {SlothletError} INVALID_CONFIG when the shape is invalid, a name is empty/reserved/an invalid glob, or a mode/order is unrecognized.
264
265
  * @public
265
266
  *
@@ -283,11 +284,13 @@ export class Config extends ComponentBase {
283
284
  mode?: string;
284
285
  recursive?: boolean;
285
286
  order?: string;
287
+ cascade?: boolean;
286
288
  }>): Array<{
287
289
  name: string;
288
290
  mode: "manual" | "startup" | "shutdown" | "destroy";
289
291
  recursive: boolean;
290
292
  order: "mount" | "depth";
293
+ cascade: boolean;
291
294
  }>;
292
295
  /**
293
296
  * Normalize permissions configuration.
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Compile a glob pattern into a matcher function.
3
3
  * Supports: * (any chars except .), ** (any chars including .), ? (single char),
4
- * {a,b} brace expansion, !pattern negation
4
+ * {a,b} brace expansion, !pattern whole-pattern negation, and !(a|b) scoped exclusion —
5
+ * a per-segment complement matching any single segment except the listed literal alternatives
6
+ * (e.g. `admin.!(initialize)` matches `admin.start` but not `admin.initialize`).
5
7
  *
6
8
  * @param {string} pattern - Glob pattern
7
9
  * @param {object} [options={}] - Options
@@ -38,8 +38,10 @@ export class Utilities extends ComponentBase {
38
38
  * Strategy:
39
39
  * 1. Try `structuredClone` — fast and spec-correct for plain data.
40
40
  * 2. Fall back to a manual recursive copy for Proxies, callables, and other
41
- * non-serialisable objects; errors on individual property clones are swallowed
42
- * and the original reference is retained for that key.
41
+ * non-serialisable objects. Callables (functions / callable Proxies) are kept
42
+ * BY REFERENCE — they cannot be reconstructed from a property copy — while the
43
+ * surrounding data is still deep-cloned; errors on individual property clones are
44
+ * swallowed and the original reference is retained for that key.
43
45
  *
44
46
  * @param {unknown} obj - Value to clone.
45
47
  * @returns {unknown} Deep clone of `obj`.
@@ -78,7 +78,7 @@ export class Flatten extends ComponentBase {
78
78
  };
79
79
  /**
80
80
  * Build category-level flattening decisions.
81
- * Implements conditions C10-C33 from buildCategoryDecisions().
81
+ * Implements conditions C10-C24 from buildCategoryDecisions().
82
82
  * @param {object} options - Category options
83
83
  * @param {string} options.categoryName - Category name
84
84
  * @param {object} options.mod - Module exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cldmv/slothlet-types",
3
- "version": "3.16.2",
3
+ "version": "3.17.0",
4
4
  "description": "TypeScript declaration files (.d.mts) for @cldmv/slothlet. Install alongside @cldmv/slothlet for editor and type-checker support.",
5
5
  "keywords": [
6
6
  "slothlet",
@@ -79,7 +79,7 @@
79
79
  "LICENSE"
80
80
  ],
81
81
  "peerDependencies": {
82
- "@cldmv/slothlet": "3.16.2"
82
+ "@cldmv/slothlet": "3.17.0"
83
83
  },
84
84
  "peerDependenciesMeta": {
85
85
  "@cldmv/slothlet": {
package/slothlet.d.mts CHANGED
@@ -156,13 +156,14 @@ export type SlothletOptions = {
156
156
  */
157
157
  collectLifecycleHooks?: boolean | undefined;
158
158
  /**
159
- * - Stackable lifecycle routines (#341). Every mounted module exporting a function matching a configured routine name is composed into one callable at its exact composed api path, plus a root cascade (`self.<name>()` ≡ `api.slothlet.<name>()`) that runs every matching contribution anywhere. Entries: `"name"` (mode `"manual"`), `"name:mode"`, or `{ name, mode?, recursive?, order? }` (`recursive`/`order` only settable via the object form). `name` is mount-relative by default (a bare name matches only a mount's own top level; a dotted name matches a fixed relative sub-path, or with `recursive: true` any depth within the mount); a `^`-prefixed name is root-anchored, matched via glob (`*`, `**`, `{}`, `!`) against the full api path, crossing mount boundaries. `order` (`"mount"` | `"depth"`, mode-defaulted) controls the root cascade's grouping order. Providing `routines` at all REPLACES the built-in defaults (`slothlet.defaults.routines`: `initialize` → `startup`, `shutdown` → `shutdown`) — spread `slothlet.defaults.routines` to extend them instead, or pass `[]` to disable every routine. Every configured routine is always wrapped and directly callable regardless of `autoRoutines`. Whether two or more contributors colliding at the identical api path all run is governed by `stackRoutines` (#365), independent of `collisionMode` — by default only the single contribution that actually owns that path runs, matching ordinary collision behavior. A throwing contributor doesn't stop the chain — every contributor runs (best-effort), and one aggregate `ROUTINE_FAILED` error is thrown afterward if any failed. See [LIFECYCLE.md](docs/LIFECYCLE.md#routines).
159
+ * - Stackable lifecycle routines (#341). Every mounted module exporting a function matching a configured routine name is composed into one callable at its exact composed api path, plus a root cascade (`self.<name>()`, i.e. `api.<name>()`) that runs every matching contribution anywhere. Entries: `"name"` (mode `"manual"`), `"name:mode"`, or `{ name, mode?, recursive?, order?, cascade? }` (`recursive`/`order`/`cascade` only settable via the object form). `name` is mount-relative by default (a bare name matches only a mount's own top level; a dotted name matches a fixed relative sub-path, or with `recursive: true` any depth within the mount); a `^`-prefixed name is root-anchored, matched via glob (`*`, `**`, `{}`, `!`) against the full api path, crossing mount boundaries. `order` (`"mount"` | `"depth"`, mode-defaulted) controls the root cascade's grouping order. `cascade` (default `true`) controls whether the root `api.<name>()` run-all cascade is created at all — set `cascade: false` (#400) for a per-entity lifecycle routine, where the host must invoke exactly one co-owner by key via `api.<path>.<name>.for(moduleID)(...)` rather than a run-all; every stacked path also exposes `api.<path>.<name>.contributors` (the moduleIDs present there). `.for(key)` runs that one contributor in its own extent/identity with the args passed straight through, and bypasses the `stackRoutines` owner-filter so a specific co-owner runs even if it lost the shared-path collision. Providing `routines` at all REPLACES the built-in defaults (`slothlet.defaults.routines`: `initialize` → `startup`, `shutdown` → `shutdown`) — spread `slothlet.defaults.routines` to extend them instead, or pass `[]` to disable every routine. Every configured routine is always wrapped and directly callable regardless of `autoRoutines`. Whether two or more contributors colliding at the identical api path all run is governed by `stackRoutines` (#365), independent of `collisionMode` — by default only the single contribution that actually owns that path runs, matching ordinary collision behavior. A throwing contributor doesn't stop the chain — every contributor runs (best-effort), and one aggregate `ROUTINE_FAILED` error is thrown afterward if any failed. See [LIFECYCLE.md](docs/LIFECYCLE.md#routines).
160
160
  */
161
161
  routines?: (string | {
162
162
  name: string;
163
163
  mode?: ("manual" | "startup" | "shutdown" | "destroy");
164
164
  recursive?: boolean;
165
165
  order?: ("mount" | "depth");
166
+ cascade?: boolean;
166
167
  })[] | undefined;
167
168
  /**
168
169
  * - The non-deprecated replacement for `collectLifecycleHooks`. TEMPORARY v3-compat default (#341): `false` for now, so a project upgrading sees no behavior change from a pre-existing nested leaf that happens to share a routine's name (e.g. `shutdown`) — it stays stacked and directly callable, but does not start auto-firing. When `true`, every `mode: "startup"` routine's cascade runs at the end of compose, and every `mode: "shutdown"`/`"destroy"` routine's cascade runs on the corresponding dispose call. Planned to default to `true` in v4 (`collectLifecycleHooks` removed at the same time) — see [LIFECYCLE.md](docs/LIFECYCLE.md#routines).