Relationships
#2996 Datastore rework Phase 1 close-out: lock repository marks at zero, remove dead helpers, document the end state (no behaviour change)
Opened by stack72 · 10/3/2026· Shipped 10/3/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). Phase 1 introduces a unit of work without changing behaviour. This issue is the last Phase 1 step: it locks in the end state, deletes what the moves left unused, and records where Phase 1 stops.
What Phase 1 delivered (all merged, main at dff522a4):
- swamp-club#2970, the
UnitOfWorkport and legacy adapter:src/domain/datastore/unit_of_work.ts:StagedChangeiswrite,removeorbulk.src/infrastructure/persistence/legacy_unit_of_work.ts: the adapter forwards each staged change to the repository'sMarkDirtyHookstraight away.
- swamp-club#2971, the ambient unit of work (
src/infrastructure/persistence/unit_of_work_scope.ts):runInUnitOfWorkandcurrentUnitOfWork.signalChange(markDirty, change): it stages into the ambient unit when that unit wraps this hook; otherwise it calls the hook directly; with no hook it does nothing.changeFor(relPath, reason)maps the oldnotifyDirty(path?)form to aStagedChange.- No production code opens a scope (
PINNED_UNIT_OF_WORK_SCOPESis empty).
- The repository moves: every hooked datastore-tier repository now stages a typed change at each call site with
signalChange(this.<hook>, { kind, path }). No repository definesnotifyDirty, and that is pinned.- swamp-club#2979: data and output;
- swamp-club#2980: definition, workflow and evaluated;
- swamp-club#2992: workflow run;
- swamp-club#2995: vault config, where serve's create, edit and migrate now write through the hooked factory repository and their hand marks are gone.
Where things stand:
- Repository marks are already at zero.
PINNED_MARK_CALL_SITESinintegration/datastore_write_seams_rules_test.tshas no repository entry left. The only persistence entries arelegacy_unit_of_work.ts: createLegacyUnitOfWorkandunit_of_work_scope.ts: signalChange. changeForhas no caller outside its own test (unit_of_work_scope_test.ts).createVaultConfigRepository(src/infrastructure/persistence/repository_factory.ts) has no caller. It is pinned inPINNED_UNHOOKED_WRITERSas "Factory helper with no callers".
The hook fallback in signalChange stays. Some earlier issues (#2992, #2995) said this step would remove "the hook fallback in signalChange". That was wrong. In Phase 1 no production code opens a scope, so the fallback, where signalChange calls the hook directly, is the only path every production write takes. Removing it would stop all marks reaching the sync service. It is removed in Phase 2, once every write path runs inside a unit-of-work scope.
Rules: no behaviour change. Every datastore test passes with no change to its expectations:
integration/repository_dirty_coverage_test.ts;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;- all
integration/usecase_sync_characterization_*_test.ts; - the unit-of-work and scope tests;
src/cli/repo_context_test.ts;- the swamp-uat datastore suite.
Pinned lists change only as described below. Follow AGENTS.md (named exports, no any, license headers, verification workflows before the PR).
Goal
Lock in "repositories never call a mark hook directly" as a permanent rule, delete what is now dead, and document the end-of-Phase-1 state so Phase 2 starts from an accurate picture.
Work
A rule that keeps repository marks at zero. In
integration/datastore_write_seams_rules_test.ts, add a test that fails when any file undersrc/infrastructure/persistence/other thanlegacy_unit_of_work.tsandunit_of_work_scope.tsappears in the mark-call-site scan. Today this holds only because the list happens to have no such entries. Name the two allowed files in the test with a one-line reason each. Phase 3 can then add a new adapter there on purpose.A rule that repositories use their hook only through
signalChange.- Inside the classes in
DATASTORE_TIER_REPOSITORIES, every reference to the hook field must be the first argument of asignalChange(…)call. The field isthis.markDirtyorthis.markDirtyHook, depending on the class. - Anything else is a violation: calling the hook, passing it elsewhere, or storing it.
- Pin the violations as an empty list. Add a self-test, like the existing scans have, showing it catches a direct call and a pass-through and ignores comments and the constructor parameter.
- Inside the classes in
Delete
changeFor. Remove it fromunit_of_work_scope.ts, together with its test inunit_of_work_scope_test.tsand its mention in thestagedChangescomment indatastore_write_seams_rules_test.ts. Search the whole repository, docs included, for remaining references.Delete
createVaultConfigRepositoryfromrepository_factory.ts, and its entry inPINNED_UNHOOKED_WRITERS. Confirm nothing imports it, including tests andpackages/.Document why the fallback stays.
signalChangeJSDoc: route 2, calling the hook directly, is the only production route in Phase 1 because nothing opens a scope. Phase 2 removes it once every write path runs inside a scope, andPINNED_UNIT_OF_WORK_SCOPESshows how far that has got. Change no code insignalChange.legacyUnitOfWorkTarget: add a short note that the binding only recognises legacy units, so a Phase 3 adapter needs its own way to claim a repository context.
Record the end-of-Phase-1 state in
design/enablers/datastores.md. Update the "Unit of work (datastore rework Phase 1)" paragraph (around line 1066) to describe what is true now:- every hooked datastore-tier repository stages typed changes through
signalChange, and the rule from item 2 holds them to it; - writes still mark through the hook fallback, because no scope is opened;
- what still marks by hand, which Phase 2 owns. Take each item from
PINNED_MARK_CALL_SITES:- the CLI commands with bare marks;
pushManagedConfigChangesandpushManagedConfigPaths;namespace_migrate;- serve's device-auth, grant-tracking, access-reload and lockfile marks;
- the serve start-up definition migration.
- the lockfile is deferred to Phase 2.
ManagedLockfileTransaction.publishmarks as a publish signal, withmustUploadand a pending retry; swamp-club#2865 has the reasoning.
Keep it to facts a Phase 2 author needs, with file references.
- every hooked datastore-tier repository stages typed changes through
Tests
- The two new rules from items 1 and 2, with their self-tests.
- Everything in Background passes unchanged. The only pinned-list changes are the removed
createVaultConfigRepositoryentry and the new empty pins. - Deleting
changeFormust not change any mark. The equivalence test inrepository_dirty_coverage_test.tsproves this.
Dependencies
Blocked by: nothing. Every Phase 1 move (#2979, #2980, #2992, #2995) has merged. Blocks: Phase 2, where use cases own the unit of work. When this merges, every Phase 1 work item on swamp-club#2865 is done.
Done when
- Both rules exist, pin empty lists and pass, and their self-tests pass.
changeForandcreateVaultConfigRepositoryare gone, with no remaining references.signalChange's hook fallback is unchanged and documented as the Phase 1 production route that Phase 2 removes.design/enablers/datastores.mddescribes the end-of-Phase-1 state, including the remaining hand marks and the deferred lockfile.- Every suite in Background passes with no expectation changes, and verification workflows pass on the final commit.
Out of scope
- Removing the hook fallback in
signalChange, removing hook constructor parameters, or opening scopes (all Phase 2). - Any remaining hand mark in CLI, serve or libswamp code (Phase 2).
- The lockfile (Phase 2).
- Fixing any
KNOWN_UNMARKEDgap.
Related
- Tracking: swamp-club#2865 (Phase 1 close-out).
- Built on: swamp-club#2970, #2971, #2979, #2980, #2992, #2995.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.