Complete Guide to All API Generation Behaviors
This is the middle layer of slothlet's three-tier documentation system:
📋 API-RULES/API-FLATTENING.md (F##) ← User Guide: Clear examples and flowcharts
↑ links to ↓ links to
📊 API-RULES.md (1-13) ← YOU ARE HERE: Complete behavior catalog
↑ links to ↓ links to
🔧 API-RULES/API-RULES-CONDITIONS.md ← Technical: Exact source code locations
↓ mapped in
🗺️ API-RULES/API-RULE-MAPPING.md ← Traceability Matrix: Rule # ↔ F## ↔ C##
Cross-Reference Navigation:
- For Users: See API-RULES/API-FLATTENING.md for user-friendly explanations with examples
- For Developers: See API-RULES/API-RULES-CONDITIONS.md for exact source code locations
- Rule Mapping: See API-RULES/API-RULE-MAPPING.md for complete Rule # ↔ F## ↔ C## traceability matrix
This document catalogs all 13 API generation behaviors in slothlet with:
- Verified examples from actual test files with source attribution
- Cross-references to user guide (F##) and technical details (C##)
- Source code locations with function names and file references
- Test file sources demonstrating each behavior in action
- Processing contexts (Root/Subfolder/Multi-Default/AddApi)
Why 13 Rules vs Flattening Patterns?
The Flattening guide focuses on when content gets promoted/flattened. This comprehensive guide covers all API behaviors including cases where flattening doesn't occur but specific handling is still needed:
- Flattening Rules (1, 7, 8, 10, 11, 13): Core flattening patterns
- Non-Flattening Rules (2, 3, 4, 5, 6, 9): Export collection, function naming, empty modules, mixed exports
- AddApi Rules (11, 12, 13): Runtime API extension behaviors
Methodology: Each rule has been systematically verified against test files and source code.
| Category | Rules | Focus | Cross-References |
|---|---|---|---|
| Basic Flattening | 1, 7, 8 | Core flattening patterns | F01-F05 → C01-C11 |
| Export Handling | 2, 4, 5 | Default vs Named exports | F04-F05 → C08-C21 |
| Special Cases | 3, 6, 9, 10 | Edge cases and protections | C10, C01, C16-C19 |
| AddApi Extensions | 11, 12, 13 | Runtime API extensions | F06-F08 → C33, C34 |
- Rule 1: Filename Matches Container Flattening
- Rule 2: Named-Only Export Collection
- Rule 3: Empty Module Handling
- Rule 4: Named Export with Function Name Preservation
- Rule 5: Multiple Module Default Export Handling
- Rule 6: Multiple Module Mixed Exports
- Rule 7: Single Module Named Export Flattening
- Rule 8: Single Module Default Export Promotion
- Rule 9: Function Name Preference Over Sanitization
- Rule 10: Generic Filename Parent-Level Promotion
- Rule 11: AddApi Special File Pattern
- Rule 12: Module Ownership and Selective API Overwriting
- Rule 13: AddApi Path Deduplication Flattening
- Verification Status
- Cross-Reference Index
Category: Basic Flattening
Status: ✅ Verified (api_tests/api_test)
User Guide: F01
Technical: C05, C09b
Condition: Filename matches folder name AND no default export AND has named exports
Source Files: api_tests/api_test/math/math.mjs
Implementation: buildCategoryDecisions() → getFlatteningDecision() → processModuleForAPI()
Verified Examples:
// File: api_tests/api_test/math/math.mjs
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
// Without Rule 1: api.math.math.add(2, 3) ❌ (redundant nesting)
// With Rule 1: api.math.add(2, 3) ✅ (clean flattening)
api.math.add(2, 3); // 5
api.math.subtract(5, 2); // 3Technical Implementation:
- Primary Condition: C05 -
fileName === categoryName && !moduleHasDefault && moduleKeys.length > 0 - Processing: C09b -
flattenToCategory: true→ category-level flattening
// C05: Filename Matches Container (Category-Level Flatten)
// Location: src/lib/helpers/api_builder/decisions.mjs
if (categoryName && fileName === categoryName && !moduleHasDefault && moduleKeys.length > 0) {
return {
shouldFlatten: true,
flattenToRoot: false,
flattenToCategory: true,
preserveAsNamespace: false,
useAutoFlattening: false,
reason: "filename matches container, flatten to category"
};
}Processing Path: Subfolder processing via getFlatteningDecision() (currentDepth > 0)
Source Code Location: src/lib/helpers/api_builder/decisions.mjs - getFlatteningDecision()
Category: Export Handling
Status: ✅ Verified (api_tests/api_test)
User Guide: F04
Technical: C15, C09d
Condition: Directory contains files with only named exports (no default exports) Behavior: All named exports collected and made accessible at the appropriate namespace level
Verified Examples:
// File: constants/values.mjs
export const PI = 3.14159;
export const E = 2.71828;
// File: constants/messages.mjs
export const SUCCESS = "Operation completed";
export const ERROR = "Operation failed";
api.constants.values.PI; // 3.14159
api.constants.values.E; // 2.71828
api.constants.messages.SUCCESS; // "Operation completed"
api.constants.messages.ERROR; // "Operation failed"Technical Implementation:
- Detection: C15 -
defaultExportCount === 0 - Processing: C09d - Standard namespace preservation
- Strategy:
processingStrategy = "named-only"→ category-level collection
Key Behavior:
- Preserves all named export names and values
- Maintains clear namespace separation between files
- No flattening when multiple named exports exist (prevents naming conflicts)
Source Code Location: src/lib/helpers/api_builder/decisions.mjs - processModuleForAPI()
Processing Path: Both Root and Subfolder processing via processModuleForAPI
Category: Special Cases Status: ✅ Verified Technical: C10
Condition: Directory contains no loadable module files
Behavior: Graceful handling - creates empty namespace
Processing Path: Early detection in buildCategoryDecisions()
Mode Differences:
- EAGER: Empty folder →
{}object (not callable) - LAZY: Empty folder → lazy proxy that resolves to
{}when called
Technical Implementation:
- Detection: C10 -
moduleFiles.length === 0 - Strategy:
processingStrategy = "empty"→ graceful empty handling
// Detection in analyzeDirectoryStructure
if (moduleFiles.length === 0) {
processingStrategy = "empty";
}Source Code Location: src/lib/helpers/api_builder/analysis.mjs
Processing Path: All paths (detected in analyzeDirectoryStructure)
Category: Export Handling
Status: ✅ Verified (api_tests/api_test)
User Guide: F04
Technical: C16, C23
Condition: Named export with a function name that differs from the sanitized filename Behavior: Preserves the original function name rather than using the filename-derived name Priority: Function names take precedence over filename-based naming
Verified Examples:
// File: auto-ip.mjs
export function autoIP() {
/* ... */
}
api.autoIP(); // ✅ Function name preserved (not api.autoIp)
// File: json-parser.mjs
export function parseJSON(data) {
/* ... */
}
api.parseJSON(data); // ✅ Original casing preserved (not api.jsonParser)Function Name Priority:
- Original function name (if available)
- Filename-based sanitization (if no function name)
Technical Implementation:
Category: Export Handling
Status: ✅ Verified (api_tests/api_test)
Technical: C02, C03
Condition: Category contains multiple modules with default exports
Behavior: Files with a default export (C02) are preserved as their own named namespace. Files without a default export (C03) have their named exports hoisted directly into the parent folder namespace — the file's own intermediate namespace is dissolved.
Processing Path: C02 → preserveAsNamespace: true; C03 → flattenToRoot: true (keys merged into targetApi)
Bug Fix (PR #116): Prior to this fix, C03 fell through to standard namespace wrapping, causing named-only files to appear nested under a
filenamesub-namespace (e.g.api.notifications.helpers.formatPhone) instead of being hoisted to the folder level (api.notifications.formatPhone). If you depend on the pre-fix nested behavior and need time to migrate, you can temporarily restore it withsuppressFixes: ["C03_116"]— see Bug-Fix Suppression in the configuration docs. This option will be removed in v4.
Verified Examples:
// File: notifications/email.mjs
export default function send(to, msg) { /* ... */ }
// File: notifications/sms.mjs
export default function send(to, msg) { /* ... */ }
// File: notifications/helpers.mjs (no default export)
export function formatPhone(p) { /* ... */ }
export const RETRY_LIMIT = 3;
api.notifications.email("a@x", "hi"); // ✅ Default callable (C02 — has default, preserved)
api.notifications.sms("+1...", "hi"); // ✅ Default callable (C02 — has default, preserved)
api.notifications.formatPhone("..."); // ✅ Hoisted (C03 — no default, dissolved)
api.notifications.RETRY_LIMIT; // ✅ Hoisted (C03 — no default, dissolved)
// api.notifications.helpers — does not existTechnical Implementation:
- Detection:
hasMultipleDefaultsflag set by category scan when ≥ 2 files in the folder have a default export - C02 (details): module has a default →
preserveAsNamespace: true - C03 (details): module has no default →
flattenToRoot: true(named exports hoisted into parent namespace)
Key Behavior:
- Default-exporting files maintain their own named namespace (C02)
- Named-only files have their exports dissolved into the folder level (C03)
- Applies consistently regardless of how many files are in the folder, as long as ≥ 2 have defaults
Category: Special Cases
Status: ✅ Verified (api_tests/api_test_mixed)
Technical: C14, C09d
Condition: Category contains modules with mixed export types (some default, some named-only) Behavior: Standard namespace processing - each module maintains a distinct namespace Processing Path: Conservative approach to prevent conflicts
Verified Examples:
// File: mixed/calculator.mjs (default export)
export default function calculate(operation, a, b) {
/* ... */
}
// File: mixed/constants.mjs (named exports only)
export const PI = 3.14159;
export const E = 2.71828;
api.mixed.calculator("add", 2, 3); // ✅ Default accessible
api.mixed.constants.PI; // ✅ Named exports accessible
api.mixed.constants.E; // ✅ Clear namespace separationTechnical Implementation:
Category: Basic Flattening
Status: ✅ Verified (api_tests/api_test)
User Guide: F02
Technical: C06, C09b
Condition: Category has one module file, module has named exports (no default export), filename ≠ category name
Source Files: api_tests/api_test/config/settings.mjs
Implementation: getFlatteningDecision() → single module named export flattening
Verified Examples:
// File: api_tests/api_test/config/settings.mjs
export const DATABASE_URL = "mongodb://localhost:27017/testdb";
export const API_PORT = 3000;
export const DEBUG_MODE = true;
// Without Rule 7: api.config.settings.DATABASE_URL ❌ (unnecessary nesting)
// With Rule 7: api.config.DATABASE_URL ✅ (clean flattening)
api.config.DATABASE_URL; // "mongodb://localhost:27017/testdb"
api.config.API_PORT; // 3000
api.config.DEBUG_MODE; // trueTechnical Implementation:
- Primary Condition: C06 -
moduleCount === 1 && !moduleHasDefault && moduleKeys.length > 0 - Processing: C09b -
flattenToCategory: true
Category: Basic Flattening
Status: ✅ Verified (api_tests/api_test)
User Guide: F03
Technical: C07, C09c
Condition: Category has one module file with a default export
Source Files: api_tests/api_test/logger.mjs
Implementation: getFlatteningDecision() → single module default export promotion
Verified Examples:
// File: api_tests/api_test/logger.mjs
export default function logger(message) {
console.log(`[LOG] ${message}`);
}
// Without Rule 8: api.logger.logger("Hello World") ❌ (redundant nesting)
// With Rule 8: api.logger("Hello World") ✅ (direct callable)
api.logger("Hello World"); // [LOG] Hello World
typeof api.logger; // "function"Callable Namespace Pattern: When a folder contains a file matching the folder name with a default export (e.g. logger/logger.mjs), the default function becomes the namespace itself. Other files in the folder become properties on that function:
// File: logger/logger.mjs → export default function log()
// File: logger/utils.mjs → named exports
api.logger("message"); // calls the default function
api.logger.utils.debug("x"); // other files remain as namespace propertiesThis pattern applies consistently at root level and category level.
Technical Implementation:
- Primary Condition: C07 -
moduleCount === 1 && moduleHasDefault - Processing: C09c -
promoteToCategory: true
Category: Special Cases Status: ✅ Fully Verified (autoIP, parseJSON, getHTTPStatus, XMLParser) User Guide: API-RULES/API-FLATTENING.md - Name Preservation Technical: C16, C19
Condition: Exported function has an explicit name that differs from the sanitized filename Behavior: Preserve the original function name over the filename-based API path
Verified Examples:
// File: auto-ip.mjs
export function autoIP() {
/* Get automatic IP */
}
// Sanitized filename: "autoIp" ❌
// Function name: "autoIP" ✅
// File: get-http-status.mjs
export function getHTTPStatus() {
/* ... */
}
// Sanitized filename: "getHttpStatus" ❌
// Function name: "getHTTPStatus" ✅
// File: parse-json.mjs
export function parseJSON(data) {
/* ... */
}
// Sanitized filename: "parseJson" ❌
// Function name: "parseJSON" ✅Technical Implementation:
- Primary Check: C16 -
exportedFunctionName !== sanitizedName - Detailed Check: C19 -
exportedFunction.name !== sanitizedFileName - Precedence: Function name takes precedence over filename in API structure
Common Preserved Patterns:
- Technical acronyms: IP, HTTP, API, URL, JSON, XML, HTML
- Protocol names: TCP, UDP, FTP, SSH, SSL, TLS
- Format specs: JSON, XML, CSV, YAML, TOML
- Industry standards: OAuth, JWT, REST, GraphQL
Category: Special Cases
Status: ✅ Verified (api_tests/api_test/nest4/singlefile.mjs)
User Guide: API-RULES/API-FLATTENING.md - Index File Pattern
Technical: C17
Condition: File has a generic name (index, main, default, etc.)
Behavior: Generic filename becomes transparent; content is promoted to the meaningful parent name
Verified Examples:
// File: database/main.mjs
export function connect() {
/* ... */
}
export function query() {
/* ... */
}
// Without Rule 10: api.database.main.connect() ❌ (generic 'main' adds no value)
// With Rule 10: api.database.connect() ✅ (promoted to parent level)
// File: auth/index.mjs
export function login() {
/* ... */
}
export function logout() {
/* ... */
}
// Without Rule 10: api.auth.index.login() ❌ (generic 'index' is noise)
// With Rule 10: api.auth.login() ✅ (clean parent-level promotion)Technical Implementation:
- Detection: C17 -
isGenericFilename(fileName) - Promotion: Content promoted to parent namespace; generic filename becomes invisible
Note: Promotion is guarded against name collisions - checked against existing parent namespace properties before promoting.
Category: AddApi
Status: ✅ Verified (api_tests/api_smart_flatten_addapi)
User Guide: F06
Technical: C33
Condition: A file named addapi.mjs is loaded via api.slothlet.api.add()
Behavior: Exports are always flattened to the mount namespace regardless of other settings
Processing Path: Detection in getFlatteningDecision() (src/lib/processors/flatten.mjs); execution in src/lib/builders/modes-processor.mjs
Verified Example:
// File: plugin-folder/addapi.mjs
export function initializePlugin() {
/* ... */
}
export function cleanup() {
/* ... */
}
export function configure() {
/* ... */
}
await api.slothlet.api.add("plugins", "./plugin-folder");
// addapi.mjs exports are always flattened - never nested:
api.plugins.initializePlugin(); // ✅
api.plugins.cleanup(); // ✅
api.plugins.configure(); // ✅
// NOT: api.plugins.addapi.initializePlugin() ❌Technical Implementation:
// C33: AddApi Special File Detection
if (moduleKeys.includes("addapi")) {
const addapiModule = newModules["addapi"];
const otherModules = { ...newModules };
delete otherModules["addapi"];
modulesToMerge = { ...addapiModule, ...otherModules };
}Use Cases:
- Plugin systems that extend the API at a known namespace
- Hot-reloadable API extension points
- Clean integration of external modules into a live API surface
Category: AddApi
Status: ✅ Implemented (src/lib/handlers/ownership.mjs)
User Guide: F07
Technical: C19-C22
Purpose: Track which module registered each API path, enabling safe hot-reloading and cross-module conflict protection.
Implementation: Stack-based ownership system. Each API path maintains an independent ownership history stack. Removing a module automatically rolls back to the previous owner. Collision behavior is controlled by the api.collision configuration.
const api = await slothlet({
dir: "./api",
api: {
collision: {
initial: "merge", // During initial API build
api: "replace" // During api.slothlet.api.add()
}
}
});Each api.slothlet.api.add() call accepts an optional moduleID in its options object (the third argument — api.add(apiPath, folderPath, options); there is no separate fourth options argument). This is the key for ownership tracking:
// Module A registers plugins namespace
await api.slothlet.api.add("plugins.moduleA", "./modules/moduleA", {
moduleID: "moduleA"
});
// Module B registers in the same parent namespace
await api.slothlet.api.add("plugins.moduleB", "./modules/moduleB", {
moduleID: "moduleB"
});
// Hot-reload Module A - ownership system allows this because moduleA owns these paths
await api.slothlet.api.add("plugins.moduleA", "./modules/moduleA-v2", {
moduleID: "moduleA",
forceOverwrite: true
});
// Cross-module overwrite - blocked if the instance's configured collision mode is "error"
await api.slothlet.api.add("plugins.moduleB", "./modules/other", {
moduleID: "moduleA", // moduleA does not own moduleB's paths
forceOverwrite: true // Throws OWNERSHIP_CONFLICT when the instance's collision mode is "error"
});Each API path has a history stack. When a module is removed, the previous owner is automatically restored:
// Stack for "plugins.tools": [module-a, module-b] (module-b is current owner)
await api.slothlet.api.remove("module-b");
// Stack restored to: [module-a] (module-a is active again)Collision mode is fixed at instance initialization (the collision config option — see CONFIGURATION.md) and cannot be overridden per api.add() call; forceOverwrite (below) is the only per-call escape hatch.
| Mode | Behavior |
|---|---|
"merge" (default) |
Preserve existing properties, add new ones |
"merge-replace" |
Add new properties, overwrite existing |
"replace" |
Completely replace the existing value |
"skip" |
Keep existing value, silently ignore new |
"warn" |
Keep existing value, log a warning |
"error" |
Throw OWNERSHIP_CONFLICT error |
forceOverwrite: true requires an explicit moduleID and performs a complete replacement regardless of the instance's configured collision mode. Use for cases where a module must fully replace its own prior registration:
await api.slothlet.api.add("config", "./new-config", {
moduleID: "config-v2",
forceOverwrite: true
});Source Code: src/lib/handlers/ownership.mjs
New in v3
Category: AddApi
Status: ✅ Implemented (api_tests/smart_flatten/api_smart_flatten_folder_config)
User Guide: F08
Technical: C34
Purpose: When api.slothlet.api.add("config", folder) is called and the folder contains a subfolder whose name matches the last segment of the mount path (e.g. config/config.mjs), prevent double-nesting api.config.config.* by hoisting the subfolder's exports up to api.config.*.
Condition: After buildAPI returns newApi, if newApi contains a key equal to lastPart (last segment of normalizedPath) AND the matching value's filePath has its parent directory equal to resolvedFolderPath/lastPart (direct child check), hoist that key's own exports to the same level as the other keys in newApi and remove the duplicate key.
Verified Example:
// Folder structure: api_smart_flatten_folder_config/
// main.mjs ← exports getRootInfo, setRootConfig
// config/
// config.mjs ← exports getNestedConfig, setNestedConfig
await api.slothlet.api.add("config", "./api_smart_flatten_folder_config", {});
// Without Rule 13 (double-nested):
api.config.config.getNestedConfig(); // ❌
// With Rule 13 (hoisted):
api.config.getNestedConfig(); // ✅ subfolder exports promoted
api.config.setNestedConfig(); // ✅
api.config.main.getRootInfo(); // ✅ other files unaffectedGuard - isDirectChild: Rule 13 only fires when the matching key's filePath is directly inside resolvedFolderPath/lastPart. This prevents false positives when a deeper nested folder coincidentally shares the mount-path name:
// Should NOT hoist (services/services/services.mjs):
// api.add("services", folder) → newApi has key "services"
// but filePath = .../services/services/services.mjs
// dirname = .../services/services ≠ resolvedFolderPath/services
// → Rule 13 does NOT fire
// → api.services.services.getNestedService remains properly nested ✅Childless leaf case: When the matching key's value has no children of its own — a single self-named file such as thing.mjs exporting a function named thing, mounted at api.add("thing", folder) — there is nothing to spread up from it. The value itself is what belongs at the mount path, so Rule 13 uses it directly rather than hoisting an empty set of children:
// Folder structure: addfolder/
// thing.mjs ← exports a single function named `thing`, no siblings
await api.slothlet.api.add("thing", "./addfolder", {});
api.thing("x"); // ✅ the leaf itself, mounted directly (nothing to hoist)Implementation: src/lib/handlers/api-manager.mjs - immediately after buildAPI call, before setValueAtPath
| Rule | Title | Status | Test Source |
|---|---|---|---|
| 1 | Filename Matches Container Flattening | ✅ Verified | api_tests/api_test |
| 2 | Named-Only Export Collection | ✅ Verified | api_tests/api_test |
| 3 | Empty Module Handling | ✅ Verified | debug testing |
| 4 | Named Export with Function Name Preservation | ✅ Verified | api_tests/api_test, api_tests/api_tv_test |
| 5 | Multiple Module Default Export Handling | ✅ Verified | api_tests/api_tv_test |
| 6 | Multiple Module Mixed Exports | ✅ Verified | api_tests/api_test_mixed |
| 7 | Single Module Named Export Flattening | ✅ Verified | api_tests/api_test |
| 8 | Single Module Default Export Promotion | ✅ Verified | Multiple test files |
| 9 | Function Name Preference Over Sanitization | ✅ Verified | autoIP, parseJSON, getHTTPStatus, XMLParser |
| 10 | Generic Filename Parent-Level Promotion | ✅ Verified | api_tests/api_test/nest4/singlefile.mjs |
| 11 | AddApi Special File Pattern | ✅ Verified | api_tests/api_smart_flatten_addapi |
| 12 | Module Ownership and Selective API Overwriting | ✅ Verified | src/lib/handlers/ownership.mjs |
| 13 | AddApi Path Deduplication Flattening | ✅ Verified | api_tests/smart_flatten/api_smart_flatten_folder_config |
| Flattening Pattern | API Rules | Technical Conditions |
|---|---|---|
| F01 | Rule 1 | C05, C09b, C11 |
| F02 | Rule 8 (Pattern A) | C12, C21a |
| F03 | Rule 7 | C04, C09a, C18, C21c, C30 |
| F04 | Rule 4, Rule 8 (Pattern B) | C08c, C24 |
| F05 | Rule 4, Rule 8 (Pattern C) | C08c, C11 |
| F06 | Rule 11 | C33 |
| F07 | Rule 12 | C19-C22 |
| F08 | Rule 13 | C34 |
| Condition | API Rules | Flattening Patterns |
|---|---|---|
| C01-C07 | Rules 1, 6, 7, 8 | F01, F03 |
| C08-C09d | Rules 4, 6, 7 | F04, F05 |
| C10-C21d | Rules 1, 2, 3, 5, 7, 8, 9, 10 | F01, F02, F03 |
| C22-C26 | Rules 4, 6 | F04, F05 |
| C27-C32 | Rules 5, 6, 7 | Multi-default scenarios |
| C33 | Rule 11 | F06 |
| C34 | Rule 13 | F08 |
| Context | Rules | Primary Conditions |
|---|---|---|
| Single-File Directories | 1, 7, 8, 10 | C11, C12, C04, C17 |
| Multi-File Directories | 1, 2, 5, 7, 9 | C13, C15, C21a-d, C16, C19 |
| Multi-Default Scenarios | 5, 6, 7 | C02, C03, C27-C32 |
| AddApi Operations | 11, 12, 13 | C33, C34, C19-C22 |
| Root-Level Processing | 4, 8, 10 | C08c, C22, C17 |
| Subfolder Processing | 4, 6, 8 | C08d, C20, C24 |