Summary
When a moduleID containing a colon is passed to api.slothlet.api.add(), the mount succeeds and the leaf resolves, and add() returns that exact id — but the moduleID→paths ownership index is not keyed under it. As a result api.slothlet.api.leaves(id) returns [] and api.slothlet.api.remove(id) returns false, so the mount cannot be reloaded or removed by the id add() handed back. It is silently unmanageable. Only the colon triggers this; /, ., and - in a moduleID all round-trip correctly.
Reproduce
Verified against @cldmv/slothlet@3.14.0 (repo v3.13.3-9-g0dd03b7), eager mode, empty api dir:
const id = await api.slothlet.api.add("t.a", () => 1, { moduleID: "vine:abc" });
id; // → "vine:abc" (the exact id back)
await api.slothlet.api.leaves("vine:abc"); // → [] (index doesn't know it)
await api.slothlet.api.leaves(id); // → []
await api.slothlet.api.remove(id); // → false (can't remove) — path still resolves
typeof api.t.a; // → "function" (still mounted)
// controls — all fine:
await api.slothlet.api.add("t.b", () => 2, { moduleID: "a/b" });
await api.slothlet.api.remove("a/b"); // → true
await api.slothlet.api.add("t.c", () => 3, { moduleID: "a.b" });
await api.slothlet.api.remove("a.b"); // → true
Expected
Either add() should reject a moduleID it can't round-trip (a named error), or — better — the id should be stored and matched verbatim so leaves(id)/remove(id)/reload(id) work for any accepted id, colon included. Per RELOAD.md the moduleID is documented only as "a stable identifier … used for targeted reload() and remove()," with no reserved characters, so a caller has no way to know a colon is unsafe.
Impact
A consumer that adopts a namespaced id convention like vine:<nonce> or plugin:<name> gets mounts that leak — they can never be removed or reloaded by id, and leaves(id) reports nothing, so ownership tooling silently under-reports. Found while building @cldmv/slothlet-vine; the workaround was to switch link ids to hyphens and add a post-remove verification + per-path fallback. Likely cause: the colon is parsed as an internal separator somewhere in the ownership-index key.
Severity
Correctness — silent. No corruption, but an accepted id that add() returns yet remove()/leaves() don't honor violates the documented ownership contract.
Summary
When a
moduleIDcontaining a colon is passed toapi.slothlet.api.add(), the mount succeeds and the leaf resolves, andadd()returns that exact id — but the moduleID→paths ownership index is not keyed under it. As a resultapi.slothlet.api.leaves(id)returns[]andapi.slothlet.api.remove(id)returnsfalse, so the mount cannot be reloaded or removed by the idadd()handed back. It is silently unmanageable. Only the colon triggers this;/,., and-in a moduleID all round-trip correctly.Reproduce
Verified against
@cldmv/slothlet@3.14.0(repov3.13.3-9-g0dd03b7), eager mode, empty api dir:Expected
Either
add()should reject a moduleID it can't round-trip (a named error), or — better — the id should be stored and matched verbatim soleaves(id)/remove(id)/reload(id)work for any accepted id, colon included. Per RELOAD.md the moduleID is documented only as "a stable identifier … used for targeted reload() and remove()," with no reserved characters, so a caller has no way to know a colon is unsafe.Impact
A consumer that adopts a namespaced id convention like
vine:<nonce>orplugin:<name>gets mounts that leak — they can never be removed or reloaded by id, andleaves(id)reports nothing, so ownership tooling silently under-reports. Found while building@cldmv/slothlet-vine; the workaround was to switch link ids to hyphens and add a post-remove verification + per-path fallback. Likely cause: the colon is parsed as an internal separator somewhere in the ownership-index key.Severity
Correctness — silent. No corruption, but an accepted id that
add()returns yetremove()/leaves()don't honor violates the documented ownership contract.