Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLI
Assigneesstack72

#233 Extract LockfileRepository (W2 prequel for swamp-club#231)

Opened by stack72 · 5/4/2026· Shipped 5/4/2026

Problem

upstream_extensions.json read and write logic is scattered across the codebase:

  • 16 reader callsites import readUpstreamExtensions directly from src/infrastructure/persistence/upstream_extensions.ts
  • Write functions (updateUpstreamExtensions, removeUpstreamExtension) live in src/libswamp/extensions/pull.ts with a duplicate removeUpstreamExtension definition in rm.ts
  • W1b's ExtensionRepository takes a read-only getLockedVersion: (name) => string | null closure injected at construction (per src/infrastructure/persistence/extension_repository.ts), exposing nothing for write

The lifecycle services planned in W2 (swamp-club#231 — Install / Remove / Upgrade) need to write the lockfile as part of their unit-of-work. Two options: scatter updateUpstreamExtensions calls inside each service, or extract a single repository owning both sides.

Proposed solution

Introduce LockfileRepository at src/infrastructure/persistence/lockfile_repository.ts with:

  • getEntry(name): UpstreamExtensionEntry | null
  • getAllEntries(): UpstreamExtensionsMap
  • writeEntry(name, version, files, opts): Promise<void>
  • removeEntry(name): Promise<void>
  • getLockedVersion(name): string | null — replaces the W1b injection closure

Inject into both ExtensionRepository (read path) and the future W2 lifecycle services (write path). Migrate every callsite. Delete the duplicate removeUpstreamExtension definitions in pull.ts and rm.ts.

Internal mechanics (acquireLock advisory file lock with retry-backoff, atomicWriteTextFile for crash safety) stay unchanged — only the surface shape changes.

Why a separate PR (refactor-prequel)

Audited as part of W2's Phase A pre-implementation gate (per swamp-club#231 plan v4):

  • ~160 LOC for the new repository
  • ~16 callsites touched (readers across cli/, libswamp/, infrastructure/)
  • Pre-committed mechanical threshold rule said: >7 callsites → SPLIT as refactor-prequel

Bundling into W2 would push that PR over ~1500 LOC and balloon the auto-ship-on-merge soak risk. Splitting matches the W1a → W1b precedent (each ships and soaks before the next builds on it).

Out of scope

  • W2 lifecycle services (swamp-club#231) — depends on this landing first
  • Cross-platform lockfile concurrency improvements — W3 territory; this prequel does not change the existing acquireLock race window
  • ADV-13 test-seam audit (fault-injection between SQLite commit and lockfile write) — stays with W2 since this prequel doesn't introduce that boundary

Auto-ship-on-merge

Same gate as W1a/W1b/W2:

  • CI green (type-check + lint + fmt + tests)
  • Author smoke + reviewer smoke
  • ~2 day diversity-matrix soak (shorter than W2 because the surface is smaller and 14 of 16 callsites are 1-line read-substitutions)
  • Forward-only revert (no schema changes; deleting upstream_extensions.json is recovered by a re-pull, same as today)

Acceptance

  • All 16 read callsites migrated to lockfileRepository.*
  • Both updateUpstreamExtensions and removeUpstreamExtension deleted from pull.ts/rm.ts; only LockfileRepository writes the file
  • ExtensionRepository constructor takes a LockfileRepository instead of a getLockedVersion closure
  • All existing tests pass on Linux + macOS (Windows non-blocking per W1 precedent)

References

  • Parent: swamp-club#231 (W2 lifecycle services — blocked on this landing)
  • Predecessors: swamp-club#211 (W1 tracking), swamp-club#223 (W1b shipped), swamp-club#181 (W1a shipped via #1292)
  • W2's Phase A audit memo (informal, captured in #231's implementing lifecycle)
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 8 MOREREVIEW+ 3 MOREPR_MERGEDSHIPPED

Shipped

5/4/2026, 11:41:18 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/4/2026, 10:32:23 PM

Sign in to post a ripple.