Repository navigation
Description
Summary
Merely reading a standard function property such as .apply/.call/.bind off a materialized function leaf permanently rewrites that leaf's loader record: the leaf flips from kind: "function" to kind: "namespace", and a phantom child leaf (<path>.apply, kind: "function") appears. The property access is treated as an api-tree traversal that materializes a child, rather than returning the function's own Function.prototype.apply. A later leaves(".", { details: true }) — or a second composition/serve of the same instance — then reports the corrupted shape.
Reproduce
Verified against @cldmv/slothlet@3.14.0 (repo v3.13.3-9-g0dd03b7), eager mode. api dir with one file tool.mjs exporting export function ping() { return "pong"; }:
const api = await slothlet({ base: dirUrl, silent: true, mode: "eager" });
await api.slothlet.api.leaves(".", { details: true });
// → [ { path: "tool", kind: "namespace" }, { path: "tool.ping", kind: "function" } ]
const ap = api.tool.ping.apply; // just READ it — no call
typeof ap; // "function"
api.tool.ping(); // still "pong"
await api.slothlet.api.leaves(".", { details: true });
// → [ { path: "tool", kind: "namespace" },
// { path: "tool.ping", kind: "namespace" }, ← was "function"
// { path: "tool.ping.apply", kind: "function" } ] ← phantom leaf
// Reflect.apply(api.tool.ping, thisArg, args) does NOT trigger this.Expected
Accessing Function.prototype members (apply/call/bind/name/length/…) on a callable leaf should return the function's own properties and leave the record untouched — the leaf is a function; those are its methods, not child endpoints. Only genuine child names should materialize a namespace.
Impact
Any code that invokes a leaf as leaf.apply(thisArg, args) (a common idiom for forwarding this + a spread of args) silently corrupts the api's own record tree, and re-enumerating or re-composing the instance then surfaces Function.prototype.apply bound to the real leaf in place of the leaf. Found while building @cldmv/slothlet-vine: answering a forwarded call with leaf.apply(parent, args) corrupted the served surface; the workaround is Reflect.apply(leaf, parent, args), which routes around the leaf proxy's get trap entirely.
Severity
Correctness — silent record corruption from a read-only, idiomatic access. Likely the lazy/eager leaf proxy's get trap treating every property (including Function.prototype names) as a child traversal.
Activity
release: v3.15.0 - scope api.remove() to one module's node via an… (#308
cldmv-bot commentedon Aug 26, 2026
Closed in v3.15.0 — resolved by #307.
fix(wrapper): don't adopt a function's non-enumerable own properties …
fix(wrapper): don't adopt a function's non-enumerable own properties …