#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 onsourceDetails[].
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.
buildIndexruns frequently (on every command, not just doctor). Routing every failure throughrepository.saveAlladds 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.failedwas 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
- 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 --jsonsourceDetails[]with the correctstateTagAND does NOT appear inregistries.<kind>.failures[]. - The
registries.<kind>.failures[]field is either removed from the JSON output schema or guaranteed empty (depending on the W7c choice). markCatalogValidationFailedis removed; no production code calls it.- All swamp-uat extension tests pass with
sourceDetails[]-only assertions (noregistries.failures[]reads). - Performance: doctor extensions on a repo with N=10 extensions and M=2 broken sources completes within 1.5x the post-#334 baseline.
- No regression in any integration test in
integration/extensions/.
Files an implementing agent should read first
src/domain/extensions/extension_loader.ts:237-330— thebuildIndexlegacy path withresult.failedcapture; primary refactor targetsrc/domain/extensions/extension_loader.ts:300-302— the exact catch site that pushes toresult.failedsrc/libswamp/extensions/reconcile_from_disk_service.ts:483-507— the canonical aggregate-write pattern to follow (post-#334)src/domain/extensions/bundle_freshness.ts:398—markCatalogValidationFailedto remove in W7bsrc/cli/commands/doctor_extensions.ts— where the JSON shape is assembled;registries.failures[]field needs removal/deprecationswamp-uat/src/cli/helpers/schemas.ts— Zod schema updatesBLOG_BRIEFING.mdandswamp-uat/ROWSTATE_INVESTIGATION.mdfor 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.
Related issues
- #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
markCatalogValidationFailedremoval. - The Tombstoned-surfacing issue (filed alongside this one) — independent of W7; can ship before, during, or after.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.