Relationships
#2992 Datastore rework Phase 1 move C1: the workflow run repository stages typed changes (no behaviour change)
Opened by stack72 · 10/2/2026· Shipped 10/2/2026
Background (read this first)
swamp is reworking its datastore layer (tracking: swamp-club#2865; design: design/enablers/datastore-commit-log.md on the datastore-rework branch). The rework replaces "write a file, mark it dirty, push later" with "stage typed changes into a unit of work and commit it". Phase 1 must change no behaviour.
Already on main:
- swamp-club#2970, the
UnitOfWorkport and legacy adapter.src/domain/datastore/unit_of_work.tsdefinesStagedChangeas one of:{ kind: "write"; path }: a file or directory that exists after the operation;{ kind: "remove"; path }: one that is gone after it;{ kind: "bulk"; reason }.
createLegacyUnitOfWork(src/infrastructure/persistence/legacy_unit_of_work.ts) forwards every staged change straight to the repository'sMarkDirtyHook, in order. Awriteand aremoveproduce the samemarkDirty(path)call: the sync service decides "delete" at push time when the path is absent.
- swamp-club#2971, the ambient unit of work (
src/infrastructure/persistence/unit_of_work_scope.ts):runInUnitOfWorkandsignalChange(markDirty, change). Repositories stage into the ambient unit when it wraps their own hook, and otherwise call the hook as before. No production code opens a scope; that list is pinned at zero. - swamp-club#2979 (move A, data and output) and swamp-club#2980 (move B, definition, workflow and evaluated). Six repositories now call
signalChange(this.<hook>, { kind, path })directly at each call site, and have no privatenotifyDirty.- A
kindchosen at run time is written as a ternary, as in the definition and workflowdelete. - These lists track the moved repositories:
MOVED_REPOSinintegration/datastore_sync_rules_test.ts: bulk changes need a reason, and a moved file must not mentionnotifyDirty;MOVED_REPOSITORIESandUNSTAGED_ROWSinintegration/repository_dirty_coverage_test.ts: a disk-effect check that awritepath exists after the act and aremovepath is gone, and that each non-dry-run row stages at least one change;PINNED_STAGED_CHANGESinintegration/datastore_write_seams_rules_test.ts.
- A
This issue is move C1. The workflow run repository is the last one still on the old pattern. The vault config and lockfile repositories (move C2, filed separately later) have no hook at all, and their callers mark by hand.
Tests that prove nothing changed (all on main):
integration/repository_dirty_coverage_test.ts:- every file changed must be marked;
- gaps are pinned in
KNOWN_UNMARKED; - the #2971 equivalence test runs every row with and without a scope and needs identical marks.
integration/datastore_write_seams_rules_test.ts.integration/datastore_sync_rules_test.ts.integration/datastore_peer_propagation_test.ts.integration/datastore_remote_failure_test.ts.integration/usecase_sync_characterization_*_test.ts.src/infrastructure/persistence/yaml_workflow_run_repository_test.ts.- The swamp-uat datastore suite, which runs against the PR binary.
Rules:
- Every test passes with no change to its expectations. Pinned lists change only as described below.
- Don't fix any
KNOWN_UNMARKEDgap. Don't add, remove or reorder marks, and don't move a mark relative to its write: it stays before the write. - Follow AGENTS.md: named exports, no
any, license headers, no fixed sleeps or wall-clock assertions, and the verification workflows before the PR.
Goal
Phase 1 repository move C1: YamlWorkflowRunRepository stages a typed change at each call site instead of calling its private notifyDirty(path). The marks the sync service receives stay exactly the same.
Call sites (line numbers from main at 0fc928c8)
src/infrastructure/persistence/yaml_workflow_run_repository.ts has notifyDirty at :94.
| Line | Method | Marked path | Expected kind |
|---|---|---|---|
| :565 | save |
run yaml | write |
| :661 | deleteAllByWorkflowId |
the workflow's runs dir, removed recursively | remove |
| :746 | deleteOlderThan (one branch) |
run yaml | remove |
| :794 | deleteOlderThan (other branch) |
run yaml | remove |
The "expected kind" column is a starting point. The disk-effect check decides. A path that still exists after the act is a write, and one that is gone is a remove. If a branch can go either way, choose the kind at the call site, as delete does in the definition and workflow repositories.
Known gaps to leave alone. KNOWN_UNMARKED pins deleteOlderThan removing the run's .log (both the terminal and the unparseable branch) and the legacy .runs-index.json without marking them. The workflow run index (workflow_run_index.ts) is repo-local on purpose. Don't add marks for any of these.
Work
- Replace each
this.notifyDirty(path)withsignalChange(this.markDirty, { kind, path })(the hook field ismarkDirtyin this class). Keep every path, call, order and pre-write timing exactly as it is. There is no bulk call today, so don't add one. - Delete the private
notifyDirtyand stop importingchangeFor.- After this, no repository calls
changeFor. - Leave
changeFor, and the hook fallback insignalChange, in place. Removing them is the Phase 1 step "repository marks ratcheted to zero". - Update the
changeFordocstring to say no repository calls it now.
- After this, no repository calls
- Keep the constructor hook parameter and everything in
repository_factory.tsunchanged.
Tests and fitness
integration/repository_dirty_coverage_test.ts:- Add
"WorkflowRun"toMOVED_REPOSITORIESso the disk-effect check and the at-least-one-staged-change check cover its rows. - If a
WorkflowRunrow changes the cache but marks nothing (aKNOWN_UNMARKEDrow), add it toUNSTAGED_ROWSwith a comment, as move A did. - The equivalence test and
KNOWN_UNMARKEDstay unchanged.
- Add
integration/datastore_sync_rules_test.ts:- Move
yaml_workflow_run_repository.tsfromPER_PATH_WIRED_REPOStoMOVED_REPOS. PER_PATH_WIRED_REPOSis then empty, and a test over an empty list passes vacuously. Delete that list, the "bare notifyDirty()" test that loops over it, and its comment. TheMOVED_REPOScheck that no moved repository mentionsnotifyDirtynow covers every hooked repository.- Also add a guard that no file under
src/infrastructure/persistence/definesnotifyDirty, so the method can't come back in a repository that isn't on either list. Pin it as an empty list.
- Move
integration/datastore_write_seams_rules_test.ts:- Remove the
YamlWorkflowRunRepositoryentry fromPINNED_MARK_CALL_SITES. - Add its kinds to
PINNED_STAGED_CHANGES, under a// swamp-club#2992, move C1.comment.
- Remove the
src/infrastructure/persistence/yaml_workflow_run_repository_test.ts: add unit tests that runsave,deleteAllByWorkflowIdand onedeleteOlderThanbranch inside a scope. UserecordingUnitOfWorkfromsrc/infrastructure/persistence/test_helpers/staged_change_helpers.ts, and assert the staged kinds, the marks, and the disk state. Follow the tests move B added toyaml_definition_repository_test.ts.- Everything else listed in Background passes unchanged.
Dependencies
Blocked by: nothing. Moves A (#2979) and B (#2980) are merged. Blocks: the Phase 1 step "repository marks ratcheted to zero", together with move C2 (vault config and lockfile, not filed yet). C2 should start after this lands, since both edit the same shared lists.
Done when
YamlWorkflowRunRepositoryhas nonotifyDirty, and every call site stages a typed change.- The disk-effect and at-least-one-change checks pass for every
WorkflowRunrow. - The marks sent are identical: the equivalence test and every suite in Background pass with no expectation changes, and
KNOWN_UNMARKEDis unchanged. PER_PATH_WIRED_REPOSand its test are gone, and the no-notifyDirtyguard pins an empty list.PINNED_STAGED_CHANGESlists the run repository's kinds, andPINNED_MARK_CALL_SITESno longer lists it.- Verification workflows pass on the final commit.
Out of scope
- Vault config and lockfile repositories (move C2).
- Removing
changeFor, the hook fallback insignalChange, or the hook constructor parameters (the ratchet step). - Opening scopes from use cases (Phase 2).
- Fixing any
KNOWN_UNMARKEDgap.
Related
- Tracking: swamp-club#2865 (Phase 1).
- Built on: swamp-club#2970, #2971, #2979, #2980.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.