Relationships
#2975 findBySpec/findByTag lose a workflow step's output after a version is deleted, pruned or rolled back
Opened by hammz · 10/2/2026· Shipped 10/6/2026
Follow-up to swamp-club#2520 (PR swamp-club/swamp#2800).
Background
The data catalog keeps two latest flags per row (CatalogStore in src/infrastructure/persistence/catalog_store.ts):
- is_latest: one row per data name, the highest promoted version. Used by data query, data get, data.latest() and readModelData.
- is_step_latest: each workflow step's latest version of the name. Used only by the CEL helpers data.findBySpec and data.findByTag (via the latestPerStep query option), so every step's output stays visible (swamp-club#1761).
Problem
Paths that remove a version re-promote only the surviving highest version through catalogUpsert / upsertNewVersion. They never restore is_step_latest for another step whose latest was the removed row.
Example: one data name written by two steps.
v1 step s1 is_latest 0 is_step_latest 0 (superseded by s1 v3)
v2 step s2 is_latest 0 is_step_latest 1
v3 step s1 is_latest 1 is_step_latest 1findBySpec returns v2 and v3. After v3 is removed, v2 is re-promoted, but v1 stays at is_step_latest 0, so findBySpec returns only v2 and step s1's output silently disappears. A full catalog rebuild (computeLatestFlags) would correctly mark v1 as s1's step latest.
Affected removal paths in src/infrastructure/persistence/unified_data_repository.ts:
- swamp data delete with a version (delete-specific-version, re-promotes newLatest)
- GC pruning (collectGarbage) and the write-time version cap (pruneExcessVersions)
- rollbackVersions for deferred writes. Here a lower version of the same step promoted while a higher deferred write of that step was in flight stays at is_step_latest 0, because upsertNewVersion counts the in-flight row as a higher row in scope. If the deferred write is then rolled back, the step has no step latest.
Only the per-step view is affected. is_latest (data query, data get, data.latest, readModelData) is restored correctly by these paths.
The delete case predates #2520: the old per-step is_latest had the same hole. It is documented as a known gap in design/enablers/data-query.md (Step-aware versioning).
Suggested fix
After removing a version, recompute both flags for that one (namespace, type, model, name) group inside the same transaction: load the group's remaining rows, run computeLatestFlags (src/domain/data/data_query_service.ts), and write back changed rows. This is what enforceUniqueLatest already does catalog-wide. Then remove the known-gap note from the design doc.
Tests
- catalog_store / unified_data_repository tests for delete-version, GC prune, version-cap prune and rollback, each leaving every step with a step latest that matches computeLatestFlags.
- Extend the fast-check property in src/domain/data/data_query_service_property_test.ts to interleave removals with promotions.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.