Conversation
… (swamp-club#265) When bundleWithCache returns a stale cached bundle (via the isExpectedBundleFailure fast-path or the error fallback), rebundleAndUpdateCatalog was unconditionally writing the new source_fingerprint to the catalog. This poisoned the freshness check — findStaleFiles saw matching fingerprints and never retried, permanently serving stale code with no user-visible indication. The fix adds a kind-agnostic BundleResult type to bundle_freshness.ts that signals whether the JS came from a fresh build or a cache fallback. When fromCache is true, rebundleAndUpdateCatalog preserves the catalog's stored fingerprint so findStaleFiles retries on the next warm-start. A structured warning fires only on the fallback case (fingerprint differs from catalog), not on legitimate cache hits. Applied mechanically across all 5 extension loaders (models, drivers, vaults, datastores, reports). bundleAndIndexOne surfaces fromCache as an additive return field for W4 to wire up per-origin when it unifies the loaders. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
No unit test for
findBySourcePath(src/infrastructure/persistence/extension_catalog_store.ts:695): The new public method onExtensionCatalogStorehas no dedicated unit test. It is exercised by the integration test, and it's a simple PK lookup, but the existing test file has tests for the sibling methodsremoveBySourcePathandfindByKind— adding a test for consistency would be cheap. -
Early-load warning may be noisy (all 5 loaders, e.g.
user_model_loader.ts:440-442): In theloadModels/loadAllcold-start path, thefromCachewarning fires on every cache fallback, including legitimate hits where the source hasn't changed. TherebundleAndUpdateCatalogpath correctly distinguishes these cases (only warns when fingerprints differ), but the cold-start path lacks catalog access to make the same distinction. This could cause warn-level noise on every invocation for extensions with bare specifiers. Worth considering gating the cold-start warning on some condition in W4's loader consolidation. -
Duplicated fingerprint preservation logic: The ~15-line
effectiveFingerprintblock is copy-pasted across all 5 loaders'rebundleAndUpdateCatalogmethods. The PR description correctly notes this is intentional temporary duplication pending W4'sKindAdapterconsolidation — just flagging it to ensure it's on the W4 radar.
Overall
Solid bug fix. The root cause analysis is clear — writing the new source fingerprint alongside a stale cached bundle permanently masked the staleness from findStaleFiles. The fix correctly preserves the catalog's stored fingerprint when bundleWithCache returns a cache fallback, ensuring retry on the next warm-start. The BundleResult type is well-placed in the domain layer (bundle_freshness.ts) as a value object. The integration test covers the three critical scenarios (unchanged source → no warning, failed rebuild → warning + fingerprint preserved, second retry → deterministic). Design doc updated. No import boundary violations, no security concerns, correct LogTape interpolation.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
First-run catalog bootstrap still writes new fingerprint with stale cache (
src/domain/models/user_model_loader.ts:1064-1079)The fingerprint preservation logic is correctly applied in all 5 loaders'
rebundleAndUpdateCatalogmethods (warm-start path). However,populateCatalogFromDir(line 1064-1079) — the first-run catalog bootstrap path that runs when the catalog is empty — unconditionally computes and writes the current source fingerprint even when the bundle is stale from cache.Breaking scenario: User deletes the
.swampdatabase (or first initializes a repo that already has cached.jsbundles from a prior environment). Source has since changed to use bare specifiers.loadModels→bundleWithCache→ returns stale cache (fromCache: true). The warning fires (line 440-442) but nothing preserves the old fingerprint. ThenpopulateCatalogFromDirwrites the new fingerprint → poisoned. Subsequent warm-starts see matching fingerprints and never retry.This is the same class of bug as #265 on a less-common path. The warm-start fix in this PR is the right priority since it covers the issue's primary scenario. Noting this as a gap for awareness — the
populateCatalogFromDirandReconcileFromDiskService(which recomputes fingerprints at line 376) paths would need similar treatment for complete coverage. -
loadModels/loadDrivers/etc. initial-load warning fires on legitimate cache hits (src/domain/models/user_model_loader.ts:440-442)In the initial full-load path, the warning
Using cached bundle for ${file} — source may have changed but bundle could not be regeneratedfires unconditionally wheneverfromCacheis true — even when the source hasn't changed and the cached bundle is perfectly valid (e.g., source-mounted extension with bare specifiers that was bundled externally, nothing modified since). TherebundleAndUpdateCatalogpath correctly gates its warning onexisting.source_fingerprint !== sourceFingerprint, but the initial-load path lacks this guard because there's no catalog to compare against yet.This produces false-alarm warnings on every cold start with source-mounted extensions that use bare specifiers, which could cause user confusion or alert fatigue.
Low
-
bundleAndIndexOnereturnsfromCachebut no caller uses it (src/domain/models/user_model_loader.ts:1158)The PR adds
fromCacheto the return type ofbundleAndIndexOneacross all 5 loaders, but neitherReconcileFromDiskService(line 369 inreconcile_from_disk_service.ts) norInstallExtensionService(line 239 ininstall_extension_service.ts) reads it. The field is additive and doesn't break anything, but it's dead data in the current state. The PR description notes this is intentional ("no caller changes needed"), which is fine — just noting it's unused. -
existing?.source_fingerprinttruthy guard skips empty-string fingerprints (src/domain/vaults/user_vault_loader.ts:821and all loaders)if (existing?.source_fingerprint)evaluates to false whensource_fingerprintis""(empty string), which is the default for rows created without a computed fingerprint. In that case, the preservation logic is skipped and the new fingerprint is written. This is arguably correct behavior (nothing to preserve), but it means a row that was bootstrapped with an empty fingerprint won't get the protection. In practice, all current catalog write sites compute a fingerprint before writing, so this is a theoretical-only gap.
Verdict
PASS — The core fix is sound: rebundleAndUpdateCatalog correctly preserves the stored fingerprint when bundleWithCache returns stale cache, preventing the permanent fingerprint poisoning described in #265. The BundleResult type is clean, the findBySourcePath SQL is parameterized and uses the existing mapRow pattern, and the integration test covers the key warm-start scenarios with proper assertions. The medium findings are gaps in adjacent code paths, not errors in the changed code.
Summary
bundleWithCachereturns a stale cached bundle (viaisExpectedBundleFailurefast-path or error fallback),rebundleAndUpdateCatalognow preserves the catalog's storedsource_fingerprintinstead of writing the new one. This prevents permanent fingerprint poisoning wherefindStaleFilessees matching fingerprints and never retries the bundle.BundleResulttype tobundle_freshness.tswith{ js, fromCache }— shaped for W4'sKindAdapterto absorb when it consolidates the loaders.fromCache && fingerprint differs), not on legitimate cache hits.bundleAndIndexOnesurfacesfromCacheas an additive return field — no caller changes needed (reconcile and install services create Sources independently).findStaleFilessignature unchanged (W2'sRemoveExtensionServicesafety-net preserved).Test Plan
integration/source_extension_rebundle_test.ts) with three assertion groups:faa89ac09cda→69ead2141011), bundle stale, no warning, permanently stuckfaa89ac09cda), bundle stale but retryable, warning emitted, deterministicbundle_cache_freshness_test.ts(issue Issue 10: Create data migration tool #125 regression net) passes🤖 Generated with Claude Code