fix: surface extension loading errors and improve method extraction#879
fix: surface extension loading errors and improve method extraction#879
Conversation
…875) Extension loading errors (model validation failures, bundling errors) were silently swallowed because loadUserModels runs before logging is initialized during CLI startup. Users saw only "type not found" with no explanation of why their model failed to load. - Defer extension loading warnings and emit them after logging init - Fix extractMethods to handle shorthand (`methods,`) and variable reference (`methods: importedVar`) patterns in content metadata extraction - Pass only entry point files to extractContentMetadata, not companion files Closes #875 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
None.
Verdict
PASS — This PR fixes a silent-failure UX problem. Extension load errors that were previously discarded before logging initialization are now deferred and emitted as warnings after init. The warning format Failed to load user <kind> <file>: <error> is clear, actionable, and consistent with existing patterns. The extension push --dry-run method extraction improvement is an invisible correctness fix for users with shorthand/reference method patterns. No new flags or output formats introduced.
There was a problem hiding this comment.
Code Review
Clean, well-motivated fix. The root cause analysis in the PR description is thorough and the three changes address distinct, real issues.
Blocking Issues
None.
Suggestions
-
Minor: dead check in
resolveMethodsReference(src/domain/extensions/extension_content_extractor.ts:414): The checkrefMatch[1] !== "{"is always true because the capture group(\w+)cannot match{(\w=[a-zA-Z0-9_]). Harmless but could be removed for clarity. -
DDD note:
DeferredWarningis defined as an exported interface in the CLI layer (src/cli/mod.ts) and used only within that file. The export is harmless, but since it's purely a CLI-internal concern (bridging the gap between pre-logging startup and post-logging emission), it could be unexported. Very minor. -
Concurrency note on
deferredWarnings: The shared mutable array is passed to five concurrentPromise.alltasks. This is safe in JS's single-threaded model sinceArray.pushis synchronous, but a brief comment noting this would help future readers understand why it's safe.
Overall this is a solid fix with good test coverage for the new extraction patterns and a clear user-facing improvement for extension loading errors.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
src/domain/extensions/extension_content_extractor.ts:398-410— Shorthand detection can be defeated bymethods:appearing in comments or stringsresolveMethodsReferencechecks whether the matched shorthand is really shorthand by scanning all text up to the match formethods\s*:\s*[{\w]. This includes comments, string literals, and unrelated code above the model export.Breaking example: A model file with a comment like
// methods: {legacy API}or a string likedescription: "See methods: get, set"before theexport const model = { ... methods, }would causebeforeMethods.match(/methods\s*:\s*[{\w]/)to match, skipping the shorthand branch. The function returnsnull, and the model's methods silently disappear from extracted metadata.Suggested fix: Narrow the verification check to only inspect the text within the model/extension object literal, or anchor the
methods:check to be within the same object scope as the shorthand match. -
src/domain/extensions/extension_content_extractor.ts:333-336— Inline-first strategy can matchmethods:in unrelated objectsextractMethodsFromBlock(content, content)searches the entire file for/methods:\s*\{/. If a helper object, type annotation, or unrelated const anywhere in the file happens to containmethods: { ... }, it matches first and the model's actual methods (via shorthand or reference) are never checked.Breaking example:
const helperConfig = { methods: { unused: { description: "Not a real method" } } }; const realMethods = { sync: { description: "Sync" } }; export const model = { type: "@test/foo", methods: realMethods };
extractMethodswould return[{ name: "unused", ... }]instead of[{ name: "sync", ... }].Note: This is a pre-existing issue (the original code had the same search scope), but the refactoring adds new paths that can never be reached in these cases. Consider scoping the search to the model export's object body.
Low
-
src/cli/mod.ts:498— Shared mutable array acrossPromise.allis safe but fragiledeferredWarningsis pushed to concurrently by 5 async loaders viaPromise.all. This is safe due to JS single-threaded execution (each push happens in a separate microtask, never truly concurrent). However, a future refactor using workers orAtomicscould break this assumption. A brief comment noting the single-threaded safety assumption would help future readers. -
src/cli/mod.ts:222-225— Barecatchswallows all errors including unexpected onesThe old code logged caught errors at debug level (
logger.debug\Skipping user models: ${error}`). The new code uses a barecatch {}` that discards the error entirely. If the loader throws for an unexpected reason (e.g., permission denied on a directory that exists, corrupted file), the user gets zero signal. This is a minor regression in debuggability for the five loader functions. -
src/domain/extensions/extension_content_extractor.ts:413—refMatchregex could matchmethods: typewheretypeis a TS keyword used as identifierThe regex
/methods:\s*(\w+)\s*[,}\n]/would capture identifiers likemethods: typeormethods: default.findVariableObjectBodywould then look forconst type = {which could theoretically match an unrelated variable. In practice this is unlikely sincetypeanddefaultaren't commonly used as variable names for method objects.
Verdict
PASS — The core changes (deferred warnings, entry point filtering) are correct and well-tested. The method extraction improvements work for the stated patterns. The medium findings affect edge cases in static content extraction (not runtime model loading) and do not block.
Summary
Fixes #875
extractContentMetadatanow handles models that use imported/shorthand methodsRoot Cause Analysis
The reporter observed
swamp model type describeshowing no methods for a model using local imports (companion.tsfiles in subdirectories). Our investigation found that multi-file models actually bundle and load correctly — the bundler (deno bundle) inlines all local imports, and the runtime model registry has the methods.The real issue is that extension loading errors are silently swallowed. The
loadUserModelsfunction runs during CLI startup beforeinitializeLoggingis called. When a model fails validation for any reason (badresourcesspec, incompatible zod version, incorrectgarbageCollectionformat, etc.), thelogger.warncall has no configured sink and the message is lost. The user sees only "Model type not found" with zero diagnostic information.We reproduced this by creating a multi-file model with an intentionally invalid
resourcesspec — the model silently failed to load with no visible error output.What was actually broken
Silent error swallowing (
src/cli/mod.ts): All five extension loaders (models, vaults, drivers, datastores, reports) logged warnings vialogger.warnbefore logging was initialized. These warnings were discarded.Content metadata extraction (
extension_content_extractor.ts): TheextractMethodsfunction only matchedmethods: { ... }(inline definition). Models usingmethods,(shorthand property) ormethods: importedVar(variable reference) returned empty methods inextension push --dry-runoutput. This is a separate, real bug affecting registry metadata but not the runtimemodel type describecommand.Entry point filtering (
push.ts):extractContentMetadatareceived all model files including companion files, which could be falsely identified as separate model entries.Changes
src/cli/mod.tsDeferredWarninginterface to collect loading errors during startupdeferredWarningsarray instead of callinglogger.warndirectlyinitializeLoggingcompletes inglobalActionsrc/domain/extensions/extension_content_extractor.tsextractMethodsto handle three patterns:methods: { name: { ... } }(existing)methods,(new — resolves variable namedmethods)methods: someVar(new — resolves named variable)resolveMethodsReferenceandfindVariableObjectBodyhelperssrc/libswamp/extensions/push.tsextractContentMetadatacall to passinput.modelEntryPointsinstead ofinput.allModelFilesUser Impact
Before: Users with extension models that fail to load see only "Model type not found" with no explanation. This is especially confusing for models using local imports, since the structure looks correct but a subtle validation error (e.g., wrong
resourcesformat) causes silent failure.After: Users see a clear warning like:
This tells them exactly which file failed and why, enabling self-diagnosis.
Test plan
deno check,deno lint,deno fmtall passdeno run compilesucceeds🤖 Generated with Claude Code