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

#341 Surface Tombstoned transitions in doctor extensions output

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

Summary

Tombstoned is a documented RowState representing "source file no longer present and bundle was either evicted or never existed." In practice, the state is invisible to any observer of swamp doctor extensions --json: applyDiffForExtension in extension_repository.ts (around line 523) DELETEs Tombstoned rows in the same SQLite transaction that records the transition. By the time doctor extensions queries the catalog, the rows are gone.

The transition itself is captured in ReconcileResult.transitions[], but that result isn't exposed through any CLI surface today. So the documented state is genuinely unobservable.

Why this matters

  • Documented contract not honored. The 7-RowStates surface promises Tombstoned as a queryable signal. Users investigating "where did my extension go" can't see "it was tombstoned three commands ago" — they see absence and have to infer.
  • UAT tests test absence, not presence. The swamp-uat tombstoned_test.ts works around this by asserting "row no longer appears in sourceDetails[]" rather than "row has stateTag === 'Tombstoned'." That's a weaker contract — passing tests don't distinguish "tombstoned" from "never existed" from "catalog corrupted."
  • Diagnostic value lost. A user who deletes a local extension source file should be able to run doctor and see exactly when/why each source transitioned to Tombstoned, including the last-known fingerprint and bundle path before eviction.

Two implementation options

Option A — Expose ReconcileResult.transitions[] in doctor JSON

Add a new top-level field to the doctor extensions --json --verbose output, populated from the reconcile call that #334 introduced:

{
  "overallStatus": "...",
  "registries": { ... },
  "aggregateState": { ... },
  "recentTransitions": [
    {
      "sourcePath": ".../entry.ts",
      "fromState": "Indexed",
      "toState": "Tombstoned",
      "reason": "source file deleted from disk",
      "timestamp": "2026-05-13T..."
    }
  ]
}

Pros: rich diagnostic surface. Captures every transition, not just Tombstoned. Aligns with ReconcileResult.transitions[]'s existing shape.

Cons:

  • Transitions are scoped to a single reconcile call. Persisting them across CLI invocations requires storage (a new catalog table, or appending to bundle_meta). Not necessarily blocking — the doctor's reconcile-on-every-invocation behavior (post-#334) means each doctor run captures the latest set of transitions.
  • Adds a new field to a public CLI contract.

Option B — Persist Tombstoned rows for one reconcile cycle

Change applyDiffForExtension to NOT delete Tombstoned rows in the same transaction. Keep them with stateTag === "Tombstoned" for at least one reconcile cycle. Subsequent reconcile prunes them if still absent from disk.

Pros: Tombstoned becomes a normal observable state in sourceDetails[], no new top-level fields, simpler mental model.

Cons:

  • Slight catalog growth (rows survive one extra cycle). Bounded but real.
  • Requires pruning logic (when does a Tombstoned row get DELETEd? — needs a TTL or "two reconcile passes without source" rule).
  • Behavior change: existing consumers of sourceDetails[] start seeing Tombstoned entries; need to verify downstream presentation doesn't break (e.g., doctor extensions summary counts).

Option C — Document Tombstoned as transient-at-persistence, accept the gap

Update row_state.ts:30 documentation to clarify that Tombstoned exists only in the in-memory Extension aggregate during a reconcile pass — it is never observable in the persisted catalog. Update the swamp-uat tests to formally assert absence (not state). No code change.

Pros: zero risk. Aligns docs with reality.

Cons: punts on the diagnostic value. Users still can't see "where did my extension go."

Recommendation: Option A

Most flexible, most informative, doesn't require behavior changes to the persistence layer. The "transitions across invocations" concern is mitigated by the fact that post-#334, every doctor invocation triggers reconcile, so each call captures the latest meaningful transitions for the current process. Persistence beyond that is a follow-up if anyone needs it.

Worst case Option A turns out too noisy / too expensive — fall back to B or C.

Acceptance criteria

  1. Running swamp doctor extensions --json --verbose after deleting a local extension source file produces a recentTransitions[] entry with toState === "Tombstoned" and the source path of the deleted file.
  2. Running it after deleting a pulled extension source produces a transition with toState === "Tombstoned" and reason "orphan: no lockfile entry" (matching the existing reconcile transition reason).
  3. Running doctor without any source-deletion produces an empty recentTransitions[] (or omits the field; design choice — empty array is more uniform for consumers).
  4. The Zod schema in swamp-uat (src/cli/helpers/schemas.ts:DoctorExtensionsSchema) is updated to include the new field, with .passthrough() semantics preserved.
  5. No regression in doctor_extensions_failure_states_test.ts (from #334).

Files an implementing agent should read first

  • src/infrastructure/persistence/extension_repository.ts around applyDiffForExtension (line ~523) — current Tombstoned DELETE
  • src/libswamp/extensions/reconcile_from_disk_service.ts — the ReconcileResult.transitions[] shape and where it's populated (around line 510-540)
  • src/cli/commands/doctor_extensions.ts — where the reconcile call was added in #334; the transitions return value needs threading into the JSON output
  • src/cli/commands/doctor_extensions.ts:doctorExtensions (or the underlying libswamp helper) — where the JSON shape is assembled
  • src/domain/extensions/row_state.ts:30 — Tombstoned contract doc to update if Option C is chosen as fallback

UAT coupling

When this lands, swamp-uat tombstoned_test.ts can flip from absence-tests to presence-tests on recentTransitions[].toState. Coordinate the assertion update as a follow-up PR per #334's pattern.

This issue does NOT:

  • Persist transitions across CLI invocations beyond what reconcile naturally surfaces. If long-term history is wanted, that's a separate feature.
  • Change Tombstoned's semantics in the domain layer (the DDD model is fine; this is purely a persistence/surface concern).
  • Touch the dual-path failure-recording (registries.failures[] vs. sourceDetails[]) — that's W7.
  • #334 — sequencing fix that introduced reconcile-on-doctor; this issue threads its ReconcileResult.transitions[] into the output
  • Original investigation: swamp-uat/ROWSTATE_INVESTIGATION.md (the Tombstoned-DELETE finding lives there with code citations)
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 3 MOREPR_MERGEDSHIPPED

Shipped

5/13/2026, 1:12:34 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/13/2026, 12:42:49 AM

Sign in to post a ripple.