Stage a change in a synced folder and publish it on purpose - #189
Merged
Conversation
In a synced folder the write is the publish. There is no state in which a change exists, is complete, and has not yet been distributed, so nothing can be reviewed before it crosses. This adds the tests for that state and the declarations they need. A staged file lives under the fabric home, outside every synced folder, and publishes only on request. The tests prove a staged file never enters a manifest or reaches a peer, survives a restart unpublished, publishes as one scan and one reconcile, refuses to publish over a file that moved since it was staged, and is acknowledged by the watcher without a second scan. A leak control places the same file inside the folder and must fail, so the absence assertion is known to see a leak. The daemon-level test proves the same over real connections. Every staging function and the engine's publish entry point still return "not implemented", so all of these fail. The leak control passes, as it must.
A staged file lives under the fabric home, outside every synced folder. A daemon publishes only from the folder it walks and the include globs in its own config, so no build that has shipped can publish a staged file, including an older one after a rollback. fabric sync stage <target> resolves the entry by folder and include, seeds the staged copy from the published file or from --from, prints the path to edit, and records the published file's hash as the base. fabric sync staged lists staged files as new, edit, or stale. fabric sync publish hands the reviewed bytes to the daemon, or writes them atomically into the folder itself when no daemon can take them, and refuses a stale file unless --force. fabric sync discard removes staged copies. fabric sync ls now counts staged files per entry. The daemon side of publish is the next commit; until then the engine still answers "not implemented" and the publish tests stay red.
The daemon now takes a publish request and performs it under the entry's operation guard: it checks every base against the live manifest before writing anything, writes each file atomically through the engine write path with a receipt in the write journal, scans once, persists once, and wakes the entry loop. The set is therefore one manifest change and one reconcile on each peer, the watcher acknowledges the daemon's own writes without a second scan, and a refused set changes nothing. Atomic per file, not across a crash: a crash after some writes leaves those files to publish on the next start and the rest still staged. All seven staging tests pass, including the leak control that must fail. The daemon-level test passes over real connections.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A change to a synced folder can now be staged, reviewed, and published on purpose.
The gap
In a synced folder the write is the publish. The moment bytes land on disk, every peer receives them. An edit made over several saves publishes each save, a half-written file is a published file, and nothing can be reviewed before it crosses. Nothing in the config or the CLI expresses "prepared, not yet published".
The mechanism
A staged file lives at
<fabric home>/staging/<entry>/<rel>, never inside a synced folder. Four verbs:fabric sync stage <target> [--from <file>] [--entry <name>]resolves the entry from the target path and the include globs, seeds the staged copy from the published file or from--from, prints the path to edit, and records the published file's hash as the base. It refuses a target no include glob matches and refuses when the staging tree would lie inside a synced folder.fabric sync staged [--entry <name>] [--json]lists staged files asnew,edit, orstale, with both paths for a reviewer to diff.fabric sync publish <target>... | --all --entry <name> [--force]hands the reviewed bytes to the daemon. The daemon publishes under the entry's operation guard: it checks every base against the live manifest before writing anything, writes each file atomically through the engine write path with a receipt in the write journal, scans once, persists once, and wakes the entry loop. A set is one manifest change and one reconcile on each peer, the watcher does not rescan the daemon's own writes, and a refused set changes nothing. Without a daemon, or with one older than this request, the CLI writes each file atomically into the folder itself and says so.fabric sync discard <target>...removes staged copies and touches nothing in the folder.fabric sync lsnow showsstaged=Nper entry, so a staged change is not forgotten.Why a tree outside the folder, and not a prefix, an exclude, or a hold
A daemon decides what to publish from exactly two things: the folder it walks and the include globs in its own
syncs.toml. The entry config ignores unknown fields, so a new config key is dropped silently by an older build, and no engine state or control request reaches an older build's scan. A prefix inside the folder, an exclude a tool flips, or a per-path hold in engine state would all publish the staged bytes the moment a machine rolled back to an older binary. A tree outside every folder is never walked by any build that has shipped. That is the only guarantee that comes from code already on every machine, and it is why the fleet can keep rolling one machine at a time.The cost is that a staged file is edited at the staged path, which
stageprints and seeds.What a peer sees during staging: nothing
<home>/staging, andstagerefuses when that tree lies inside any configured folder.scan_disk_snapshot_with_limit), and the watcher watches only that folder (spawn_watcher).Proof
Seven library tests over two loopback engines, all written before the implementation and failing on it, all passing now:
should_paniccontrol that must fail forever, so the absence assertion is known to see a leak;--forcepublishes the next version;stagerefuses a target outside the include, a target outside every folder, and a staging tree inside a folder.One real-daemon test over iroh: a staged file does not reach the peer while two control files written into the folder cross in the same windows, it stays unpublished across a restart of the staging daemon, and it reaches the peer with the reviewed bytes after
SyncPublish.Full library suite on the final head: 545 passed, 5 ignored, 09:13:08Z to 09:13:45Z.
Old binary, measured: the installed
0.2.5+4dc0cacdaemon ran in a temporary home with a bus entry on a temporary folder. A file under that home's staging tree stayed atpresent=0and appeared in no state file after 4 passes and 8 scans. The same bytes written inside the folder reachedpresent=1and the state log at once. Window 09:10:08Z to 09:10:18Z. Both temporary directories were removed.CLI smoke on a temporary daemon built from this branch: stage, staged,
staged=1insync ls, the include refusal, publish at version 1, a stale refusal printing both hashes,--forceto version 3, the local fallback with the daemon down, and discard. Window 09:14:47Z to 09:14:51Z.Limits, stated
A multi-file publish is atomic per file, not across a crash. A crash after some writes and before the scan leaves those files to publish on the next start and the rest still staged; publish again to finish. Publishing over a path a peer edited concurrently follows newer-wins, as any two writes do; the base check refuses the accidental case and
--forcechooses the deliberate one. A reviewer on another machine cannot see a staged file, by design.