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

Relationships

#2995 Datastore rework Phase 1 move C2: the vault config repository stages typed changes, serve stops marking vault files by hand (no behaviour change)

Opened by stack72 · 10/2/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). Repositories stage typed changes into a unit of work instead of marking files dirty for a later push. Phase 1 must change no behaviour.

Already on main:

  • swamp-club#2970: the UnitOfWork port and legacy adapter.

    • StagedChange is one of:
      • write: a path that exists after the operation;
      • remove: a path that is gone after it;
      • bulk.
    • The legacy adapter forwards each change straight to the repository's MarkDirtyHook as markDirty(path).
  • swamp-club#2971: the ambient unit of work (src/infrastructure/persistence/unit_of_work_scope.ts) and signalChange(markDirty, change).

  • The repository moves stage typed changes at every call site with signalChange(this.<hook>, { kind, path }):

    • swamp-club#2979: data and output;
    • swamp-club#2980: definition, workflow and evaluated;
    • swamp-club#2992: workflow run.

    No repository defines notifyDirty any more, which is pinned in integration/datastore_sync_rules_test.ts.

  • The tests that pin all this:

    • integration/repository_dirty_coverage_test.ts: every changed file must be marked, gaps are pinned in KNOWN_UNMARKED, and an equivalence test compares runs with and without a scope. MOVED_REPOSITORIES and UNSTAGED_ROWS add a disk-effect check that a write path exists after the act and a remove path is gone, and that each non-dry-run row stages at least one change.
    • integration/datastore_sync_rules_test.ts: MOVED_REPOS (bulk changes need a reason), plus the no-notifyDirty guard.
    • integration/datastore_write_seams_rules_test.ts: PINNED_MARK_CALL_SITES, PINNED_REPO_CONSTRUCTIONS, PINNED_UNHOOKED_WRITERS, PINNED_STAGED_CHANGES, PINNED_UNIT_OF_WORK_SCOPES, and a self-check that hook argument positions match the constructors.
    • integration/usecase_sync_characterization_vault_test.ts: what the vault use cases mark and push, through the CLI and through serve.

This issue is move C2, the last Phase 1 repository move: the vault config repository.

The lockfile is deliberately not part of it. It was originally grouped with vault config. But its only mark, in ManagedLockfileTransaction.publish (src/libswamp/extensions/managed_lockfile_transaction.ts), is a publish signal rather than a write signal:

  • the transaction marks the lockfile and then requires the push to upload something (mustUpload);
  • a failed publish stays pending and is retried by a later extension change, sometimes in a process that wrote nothing.

That mark has to stay. Having LockfileRepository also stage its writes would add marks rather than move them, which breaks Phase 1's identical-marks rule. LockfileRepository never calls markDirty itself, so it doesn't block the Phase 1 ratchet. The lockfile moves to Phase 2, when the managed lockfile transaction becomes a unit-of-work commit (noted on swamp-club#2865).

How vault config marks today (main at 285b0a5a)

YamlVaultConfigRepository (src/infrastructure/persistence/yaml_vault_config_repository.ts) has no hook:

  • the constructor is (repoDir, eventBus?, baseDir?);
  • save writes {baseDir}/{type}/{id}.yaml with atomicWriteTextFile;
  • delete removes it.

Serve marks the written files by hand, after the write, just before it pushes:

Handler Site Marks Repository the use case writes through
vault create src/serve/handlers/vault_handlers.ts (around line 890) the created config path its own unhooked one, from createVaultCreateDeps(ctx.repoDir)
vault edit same file (around line 1090) the edited config path the factory-built ctx.repoContext.vaultConfigRepo, injected through createVaultEditDeps
vault migrate src/serve/handlers/admin_handlers.ts (around lines 1333-1338) the target-type config path, then the source-type config path its own unhooked one, from createVaultMigrateDeps(repoDir)

Migrate's use case calls saveConfig and then deleteConfig (src/libswamp/vaults/migrate.ts).

The factory builds ctx.repoContext.vaultConfigRepo in createRepositoryContext (src/infrastructure/persistence/repository_factory.ts) without a hook. Elsewhere in serve it's only used for reads: findAll, findByName, getPath, and the doctor's vault list.

The CLI vault commands stay as they are. vault create, vault edit and vault migrate build their own unhooked repositories and publish with pushManagedConfigChanges (src/cli/managed_config_sync.ts). That function sends a bare mark, which also covers model and workflow writes, and removing it is Phase 2 work.

Other vault use cases build unhooked repositories and only read vault configs: delete (of a secret), annotate, put, inspect, get, read-secret and list-keys. Don't hook them.

Work

  1. Give YamlVaultConfigRepository an optional hook, as a new final constructor parameter markDirty?: MarkDirtyHook.
    • save stages { kind: "write", path } with signalChange(this.markDirty, …) before atomicWriteTextFile.
    • delete stages { kind: "remove", path } before Deno.remove.
    • With no hook, nothing is sent, exactly as today.
  2. Wire the hook in createRepositoryContext only: pass markDirty to the factory's YamlVaultConfigRepository. Leave createVaultConfigRepository and every other construction unhooked.
  3. Route serve create and migrate through the hooked repository.
    • Add an optional injected repository to createVaultCreateDeps and createVaultMigrateDeps, as createVaultEditDeps(repoDir, injectedRepo?) already does. Keep the existing options parameter on migrate.
    • In serve, pass ctx.repoContext.vaultConfigRepo to all three. Edit already does this.
    • The CLI callers keep their current calls with no injected repository.
    • Check the injected repository resolves the same vaults dir as the one each use case built itself. All of them use the effective vaults dir, which is the config tier under managedConfig, but confirm it.
  4. Delete the three hand marks in serve (create, edit, migrate). The repository now sends the same marks, for the same paths in the same order (migrate: target then source), and before the push as before. They move from after the write to before it, which is what the markDirty contract requires (rule 1) and doesn't change what is pushed. Keep each handler's pushChanged call and its error handling exactly as they are.
  5. Change nothing else. Leave pushManagedConfigChanges, the CLI vault commands, LockfileRepository, ManagedLockfileTransaction, and every other hand mark (access, device auth, grant tracking, the serve definition migration) untouched.

Tests and fitness

  1. integration/usecase_sync_characterization_vault_test.ts passes with no expectation changes. The serve rows for create, edit and migrate must record the same marks, with the same paths and order, and the same pushes and remote state. If the recorded marks differ in any way, stop: the move isn't behaviour-preserving.
  2. integration/repository_dirty_coverage_test.ts:
    • Add VaultConfig rows for save (new), save (update) and delete, built with the hook, and VaultConfig to MOVED_REPOSITORIES.
    • Each row must mark every changed file, pass the disk-effect check, and stage at least one change.
    • The equivalence test (with and without a scope) must cover the new rows. KNOWN_UNMARKED is unchanged.
  3. integration/datastore_sync_rules_test.ts: add yaml_vault_config_repository.ts to MOVED_REPOS. The bulk-reason rule and the no-notifyDirty guard then cover it.
  4. integration/datastore_write_seams_rules_test.ts:
    • remove the three serve hand-mark entries from PINNED_MARK_CALL_SITES;
    • add the vault repository's write and remove to PINNED_STAGED_CHANGES under // swamp-club#2995, move C2.;
    • remove the factory's vault construction from PINNED_UNHOOKED_WRITERS (it now has a hook), keeping every other vault construction pinned;
    • update the hook-argument-position self-check for the new constructor parameter.
  5. src/infrastructure/persistence/yaml_vault_config_repository_test.ts: add scoped unit tests using recordingUnitOfWork from src/infrastructure/persistence/test_helpers/staged_change_helpers.ts:
    • save stages a write before the file exists on disk (assert the mark arrives before the write, for example from inside the hook);
    • delete stages a remove;
    • with no hook, nothing is sent.
  6. src/libswamp/vaults/create_test.ts and migrate_test.ts (or the existing test files for these deps): an injected repository is used for saves and deletes, and the default path still builds its own.
  7. These pass unchanged:
    • every other use-case characterisation file;
    • integration/datastore_peer_propagation_test.ts and integration/datastore_remote_failure_test.ts;
    • src/cli/repo_context_test.ts;
    • the serve vault handler tests;
    • the swamp-uat suite.

Dependencies

Blocked by: swamp-club#2992 (move C1). It edits the same shared lists, so start once it has merged. Blocks: the Phase 1 step "repository marks ratcheted to zero", which removes changeFor and the hook fallback in signalChange and closes Phase 1.

Done when

  • YamlVaultConfigRepository stages write in save and remove in delete, before the disk change. Only the factory-built instance has a hook.
  • Serve create, edit and migrate write through the factory repository, and their hand marks are gone.
  • usecase_sync_characterization_vault_test.ts and every other suite in Background pass with no expectation changes.
  • The new dirty-coverage rows and the unit tests pass, and the pinned lists are updated as above.
  • Verification workflows pass on the final commit.

Out of scope

  • LockfileRepository and ManagedLockfileTransaction (Phase 2; see Background).
  • pushManagedConfigChanges and the CLI vault commands' bare mark (Phase 2).
  • Hooking any other vault use case.
  • Opening scopes (Phase 2), and fixing any KNOWN_UNMARKED gap.
  • Tracking: swamp-club#2865 (Phase 1).
  • Built on: swamp-club#2970, #2971, #2979, #2980, #2992.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 8 MOREFINDINGS+ 10 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/3/2026, 12:02:19 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/2/2026, 10:11:46 PM

Sign in to post a ripple.