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

#211 Implement W1: Repository and RowState (extension catalog rearchitecture)

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

Problem

The extension catalog (.swamp/_extension_catalog.db) has no aggregate root and no ownership graph. Issue #201 (stale rows, non-deterministic resolution) and the pattern that produced PRs #208 and #1286 (per-failure-mode sentinels and BOOL columns) are symptoms of the same structural gap. Full architectural analysis and sequenced workstream plan: design/extension-rearchitecture.md.

This issue covers W1 — Repository and RowState, the keystone workstream. W1 is a pure refactor + schema migration with no user-visible behavior changes. It must land before W2–W6 can begin.

Scope

Carve into two PRs.

W1a — Schema migration only

  • Extend migrateSchema() in src/infrastructure/persistence/extension_catalog_store.ts:
    • Absorb the validation_failed BOOL column (added in PR #1286) into a new state TEXT NOT NULL column representing a 7-tag RowState discriminant.
    • Add extension_name TEXT NOT NULL and extension_version TEXT NOT NULL columns. Backfill via path heuristic: rows under <repo>/.swamp/pulled-extensions/<name>/<version>/ parse name+version from path; rows under <repo>/extensions/<kind>/ get `@local/` and `"0.0.0"`. Drop unmatched rows.
    • Bump `BUNDLE_LAYOUT_VERSION` to `"per-extension-aggregate-v3"` so the cold-start guard rescans on first run after upgrade.
  • New helpers under `src/infrastructure/persistence/`:
    • `canonicalizePath(p: string): string`
    • `findRepoRoot(start: string): string`

W1b — Domain model + repository

  • New value objects in `src/domain/extensions/`: `Extension` (aggregate root), `Source`, `RowState` (discriminated union, 7 states), `SourceLocation`, `BundleLocation`.
  • New `ExtensionRepository` in `src/infrastructure/persistence/` with diff-based `save(extension)` and `saveAll(extensions[])` inside SQLite transactions. `saveAll` evaluates the global `(kind, typeNormalized)` uniqueness invariant on post-save state — supports the upgrade-as-atomic-transition pattern `saveAll([vN.tombstoneAll(), vN+1])`.
  • Replace per-loader cold-start guard sets with a single call to `repository.invalidationGuards(kind)` — closes the audit's "model loader has four guards, the four siblings have one" gap as a side effect.
  • Fold the standalone `forceCatalogRescan` helper into `repository.invalidateAll()`.

Full state model, invariants, transition methods, and the `RowState` enumeration are specified in `design/extension-rearchitecture.md` under "The aggregate / state model" and "W1 — Repository and RowState (the keystone)".

Pre-work decisions to pin in the W1a PR description

These contracts bake into every subsequent workstream:

  1. Path canonicalization rule — recommendation: lowercased + forward-slash on Windows; raw on POSIX.
  2. Repo-root identification — recommendation: nearest ancestor of the source path containing a `.swamp/` directory; first match wins.
  3. Unmatched-row backfill behavior — recommendation: drop the row; cold-start guard repopulates from disk.

Out of scope (deferred to later workstreams)

  • `extension rm` pruning rows / lifecycle services owning catalog writes — W2 (this is what closes #201)
  • Cross-extension `DuplicateType` errors at lifecycle save time — W2
  • `ReconcileFromDisk` service / freshness-as-aggregate-query / removing `UNREADABLE_DEP_SENTINEL` — W3
  • Loader unification / `KindAdapter` — W4
  • Per-fingerprint import URLs and subprocess test harness — W5
  • `swamp doctor extensions` — W6

Success criteria

  • `pragma_table_info('bundle_types')` post-migration shows `state`, `extension_name`, `extension_version`; no `validation_failed`.
  • Migration is idempotent; falls back to "drop catalog and rebuild from disk" on backfill verification failure.
  • `Extension`, `Source`, `RowState`, `SourceLocation`, `BundleLocation` exported from `src/domain/extensions/`.
  • `ExtensionRepository` round-trips aggregates with diff-based saves; `saveAll` evaluates the global uniqueness invariant on post-save state and supports the upgrade-as-atomic-transition pattern.
  • The four sibling loaders' cold-start invalidation matches the model loader's coverage.
  • All existing tests pass on Linux, macOS, and Windows.

Suggested test additions

  • Migration backfill against fixture catalogs covering: post-#1286 schema with `validation_failed = 1` rows, mixed pulled + local rows, unmatched-path rows.
  • Migration idempotence (run twice, second is a no-op).
  • Repository diff-save: add Source → INSERT; drop Source → DELETE; transition Source state → UPDATE.
  • `saveAll` atomicity: `[vN.tombstoneAll(), vN+1]` succeeds; two extensions with overlapping `(kind, type)` rejects with a `DuplicateType` event.
  • Cross-platform: `SourceLocation` equality on Windows fixtures with mixed casing.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 13 MOREREVIEW+ 2 MOREPR_LINKEDCOMPLETE

Shipped

5/4/2026, 4:44:21 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/4/2026, 11:52:49 AM

Sign in to post a ripple.