fix: detect transitive dep changes and surface rebundle errors in bundle cache#1099
fix: detect transitive dep changes and surface rebundle errors in bundle cache#1099
Conversation
… in bundle cache Fixes two bugs in the bundle cache system: 1. findStaleFiles() only checked entry-point mtimes against the catalog, missing changes in transitive dependencies (imported .ts files). Now resolves local imports and compares their mtimes against the cached bundle file, matching the check bundleWithCache() already performs. 2. bundleWithCache() silently swallowed all rebundle failures and touched the bundle mtime to suppress future retries. Now distinguishes expected failures (pulled extensions without project config + bare specifiers) from unexpected failures (user extensions with real errors). Unexpected failures log at warn level and do not touch the mtime, so subsequent runs retry bundling until the error is fixed. Applied consistently across all 4 extension loaders (models, drivers, vaults, datastores). Adds isExpectedBundleFailure() shared helper to bundle.ts to avoid duplicating the deno.json discovery + bare-specifier check. Closes #1094 Co-authored-by: Magistr <[email protected]> Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Code Review
Clean, well-tested fix. The transitive dependency detection and error classification are sound, and the changes are consistent with existing patterns.
Blocking Issues
None.
Suggestions
-
Duplicated
bundleWithCacheerror-handling across 4 loaders — TheisExpectedBundleFailure+ log-level branching block is now copy-pasted identically inuser_model_loader.ts,user_datastore_loader.ts,user_driver_loader.ts, anduser_vault_loader.ts. Consider extracting a shared helper (e.g.,handleBundleFallback(bundlePath, absolutePath, relativePath, repoDir, error)) inbundle.tsto reduce the surface area for future divergence. Not blocking since the duplication is pre-existing and this PR just modified the existing pattern consistently. -
isExpectedBundleFailureuses sync I/O —Deno.statSyncandDeno.readTextFileSyncare called from asyncbundleWithCachecontexts. This is consistent withfindNearestDenoConfigwhich does the same, so not a concern for this PR, but an async version would avoid blocking the event loop on slow filesystems. -
Transitive dep checking is model-only — The
findStaleFilestransitive dependency detection was added toUserModelLoaderbut not to the datastore, driver, or vault loaders (they already importresolveLocalImports). If those loaders also have catalog-based incremental rebuild, they could benefit from the same check. If they don't, this is fine as-is.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
Off-by-one in
isExpectedBundleFailuredirectory traversal —src/domain/models/bundle.ts:220-221The
repoDirboundary check (break) executes before the deno.json search for that directory:while (dir !== root) { if (repoDir && resolve(dir) === resolve(repoDir)) break; // breaks BEFORE checking repoDir for (const name of ["deno.json", "deno.jsonc"]) { ... }
If
deno.jsonexists at the repo root (a common layout), the traversal stops without finding it. The function then checkssourceHasBareSpecifiers()and may returntrue(expected failure), when the failure is actually unexpected because the project config does exist.Breaking example: Extension at
<repoDir>/extensions/myext/model.tsimports"zod"(bare specifier).deno.jsonat<repoDir>/deno.jsonmaps"zod"tonpm:zod@4. Bundling fails for some other reason (network, syntax error).isExpectedBundleFailurewalks up to<repoDir>, hits the break, never seesdeno.json, returnstrue. The cache mtime is touched, suppressing retries on subsequent runs — the user never sees the warning that would have helped them debug.Fix: Swap the order — check for deno.json before the boundary break:
while (dir !== root) { for (const name of ["deno.json", "deno.jsonc"]) { ... } if (repoDir && resolve(dir) === resolve(repoDir)) break; // ... }
-
resolveLocalImportsincludes the entry point inresolvedFiles—src/domain/models/user_model_loader.ts:898-916resolveLocalImports([absolutePath], dir)returns the entry point and its transitive deps inresolvedFiles. The entry point's mtime is then included in thenewestDepMtimecalculation and compared againstbundleStat.mtime. This is functionally harmless (entry point was already verified fresh against the catalog, and its mtime should be older than the bundle's), but it adds unnecessary stat calls and makes the intent less clear. Consider filtering the entry point out ofresolvedFilesbefore the dep check, or just documenting that it's intentionally included.
Low
-
Sync I/O in async context —
src/domain/models/bundle.ts:223,237isExpectedBundleFailureusesDeno.statSyncandDeno.readTextFileSync. It's called from the asyncbundleWithCacheerror path. Since this is a cold error-handling path (bundling already failed), the blocking impact is negligible in practice. Noting for awareness only. -
Identical error-handling blocks across 4 loaders —
user_model_loader.ts,user_datastore_loader.ts,user_driver_loader.ts,user_vault_loader.tsThe
isExpectedBundleFailureconditional block is copy-pasted across all 4 loader files. A future change to the error-handling logic would need to be applied in 4 places. Not a correctness issue, but increases the maintenance surface. The PR author clearly recognizes this by extracting the sharedisExpectedBundleFailurehelper, so this is just the remaining duplication.
Verdict
PASS — The logic is sound and well-tested. The off-by-one in the directory traversal boundary (Medium #1) is a real bug that should be addressed, but it only affects failure classification (not data correctness or crashes), so it doesn't block the merge. The transitive dependency detection in findStaleFiles and the expected-vs-unexpected failure distinction in bundleWithCache are both correctly implemented.
Summary
resolveLocalImports()and compares their mtimes against the cached bundle file. This catches edits to imported.tsfiles that don't touch the entry point itself.isExpectedBundleFailure()shared helper inbundle.tsto avoid duplicating the deno.json discovery + bare-specifier check across all 4 extension loaders (models, drivers, vaults, datastores).buildIndex().Closes #1094
Test Plan
buildIndex detects transitive dependency changes— verifies the catalog path correctly triggers rebundling when only an imported file changesbundleWithCache preserves cached bundle on unexpected failure— verifies fallback returns old valid bundle content without contamination from broken sourceisExpectedBundleFailure()— covers bare specifiers without config (expected), deno.json present (unexpected), npm-only imports (unexpected)🤖 Generated with Claude Code