Skip to content

v3.10.0

Latest

Choose a tag to compare

@cldmv-bot cldmv-bot released this 09 Jun 04:16
v3.10.0
5fcb8f8

release: v3.10.0 - synthetic / in-memory leaf for api.slothlet.api.add()… (#135)

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 and docs/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 — this changelog.
  • docs/HOOKS.md / docs/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 gains a current-hardware era and 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.
👥 Contributors