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

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 UnitOfWork port and legacy adapter:
    • src/domain/datastore/unit_of_work.ts: StagedChange is write, remove or bulk.
    • src/infrastructure/persistence/legacy_unit_of_work.ts: the adapter forwards each staged change to the repository's MarkDirtyHook straight away.
  • swamp-club#2971, the ambient unit of work (src/infrastructure/persistence/unit_of_work_scope.ts):
    • runInUnitOfWork and currentUnitOfWork.
    • 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 old notifyDirty(path?) form to a StagedChange.
    • No production code opens a scope (PINNED_UNIT_OF_WORK_SCOPES is 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 defines notifyDirty, 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_SITES in integration/datastore_write_seams_rules_test.ts has no repository entry left. The only persistence entries are legacy_unit_of_work.ts: createLegacyUnitOfWork and unit_of_work_scope.ts: signalChange.
  • changeFor has 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 in PINNED_UNHOOKED_WRITERS as "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

  1. A rule that keeps repository marks at zero. In integration/datastore_write_seams_rules_test.ts, add a test that fails when any file under src/infrastructure/persistence/ other than legacy_unit_of_work.ts and unit_of_work_scope.ts appears 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.

  2. 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 a signalChange(…) call. The field is this.markDirty or this.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.
  3. Delete changeFor. Remove it from unit_of_work_scope.ts, together with its test in unit_of_work_scope_test.ts and its mention in the stagedChanges comment in datastore_write_seams_rules_test.ts. Search the whole repository, docs included, for remaining references.

  4. Delete createVaultConfigRepository from repository_factory.ts, and its entry in PINNED_UNHOOKED_WRITERS. Confirm nothing imports it, including tests and packages/.

  5. Document why the fallback stays.

    • signalChange JSDoc: 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, and PINNED_UNIT_OF_WORK_SCOPES shows how far that has got. Change no code in signalChange.
    • 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.
  6. 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;
      • pushManagedConfigChanges and pushManagedConfigPaths;
      • 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.publish marks as a publish signal, with mustUpload and a pending retry; swamp-club#2865 has the reasoning.

    Keep it to facts a Phase 2 author needs, with file references.

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 createVaultConfigRepository entry and the new empty pins.
  • Deleting changeFor must not change any mark. The equivalence test in repository_dirty_coverage_test.ts proves 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.
  • changeFor and createVaultConfigRepository are 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.md describes 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_UNMARKED gap.
  • Tracking: swamp-club#2865 (Phase 1 close-out).
  • Built on: swamp-club#2970, #2971, #2979, #2980, #2992, #2995.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 14 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/3/2026, 1:02:07 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/3/2026, 12:07:07 AM

Sign in to post a ripple.