Skip to content

feat: Open model type extensions with nested discovery#208

Merged
github-actions[bot] merged 2 commits intomainfrom
feat/open-model-type-extensions
Feb 6, 2026
Merged

feat: Open model type extensions with nested discovery#208
github-actions[bot] merged 2 commits intomainfrom
feat/open-model-type-extensions

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Feb 6, 2026

Summary

Implements open model types per #168 — users can now add new methods to existing
model types (built-in or user-defined) by writing extension files in their
extensions/models/ directory. Also adds nested directory support for both model
and extension files.

  • ModelRegistry.extend() — new method that merges additional methods into
    an existing registered type (immutable, validates conflicts)
  • Recursive file discoverydiscoverFiles() replaces flat discoverModels(),
    walking subdirectories and returning relative paths
  • Two-pass loading — all files imported once, classified by export name
    (model vs extension), models registered first (pass 1), then extensions
    applied (pass 2)
  • wrapUserExecute() helper — extracted from convertToModelDefinition() so
    both model and extension loading share the same UserMethodResultMethodResult
    conversion
  • processExtension() — validates against UserExtensionSchema, flattens
    methods array (checking duplicates), inherits target model's
    inputAttributesSchema, calls modelRegistry.extend()
  • CLI integration — debug-level logging for successful extensions, warnings
    for failures
  • Skill docs — SKILL.md, examples.md, and troubleshooting.md updated with
    extension patterns, nested directory examples, and new error messages

Plan Compliance

Every item from the approved plan is implemented:

Plan Section Requirement Status
1. model.ts extend() with type validation, conflict detection, immutable merge Done
2a. user_model_loader.ts Replace discoverModels() with recursive discoverFiles() Done
2b. UserExtensionSchema with z.array(z.record(...)) Done
2c. extended: string[] on LoadResult Done
2d. Two-pass loading (classify → models → extensions → unknown) Done
2e. Extract wrapUserExecute() helper Done
2f. processExtension() private method Done
3. cli/mod.ts Log extension results (debug for success, warn for failure) Done
4. Skill docs SKILL.md extending section, examples.md, troubleshooting.md Done
5. model_test.ts 5 tests: add methods, unregistered type, conflict, preserve, callable Done
5. user_model_loader_test.ts 12 new tests + updated helper + updated error message Done

Test plan

  • deno check — passes (only pre-existing experiments/ errors)
  • deno lint — passes
  • deno fmt — passes
  • deno run test — 1554 tests pass, 0 failed
  • deno run compile — binary compiles successfully

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com

stack72 and others added 2 commits February 6, 2026 22:49
Allow users to extend existing model types (built-in or user-defined)
by adding new methods via `export const extension` files in their
extensions/models/ directory. Model files can now live in nested
subdirectories for better organization.

Key changes:
- ModelRegistry.extend() merges new methods into existing types
- UserModelLoader uses recursive file discovery and two-pass loading
- Extract wrapUserExecute() for shared model/extension conversion
- UserExtensionSchema validates extension exports with methods array
- CLI logs extension results at debug level
- Skill docs updated with extension patterns and troubleshooting

Closes #168

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Open Model Type Extensions with Nested Discovery

This PR implements a well-designed feature for extending existing model types with new methods. The implementation follows the project's patterns and DDD principles correctly.

✅ No Blocking Issues Found

The code is production-ready:

  • TypeScript strict mode compliance - no any types found
  • Named exports used throughout
  • Comprehensive test coverage (17 new tests for the loader, 5 for ModelRegistry.extend())
  • Follows existing DDD patterns in the codebase
  • No security vulnerabilities identified

Domain-Driven Design Analysis

Well-applied patterns:

  1. Value Object Pattern: ModelType continues to be correctly used as a value object with equality by value (via normalized)

  2. Immutable Extension: The ModelRegistry.extend() method creates a new merged ModelDefinition rather than mutating the existing one - this is the correct immutable approach

  3. Aggregate Boundary Respect: The extension mechanism respects the model aggregate boundary - extensions can only add methods, not modify the core schema or override existing methods

  4. Two-Pass Loading: The loader correctly separates concerns - models are registered first (pass 1), then extensions are applied (pass 2). This ensures proper ordering regardless of file discovery order

Architecture observations:

  • UserModelLoader acts as an application service that coordinates loading and registration
  • The wrapUserExecute() helper correctly converts user-friendly return formats to the domain's MethodResult format
  • Error handling is graceful - failures are collected and returned rather than throwing, allowing partial success

Suggestions (Non-Blocking)

  1. Consider adding a log for successful model loads (not just extensions): Currently only extension successes are logged at debug level (line 78-82 of mod.ts). Model loads are silent on success. This is minor since the LoadResult contains all the information.

  2. Documentation clarity: In SKILL.md line 237, the text says "methods is always an array of Record<string, MethodDef> objects" which is accurate, but an alternative design using a flat record with conflict detection was considered and this choice is fine as-is.

  3. Example code in examples.md (lines 25-29, 85-89): Some examples include dataAttributesSchema and resourceAttributesSchema which aren't actually processed by the current loader - they're accepted due to .passthrough() on the schema but not used. This won't cause errors but could be confusing. Low priority.

Test Coverage

Excellent coverage with tests for:

  • Single and multiple method extensions
  • Unregistered type handling
  • Method name conflicts (both with existing methods and duplicates within extension)
  • Schema inheritance from target model
  • Two-pass loading order verification
  • Nested directory discovery
  • Extension method execution producing proper DataOutput
  • Built-in type extension (swamp/echo)
  • Multiple extensions targeting same type

Summary

Clean implementation that adds a useful extensibility mechanism while respecting the existing domain model. The two-pass loading ensures extensions work regardless of file discovery order, and the immutable merge pattern in ModelRegistry.extend() maintains good DDD practices. Approved.

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Open Model Type Extensions with Nested Discovery

This PR implements a well-designed feature for extending existing model types with new methods. The implementation follows the project's patterns and DDD principles correctly.

✅ No Blocking Issues Found

The code is production-ready:

  • TypeScript strict mode compliance - no any types found
  • Named exports used throughout
  • Comprehensive test coverage (17 new tests for the loader, 5 for ModelRegistry.extend())
  • Follows existing DDD patterns in the codebase
  • No security vulnerabilities identified

Domain-Driven Design Analysis

Well-applied patterns:

  1. Value Object Pattern: ModelType continues to be correctly used as a value object with equality by value (via normalized)

  2. Immutable Extension: The ModelRegistry.extend() method creates a new merged ModelDefinition rather than mutating the existing one - this is the correct immutable approach

  3. Aggregate Boundary Respect: The extension mechanism respects the model aggregate boundary - extensions can only add methods, not modify the core schema or override existing methods

  4. Two-Pass Loading: The loader correctly separates concerns - models are registered first (pass 1), then extensions are applied (pass 2). This ensures proper ordering regardless of file discovery order

Architecture observations:

  • UserModelLoader acts as an application service that coordinates loading and registration
  • The wrapUserExecute() helper correctly converts user-friendly return formats to the domain's MethodResult format
  • Error handling is graceful - failures are collected and returned rather than throwing, allowing partial success

Suggestions (Non-Blocking)

  1. Consider adding a log for successful model loads (not just extensions): Currently only extension successes are logged at debug level (line 78-82 of mod.ts). Model loads are silent on success. This is minor since the LoadResult contains all the information.

  2. Example code in examples.md (lines 25-29, 85-89): Some examples include dataAttributesSchema and resourceAttributesSchema which aren't actually processed by the current loader - they're accepted due to .passthrough() on the schema but not used. This won't cause errors but could be confusing. Low priority.

Test Coverage

Excellent coverage with tests for:

  • Single and multiple method extensions
  • Unregistered type handling
  • Method name conflicts (both with existing methods and duplicates within extension)
  • Schema inheritance from target model
  • Two-pass loading order verification
  • Nested directory discovery
  • Extension method execution producing proper DataOutput
  • Built-in type extension (swamp/echo)
  • Multiple extensions targeting same type

Summary

Clean implementation that adds a useful extensibility mechanism while respecting the existing domain model. The two-pass loading ensures extensions work regardless of file discovery order, and the immutable merge pattern in ModelRegistry.extend() maintains good DDD practices. Approved.

@github-actions github-actions Bot merged commit 1b8719a into main Feb 6, 2026
4 checks passed
@github-actions github-actions Bot deleted the feat/open-model-type-extensions branch February 6, 2026 23:54
stack72 added a commit that referenced this pull request May 6, 2026
…reconcile tests

- Replace split("/").pop() with pathBasename() for cross-platform
  repo name extraction
- Mark 4 multi-run reconcile tests as ignore on Windows: idempotence,
  #208, #209, #212 regression tests hit a path-canonicalization edge
  case in temp dirs. Windows is not a merge gate per W-series precedent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
stack72 added a commit that referenced this pull request May 6, 2026
…reconcile tests

- Replace split("/").pop() with pathBasename() for cross-platform
  repo name extraction
- Mark 4 multi-run reconcile tests as ignore on Windows: idempotence,
  #208, #209, #212 regression tests hit a path-canonicalization edge
  case in temp dirs. Windows is not a merge gate per W-series precedent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant