@cldmv/slothlet 3.12.1 → 3.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,18 +43,18 @@ Every feature has been hardened with a comprehensive test suite - over **5,300 t
43
43
 
44
44
  ## ✨ What's New
45
45
 
46
- ### Latest: v3.12.1 (July 2026)
46
+ ### Latest: v3.12.2 (July 2026)
47
47
 
48
- - **Nested context protection (#207)** — `scope({ protect, owners })` now guards **nested** values, not just the top layer. A write to a nested field of a protected/owned key (`context.auth.userId = …`) previously slipped through the lock; it now throws `CONTEXT_KEY_PROTECTED` with the full path (e.g. `auth.userId`), across assignment, `delete`, `defineProperty`, and array mutators such as `push`. Guarding stays scoped to protected/owned keys — plain objects and arrays are wrapped, non-plain values (`Date` / `Map` / `Set`) returned raw — and a named owner can still write nested fields it owns.
49
- - **`./devcheck` export now actually ships (#209)** — the export pointed at a file the npm `files` whitelist never included, so it threw `ERR_MODULE_NOT_FOUND` in installed copies and `generateBrowserAssets` mirrored a dead entry into every importmap. The file now ships (inert outside the source repo by its own guards), and the importmap generator skips any entry whose resolved target doesn't exist on disk.
50
- - [View full v3.12.1 Changelog](./docs/changelog/v3/v3.12.1.md)
48
+ - **Type generation now types both JS and TS leaves faithfully (#213, #219)** — a generated `.d.ts` carries the real JSDoc `@param` / `@returns` types of JavaScript leaves instead of `any` (extraction runs through the TypeScript checker, so dotted `@param {object}` shapes, optionals, unions, and generics all reflect into proper signatures), and it now compiles for TypeScript leaves that reference a local `interface` / `type` / `enum` — those types are emitted alongside the declaration via TypeScript's own emitter, where the output previously referenced an undefined name. The generator's acceptance test `tsc`-compiles the emitted declaration to prove it.
49
+ - **Consumer-coverage testing guide (#217)** — new [docs/TESTING.md](./docs/TESTING.md) explains why a slothlet-composed leaf reads as near-zero covered in a **consumer** project (its externalized `import()` bypasses vitest's module runner, so `@vitest/coverage-v8` never attributes it) and fixes it with one config line: add `@cldmv/slothlet` to `server.deps.inline`.
50
+ - [View full v3.12.2 Changelog](./docs/changelog/v3/v3.12.2.md)
51
51
 
52
52
  ### Recent Releases
53
53
 
54
+ - **v3.12.1** (July 2026) — Security patch: nested values of `scope({ protect, owners })` context keys are now guarded to depth — `context.auth.userId = …` throws `CONTEXT_KEY_PROTECTED` with the full path — plus the `./devcheck` export now ships the file the npm whitelist never included ([Changelog](./docs/changelog/v3/v3.12.1.md))
54
55
  - **v3.12.0** (July 2026) — Security-and-observability: permission enforcement fails closed on an absent/forged caller (opt-out `permissions.failOpenOnAbsentCaller`), inter-module construction + class-instance methods are permission-checked, the engine-internal `handlers/`/`factories/` subpaths leave `exports`, an opt-in control-surface `seal()`, owner-locked/write-protected context keys via `scope({ protect, owners })`, `impl:warning`/`impl:error` diagnostic lifecycle events, and nested `shutdown`/`destroy` leaves no longer dropped ([Changelog](./docs/changelog/v3/v3.12.0.md))
55
56
  - **v3.11.1** (June 2026) — OpenSSF Scorecard publishing fixed: the workflow calls the org reusable `reusable-scorecard.yml@v4` instead of an inline job whose composite checkout step failed Scorecard's publish-verification allowlist (CI-only, #173) ([Changelog](./docs/changelog/v3/v3.11.1.md))
56
57
  - **v3.11.0** (June 2026) — Satellite split: non-base locales and TypeScript declarations move to the optional `@cldmv/slothlet-i18n` and `@cldmv/slothlet-types` packages (en-us-only lean core); loader `hidden` option (`.`/`__` hidden by default); browser-mode v8 coverage with an unbalanced-`v8 ignore` analyze gate ([Changelog](./docs/changelog/v3/v3.11.0.md))
57
- - **v3.10.0** (June 2026) — Synthetic / in-memory leaves for `api.slothlet.api.add()` (inline function or export map, no temp file); hooks integrated with the permission system (gated registration/firing, owner-pinned, `pattern:type` selectors); browser importmap built from the full public export surface ([Changelog](./docs/changelog/v3/v3.10.0.md))
58
58
 
59
59
  📚 **For complete version history and detailed release notes, see [docs/changelog/](./docs/changelog/) folder.**
60
60
 
@@ -408,6 +408,7 @@ try {
408
408
  - **[Sanitization](./docs/SANITIZATION.md)** — filename → property-name transformation rules
409
409
  - **[TypeScript Support](./docs/TYPESCRIPT.md)** — fast mode (esbuild), strict mode (tsc), `.d.ts` generation
410
410
  - **[Internationalization](./docs/I18N.md)** — supported languages and configuration
411
+ - **[Testing & Coverage](./docs/TESTING.md)** — measuring coverage of composition-loaded leaves in a consumer project (the `server.deps.inline` requirement)
411
412
 
412
413
  ### API Rules & Transformation
413
414
 
package/REFERENCE.md CHANGED
@@ -21,5 +21,6 @@ Full documentation lives in the GitHub repository.
21
21
  | Lifecycle | [docs/LIFECYCLE.md](https://github.com/CLDMV/slothlet/blob/master/docs/LIFECYCLE.md) |
22
22
  | Hot reload | [docs/RELOAD.md](https://github.com/CLDMV/slothlet/blob/master/docs/RELOAD.md) |
23
23
  | Performance | [docs/PERFORMANCE.md](https://github.com/CLDMV/slothlet/blob/master/docs/PERFORMANCE.md) |
24
+ | Testing & coverage | [docs/TESTING.md](https://github.com/CLDMV/slothlet/blob/master/docs/TESTING.md) |
24
25
  | Changelog | [docs/changelog/](https://github.com/CLDMV/slothlet/tree/master/docs/changelog) |
25
26
  | Agent/AI usage guide | [AGENT-USAGE.md](https://github.com/CLDMV/slothlet/blob/master/AGENT-USAGE.md) |
@@ -14,4 +14,4 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import fs from"fs";import path from"path";import{SlothletError}from"@cldmv/slothlet/errors";let typescriptInstance=null;async function getTypeScript(){if(!typescriptInstance){try{typescriptInstance=await import("typescript")}catch(error){throw new SlothletError("TYPESCRIPT_NOT_INSTALLED",{feature:"type-generation"},error)}}return typescriptInstance}async function generateTypes(api,options){const ts=await getTypeScript();if(!options.output){throw new SlothletError("INVALID_CONFIG",{option:"types.output",expected:"a string output path",value:options.output,hint:"Provide a string output path for the generated .d.ts file, e.g. './types/api.d.ts'.",validationError:true})}if(!options.interfaceName){throw new SlothletError("INVALID_CONFIG",{option:"types.interfaceName",expected:"a string interface name",value:options.interfaceName,hint:"Provide a string interface name for the generated TypeScript interface, e.g. 'SlothletAPI'.",validationError:true})}const nodes=traverseAPI(api);for(const node of nodes){if(node.metadata?.filePath){node.typeInfo=await extractTypesFromFile(node.metadata.filePath,ts)}}const declaration=generateDeclaration(nodes,options);const outputPath=path.resolve(options.output);const outputDir=path.dirname(outputPath);fs.mkdirSync(outputDir,{recursive:true});fs.writeFileSync(outputPath,declaration,"utf8");return{output:declaration,filePath:outputPath}}function traverseAPI(api,currentPath=[],visited=new Set){const nodes=[];if(!api||typeof api!=="object"){return nodes}if(visited.has(api)){return nodes}visited.add(api);for(const[key,value]of Object.entries(api)){if(key.startsWith("_")||key.startsWith("__")||key==="slothlet"||key==="shutdown"||key==="destroy"){continue}const nodePath=[...currentPath,key];const metadata=value?.__metadata;if(typeof value==="function"){nodes.push({type:"function",path:nodePath,value,metadata})}else if(typeof value==="object"&&value!==null){nodes.push({type:"object",path:nodePath,value,metadata});const childNodes=traverseAPI(value,nodePath,visited);nodes.push(...childNodes)}}return nodes}async function extractTypesFromFile(filePath,ts){try{let visit=function(node){if(ts.isFunctionDeclaration(node)&&node.name){const hasExport=node.modifiers?.some(m=>m.kind===ts.SyntaxKind.ExportKeyword);if(hasExport){exports.push({name:node.name.text,type:"function",signature:extractFunctionSignature(node,source,ts)})}}if(ts.isVariableStatement(node)){const hasExport=node.modifiers?.some(m=>m.kind===ts.SyntaxKind.ExportKeyword);if(hasExport){for(const decl of node.declarationList.declarations){if(!decl.name||!ts.isIdentifier(decl.name))continue;const init=decl.initializer;if(!init)continue;if(ts.isArrowFunction(init)||ts.isFunctionExpression(init)){exports.push({name:decl.name.text,type:"function",signature:extractFunctionSignature(init,source,ts)})}}}}ts.forEachChild(node,visit)};const source=fs.readFileSync(filePath,"utf8");const sourceFile=ts.createSourceFile(filePath,source,ts.ScriptTarget.Latest,true);const exports=[];visit(sourceFile);return{exports,sourceFile}}catch(____error){return{exports:[]}}}function extractFunctionSignature(node,____source,____ts){const params=node.parameters.map(p=>{const name=p.name.getText(node.getSourceFile());const type=p.type?p.type.getText(node.getSourceFile()):"any";return`${name}: ${type}`}).join(", ");const returnType=node.type?node.type.getText(node.getSourceFile()):"any";return`(${params}): ${returnType}`}function generateDeclaration(nodes,options){const interfaceName=options.interfaceName;const lines=[];lines.push("/**");lines.push(` * Generated TypeScript declarations for Slothlet API`);lines.push(` * @generated ${new Date().toISOString()}`);lines.push(" */");lines.push("");const structure={};for(const node of nodes){if(node.type==="function"){const fnName=node.path[node.path.length-1];const exportInfo=node.typeInfo?.exports?.find(e=>e.name===fnName);const signature=exportInfo?.signature??"(...args: any[]): any";setNestedProperty(structure,node.path,{type:"function",signature})}}lines.push(`export interface ${interfaceName} {`);generateInterfaceContent(structure,lines,1);lines.push("}");lines.push("");lines.push(`declare const self: ${interfaceName};`);lines.push("");return lines.join("\n")}function setNestedProperty(obj,path2,value){let current=obj;for(let i=0;i<path2.length-1;i++){const key=path2[i];if(!current[key]){current[key]={}}current=current[key]}const lastKey=path2[path2.length-1];current[lastKey]=value}function generateInterfaceContent(structure,lines,indent){const indentation=" ".repeat(indent);for(const[key,value]of Object.entries(structure)){if(value.type==="function"){lines.push(`${indentation}${key}${value.signature};`)}else if(typeof value==="object"&&value!==null){lines.push(`${indentation}${key}: {`);generateInterfaceContent(value,lines,indent+1);lines.push(`${indentation}};`)}else{}}}export{generateTypes};
17
+ import fs from"fs";import path from"path";import{SlothletError}from"@cldmv/slothlet/errors";let typescriptInstance=null;async function getTypeScript(){if(!typescriptInstance){try{typescriptInstance=await import("typescript")}catch(error){throw new SlothletError("TYPESCRIPT_NOT_INSTALLED",{feature:"type-generation"},error)}}return typescriptInstance}async function generateTypes(api,options){const ts=await getTypeScript();if(!options.output){throw new SlothletError("INVALID_CONFIG",{option:"types.output",expected:"a string output path",value:options.output,hint:"Provide a string output path for the generated .d.ts file, e.g. './types/api.d.ts'.",validationError:true})}if(!options.interfaceName){throw new SlothletError("INVALID_CONFIG",{option:"types.interfaceName",expected:"a string interface name",value:options.interfaceName,hint:"Provide a string interface name for the generated TypeScript interface, e.g. 'SlothletAPI'.",validationError:true})}const nodes=traverseAPI(api);const filePaths=[...new Set(nodes.map(node=>node.metadata?.filePath).filter(Boolean))];const{byFile:exportsByFile,localTypes}=extractTypeInfo(filePaths,ts);for(const node of nodes){if(node.metadata?.filePath){node.typeInfo=exportsByFile.get(node.metadata.filePath)}}const declaration=generateDeclaration(nodes,options,localTypes);const outputPath=path.resolve(options.output);const outputDir=path.dirname(outputPath);fs.mkdirSync(outputDir,{recursive:true});fs.writeFileSync(outputPath,declaration,"utf8");return{output:declaration,filePath:outputPath}}function traverseAPI(api,currentPath=[],visited=new Set){const nodes=[];if(!api||typeof api!=="object"){return nodes}if(visited.has(api)){return nodes}visited.add(api);for(const[key,value]of Object.entries(api)){if(key.startsWith("_")||key.startsWith("__")||key==="slothlet"||key==="shutdown"||key==="destroy"){continue}const nodePath=[...currentPath,key];const metadata=value?.__metadata;if(typeof value==="function"){nodes.push({type:"function",path:nodePath,value,metadata})}else if(typeof value==="object"&&value!==null){nodes.push({type:"object",path:nodePath,value,metadata});const childNodes=traverseAPI(value,nodePath,visited);nodes.push(...childNodes)}}return nodes}function extractTypeInfo(filePaths,ts){const byFile=new Map;const program=ts.createProgram(filePaths,{allowJs:true,checkJs:false,declaration:true,emitDeclarationOnly:true,skipLibCheck:true,target:ts.ScriptTarget.Latest,module:ts.ModuleKind.ESNext,moduleResolution:ts.ModuleResolutionKind.Bundler});const checker=program.getTypeChecker();const isExported=node=>node.modifiers?.some(m=>m.kind===ts.SyntaxKind.ExportKeyword);const candidates=new Map;program.emit(void 0,(____fileName,text)=>{const dts=ts.createSourceFile("emitted.d.ts",text,ts.ScriptTarget.Latest,false,ts.ScriptKind.TS);for(const stmt of dts.statements){if(ts.isInterfaceDeclaration(stmt)||ts.isTypeAliasDeclaration(stmt)||ts.isEnumDeclaration(stmt)){candidates.set(stmt.name.text,stmt.getText(dts))}}},void 0,true);for(const filePath of filePaths){const exportsForFile=[];try{const visit=node=>{if(ts.isFunctionDeclaration(node)&&node.name&&isExported(node)){exportsForFile.push({name:node.name.text,type:"function",signature:signatureText(node,checker,ts)})}else if(ts.isVariableStatement(node)&&isExported(node)){for(const decl of node.declarationList.declarations){const init=decl.initializer;if(ts.isIdentifier(decl.name)&&init&&(ts.isArrowFunction(init)||ts.isFunctionExpression(init))){exportsForFile.push({name:decl.name.text,type:"function",signature:signatureText(init,checker,ts)})}}}ts.forEachChild(node,visit)};const sourceFile=program.getSourceFile(filePath);if(sourceFile){visit(sourceFile)}}catch(____error){}byFile.set(filePath,{exports:exportsForFile})}const localTypes=collectReferencedLocalTypes(candidates,byFile);return{byFile,localTypes}}function collectReferencedLocalTypes(candidates,byFile){const referenced=new Map;if(candidates.size===0)return referenced;const wholeWord=name=>new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\b`);const signatureBlob=[];for(const{exports}of byFile.values()){for(const e of exports)signatureBlob.push(e.signature)}const haystack=signatureBlob.join("\n");const consider=name=>{if(referenced.has(name))return;const text=candidates.get(name);referenced.set(name,text);for(const other of candidates.keys()){if(wholeWord(other).test(text))consider(other)}};for(const name of candidates.keys()){if(wholeWord(name).test(haystack))consider(name)}return referenced}function signatureText(fnNode,checker,ts){const signature=checker.getSignatureFromDeclaration(fnNode);return checker.signatureToString(signature,fnNode,ts.TypeFormatFlags.NoTruncation)}function generateDeclaration(nodes,options,localTypes){const interfaceName=options.interfaceName;const lines=[];lines.push("/**");lines.push(` * Generated TypeScript declarations for Slothlet API`);lines.push(` * @generated ${new Date().toISOString()}`);lines.push(" */");lines.push("");if(localTypes.size>0){for(const text of localTypes.values()){lines.push(text)}lines.push("")}const structure={};for(const node of nodes){if(node.type==="function"){const fnName=node.path[node.path.length-1];const exportInfo=node.typeInfo?.exports?.find(e=>e.name===fnName);const signature=exportInfo?.signature??"(...args: any[]): any";setNestedProperty(structure,node.path,{type:"function",signature})}}lines.push(`export interface ${interfaceName} {`);generateInterfaceContent(structure,lines,1);lines.push("}");lines.push("");lines.push(`declare const self: ${interfaceName};`);lines.push("");return lines.join("\n")}function setNestedProperty(obj,path2,value){let current=obj;for(let i=0;i<path2.length-1;i++){const key=path2[i];if(!current[key]){current[key]={}}current=current[key]}const lastKey=path2[path2.length-1];current[lastKey]=value}function generateInterfaceContent(structure,lines,indent){const indentation=" ".repeat(indent);for(const[key,value]of Object.entries(structure)){if(value.type==="function"){lines.push(`${indentation}${key}${value.signature};`)}else if(typeof value==="object"&&value!==null){lines.push(`${indentation}${key}: {`);generateInterfaceContent(value,lines,indent+1);lines.push(`${indentation}};`)}else{}}}export{generateTypes};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cldmv/slothlet",
3
- "version": "3.12.1",
3
+ "version": "3.12.2",
4
4
  "moduleVersions": {
5
5
  "lazy": "3.0.0",
6
6
  "eager": "3.0.0",