Skip to content

deps: bump vitest from 5.0.1 to 5.0.2 in the vitest group - #538

Merged
cldmv-bot[bot] merged 1 commit into
nextfrom
dependabot/npm_and_yarn/next/vitest-4ca34babcb
Sep 30, 2026
Merged

cldmv-bot[bot] merged 1 commit into
nextfrom
dependabot/npm_and_yarn/next/vitest-4ca34babcb

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Sep 30, 2026

Copy link
Copy Markdown
Contributor

Bumps the vitest group with 1 update: vitest.

Updates vitest from 5.0.1 to 5.0.2

Release notes

Sourced from vitest's releases.

v5.0.2

   🐞 Bug Fixes

    View changes on GitHub
Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions

Bumps the vitest group with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vitest` from 5.0.1 to 5.0.2
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.2/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 5.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: vitest
...

Signed-off-by: dependabot[bot] <support@github.com>
@cldmv-bot cldmv-bot Bot added the type: dependencies Relates to dependency updates, version bumps, or package management label Sep 30, 2026

@cldmv-bot cldmv-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved by CLDMV-bot: patch bump from 5.0.1 to 5.0.2.

@cldmv-bot
cldmv-bot Bot enabled auto-merge September 30, 2026 01:05
@cldmv-bot
cldmv-bot Bot merged commit 779d1f5 into next Sep 30, 2026
27 checks passed
@dependabot
dependabot Bot deleted the dependabot/npm_and_yarn/next/vitest-4ca34babcb branch September 30, 2026 01:08
cldmv-bot Bot added a commit that referenced this pull request Oct 3, 2026
…ach…

# Slothlet v3.22.0 Changelog

**Release Date**: October 2026
**Release Type**: Minor
**Branch**: `release/3.22.0`

---

## Overview

Version 3.22.0 is a large release. It adds **`api.slothlet.restart()`**, a clean-slate rebuild of an instance from its original config behind the same `api` reference, and an **`around` hook** type that wraps the whole call pipeline. It also lets a host take over **event delivery** (`event.strategy` / `event.deliver`), for example to deliver events only after a transaction commits. On the permission side it adds **`permissions.owner`**, **`slothlet.lockCaller.caller`** and **`permissions.global.checkCall`**. **Typegen** now types the composed `api`, `self` and `slothlet()` from each node's real module origin, so an imported `self` and an awaited `slothlet()` are typed with no annotation.

Every module a leaf imports through a relative or `file:` specifier is now **per instance**, across `.mjs`, `.ts`, `.cjs` and CommonJS `.js` leaves and across ESM/CommonJS boundaries. This uses the in-thread `module.registerHooks()` resolve hook, so **the minimum Node.js version rises from `22.12.0` to `22.15.0`**.

The release also fixes caller attribution in the live runtime, makes the framework's lifecycle methods host-only under a permissions config, and fixes a long list of reload, remove and ownership bugs around `forceOverwrite` and co-mounted modules. Read **Requirement and behavior changes** and the upgrade notes at the end before upgrading.

---

## ⚠️ Requirement and behavior changes

### Node.js `>=22.15.0` is now required (#518)

`engines.node` rises from `>=22.12.0` to `>=22.15.0`. Per-instance helper isolation (see [Helper modules are per instance](#helper-modules-are-per-instance-518-534)) is built on `module.registerHooks()`, the synchronous in-thread resolve hook added in Node 22.15 / 23.5. The off-thread `module.register()` was deliberately not used: it puts a cross-thread round trip on every import in the process and changes evaluation timing enough to break a leaf that starts a fire-and-forget runtime import. Changed in [#537](#537).

**Upgrade:** move to Node 22.15 or later (any Node 24 or 26 release qualifies). On older Node versions npm reports `EBADENGINE` and helper isolation cannot be installed.

### Live-runtime caller attribution is now correct (#512)

With `runtime: "live"`, the caller was resolved from the call stack whenever two or more module calls were suspended, and frames were matched only by the exact file a suspended call had entered through. V8's async stack traces also carry the frames of the outer suspended calls, so:

- a module entered synchronously (a nested leaf, or a `lockCaller`-pinned callback) whose own file was not a suspended call's was attributed to the outer suspended caller further down the stack;
- a suspended call resuming in a sibling file of its module fell through to the outer suspended caller the same way;
- the fallback read a shared field that is restored in settle order, not entry order, so a resumed module could be handed the rights of a caller that had already finished.

The caller is now resolved from what is actually in flight: the call whose function body is on the JS stack right now; otherwise the baseline caller when nothing is in flight; otherwise the single suspended call; and with two or more suspended calls, the stack frames matched by location against each candidate's entry file or module root folder. A frame that matches several candidates ambiguously fails closed. A call pinned to the host with `lockCaller.caller()` now runs as the host on the running-call stack instead of being attributed to the module that invoked it. Fixed in [#519](#519); the rule and its limits are documented under **Overlapping calls** in [docs/PERMISSIONS.md](../../PERMISSIONS.md).

**Upgrade:** hosts on the live runtime with `defaultPolicy: "deny"` should re-check their permission grants. Calls that were previously misattributed, typically to an outer boot or host-chain caller with broader rights, are now attributed to the module that actually made them, so a grant that only worked because of the misattribution now denies. The async runtime is unaffected.

### New built-in denies when a permissions config is present (#529, #504, #477, #508)

Reloading, restarting or shutting down the instance is a host decision, so new built-in rules deny it to modules:

| Built-in deny target                                                               | Applies                                                                                                                                      |
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `slothlet.reload`, `slothlet.shutdown`                                             | always                                                                                                                                       |
| `slothlet.restart`                                                                 | always                                                                                                                                       |
| `initialize` (root `api.initialize()` cascade), `shutdown` (root `api.shutdown()`) | only while the matching default routine (`initialize` / `startup`, `shutdown` / `shutdown`) is in the instance's effective `routines` config |
| `slothlet.lockCaller.caller`                                                       | always                                                                                                                                       |
| `slothlet.permissions.global.checkCall`                                            | always                                                                                                                                       |

The root `api.shutdown()`, `api.destroy()` and routine cascades now check their own path at entry, so ordinary rules on `shutdown`, `destroy` or a routine name apply to them. Framework-internal teardown runs as the host, so a permitted caller is never refused halfway. As with every built-in rule, these apply only when the instance has a `permissions` config, and an instance rule on the same exact target re-opens one for a trusted module. The new `event.strategy` / `event.deliver` surfaces are host-only through the existing `slothlet.event.**` deny. Changed in [#544](#544), [#545](#545), [#516](#516) and [#513](#513).

> **Caveat:** removing the default routines (for example `routines: []`) removes the built-in rules on their root paths, but the top-level `api.shutdown()` / `api.destroy()` still tear the instance down. A host that removes the defaults and wants modules kept away from teardown must add its own rules:
>
> ```javascript
> { caller: "**", target: "shutdown", effect: "deny" }
> { caller: "**", target: "destroy",  effect: "deny" }
> ```

**Upgrade:** a module that legitimately calls `self.slothlet.reload()`, `restart()`, `shutdown()`, the root `shutdown()` / `initialize()`, `lockCaller.caller()` or `permissions.global.checkCall()` needs an exact-target allow rule from the host, for example `{ caller: "admin.**", target: "slothlet.reload", effect: "allow" }`.

### TypeScript 7: strict mode fails with a clear error (#510)

TypeScript 7's current npm release exposes no compiler API (it lives under unstable paths until 7.1). Strict mode crashed with `Cannot read properties of undefined (reading 'ES3')`; it now throws `TYPESCRIPT_STRICT_REQUIRES_TS6`, naming the installed version and pointing at `typescript@6`, before any strict-mode code runs. The peer range (`^6.0.3 || ^7.0.0`) is unchanged so TypeScript 7 projects that don't use strict mode still install cleanly. Fast mode (esbuild) and `slothlet typegen` don't use the compiler API and work on TypeScript 7. Fixed in [#532](#532).

**Upgrade:** stay on `typescript@6` if you use `typescript: { mode: "strict" }`.

### Other behavior changes

- **Event-rule conditions see the user context (#511).** An event rule's condition received the whole async-context store, while a call rule's condition receives the user context from `context.run()`, so the same condition behaved differently on the two rule types. Both now receive the user context, and principal key resolvers on event rules see it too. A condition that worked around the bug by reading `ctx.context.*` must read `ctx.*`. Fixed in [#514](#514).
- **Helpers are no longer shared across instances (#518, #534).** Module-level state in a relative helper used to be one copy per process; it is now one copy per instance. Code that relied on two instances sharing a helper's state should move that state into a package (bare specifiers stay shared) or pass it in through `reference`.
- **TypeScript source maps default on during coverage runs (#484, #486).** When `typescript.sourcemap` is not set, source maps are now on exactly during a coverage run; see **TypeScript coverage works out of the box** under Bug Fixes.
- **Function impls no longer expose non-enumerable own properties as api children.** On Node 22, every sloppy-mode (CommonJS) function carries own non-enumerable `arguments` and `caller`, so a `module.exports = function` leaf reported `Object.keys(api.x)` as `["arguments", "caller"]`, and typegen emitted them as `unknown` members. Only a function's own enumerable properties are children now; the values stay readable. Fixed in [#498](#498).

---

## ✨ Features

### `api.slothlet.restart()` — a clean-slate rebuild behind the same `api` (#504)

`reload()` deliberately keeps runtime state. `restart()` is the opposite: it tears the instance down through the normal teardown path and builds a new one from the original `slothlet({...})` config, swapped in behind the same `api` proxy.

```javascript
const api = await slothlet({ base: "./api" });

api.conn.handlers = { onSend }; // runtime assignment
await api.slothlet.api.add("plugins", "./plugins");

await api.slothlet.restart(); // both gone; the instance is back to its original config
```

- **Original config:** the config passed to `slothlet()` is snapshotted at creation. Plain objects and arrays are copied and frozen, `reference` and `context` are kept by identity, and a relative `base` stays resolved against the creating call.
- **Nothing runtime carries over:** `add()` / `remove()` history, runtime hooks, permission and event rules, principals, event and lifecycle subscriptions, metadata, runtime assignments and module-scope state are dropped; config-declared settings are re-applied. The new instance is not sealed, even after `control.seal()`.
- **Held references are re-pointed:** after the swap, `const conn = api.conn` forwards every operation to the node at the same path in the new instance. A path that no longer exists throws `RESTART_REFERENCE_UNRESOLVED`.
- **Lifecycle:** `restart` (old instance) → `shutdown` (old) → `init` (new) → `restarted` (new). `shutdown` now fires on every teardown and `init` on the cold start. Handlers in the `lifecycle` config option receive all four; runtime subscribers go with the old instance.
- **Concurrency:** concurrent `restart()` calls share one restart, and calls already in flight finish on the old implementation. Gated by `api.mutations.reload`, and host-only under a permissions config.

Implemented in [#545](#545). See [docs/RELOAD.md → `api.slothlet.restart()`](../../RELOAD.md#apislothletrestart) and [docs/LIFECYCLE.md → Instance Events](../../LIFECYCLE.md#instance-events).

### `around` hooks (#496)

A fifth hook type wraps the rest of the call pipeline:

```javascript
api.slothlet.hook.on("db.**:around", async ({ path, args, next, caller, entry }) => {
	const started = Date.now();
	try {
		return await next(args); // remaining around hooks → before → function → after
	} finally {
		metrics.record(path, Date.now() - started);
	}
});
```

- `next(args?)` runs the remaining (lower-priority) around hooks, then the before hooks, the function and the after hooks, and returns the result or throws. With no argument it forwards the current args. A handler that never calls `next` short-circuits, and its return value is the result. Calling `next` twice throws `HOOK_AROUND_NEXT_CALLED_TWICE`.
- Order, outermost first: `always` / `error` observers → around hooks (highest priority outermost) → `before` → function → `after`. `error` hooks and `suppressErrors` apply only to what escapes the around chain; an error an around throws itself carries the new `around` source type.
- `entry` is `true` when the call has no module caller in the active flow, and `caller` is that caller's metadata.
- A synchronous around keeps a synchronous call synchronous; an async one promotes it, as with before/after hooks.
- Around hooks are permission-gated like before hooks, and are pinned when registered from a module. A pinned around's `next()` re-enters the intercepted call's own flow, so the target still sees its real caller.

Implemented in [#517](#517). See [docs/HOOKS.md](../../HOOKS.md).

### Host-controlled event delivery — `event.strategy` / `event.deliver` (#497)

A host that wraps work in a transaction can deliver events emitted inside it only if the transaction commits, each listener as its own retried unit of work, without editing the modules that emit.

```javascript
const pending = [];

api.slothlet.event.strategy((envelope, listeners, defaultDeliver) => {
	if (!inTransaction()) return defaultDeliver(); // deliver now
	pending.push({ envelope, listeners }); // defer; emit resolves once this returns
});

// On commit:
for (const { envelope, listeners } of pending.splice(0)) {
	for (const id of listeners) await retry(() => api.slothlet.event.deliver(envelope, id));
}
```

- `strategy(fn)` is called per emit as `fn(envelope, listeners, defaultDeliver)`. The frozen envelope carries the event, payload, emitter and each recipient's emit-time level. `strategy(null)` clears it; it survives a full reload.
- `deliver(envelope, listenerId)` delivers a held envelope to one listener, so a retry never re-runs listeners that already succeeded. It resolves `{ delivered: true, level }` or `{ delivered: false, reason: "listener-gone" | "denied" }`, and rejects with the listener's own error. Rules and levels are enforced at delivery time, inside the flow captured at emit.
- `on()` / `once()` now also return a stable listener **`id`** (`<owner moduleID>:<event>:<n>`, or an explicit `{ key }` in place of `n`), which survives reloads once the module re-registers.

Both surfaces are host-only. Implemented in [#535](#535). See [docs/EVENTS.md](../../EVENTS.md).

### `permissions.owner` — a module reaches every leaf it owns (#509)

Under `defaultPolicy: "deny"`, a module added with one `api.add(path, folder, { moduleID })` couldn't call or read its own leaves across subdirectories, because each subdirectory is its own permission module. `permissions: { owner: true }` (default `false`) adds an implicit allow that stands in for the default policy: when no rule decided, a caller may access a target whose current owner is the caller's own current owner. Matching is by owner, not path, so another module merged into the same namespace gets nothing; an explicit deny still wins; and ownership changes from add, remove and reload are followed live. The reserved roots (`slothlet.*`, `shutdown`, `destroy`) are never granted. Granted accesses emit `permission:owner-allow` under `audit: "verbose"`. Implemented in [#528](#528). See **Owner Grant** in [docs/PERMISSIONS.md](../../PERMISSIONS.md#owner-grant).

### `slothlet.lockCaller.caller(fn)` — pin your caller onto a callback (#477)

`lockCaller(fn)` pins the leaf that calls it, so a service that accepts a callback on behalf of its caller (a scheduler's `every()`, a registry) could only pin itself. `self.slothlet.lockCaller.caller(fn)` pins the current leaf's **caller**: the identity `metadata.caller()` reports. When the leaf was called from outside any module, the callback runs as the host. Acting as your caller is a privilege, so the path has a built-in deny for every module; the host grants it, for example `{ caller: "scheduler.**", target: "slothlet.lockCaller.caller", effect: "allow" }`. Implemented in [#516](#516). See [docs/HOOKS.md](../../HOOKS.md).

### `permissions.global.checkCall(caller, target, args)` (#508)

`global.checkAccess` is a silent query: it passes no call metadata to conditions, never resolves a stale principal, emits no audit event, and treats a caller with no source file as the host. A trusted boundary layer that forwards a remote call (such as `@cldmv/slothlet-vine`) needs the answer the call gate would give. `api.slothlet.permissions.global.checkCall(callerPath, targetPath, args)` forwards `{ args, target }` to function conditions, resolves stale `requires` principals (returning a Promise only on that path), treats the caller as a module (no self-call bypass, `_`-private targets denied), and emits the gate's audit events with `via: "checkCall"`. It is host-only by default. Implemented in [#513](#513). See [docs/PERMISSIONS.md](../../PERMISSIONS.md).

### Typed composed `api`, `self` and `slothlet()` via typegen (#484)

Typegen used to guess each node's type by matching the last path segment against a file's ESM export syntax, so CommonJS leaves, default exports, flattened and hoisted members, object-leaf methods and non-function values came out as `any` or went missing. The builder now records each node's module origin (`exportPath` next to `filePath` in the ownership registry, readable through `ownership.getOrigin(apiPath)`), in eager mode, lazy materialization, reload and `api.add()` mounts alike. The type generator emits `typeof import("<leaf>")[...]` for every member, so each leaf is typed from its own source whatever the format (`.mjs` JSDoc, `.cjs`, `.ts` / `.mts`). Typegen no longer needs the `typescript` package.

`@cldmv/slothlet/runtime` exports a `SlothletSelf` type anchor and types `self` as it; the generated declaration extends it, and `slothlet()` is `<T = SlothletSelf>(...) => Promise<SlothletAPI & T>`. An imported `self` and an awaited `slothlet()` are both typed with no annotation. `augmentRuntime: false` (`--no-augment-runtime`) opts out for programs that load more than one api. Implemented in [#498](#498). See [docs/TYPESCRIPT.md](../../TYPESCRIPT.md#generating-types-on-demand-slothlet-typegen).

### Helper modules are per instance (#518, #534)

A module a leaf imports through a relative or `file:` specifier, at any depth, is now evaluated once per slothlet instance instead of once per process, for `.mjs` / `.js`, `.ts` / `.mts` and `.cjs` leaves. Base leaves and every `api.add()` mount of an instance share that instance's copy. A full reload (or a restart) gives the instance fresh helpers; a partial reload keeps the existing copy. Bare specifiers (packages, `node:` builtins, `#imports`, `@cldmv/slothlet` itself) stay shared.

Isolation also holds where the module systems meet: an ES module leaf importing a `.cjs` helper gets the helper through the instance's own CommonJS cache, and a relative `require()` of an ES module from a CommonJS leaf returns the instance's copy with Node's `require(esm)` shape. Under vitest, when leaves load through vite's module graph, add the `slothletInstanceImports()` plugin from `@cldmv/slothlet/helpers/instance-imports`. Implemented in [#537](#537) and [#551](#551). See [docs/MODULE-STRUCTURE.md → Helper Modules Are Per Instance](../../MODULE-STRUCTURE.md#helper-modules-are-per-instance) and [docs/TESTING.md](../../TESTING.md#per-instance-helpers-under-vitest-slothletinstanceimports).

---

## 🐛 Bug Fixes

### Loading and TypeScript

- **CommonJS `.js` leaves are isolated per instance (#521).** A `.js` file Node treats as CommonJS went through `import()` with an instance query that Node's path-keyed CommonJS cache ignores, so two instances shared one module scope. The loader now classifies `.js` leaves the way Node does (nearest `package.json` `type`, then syntax detection) and routes CommonJS ones through the per-instance CommonJS loader ([#523](#523)).
- **TypeScript coverage works out of the box (#484, #486).** A TypeScript leaf executes from its `.slothlet-cache` copy, so coverage reaches the `.ts` source only through the copy's inline source map. `typescript.sourcemap` is now tri-state: an explicit boolean wins, and when unset, source maps are on exactly during a coverage run (vitest coverage, or any process with `NODE_V8_COVERAGE` set). A coverage run with `sourcemap: false` warns once with `WARNING_COVERAGE_TS_SOURCEMAP_OFF`. [docs/TESTING.md](../../TESTING.md#typescript-leaves) gains a TypeScript section, including adding `**/.slothlet-cache/**` to `coverage.include` ([#527](#527)).
- **`module`, `strict`, `compilerOptions` and `sourcemap` TypeScript options are honoured (#499).** Normalization dropped the first three, so strict mode always used fixed defaults, and `sourcemap` had no effect in either mode. They are now carried through and validated (`INVALID_CONFIG` on a bad value); `compilerOptions` takes the `tsconfig.json` form, and both modes emit an inline source map pointing at the leaf's absolute path ([#515](#515)).
- **Strict mode's type-generation worker ships in the package (#500).** The worker lived under `tools/` and imported `src/`, neither of which is published, so strict mode failed with `MODULE_NOT_FOUND` from an npm install. It now ships as `dist/lib/processors/type-generation-worker.mjs` ([#502](#502)).

### Reload, remove and ownership

- **A scoped reload keeps co-mounted modules' contributions (#525).** Reloading one module handed the shared namespace only that module's fresh impl in `replace` mode, clearing every child another module had added at the same path. Children are now split by contributor at every depth: other modules' children are reattached unchanged, shared subfolders are rebuilt one level down, and ownership stacks are restored afterwards so a later remove still reverts correctly. A lazy add that collides with an existing namespace under `merge` / `merge-replace` now merges that folder's children too ([#549](#549)).
- **A scoped reload keeps a `replace` / `forceOverwrite` add's outcome (#530).** After `add("launcher.session", dir, { moduleID: "shadow", forceOverwrite: true })`, `reload("launcher")` put launcher's shadowed members back under `launcher.session` while ownership still named `shadow`. The overwritten subtree now stays as the add left it, and the shadowed members are refreshed with the reloaded code so a later remove restores current code ([#550](#550)).
- **`forceOverwrite` adds own the paths they overwrite (#524).** The ownership registry kept the overwritten module as current owner, and eager and lazy mode disagreed on which paths were affected. The add's collision mode is now recorded per module and endpoint, and a `replace` / `merge-replace` add claims every path it provides ([#536](#536)).
- **Removing an overwriting module restores the overwritten namespace in place (#531).** In lazy mode, removing a module that `forceOverwrite`-replaced part of another mount could lose the overwritten module's nested leaves or overflow the stack. The surviving owners' content is now restored into the live wrappers, shallow-first and before anything is deleted, so references taken while the overwriting module was live keep working and values set by hand are kept ([#546](#546)).
- **Values assigned to a lazy namespace during materialization are kept (#543).** A value assigned while a `replace`-mode namespace (such as one placed by a `forceOverwrite` add) was still materializing was deleted as a stale child. User-assigned keys are now tracked and survive the adoption ([#547](#547)).
- **A namespace becomes callable when a later module supplies its function (#533).** A Proxy's callability is fixed at creation, so a namespace created as a plain object by one module stayed non-callable after another module added a function at the same path, and a `merge` dropped the function entirely. The namespace is now upgraded to a callable proxy so `api.<path>()` works: `merge` keeps an existing function and fills an empty slot, and `merge-replace` takes the incoming function. Removing a module rebuilds the namespace's function from the remaining modules in add order, so removing the module whose function won reverts to the previous function, while removing a module whose function lost, or one that contributed only children, keeps the current one. A reference held from before the upgrade keeps reading, writing and enumerating the namespace but stays non-callable; re-read it from `api`. The upgrade is one-way: if the function later goes away, calling the namespace throws `INVALID_CONFIG_NOT_A_FUNCTION` ([#556](#556)).
- **Removing the module that created a namespace keeps other modules' children (#555).** When a module's callable default export created a namespace (for example `plugins`) and other modules merged children into it (`plugins.gamma`), removing that module invalidated every one of those children recursively, because the routine manager's prune walked the live namespace; calling `api.plugins.gamma()` then threw "plugins.gamma is invalidated". Only wrappers that are no longer live are invalidated now, so every other module's children stay reachable and callable, with the same references, in eager and lazy mode ([#558](#558)).
- **`remove()` no longer throws on frozen or sealed exports (#485).** Removing a module that exported a frozen or sealed object aborted with "Cannot delete property", leaving stale leaves behind ([#491](#491)).

### Lifecycle and routines

- **A root `shutdown` export runs once per teardown (#542).** With `autoRoutines: true`, a module's root `shutdown` export is both a contribution to the default `shutdown` routine and the root shutdown hook, so `api.shutdown()` ran it twice, and `api.destroy()` and `restart()` inherited that. A root `destroy` export under a `mode: "destroy"` routine was invoked twice the same way. Each now runs exactly once per teardown, `restart()` included. `api.slothlet.shutdown()` stays framework-only: it runs the shutdown-mode routines but never calls the root hooks itself, so with `autoRoutines: false` it runs no module code ([#553](#553)).

### Wrapper, context and events

- **Object and function writes through a live view land on the underlying object (#495).** A wrap-on-set view (`self.X = obj`) forwarded primitive writes to the live object but stored object- and function-valued writes on the wrapper, which then shadowed later raw updates. Those writes now go to the underlying object, and a refused write on a frozen object throws `TypeError` like the primitive path ([#501](#501)).
- **`once("data")` fires with the EventEmitter context patch enabled (#503).** The patched `once` attached through the base `EventEmitter.prototype.on`, bypassing `Readable`'s override that resumes a paused stream, so `socket.once("data")` and `events.once(socket, "data")` hung. It now attaches through the emitter's own `on` / `removeListener` ([#505](#505)).

### Packaging

- **The rolldown native binding is no longer installed for consumers (#461).** `@rolldown/binding-linux-x64-gnu` was listed in `optionalDependencies` as a lockfile workaround, so every consumer installed a 20 MB Linux bundler binary slothlet never uses. The entry is removed ([#493](#493)).

---

## 🔧 CI & tooling

- **Release merge re-arms on every check-producing workflow.** `release-merge.yml` re-synced from the `CLDMV/.github` v4.29.2 template, so an approval given before CodeQL or another non-CI check finishes no longer leaves the release PR stuck ([#490](#490)).
- **The required PR check can no longer pass while tests are still running.** On an in-repo feature PR, the skipped `pull_request` run posted a skipped `✅ Required PR Check`, which GitHub treats as satisfied, so a PR could merge before the push run finished testing. On that path the mirror job now runs as a no-op under the name `⏭️ Required PR Check (reported by the push run)`, so `✅ Required PR Check` only ever comes from a run that has finished the test matrix, synced from `CLDMV/.github` ([#554](#554), [#557](#557)).
- **Bundle-size measures the published JS files.** The workflow now measures exactly the 62 JS files `npm pack` publishes instead of `dist/**` ([#494](#494)).
- **`analyze` and `i18n:check` skip gitignored paths** through a shared `.gitignore` matcher ([#492](#492)).
- **Test discovery excludes `tmp/` and `trash/`** via `@cldmv/vitest-runner` 1.5.1's `exclude` option, plus a repeatable `--exclude` flag on the runner wrapper (#520, [#522](#522)).
- **Tests:** a regression guard that a lazy `api.add()` loads only the shared subfolders its merge compares; the background loading reported in #548 could not be reproduced ([#552](#552)). A `resolve-from-caller` test no longer depends on the checkout's folder name (#506, [#507](#507)).

---

## 📚 Documentation

- **NEW:** [docs/changelog/v3/v3.22.0.md](./v3.22.0.md) — this changelog.
- [docs/RELOAD.md](../../RELOAD.md#apislothletrestart) — `api.slothlet.restart()`, reload vs restart, held references, and scoped-reload behavior for co-mounted and overwritten modules (#504, #525, #530).
- [docs/LIFECYCLE.md](../../LIFECYCLE.md) — the `restart` / `shutdown` / `init` / `restarted` instance events, and how each teardown entry point treats root `shutdown` / `destroy` exports (#504, #542).
- [docs/HOOKS.md](../../HOOKS.md) — the `around` hook type, and `lockCaller.caller` (#496, #477).
- [docs/EVENTS.md](../../EVENTS.md) — listener ids, `event.strategy` / `event.deliver`, and event-rule conditions (#497, #511).
- [docs/PERMISSIONS.md](../../PERMISSIONS.md) — Owner Grant, `checkCall`, Overlapping calls, and the host-only lifecycle methods and default routines (#509, #508, #512, #529, #504).
- [docs/PERMISSIONS-CONDITIONS.md](../../PERMISSIONS-CONDITIONS.md) and [docs/CONTEXT-PROPAGATION.md](../../CONTEXT-PROPAGATION.md) — cross-references for `checkCall` and `lockCaller.caller`.
- [docs/MODULE-STRUCTURE.md](../../MODULE-STRUCTURE.md#helper-modules-are-per-instance) — Helper Modules Are Per Instance, and which `.js` files are CommonJS (#518, #534, #521).
- [docs/TESTING.md](../../TESTING.md) — TypeScript leaves under coverage and the `slothletInstanceImports()` vite plugin (#484, #486, #534).
- [docs/TYPESCRIPT.md](../../TYPESCRIPT.md) — strict-mode options, the `sourcemap` default, TypeScript 7, and origin-based typegen with `SlothletSelf` (#499, #484, #510).

---

## 🔧 Dependencies

- **Removed** `@rolldown/binding-linux-x64-gnu` from `optionalDependencies` (#461).
- **Engines:** `node` `>=22.12.0` → `>=22.15.0` (#518).
- Development only: `@cldmv/vitest-runner` `^1.5.1` ([#522](#522)), `vitest` 5.0.2 ([#538](#538)), `prettier` 3.9.9 ([#539](#539)), `@types/node` 26.6.3 ([#541](#541)), and `ignore` added for the gitignore-aware tooling ([#492](#492)).

---

## Upgrade notes

1. **Node.js 22.15 or later is required.** Upgrade the runtime before installing 3.22.0.
2. **Live runtime with `defaultPolicy: "deny"`:** re-check permission grants. Callers that were previously misattributed to an outer caller are now attributed to the module that made the call, so a grant that depended on the misattribution now denies.
3. **Permissions config present:** modules can no longer call `slothlet.reload`, `slothlet.shutdown`, `slothlet.restart`, `slothlet.lockCaller.caller`, `slothlet.permissions.global.checkCall`, or the root `initialize` / `shutdown` paths while the default routines are configured. Grant trusted modules an exact-target allow rule. With `routines: []`, add your own deny rules on `shutdown` and `destroy`, because the top-level `api.shutdown()` / `api.destroy()` still tear the instance down.
4. **TypeScript strict mode:** stay on `typescript@6`. Strict mode throws `TYPESCRIPT_STRICT_REQUIRES_TS6` on TypeScript 7; fast mode and typegen are unaffected.
5. **Event-rule conditions** that read `ctx.context.*` to work around the old store shape must read `ctx.*`.
6. **Helper state is per instance.** If two instances relied on sharing module-level state in a relative helper, move that state into a package or pass it through `reference`.
7. **Coverage:** TypeScript source maps are now on automatically during coverage runs. Add `**/.slothlet-cache/**` to `coverage.include` to see `.ts` leaves reported; set `typescript.sourcemap` explicitly to override.
8. **Typegen:** regenerate declarations to pick up origin-based types. Programs loading more than one api should pass `--no-augment-runtime` for the extra declarations and type `slothlet<OtherApi>()` explicitly.



<details>
<summary>👥 Contributors</summary>

- @Shinrai

</details>

---

<!-- coverage-start -->

![coverage](https://img.shields.io/badge/coverage-99.5%25-brightgreen?style=for-the-badge&logo=vitest&logoColor=white)

| Metric | Coverage |
|--------|----------|
| Statements | 99.6% |
| Branches   | 99.1% |
| Functions  | 99.7% |
| Lines      | 99.8% |

*Avg: **99.5%** · `a82800d` · Node lts/**

<!-- coverage-end -->

<!-- co-authors -->

Co-authored-by: Shinrai <Shinrai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: dependencies Relates to dependency updates, version bumps, or package management

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants