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

#342 W7 — unify extension failure surfaces; collapse registries.failures[] into sourceDetails[]

Opened by stack72 · 5/12/2026· Shipped 5/14/2026

Summary

After #334 (invalidate-then-reconcile sequencing) and the planned follow-ups for ValidationFailed distinction and Tombstoned surfacing, aggregateState.sourceDetails[] becomes the canonical authoritative view of every extension source's current state. But the legacy registries.<kind>.failures[] surface still exists as a parallel failure-reporting channel — driven by the legacy buildIndex path in ExtensionLoader.buildIndex (around extension_loader.ts:300).

The two surfaces report overlapping information with different shapes, field names, and semantics. This is architectural debt that:

  • Doubles the work for tooling consumers (have to merge two arrays to get a complete failure picture)
  • Adds a maintenance burden (two code paths recording similar information)
  • Risks future divergence (a fix to one path can silently skip the other)

This is the W7 workstream — the natural successor to W1-W6's extension catalog rearchitecture. Eliminating the dual surface closes the architectural-debt class entirely.

Why this matters

  • Single source of truth. Tooling, UI, and tests should consult one place for failure state. Today they have to merge two.
  • DDD consistency. sourceDetails[] is populated through the Extension aggregate (I-Repo-1 enforced). registries.failures[] bypasses the aggregate. Routing everything through the aggregate closes the consistency gap.
  • Test simplification. swamp-uat currently has tests that assert on registries.failures[] for legacy-path scenarios. After W7, those tests collapse to single assertions on sourceDetails[].

Scope

Three pieces of work, in priority order:

W7a — Migrate buildIndex failure paths to the aggregate

In ExtensionLoader.buildIndex (extension_loader.ts:237-330), the result.failed array captures errors from rebundleAndUpdateCatalog. Migrate these to write through repository.saveAll([recordBundleBuildFailed(...)]) instead. Once all failures route through the aggregate, the result.failed array becomes empty — the legacy surface is dead code.

W7b — Remove markCatalogValidationFailed

After W7a and the ValidationFailed-distinction issue ship, markCatalogValidationFailed in bundle_freshness.ts:398 has zero production callers (currently it's the only producer of ValidationFailed catalog rows via direct upsert). Remove it and its tests; route any remaining validation-write needs through the aggregate.

W7c — Deprecate or remove registries.<kind>.failures[]

Once W7a is in place, the failures[] array is always empty for catalog-recorded failures. Two options:

  • Remove the field entirely. Cleaner contract. Requires consumers (UI, tests, MCP integrations) to migrate to sourceDetails[].
  • Keep the field but mark it deprecated. Always empty; documentation steers consumers to sourceDetails[]. Removable in a future major-version bump.

Recommendation: remove the field. swamp-uat assertions already migrate to sourceDetails[] as a result of W7a. No external consumers of this field are known. Cleaner break is better than a slow rot.

Risks

  • Breaks consumers that depend on registries.failures[] field presence. Need to audit. Worth checking:
    • swamp-club UI rendering of doctor results
    • MCP tool consumers that fetch doctor JSON
    • Any in-tree CLI commands that present failures
  • Performance. buildIndex runs frequently (on every command, not just doctor). Routing every failure through repository.saveAll adds one transaction per failed file per command. Acceptable in normal cases (failures are rare in healthy repos); pathological cases (many broken extensions, frequent commands) need verification.
  • Atomicity. The legacy result.failed was in-memory only. The aggregate path persists. If a command fails after writing BundleBuildFailed to the catalog but before completing, the row stays — which is actually the desired behavior (next invocation surfaces the failure). But behavior change is real and worth flagging.

Acceptance criteria

  1. After running any command (not just doctor) against a repo containing a broken local extension, the broken extension's source appears in swamp doctor extensions --json sourceDetails[] with the correct stateTag AND does NOT appear in registries.<kind>.failures[].
  2. The registries.<kind>.failures[] field is either removed from the JSON output schema or guaranteed empty (depending on the W7c choice).
  3. markCatalogValidationFailed is removed; no production code calls it.
  4. All swamp-uat extension tests pass with sourceDetails[]-only assertions (no registries.failures[] reads).
  5. Performance: doctor extensions on a repo with N=10 extensions and M=2 broken sources completes within 1.5x the post-#334 baseline.
  6. No regression in any integration test in integration/extensions/.

Files an implementing agent should read first

  • src/domain/extensions/extension_loader.ts:237-330 — the buildIndex legacy path with result.failed capture; primary refactor target
  • src/domain/extensions/extension_loader.ts:300-302 — the exact catch site that pushes to result.failed
  • src/libswamp/extensions/reconcile_from_disk_service.ts:483-507 — the canonical aggregate-write pattern to follow (post-#334)
  • src/domain/extensions/bundle_freshness.ts:398markCatalogValidationFailed to remove in W7b
  • src/cli/commands/doctor_extensions.ts — where the JSON shape is assembled; registries.failures[] field needs removal/deprecation
  • swamp-uat/src/cli/helpers/schemas.ts — Zod schema updates
  • BLOG_BRIEFING.md and swamp-uat/ROWSTATE_INVESTIGATION.md for the full architectural-debt story behind this work

Plan-review expectation

This is a larger refactor than #334. Expect v1→v2→v3 plan-review treatment. Particularly invest in v2 on:

  • Performance impact of per-failure transactions in buildIndex's hot path
  • Behavior changes consumers might notice (the persistent-failures property is new for non-doctor commands)
  • Order of W7a/W7b/W7c sub-deliverables — can ship as one big PR or three smaller ones; both have tradeoffs

UAT coupling

Significant. The swamp-uat matrix has tests asserting on registries.failures[] for legacy-path scenarios. Those tests need to either:

  • Migrate to sourceDetails[]-only assertions (preferred), or
  • Be deleted if they were testing only the dual-path behavior itself

Treat the swamp-uat migration as a deliverable of W7, not a follow-up. This is a bigger UAT coupling than #334; coordinate as a single combined PR set landing simultaneously, not staggered.

Out of scope

  • Renaming or restructuring sourceDetails[] — it's the destination, don't redesign it in the same PR
  • Changes to the catalog SQLite schema — the W1a schema is fine
  • Changes to invariants I1-I3 — they're enforced; this refactor doesn't relax them

Why "W7" and not just "follow-up to #334"

This is large enough and structurally distinct enough to deserve a workstream label aligned with the W1-W6 sequence. Labeling it W7 makes the rearchitecture story complete: W1-W6 built the new model; W7 removes the last vestige of the old one.

  • #334 — sequencing fix; prerequisite. Ship that and let it soak before starting W7.
  • The ValidationFailed-distinction issue (filed alongside this one) — should ship before W7b's markCatalogValidationFailed removal.
  • The Tombstoned-surfacing issue (filed alongside this one) — independent of W7; can ship before, during, or after.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 2 MORETRIAGE+ 8 MOREREVIEW+ 3 MOREPR_MERGEDSHIPPED

Shipped

5/14/2026, 12:32:46 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/13/2026, 4:54:37 PM

Sign in to post a ripple.