# Slothlet v3.10.0 Changelog **Release Date**: June 2026 **Release Type**: Minor **Branch**: `release/3.10.0` --- ## Overview Version 3.10.0 adds two features and closes the last browser-mode resolution gap. The headline is **synthetic / in-memory leaves**: `api.slothlet.api.add()` now accepts inline content — a function or an export-map object — so a single leaf (or a whole branch) can be mounted without first writing it to a temp directory (#117). The second feature **integrates the hook system with the permission system** (#118): when permissions are configured, a hook can no longer intercept a leaf its registrant has no permission to reach, hooks are pinned to their owner by default, and hook selectors move to a `pattern:type` suffix syntax. The release also finishes the browser importmap — it is now built from the package's full public export surface, so every exported endpoint (including the documented `@cldmv/slothlet/runtime` aggregator) resolves in a real browser (#137). **Compatibility.** No API breaking changes. The synthetic-leaf forms are additive overloads of `api.add()`. Hook permission gating is inert unless a `permissions` block is configured; the legacy `type:pattern` hook selector still works but is deprecated in favor of `pattern:type`. One behavior change to note: module-registered hooks are now force-pinned to their owner by default (see Upgrade notes). --- ## ✨ Features ### Synthetic / in-memory leaf for `api.slothlet.api.add()` (#117) `api.add()` previously required a _directory_ to scan — mounting a single in-memory function meant writing it to a temp file first. It now accepts inline content directly: a bare function, a plain `{ default?, ...named }` export map, or a `{ exports, ...options }` shorthand. The inline exports flow through the same smart-flatten + wrap pipeline a file-backed leaf does, so a synthetic leaf behaves identically to one loaded from disk — `self`, per-request context, lifecycle events, and permissions all apply. Malformed inputs (arrays, class instances, empty export maps) fail up front with a structured `SlothletError` instead of a confusing `TypeError` deep in the flatten pipeline. See the `api.slothlet.api.add()` section of the README. ### Hooks integrated with the permission system (#118) The hook system and the permission system were two independent interception layers stacked in the same call path. Any module that could reach `api.slothlet.hook.on` could register a hook on **any** path — observing or tampering with the arguments, results, and errors of leaves the permission rules were specifically configured to keep it away from. This release closes that side-channel: - **Permission-gated registration and firing.** When a `permissions` block is configured, registering and firing a hook is checked through the same decision function as calls and reads (`enforceHookAccess`): a module can only hook a path it is itself allowed to access. Rule targets use the `pattern:type` suffix form (e.g. `"db.*:error"`), with `:hook` matching any hook type on a path. - **Force-pinned ownership.** To stop a hook from laundering access through the bound `api`, module-registered hooks are pinned to their owner module by default — the handler's `self.*` calls and permission checks run as the registering module. Opt out per-instance with `hook: { pin: false }` at init, or `api.slothlet.hook.pin.disable()` at runtime. - **`pattern:type` selector syntax.** Hook selectors now read path-first with the type as a trailing suffix (`"math.*:before"`); the legacy type-first prefix (`"before:math.*"`) is deprecated — it still works but emits a deprecation warning and is removed in v4. All of this reuses the existing rule shape, glob dialect, and decision function — no new matcher or precedence model — and is inert unless a `permissions` block is present. See [docs/HOOKS.md](../../HOOKS.md#permissions-and-pinning) and [docs/PERMISSIONS.md](../../PERMISSIONS.md). --- ## 🐛 Bug Fixes ### Browser importmap omitted public endpoints (#137) `generateImportMap()` built its map by scanning slothlet's source for `@cldmv/slothlet/*` imports, so any public export the source never imported itself was missing — including the bare `@cldmv/slothlet/runtime` aggregator that API modules are _documented_ to import for `self` / `context`. A browser following the docs hit `TypeError: Failed to resolve module specifier "@cldmv/slothlet/runtime"`. The importmap is now built from the package's **public export surface**: every flat export is seeded from `package.json` `exports`, and every wildcard export directory is enumerated per file, so a browser can resolve any exported endpoint by construction — not just the ones slothlet imports internally. The source scan remains only as a defense-in-depth backstop. ### Eager root-level `reload(".")` was a silent no-op (#134) In eager mode, a root-level `reload()` rebuilt the API but never applied the fresh implementation to the live root wrapper — `_restoreApiTree`'s eager-root path extracted the new impl into a local and then dropped it, so the existing wrapper kept running the old code after reload. The eager-root path now applies the rebuilt impl via `___setImpl`, mirroring the working non-root path, and a stale function→object extraction that would have made reloaded function leaves non-callable was removed. Regression tests cover `reload(".")` applying a mutated root leaf in both eager and lazy modes. ### CI: scorecard-action pinned to a real release (#133) The OpenSSF Scorecard workflow was pinned to a non-existent `v3.x` tag; it is now pinned to the real `v2.4.3`, and `upload-sarif` is bumped to `v4`. --- ## 🧪 Tests - **#117 / #118 / #137** — synthetic-leaf mounting, hook permission gating, and importmap completeness are covered by suites added alongside their features. - **Coverage backfill** — added unit tests for the previously-uncovered error-detail branches in `buildAPI` synthetic validation (class-instance and empty-name reporting), the eager/lazy `preloadedStructure` shape guard, and the importmap wildcard enumeration's missing-dir and non-`.mjs` skips. `collectSlothletSpecifiers` is now exported so the latter can be driven against a temp-fixture package root. - Full coverage gate green: ~100% statements / branches / functions / lines. --- ## 📚 Documentation - **NEW:** [docs/changelog/v3/v3.10.0.md](./v3.10.0.md) — this changelog. - [docs/HOOKS.md](../../HOOKS.md) / [docs/PERMISSIONS.md](../../PERMISSIONS.md) — hook permission gating, the `pattern:type` rule-target form, and force-pin ownership. - README — `api.slothlet.api.add()` synthetic-leaf forms and a refreshed **What's New**. - **Performance benchmarks backfilled** — per-version docs for v3.4.0–v3.10.0 (+ v3.9.2) re-run on current hardware; [cross-version-summary.md](../../performance/cross-version-summary.md) gains a current-hardware era and [performance/README.md](../../performance/README.md) a startup mermaid across all releases. Documents why lazy startup ≈ eager on root-leaf-heavy fixtures (lazy loads all root-level leaves at init; only nested subtrees defer). --- ## 🔧 Tooling - **Prettier adopted repo-wide.** Formatting is enforced via `npm run format` / `format:check` (config at `.configs/.prettierrc`); the `analyze` audit reports an advisory "not Prettier-clean" count next to the file-header check, and precommit runs a Prettier fix-pass after `fix:headers` — the same check-in-audit / fix-in-precommit model the headers use. - **File-header audit hardened.** `analyze` now header-checks `.cjs` / `.jsonc` / `.jsonv` (previously `.mjs` only) via a `FILE_HEADER_EXTENSIONS` constant shared with `fix:headers` so the two can't drift, accepts both `/**` and `/*` openers, and flags stacked/duplicate headers — closing the gap that let a double header land in a `.jsonc` undetected. - **`@cldmv/fix-headers` 1.2.2 → 1.2.3** — fixes a data-loss bug where replacing a header with a non-canonical closer (e.g. `**/`) could delete file content up to the next `*/`, including one inside a `//` comment. --- ## Upgrade notes - **Synthetic leaves are additive.** Existing directory-based `api.add(path, dir)` calls are unchanged; the inline-content forms are new overloads. - **Hook permission gating is opt-in by configuration.** With no `permissions` block, hooks behave exactly as before. When permissions are configured, hook registration and firing now respect leaf access — audit your rules for any module that registers hooks on paths it cannot call. - **Hooks are force-pinned by default.** Module-registered hooks now run under their owner's identity; a `lockCaller: false` on a hook is ignored (with a warning). If you relied on unpinned hooks, set `hook: { pin: false }` at init or call `api.slothlet.hook.pin.disable()`. - **Hook selector syntax.** Prefer the `path.glob:type` suffix form; the legacy `type:path.glob` prefix still works but warns and is removed in v4.