Skip to main content

flint_lean/
barrier.rs

1//! The publish barrier (plan §2.1, seven steps) — the machine
2//! `lean/formal/LeanSubtree.tla` checks. Order is load-bearing:
3//! consume → scan → intent/window → uploads → manifest CAS (merge) →
4//! GC deletes LAST → baseline rewrite.
5
6use std::collections::{BTreeMap, BTreeSet};
7use std::path::Path;
8
9use bytes::Bytes;
10
11use flint_store::{
12    crc64_nvme, crc64_to_b64, GenerationStamps, ObjectStore, PosixStamps, PutCondition, RequestCounts, StoreError,
13};
14
15use super::inbox;
16use super::docview::{CommitDocs, DocView, Opened};
17use super::manifest::{self, LeanEntry};
18use super::scan;
19use super::state::{BaselineEntry, Carrier, ConflictRecord, IntentJournal};
20use super::{now_unix, LeanError, LeanResult, Syncer};
21
22/// The last gateway request this workspace acted on, per verb. A
23/// watermark rather than a consumed queue — see `note_verb_requests`.
24#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
25pub struct VerbWatermark {
26    #[serde(default)]
27    pub boundary_unix: u64,
28    #[serde(default)]
29    pub sync_unix: u64,
30}
31
32#[derive(Debug, Default)]
33pub struct BarrierReport {
34    pub seq: Option<u64>,
35    pub uploaded: Vec<String>,
36    pub deleted: Vec<String>,
37    /// Paths a DECLARED removal (delete/rename design) cited out this
38    /// barrier — a subset of `deleted` once the GC has run.
39    pub removed: Vec<String>,
40    /// Declared removals this barrier REFUSED (locally dirty, not a
41    /// file, uncontainable); the cell carries the reason.
42    pub removals_refused: usize,
43    pub parked: Vec<String>,
44    pub consumed: usize,
45    pub foreign_queued: usize,
46    /// Paths whose transfer drifted or was swept mid-compose: nothing
47    /// published, nothing advanced; the next scan re-queues them.
48    pub deferred: Vec<String>,
49    /// Paths the syncer may not read (`scan::Scan::held`): a directory
50    /// it may not list (with its trailing `/`), an entry it may not
51    /// lstat, a file it may not open, an absence it may not confirm; and
52    /// a file or directory whose name is not UTF-8, which no key names.
53    /// Nothing at or under one was published or deleted, and the rest of
54    /// the tree was. A boundary that holds anything is not complete: a
55    /// sentinel's ack is `partial` and names them, and the drain refuses
56    /// to attest it.
57    pub held: Vec<String>,
58    /// Paths whose object the collector LEFT BEHIND because this store
59    /// does not enforce `If-Match` on DELETE (`conformance.rs`). The
60    /// delete still reached the boundary; only the object survives.
61    ///
62    /// NOT added to `deleted`, and so NOT dropped from the merge base:
63    /// the rule step 7 follows is "drop the entry once the key no longer
64    /// holds our bytes", which is why the absent and replaced-absent
65    /// arms drop it and the skipped-etag arm does not. A leaked key
66    /// still holds them. The model names that rule
67    /// `BaselineKeepsUncollected` and it is what ships, so a leak
68    /// follows the arm that already exists rather than inventing a
69    /// third; the cost is that the collector re-offers the path (one
70    /// HEAD) at every later barrier, exactly as a skipped one is.
71    pub leaked: Vec<String>,
72    /// Paths this barrier published OVER a version it never integrated;
73    /// each has a conflict record naming the preserved copy (R7).
74    pub surfaced: Vec<String>,
75    /// Retired handles the batch delete took.
76    pub collected: usize,
77    /// Orphan handles the sweep took, when it was due.
78    pub swept: usize,
79    pub no_change: bool,
80    /// Bytes this barrier actually published — the input to the
81    /// sentinel work meter (boundary-verbs plan D3.1). Metering work
82    /// rather than calls is what keeps a sentinel storm from
83    /// un-coalescing a hot large file's republish: a counted budget
84    /// charges a 2 GiB checkpoint the same one unit as a 4 KiB file.
85    pub published_bytes: u64,
86    /// The manifest ETag this barrier installed (the ack's CAS token).
87    pub manifest_etag: Option<String>,
88    /// What the manifest HEAD/install told us the bucket is at, for the
89    /// news ticker (D5) — free, off requests the barrier already made.
90    pub observed_seq: Option<u64>,
91    pub observed_etag: Option<String>,
92    /// Paths this barrier saw absent for the FIRST time: their deletes
93    /// are withheld to the next scan (the rename-vs-walk guard). On a
94    /// declared boundary a non-empty set means the confirmation lstat
95    /// found them back on disk — the race the guard exists for.
96    pub first_absence: Vec<String>,
97    /// First-absence paths a DECLARED boundary confirmed gone by direct
98    /// lstat and published as ordinary deletes (`confirm_absences`).
99    pub absences_confirmed: usize,
100    /// This barrier finished a rescope a crash had left half-applied
101    /// before it did anything else. Reported so a run that looks like
102    /// an ordinary barrier can be told from one that had to converge a
103    /// workspace first.
104    pub rescope_replayed: bool,
105    /// Where this barrier's time went, step by step ([`StepTiming`]).
106    pub steps: Vec<StepTiming>,
107}
108
109/// One step of a barrier and what it cost: wall time, and the requests
110/// the store sent during it (`None` from a store that does not count).
111/// A step the barrier entered more than once — each CAS attempt's load,
112/// fence, merge and CAS — is one entry, summed, in the order it first
113/// ran.
114#[derive(Debug, Clone, Default, serde::Serialize)]
115pub struct StepTiming {
116    pub step: &'static str,
117    pub us: u64,
118    pub requests: Option<RequestCounts>,
119}
120
121/// Times a barrier's steps. Each `lap` charges everything since the
122/// previous one to the step it names, so the steps tile the barrier:
123/// their times sum to its wall time, and their requests to everything
124/// the store sent for it.
125struct StepClock {
126    store: std::sync::Arc<dyn ObjectStore>,
127    at: std::time::Instant,
128    counted: Option<RequestCounts>,
129    steps: Vec<StepTiming>,
130}
131
132impl StepClock {
133    fn start(store: std::sync::Arc<dyn ObjectStore>) -> Self {
134        let counted = store.request_counts();
135        Self { store, at: std::time::Instant::now(), counted, steps: vec![] }
136    }
137
138    fn lap(&mut self, step: &'static str) {
139        let now = std::time::Instant::now();
140        let counted = self.store.request_counts();
141        let us = now.duration_since(self.at).as_micros() as u64;
142        let sent = match (self.counted, counted) {
143            (Some(a), Some(b)) => Some(counts_between(a, b)),
144            _ => None,
145        };
146        match self.steps.iter_mut().find(|s| s.step == step) {
147            Some(s) => {
148                s.us += us;
149                s.requests = match (s.requests, sent) {
150                    (Some(a), Some(b)) => Some(counts_sum(a, b)),
151                    _ => None,
152                };
153            }
154            None => self.steps.push(StepTiming { step, us, requests: sent }),
155        }
156        self.at = now;
157        self.counted = counted;
158    }
159}
160
161fn counts_between(a: RequestCounts, b: RequestCounts) -> RequestCounts {
162    RequestCounts {
163        get: b.get.saturating_sub(a.get),
164        head: b.head.saturating_sub(a.head),
165        put: b.put.saturating_sub(a.put),
166        copy: b.copy.saturating_sub(a.copy),
167        delete: b.delete.saturating_sub(a.delete),
168        list: b.list.saturating_sub(a.list),
169        multipart: b.multipart.saturating_sub(a.multipart),
170    }
171}
172
173fn counts_sum(a: RequestCounts, b: RequestCounts) -> RequestCounts {
174    RequestCounts {
175        get: a.get + b.get,
176        head: a.head + b.head,
177        put: a.put + b.put,
178        copy: a.copy + b.copy,
179        delete: a.delete + b.delete,
180        list: a.list + b.list,
181        multipart: a.multipart + b.multipart,
182    }
183}
184
185/// Upload waves per chunk. The chunk is `fanout * this`, so a wave
186/// still saturates fan-out and the sync point between chunks is rare.
187const UPLOAD_CHUNK_WAVES: usize = 16;
188
189
190
191/// What a consume's read of the document established, for the barrier's
192/// no-change fast path (which must not pay for the same read twice).
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum Seen {
195    /// The pointer at this (seq, etag).
196    Pointer(u64, String),
197    /// Neither the pointer nor the legacy object exists: nothing has ever
198    /// been published here. Both reads happened in this pass.
199    Nothing,
200    /// A legacy single-object document (no pointer); the fast path asks.
201    Legacy,
202}
203
204impl Syncer {
205    /// Verify the cell still names us at OUR epoch; anything else is a
206    /// fence. Read-verify before the manifest CAS (the per-request
207    /// validation the gateway will also enforce).
208    async fn verify_not_deposed(&self) -> LeanResult<()> {
209        let lease = self
210            .lease
211            .as_ref()
212            .ok_or_else(|| LeanError::State("no lease".into()))?;
213        match self.store.epoch_read(&self.cfg.epoch_key()).await? {
214            Some(state) if state.epoch == lease.epoch && state.holder_id == lease.holder_id => {
215                Ok(())
216            }
217            Some(state) => Err(LeanError::Fenced(format!(
218                "cell at epoch {} holder {} (we are epoch {})",
219                state.epoch, state.holder_id, lease.epoch
220            ))),
221            None => Err(LeanError::Fenced("epoch cell vanished".into())),
222        }
223    }
224
225    /// Step 1 (P1-lite, 2026-09-25): what the tree is OWED, derived from
226    /// the document and taken in one pass. A path is owed where the
227    /// document differs from the baseline — which is also the merge base —
228    /// and the tree is clean there; only a path the tree HOLDS or its
229    /// scope covers (what the scope declined stays declined). Nothing is
230    /// stored between deriving it and taking it, so nothing can be
231    /// overtaken: the queue this replaces needed a prune for that (L-123).
232    /// A DIRTY path is the agent's newer work and is not owed: it
233    /// publishes, and the merge records what it publishes over (R7, the
234    /// delete override). Unless its bytes already ARE the document's —
235    /// a restart between this writer's CAS and step 7 leaves exactly
236    /// that — and then the baseline simply follows (content convergence).
237    /// Returns how many paths it took, and what its read of the document
238    /// established ([`Seen`]) — the barrier's fast path needs exactly that,
239    /// and must not pay a second GET for it (an idle tick is the cell and
240    /// the pointer).
241    pub async fn consume_owed(&mut self) -> LeanResult<(usize, Seen)> {
242        // Taken, not copied (lever D): every return below hands it back,
243        // and one that does not costs the next load a parse, nothing more.
244        let mut baseline = self.state.take_baseline()?;
245        // THE CHEAP PATH (scan trigger). Only three things make a path
246        // newly owed: the document moved (the pointer is not the one last
247        // derived against — a peer, a UI save, or this tree's own commit
248        // over a document that had moved), the baseline moved (only this
249        // tree's own commit, which moves the pointer too), or the agent
250        // backed out of a path the last derive skipped as its work. So: one
251        // small GET, a stat per skipped path, never the entries.
252        //
253        // The pointer read here is handed to the load below rather than
254        // read again: a moved pointer needs the document behind THAT read,
255        // and an absent one sends the load straight to the legacy key.
256        let mut read: Option<Option<manifest::LoadedPointer>> = None;
257        if let Some(d) = baseline.derived_etag.clone() {
258            let lp = manifest::load_pointer(self.store.as_ref(), &self.cfg).await?;
259            if let Some(p) = &lp {
260                let still_theirs = baseline.skipped.iter().all(|path| {
261                    check_contained(&self.cfg.root, path).is_ok()
262                        && local_dirty(&self.cfg.root.join(path), baseline.entries.get(path))
263                });
264                if d == p.etag && still_theirs {
265                    // The document this consume derived against: a replay of
266                    // several writers' traces (formal/trace/storm2conf.py)
267                    // places the consume after the install that wrote it.
268                    self.trace("derive", serde_json::json!({"seq": p.pointer.seq, "cheap": true}));
269                    self.state.commit_baseline(baseline)?;
270                    return Ok((0, Seen::Pointer(p.pointer.seq, p.etag.clone())));
271                }
272            }
273            read = Some(lp);
274        }
275        let Some(current) = manifest::load_from(self.store.as_ref(), &self.cfg, read).await? else {
276            // Neither the pointer nor the legacy object exists: nothing
277            // has ever been published here, and this pass read both.
278            self.state.commit_baseline(baseline)?;
279            return Ok((0, Seen::Nothing));
280        };
281        let doc = &current.manifest;
282        self.trace("derive", serde_json::json!({"seq": doc.seq, "cheap": false}));
283        // H10: whatever this barrier does next — the fast path included —
284        // the ack names this document or a later one.
285        self.note_carrier_uncited(doc)?;
286        let scope = self.state.load_scope()?.map(|v| super::sync::Scope::new(&v));
287        let same = |a: &str, b: &str| a.trim_matches('"') == b.trim_matches('"');
288        let mut taken = 0usize;
289        // Something owed could not be taken now (a fetch that lost to a
290        // newer document, an unreadable path, a failed write): the next
291        // consume derives again even if the pointer does not move.
292        let mut left = false;
293        // What is not owed because the agent is working on it: owed the
294        // moment it backs out, which the cheap path checks.
295        let mut skipped: BTreeSet<String> = BTreeSet::new();
296
297        // Deletions first: a document that turned the file `build` into
298        // the directory `build/log.txt` owes both, and the file must go
299        // before the directory can be made.
300        let gone: Vec<String> =
301            baseline.entries.keys().filter(|p| !doc.entries.contains_key(*p)).cloned().collect();
302        for path in gone {
303            if check_contained(&self.cfg.root, &path).is_err() {
304                // Never materializable here, so nothing to remove.
305                baseline.entries.remove(&path);
306                baseline.prev_scan.remove(&path);
307                continue;
308            }
309            let local = self.cfg.root.join(&path);
310            let be = baseline.entries.get(&path).cloned();
311            let record = |kind: String| ConflictRecord {
312                path: path.clone(),
313                foreign_etag: String::new(),
314                preserved_key: None,
315                kind,
316                at_unix: now_unix(),
317            };
318            match std::fs::symlink_metadata(&local) {
319                Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
320                    self.trace("tombstone", serde_json::json!({"path": path, "action": "absent"}));
321                }
322                Err(e) => {
323                    // Unreadable is not absent.
324                    self.trace("tombstone", serde_json::json!({"path": path, "action": "deferred"}));
325                    self.state.append_conflict(&record(format!(
326                        "consume-foreign-delete-deferred: cannot stat the path: {e}"
327                    )))?;
328                    left = true;
329                    continue;
330                }
331                Ok(m) if !m.is_file() || local_dirty(&local, be.as_ref()) => {
332                    // The agent's newer work: not owed. It publishes over
333                    // the delete, with a record (`commit-recreated-deleted`).
334                    // Owed again if the agent backs out.
335                    self.trace("tombstone", serde_json::json!({"path": path, "action": "kept-dirty"}));
336                    skipped.insert(path.clone());
337                    continue;
338                }
339                Ok(_) => {
340                    if let Err(e) = std::fs::remove_file(&local) {
341                        self.state.append_conflict(&record(format!(
342                            "consume-foreign-delete-failed (will retry): {e}"
343                        )))?;
344                        left = true;
345                        continue;
346                    }
347                    // CONFIRM: only NotFound is absence.
348                    if !matches!(
349                        std::fs::symlink_metadata(&local),
350                        Err(ref e) if e.kind() == std::io::ErrorKind::NotFound
351                    ) {
352                        self.state.append_conflict(&record(
353                            "consume-foreign-delete-unconfirmed (will retry)".into(),
354                        ))?;
355                        left = true;
356                        continue;
357                    }
358                    self.trace("tombstone", serde_json::json!({"path": path, "action": "removed"}));
359                    taken += 1;
360                }
361            }
362            baseline.entries.remove(&path);
363            baseline.prev_scan.remove(&path);
364        }
365
366        for (path, e) in &doc.entries {
367            let be = baseline.entries.get(path).cloned();
368            if be.as_ref().is_some_and(|b| same(&b.etag, &e.etag) && b.cites_mode(e.mode)) {
369                continue;
370            }
371            let held = be.is_some() || scope.as_ref().is_none_or(|s| s.covers(path));
372            if !held {
373                continue;
374            }
375            if let Err(err) = check_contained(&self.cfg.root, path) {
376                // A path this tree can never safely materialise: surfaced,
377                // and not left owed — nothing here will ever take it.
378                self.state.append_conflict(&ConflictRecord {
379                    path: path.clone(),
380                    foreign_etag: e.etag.clone(),
381                    preserved_key: None,
382                    kind: format!("consume-refused-containment: {err}"),
383                    at_unix: now_unix(),
384                })?;
385                self.trace("consume", serde_json::json!({"path": path, "etag": e.etag, "from": "manifest", "action": "refused"}));
386                continue;
387            }
388            let local = self.cfg.root.join(path);
389            if local_dirty(&local, be.as_ref()) {
390                // CONTENT CONVERGENCE: the tree's bytes are the document's.
391                let converged = std::fs::symlink_metadata(&local).is_ok_and(|m| m.is_file() && m.len() == e.size)
392                    && file_crc(&self.cfg.root, path).is_ok_and(|c| same(&crc64_to_b64(c), &e.crc64_b64));
393                if converged {
394                    let st = std::fs::metadata(&local)?;
395                    baseline.entries.insert(
396                        path.clone(),
397                        BaselineEntry {
398                            etag: e.etag.clone(),
399                            key: Some(e.key.clone()),
400                            generation: e.generation,
401                            size: st.len(),
402                            mtime_unix: mtime_of(&st),
403                            mtime_nanos: Some(mtime_nanos_of(&st)),
404                            // The DOCUMENT's mode, as every entry records:
405                            // the bytes converged, the mode may not have,
406                            // and the tree's left here would never be
407                            // published.
408                            mode: Some(e.mode & scan::PERM_BITS),
409                            racy: super::barrier::racy_stat(&st),
410                            crc64_b64: Some(e.crc64_b64.clone()),
411                        },
412                    );
413                    baseline.prev_scan.insert(path.clone());
414                    self.trace("consume", serde_json::json!({"path": path, "etag": e.etag, "from": "manifest", "action": "converged"}));
415                    taken += 1;
416                } else {
417                    // The agent's work, not owed — until it backs out.
418                    skipped.insert(path.clone());
419                }
420                continue;
421            }
422            let (meta, body) = match self.store.get_whole(&e.key, Some(&e.etag)).await {
423                Ok(got) => got,
424                // A newer document replaced it (and its collector took the
425                // handle): owed again against that one.
426                Err(StoreError::NotFound(_)) | Err(StoreError::PreconditionFailed(_)) => {
427                    self.trace("consume", serde_json::json!({"path": path, "etag": e.etag, "from": "manifest", "action": "missing"}));
428                    left = true;
429                    continue;
430                }
431                Err(err) => return Err(err.into()),
432            };
433            // VERIFIED before it is written, as checkout's fetch is: against
434            // the CRC the document cites, else the backend's attestation.
435            let got = crc64_to_b64(crc64_nvme(&body));
436            let want = Some(e.crc64_b64.clone()).filter(|c| !c.is_empty()).or_else(|| meta.crc64_b64.clone());
437            if let Some(want) = want {
438                if want != got {
439                    self.state.append_conflict(&ConflictRecord {
440                        path: path.clone(),
441                        foreign_etag: e.etag.clone(),
442                        preserved_key: None,
443                        kind: format!(
444                            "consume-refused-checksum: etag {} is cited with CRC-64 {want} but the \
445                             bytes fetched under it hash to {got}",
446                            e.etag
447                        ),
448                        at_unix: now_unix(),
449                    })?;
450                    self.trace("consume", serde_json::json!({"path": path, "etag": e.etag, "from": "manifest", "action": "refused-checksum"}));
451                    left = true;
452                    continue;
453                }
454            }
455            // The mode the DOCUMENT cites, as checkout applies — not the
456            // object's stamp, which a gateway write does not carry (its
457            // file came out 0644 here and its cited mode in a checkout).
458            let mode = Some(e.mode);
459            consume_window("before-write", path);
460            // H7: the licence to overwrite is re-checked with the temp
461            // written, immediately before the rename; a write since is the
462            // agent's, and it stays (and publishes).
463            let still_clean = || !local_dirty(&local, be.as_ref());
464            let st = match contained_path(&self.cfg.root, path)
465                .and_then(|target| {
466                    if self.cfg.tree_reserve {
467                        super::safefs::leave_the_reserve(&self.cfg.root, path, body.len() as u64, self.cfg.tree_room_for_tests)?;
468                    }
469                    write_file_atomic_if(&target, &body, mode, &still_clean)
470                })
471            {
472                Ok(Some(st)) => st,
473                // The agent wrote first: as a dirty path above.
474                Ok(None) => {
475                    skipped.insert(path.clone());
476                    continue;
477                }
478                Err(err) => {
479                    // TRANSIENT (a full disk is not a planted symlink): the
480                    // path stays owed and the record repeats.
481                    self.state.append_conflict(&ConflictRecord {
482                        path: path.clone(),
483                        foreign_etag: e.etag.clone(),
484                        preserved_key: None,
485                        kind: format!("consume-write-failed (will retry): {err}"),
486                        at_unix: now_unix(),
487                    })?;
488                    self.trace("consume", serde_json::json!({"path": path, "etag": e.etag, "from": "manifest", "action": "deferred"}));
489                    left = true;
490                    continue;
491                }
492            };
493            consume_window("after-rename", path);
494            // The baseline records the inode this consume WROTE (H7).
495            let stamps = GenerationStamps::from_meta(&meta.meta);
496            baseline.entries.insert(
497                path.clone(),
498                BaselineEntry {
499                    etag: meta.etag.clone(),
500                    key: Some(e.key.clone()),
501                    generation: stamps.map(|s| s.generation).unwrap_or(e.generation),
502                    size: st.len(),
503                    mtime_unix: mtime_of(&st),
504                    mtime_nanos: Some(mtime_nanos_of(&st)),
505                    mode: Some(e.mode & scan::PERM_BITS),
506                    racy: super::barrier::racy_stat(&st),
507                    crc64_b64: Some(got),
508                },
509            );
510            baseline.prev_scan.insert(path.clone());
511            self.trace("consume", serde_json::json!({"path": path, "etag": e.etag, "from": "manifest", "action": "adopted"}));
512            taken += 1;
513        }
514        baseline.derived_etag = (!left).then(|| current.etag.clone());
515        baseline.skipped.sync_to(&skipped);
516        if !left {
517            // Everything owed is taken: the tree is integrated with this
518            // document (its dirty paths are the agent's, about to publish).
519            baseline.seq = doc.seq;
520            baseline.manifest_etag = Some(current.etag.clone());
521        }
522        // The files this baseline vouches for reached the disk before the
523        // baseline does (audit 2026-09-03, finding 9).
524        self.state.sync_tree()?;
525        self.state.commit_baseline(baseline)?;
526        // The pointer as this load saw it (a legacy single object has none,
527        // and the fast path asks the bucket itself).
528        let seen = match &current.pointer {
529            Some(_) => Seen::Pointer(doc.seq, current.etag.clone()),
530            None => Seen::Legacy,
531        };
532        Ok((taken, seen))
533    }
534
535    /// The cell's verb requests alone (§2.5), as a barrier's step 1 reads
536    /// them: no consume, no fetch.
537    pub async fn read_cell_requests(&mut self) -> LeanResult<()> {
538        let doc = inbox::load(self.store.as_ref(), &self.cfg).await?.doc;
539        self.note_verb_requests(&doc)
540    }
541
542    /// Act on the inbox document's two request fields (§2.5, D14).
543    ///
544    /// The asymmetry is deliberate and is about blast radius, not
545    /// principle: a boundary request is PERFORMED (it publishes what is
546    /// already on disk and mutates nothing local), a sync request is
547    /// CARRIED into the ticker as advisory news and the agent decides.
548    ///
549    /// Both are watermarked by `requested_unix` rather than consumed by
550    /// a clearing CAS. The field is idempotent state, so a repeat is a
551    /// no-op and a lost clear cannot double-publish; and not writing
552    /// means the doors cost no request at all, which is what §2.5
553    /// promises.
554    fn note_verb_requests(&mut self, doc: &inbox::InboxDoc) -> LeanResult<()> {
555        let mut mark = self.load_verb_watermark()?;
556        if let Some(r) = &doc.sync_request {
557            if r.requested_unix > mark.sync_unix {
558                self.carry_sync_request(r.requested_unix, &r.requestor)?;
559                mark.sync_unix = r.requested_unix;
560            }
561        }
562        if let Some(r) = &doc.boundary_request {
563            if r.requested_unix > mark.boundary_unix {
564                self.request_boundary(
565                    &format!("gw:{}:{}", r.requested_unix, r.requestor),
566                    Some(format!("boundary requested by {}", r.requestor)),
567                )?;
568                mark.boundary_unix = r.requested_unix;
569            }
570        }
571        self.save_verb_watermark(&mark)
572    }
573
574    fn verb_watermark_path(&self) -> std::path::PathBuf {
575        self.cfg.state_dir().join("verb-requests.json")
576    }
577
578    pub(crate) fn load_verb_watermark(&self) -> LeanResult<VerbWatermark> {
579        let p = self.verb_watermark_path();
580        if !p.exists() {
581            return Ok(VerbWatermark::default());
582        }
583        Ok(std::fs::read(&p)
584            .ok()
585            .and_then(|b| serde_json::from_slice(&b).ok())
586            .unwrap_or_default())
587    }
588
589    fn save_verb_watermark(&self, m: &VerbWatermark) -> LeanResult<()> {
590        let bytes = serde_json::to_vec_pretty(m)
591            .map_err(|e| LeanError::State(format!("verb watermark: {e}")))?;
592        super::control::write_atomic(&self.verb_watermark_path(), &bytes)
593    }
594
595    /// The consume's dirty case: the agent's local version wins; the
596    /// FOREIGN bytes are preserved first (a conflict record keeps both
597    /// recoverable), then the recognized ETag advances so the eventual
598    /// publish supersedes them KNOWINGLY.
599    /// Preserve the bytes at `key` (a handle, or an ingress object) under
600    /// `conflicts/<uuid>/<path>`, by a server-side copy guarded on `etag`:
601    /// no bytes cross this process, and a 10 GB checkpoint is preserved
602    /// in one request. The copy is what a conflict record names, and what
603    /// outlives the handle once the collector or the orphan sweep takes it.
604    async fn preserve_conflict_copy(&self, path: &str, key: &str, etag: &str) -> LeanResult<String> {
605        let dst = self.cfg.conflict_key(&uuid::Uuid::new_v4().to_string(), path);
606        let stamps = GenerationStamps {
607            generation: 0,
608            epoch: self
609                .lease
610                .as_ref()
611                .map(|l| l.epoch)
612                .or_else(|| self.state.load_incarnation().ok().flatten().map(|i| i.epoch))
613                .unwrap_or(0),
614            flush_uuid: "conflict-preserve".into(),
615            boundary_source: None,
616            posix: None,
617        };
618        self.store
619            .copy_object(key, Some(etag), &dst, &PutCondition::IfNoneMatchAny, &stamps)
620            .await?;
621        Ok(dst)
622    }
623
624    /// Take the SECOND absence observation now, by direct `lstat`.
625    ///
626    /// `scan::classify` withholds a path's delete until absence has
627    /// survived two consecutive scans — the rename-vs-walk race guard
628    /// (`lib.rs:37`). The hazard that rule names is the WALK missing a
629    /// file renamed under it, not deletion itself. On the cadence path
630    /// the second walk arrives at the next floor tick and nobody has
631    /// been promised otherwise. On a DECLARED boundary it cannot wait:
632    /// the ack would claim a coherent point (D1 — "everything
633    /// ordered-before T") while the manifest still cites a file the
634    /// agent removed before it touched the sentinel, and it would say
635    /// so with `report.deleted: 0` and `status: "ok"`.
636    ///
637    /// A direct `lstat` is exactly what the rename-vs-walk guard asks
638    /// for and is immune to the walk race by construction, so the
639    /// second observation costs one syscall per transiently-absent
640    /// path — never a second full pass. The cadence path is unchanged
641    /// and still waits for the second walk.
642    pub(crate) fn confirm_absences(
643        &self,
644        classified: &mut scan::Classified,
645        held: &mut BTreeSet<String>,
646    ) -> LeanResult<usize> {
647        if classified.first_absence.is_empty() {
648            return Ok(0);
649        }
650        let mut confirmed = 0;
651        for path in std::mem::take(&mut classified.first_absence) {
652            // ONLY NotFound is absence. Every other errno — EACCES on a
653            // parent, EIO, EMFILE, ELOOP — used to read as "the agent
654            // deleted it", and this is the oracle that PUBLISHES the
655            // delete and then DELETES THE OBJECT. An unreadable path is
656            // the one case where guessing is unrecoverable, so it is never
657            // a delete: EACCES or EPERM holds it, as the walk holds what it
658            // may not read (`scan::Scan::held`), and every other errno
659            // fails the barrier, as it fails the walk. Two call sites, one
660            // syscall: they must not have opposite error policies.
661            let gone = match std::fs::symlink_metadata(self.cfg.root.join(&path)) {
662                Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
663                Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
664                    held.insert(path);
665                    continue;
666                }
667                Err(e) => {
668                    classified.first_absence.insert(path.clone());
669                    return Err(LeanError::State(format!(
670                        "cannot confirm the absence of {path:?}: {e} — refusing to publish \
671                         a deletion on an unreadable path"
672                    )));
673                }
674                Ok(_) => false,
675            };
676            if gone {
677                classified.deletes.insert(path);
678                confirmed += 1;
679            } else {
680                // Back on disk: the rename-vs-walk race, caught exactly
681                // as the rule intends. Still only a first absence.
682                classified.first_absence.insert(path);
683            }
684        }
685        Ok(confirmed)
686    }
687
688    /// Steps 2–7. `barrier` = one full publish cycle (the cadence arm).
689    pub async fn run_barrier(&mut self) -> LeanResult<BarrierReport> {
690        self.barrier_inner(false, "manual").await
691    }
692
693    /// The floor tick's barrier. Stamped `cadence` so the bucket can
694    /// answer "which clock installed this boundary?" without the ack —
695    /// the ack is a LOCAL file, and the fleet-visible question is asked
696    /// by operators and by the gateway's `/status`, neither of which
697    /// can see it.
698    pub async fn cadence_barrier(&mut self) -> LeanResult<BarrierReport> {
699        self.barrier_inner(false, "cadence").await
700    }
701
702    /// A barrier whose result somebody is going to ACK: a sentinel
703    /// honor (D1) or the preStop drain (D10). Identical to
704    /// `run_barrier` except that it confirms first-absence paths rather
705    /// than acking a boundary that withholds them.
706    pub async fn declared_barrier(&mut self) -> LeanResult<BarrierReport> {
707        self.barrier_inner(true, "sentinel").await
708    }
709
710    /// `declared_barrier` under an explicit provenance stamp — the
711    /// drain and the budget-deferred honor are the same barrier
712    /// reporting a different clock.
713    pub async fn declared_barrier_as(&mut self, source: &str) -> LeanResult<BarrierReport> {
714        self.barrier_inner(true, source).await
715    }
716
717    async fn barrier_inner(&mut self, declared: bool, source: &str) -> LeanResult<BarrierReport> {
718        // Every publishing path ends here — cadence, sentinel, drain, the
719        // one-shot verb — so a reader is refused here before the first
720        // request, whichever of them a caller reached. The callers that
721        // owe an answer (the publish sentinel, the drain) give it before
722        // this; this is the backstop for the ones that do not.
723        self.refuse_if_read("barrier")?;
724        let mut clock = StepClock::start(self.store.clone());
725        // Review 2026-09-18, H10: a barrier that begins while a publish
726        // declaration stands CARRIES it, whoever called — the floor's
727        // cadence barrier after a failed honor as much as the honor itself
728        // (the model's `DSet` and `InstallSource` read the live pending,
729        // not the caller). So it confirms first absences as a declared
730        // barrier does, stamps the deferred clock, and journals itself
731        // with its CAS as the install the ack names (`IntentJournal::
732        // carrier`). A record written before the id field has none and
733        // carries nothing.
734        // Step 1's first half: the cell's verb requests (§2.5's doors ride
735        // this GET), read BEFORE the carry is decided. Read after it, a
736        // gateway `boundary_request` folded into the pending here was not
737        // carried by the barrier that read it: its install journalled no
738        // carrier, and a peer's delete of a path it published, before the
739        // honour, was answered `ok` (L-135, 2026-10-14). A failure here
740        // must never fail the barrier: the request is idempotent state and
741        // the next tick re-reads it.
742        let inbox_doc = inbox::load(self.store.as_ref(), &self.cfg).await?.doc;
743        if let Err(e) = self.note_verb_requests(&inbox_doc) {
744            eprintln!("flint-sync: verb request not consumed (retrying next tick): {e}");
745        }
746        let carries: Option<String> = self
747            .load_pending(super::sentinel::Verb::Publish)
748            .ok()
749            .flatten()
750            .map(|p| p.id)
751            .filter(|id| !id.is_empty());
752        let declared = declared || carries.is_some();
753        let source = if carries.is_some() && source == "cadence" { "sentinel-deferred" } else { source };
754        clock.lap("cell");
755        // No lease is held until the commit section (design 2026-09-13
756        // §4): the consume, the scan and the uploads below run with the
757        // cell at rest, guarded by each object's own etag. What the
758        // uploads are STAMPED with is the last epoch this incarnation
759        // held — informational, like every epoch stamp on an object; the
760        // manifest entries carry the commit section's real epoch.
761        let epoch_hint = self.state.load_incarnation()?.map(|i| i.epoch).unwrap_or(0);
762        let mut report = BarrierReport::default();
763        let barrier_started = std::time::Instant::now();
764        self.trace("barrier_start", serde_json::json!({"source": source, "declared": declared}));
765
766        // STEP 0: finish any rescope a crash left half-applied, before
767        // the scan reads the tree (scoped-read design §4.3). This is the
768        // gate that makes the narrow verb safe at all: a half-applied
769        // narrow is a tree whose files and whose citations disagree, and
770        // `classify` reads that disagreement as an upload or as a
771        // DELETE depending on which half landed. Replay is idempotent,
772        // so the cost on the normal path is one `stat` of a file that
773        // is not there.
774        if let Some(r) = self.replay_scope_intent().await? {
775            report.rescope_replayed = true;
776            eprintln!(
777                "flint-sync: finished an interrupted rescope before the barrier \
778                 (uncited {}, unlinked {}, materialised {})",
779                r.uncited, r.unlinked, r.materialized
780            );
781        }
782        clock.lap("rescope_replay");
783
784        // Step 1's second half: what the tree is owed. A UI save, delete or
785        // rename COMMITS (P2), so it reaches the tree as a peer's change
786        // does: derived, not queued.
787        let (consumed, seen_pointer) = self.consume_owed().await?;
788        report.consumed = consumed;
789        clock.lap("consume");
790        // The agent runs alongside: what it does to the tree between the
791        // removal pass above and the scan below is the model's interleaving
792        // of a local edit between Consume and Scan (the rename world's
793        // tenth box run reached its counterexample through it). A test
794        // window, like the consume's (H7); nothing in it calls the store.
795        consume_window("before-scan", "");
796        // Step 2: scan-diff against the persisted baseline, taken from the
797        // store and handed back at each end of the barrier (lever D).
798        let mut baseline = self.state.take_baseline()?;
799        clock.lap("baseline_load");
800        let mut scanned = scan::scan_with(&self.cfg.root, self.cfg.scan_threads)?;
801        clock.lap("walk");
802        let mut classified = scan::classify(&scanned, &baseline);
803        let racy_cleared = scan::confirm_racy(&self.cfg.root, &scanned, &baseline, &mut classified);
804        for p in &racy_cleared {
805            if let Some(e) = baseline.entries.get_mut(p) {
806                e.racy = false;
807            }
808        }
809        // A file the syncer may not read is held, not uploaded
810        // (`Scan::held`). Asked here, before the idle check, so a standing
811        // `chmod 000` leaves an otherwise idle tick idle.
812        let unreadable: Vec<String> =
813            classified.uploads.iter().filter(|p| !scan::may_read(&self.cfg.root, p)).cloned().collect();
814        for p in unreadable {
815            classified.uploads.remove(&p);
816            scanned.held.insert(p);
817        }
818        if declared {
819            report.absences_confirmed = self.confirm_absences(&mut classified, &mut scanned.held)?;
820        }
821        report.first_absence = classified.first_absence.iter().cloned().collect();
822        report.held = scanned.held.iter().cloned().collect();
823        if !report.held.is_empty() {
824            note_held(&report.held);
825        }
826        clock.lap("classify");
827
828        // Skip-on-no-diff: nothing local, and the bucket manifest where the
829        // consume left it.
830        if classified.uploads.is_empty()
831            && classified.deletes.is_empty()
832            && classified.first_absence.is_empty()
833        {
834            // Read the POINTER, never the entries: the 0b rig measured
835            // the idle tick at 1M entries as 27 s / 1.3 GiB — dominated
836            // by fetching and parsing a 264 MiB document just to read
837            // `seq`. The pointer is a few hundred bytes and carries the
838            // seq in its body, so a GET of it costs one round trip and
839            // answers strictly more than a HEAD of the old object did.
840            //
841            // Legacy workspaces (no pointer yet) keep the HEAD they had.
842            let pointer = match seen_pointer {
843                // The consume just read it: no second GET.
844                Seen::Pointer(seq, etag) => Some(Some((seq, etag))),
845                // The consume read the pointer AND the legacy key and found
846                // neither: decided below with no request at all.
847                Seen::Nothing => None,
848                Seen::Legacy => Some(
849                    manifest::load_pointer(self.store.as_ref(), &self.cfg).await?.map(|l| (l.pointer.seq, l.etag)),
850                ),
851            };
852            let unchanged = match pointer {
853                // Nothing published: the answer the 404 arm reaches, read
854                // once in this pass instead of three times (B8 trace,
855                // 2026-10-03: three pointer GETs and a legacy HEAD a tick).
856                None => baseline.manifest_etag.is_none(),
857                Some(Some((seq, etag))) => {
858                    // The news ticker rides this request for free — D5's
859                    // "zero added bucket requests" is still literal.
860                    report.observed_seq = Some(seq);
861                    report.observed_etag = Some(etag.clone());
862                    baseline.manifest_etag.as_deref() == Some(etag.as_str())
863                }
864                Some(None) => match self.store.head(&self.cfg.manifest_key()).await {
865                    Ok(meta) => {
866                        report.observed_seq =
867                            GenerationStamps::from_meta(&meta.meta).map(|s| s.generation);
868                        report.observed_etag = Some(meta.etag.clone());
869                        baseline.manifest_etag.as_deref() == Some(meta.etag.as_str())
870                    }
871                    Err(StoreError::NotFound(_)) => baseline.manifest_etag.is_none(),
872                    Err(e) => return Err(e.into()),
873                },
874            };
875            if unchanged {
876                report.no_change = true;
877                report.seq = Some(baseline.seq);
878                // prev_scan still advances (the two-scan rule's clock), and
879                // a racy flag the hash cleared is kept (unsaved, every later
880                // barrier would hash that file again). Handed back either
881                // way: the store writes what changed, and nothing when
882                // nothing did. `sync_to` walks both sorted sides and copies
883                // only the paths that came or went.
884                baseline.prev_scan.sync_to(scanned.present(&baseline.entries));
885                self.state.commit_baseline(baseline)?;
886                clock.lap("idle_check");
887                report.steps = clock.steps;
888                self.trace_barrier_end(&report, barrier_started);
889                return Ok(report);
890            }
891        }
892
893        // Step 3: intent journal, then the window (the commitment
894        // point: the same CAS drops the consumed entries).
895        let flush_uuid = uuid::Uuid::new_v4().to_string();
896        self.trace("scan", serde_json::json!({"flush": flush_uuid, "uploads": classified.uploads.len(),
897            "deletes": classified.deletes.len(), "first_absence": classified.first_absence.len()}));
898        let mut intent = self.state.load_intent()?;
899        intent = IntentJournal { flush_uuid: flush_uuid.clone(), carrier: intent.carrier.clone() };
900        self.state.save_intent(&intent)?;
901        clock.lap("intent");
902        // No window (P2): a UI save commits without the lease, and the
903        // uploads below race it the way two writers race each other —
904        // the commit's CAS decides, and the loser is preserved and
905        // recorded.
906
907        // Step 4: guarded uploads, fanned out under a bounded window
908        // (each key's guard chain is independent; the 412 policy and
909        // conflict records are applied to the collected results below,
910        // in deterministic path order).
911        let mut upserts: BTreeMap<String, LeanEntry> = BTreeMap::new();
912        let mut parked: BTreeSet<String> = BTreeSet::new();
913        // Citations whose etag was observed with no lease held (adopted
914        // uploads, citation repairs): re-read inside the commit section.
915        let mut observed: BTreeSet<String> = BTreeSet::new();
916        let mut new_baseline_entries: BTreeMap<String, BaselineEntry> = BTreeMap::new();
917        let outcomes: Vec<(String, LeanResult<UploadOutcome>)> = {
918            use futures::stream::{self, StreamExt};
919            // CHUNKED into waves, so a wave's outcomes are collected
920            // before the next is fanned out. No lease is held here
921            // (design 2026-09-13 §4), so nothing renews or fences
922            // between chunks any more — the between-chunk fence that
923            // stopped a deposed straggler's PUTs went with the straggler:
924            // a PUT outside the lease is one legitimate writer's PUT,
925            // guarded by If-Match, and a collision is preserved and
926            // superseded, never silently overwritten.
927            let mut outcomes: Vec<(String, LeanResult<UploadOutcome>)> = Vec::new();
928            // NOT cfg.fanout. That knob was raised to 128 for the READ
929            // path, and the read-side measurement (2.5x on 20k small
930            // files) says nothing about this one, so uploads keep the
931            // value they were measured at until someone measures them.
932            // Memory is no longer the reason they are separate: this
933            // path is byte-bounded too now, by the store's gate
934            // (`with_upload_inflight_max_bytes`), which charges every
935            // whole body below and every multipart part inside the
936            // store from before its read until its PUT returns.
937            let fanout = self.cfg.upload_fanout.max(1);
938            let chunk = fanout.saturating_mul(UPLOAD_CHUNK_WAVES).max(1);
939            let all: Vec<&String> = classified.uploads.iter().collect();
940            for group in all.chunks(chunk) {
941                {
942                    let this: &Syncer = &*self;
943                    let mut part: Vec<(String, LeanResult<UploadOutcome>)> =
944                        stream::iter(group.iter().map(|path| {
945                            let path = *path;
946                            let scanned_entry = &scanned.entries[path];
947                            let base = baseline.entries.get(path);
948                            let flush_uuid = &flush_uuid;
949                            async move {
950                                let r = this
951                                    .upload_one(path, scanned_entry, base, epoch_hint, flush_uuid)
952                                    .await;
953                                (path.clone(), r)
954                            }
955                        }))
956                        .buffer_unordered(fanout)
957                        .collect()
958                        .await;
959                    outcomes.append(&mut part);
960                }
961            }
962            outcomes
963        };
964        let mut outcomes = outcomes;
965        outcomes.sort_by(|a, b| a.0.cmp(&b.0));
966        for (path, outcome) in outcomes {
967            let path = &path;
968            match outcome? {
969                UploadOutcome::Published { entry, baseline_entry, adopted } => {
970                    self.trace("upload", serde_json::json!({"flush": flush_uuid, "path": path, "etag": entry.etag,
971                        "key": entry.key, "outcome": if adopted { "adopted" } else { "put" }}));
972                    if adopted {
973                        observed.insert(path.clone());
974                    }
975                    report.published_bytes += entry.size;
976                    upserts.insert(path.clone(), entry);
977                    new_baseline_entries.insert(path.clone(), baseline_entry);
978                    report.uploaded.push(path.clone());
979                }
980                UploadOutcome::Deferred => {
981                    self.trace("upload", serde_json::json!({"flush": flush_uuid, "path": path, "outcome": "deferred"}));
982                    report.deferred.push(path.clone());
983                }
984                UploadOutcome::Unreadable => {
985                    // Made unreadable since `may_read` asked: held, as it
986                    // would have been then.
987                    self.trace("upload", serde_json::json!({"flush": flush_uuid, "path": path, "outcome": "held"}));
988                    classified.uploads.remove(path);
989                    scanned.held.insert(path.clone());
990                    report.held.push(path.clone());
991                    note_held(std::slice::from_ref(path));
992                }
993            }
994        }
995        clock.lap("upload");
996
997        // A PULL-ONLY boundary: nothing uploaded or deleted, so the merge
998        // can only add nothing — and then the commit section writes nothing
999        // to the bucket (no CAS, no GC) and what remains is local: note what
1000        // theirs owes this tree and take its seq. That needs no
1001        // fence and no window; claiming for it queued every such writer
1002        // behind the publishing ones (65 of 191 claims in the writers drill).
1003        // A merge that does add something (a mirror flag to restamp, say)
1004        // falls through to the commit section.
1005        if classified.uploads.is_empty()
1006            && classified.deletes.is_empty()
1007            && upserts.is_empty()
1008            && observed.is_empty()
1009        {
1010            let opened = if self.cfg.chunk_view {
1011                DocView::open(self.store.clone(), &self.cfg, None).await?
1012            } else {
1013                Opened::Full(manifest::load(self.store.as_ref(), &self.cfg).await?)
1014            };
1015            // When the merge adds nothing: the document's seq and etag, its
1016            // entries at the carried paths, and the foreign count.
1017            let pulled: Option<(u64, String, manifest::LeanManifest, usize)> = match opened {
1018                Opened::Full(current) => {
1019                    let m = self.merge_onto(
1020                        current.as_ref(),
1021                        &merge_base(&baseline),
1022                        &upserts,
1023                        &classified.deletes,
1024                        &parked,
1025                        &flush_uuid,
1026                    );
1027                    match m.expected.as_ref().filter(|_| m.adds_nothing()).map(|h| h.etag.clone()) {
1028                        Some(etag) => Some((m.theirs.seq, etag, m.theirs, m.foreign.len())),
1029                        None => None,
1030                    }
1031                }
1032                Opened::View(mut v) => {
1033                    // Lever G: no upload and no delete, so the merge adds
1034                    // nothing unless it restamps the mirror flag.
1035                    let full = if self.cfg.event_trace.is_some() || self.cfg.g_shadow {
1036                        let doc = v.assemble().await?;
1037                        Some(self.merge_onto(
1038                            Some(&doc),
1039                            &merge_base(&baseline),
1040                            &upserts,
1041                            &classified.deletes,
1042                            &parked,
1043                            &flush_uuid,
1044                        ))
1045                    } else {
1046                        None
1047                    };
1048                    let adds_nothing = self.cfg.sole_writer == v.pointer.sole_writer;
1049                    if let Some(f) = full.as_ref().filter(|_| self.cfg.g_shadow) {
1050                        assert_eq!(adds_nothing, f.adds_nothing(), "lever G: a pull-only adds-nothing is not the whole-document merge's");
1051                    }
1052                    if adds_nothing {
1053                        let carried = self.carried_paths()?;
1054                        v.prefetch(carried.iter().map(|p| p.as_str())).await?;
1055                        let mut entries = BTreeMap::new();
1056                        for p in &carried {
1057                            if let Some(e) = v.get(p).await? {
1058                                entries.insert(p.clone(), e);
1059                            }
1060                        }
1061                        let at = manifest::LeanManifest { seq: v.seq(), entries, ..Default::default() };
1062                        Some((v.seq(), v.etag.clone(), at, full.map(|f| f.foreign.len()).unwrap_or(0)))
1063                    } else {
1064                        None
1065                    }
1066                }
1067            };
1068            if let Some((seq, etag, theirs_at, foreign)) = pulled {
1069                // What theirs changed at a path this tree holds is owed: the
1070                // next consume derives it, because theirs is not the document
1071                // it last derived against (or is, and there is nothing).
1072                self.note_carrier_uncited(&theirs_at)?;
1073                report.seq = Some(seq);
1074                report.no_change = true;
1075                report.manifest_etag = Some(etag.clone());
1076                report.observed_seq = Some(seq);
1077                report.observed_etag = Some(etag.clone());
1078                report.foreign_queued = foreign;
1079                baseline.seq = seq;
1080                baseline.manifest_etag = Some(etag.clone());
1081                baseline.prev_scan.sync_to(scanned.present(&baseline.entries));
1082                self.state.commit_baseline(baseline)?;
1083                self.state.clear_intent_keys()?;
1084                clock.lap("pull_only");
1085                report.steps = clock.steps;
1086                self.trace_barrier_end(&report, barrier_started);
1087                return Ok(report);
1088            }
1089            clock.lap("pull_only");
1090        }
1091
1092        // THE COMMIT SECTION (design 2026-09-13 §4). Everything above ran
1093        // with no lease: the uploads are durable and etag-guarded, and
1094        // nothing is cited yet. Claim the cell now — for the merge, the
1095        // CAS, the deletes and the baseline — and hand it to the next
1096        // waiter when done. This is the only stretch two writers of one
1097        // workspace serialise on, and it is small: one pointer CAS and
1098        // the guarded deletes, milliseconds to seconds, never the upload
1099        // of a checkpoint.
1100        let held = super::lease::claim(self).await?;
1101        let epoch = held.epoch;
1102        eprintln!("flint-sync: publish fence held (epoch {epoch})");
1103        clock.lap("claim");
1104        if self.cfg.drill_hold_commit_secs > 0 {
1105            eprintln!(
1106                "flint-sync: DRILL: holding the fence for {}s inside the commit section",
1107                self.cfg.drill_hold_commit_secs
1108            );
1109            tokio::time::sleep(std::time::Duration::from_secs(self.cfg.drill_hold_commit_secs)).await;
1110            clock.lap("drill_hold");
1111        }
1112        let commit: LeanResult<()> = async {
1113            // The entries carry the epoch the commit HOLDS, not the hint
1114            // the uploads were stamped with.
1115            for e in upserts.values_mut() {
1116                e.epoch = epoch;
1117            }
1118            // Step 5: the manifest CAS (three-way merge; bounded retries).
1119            // The cell is read after each manifest load below, not here: a
1120            // read here preceded the HEAD fan-out and the window, and a
1121            // holder deposed after it CASed onto its successor's document
1122            // (review 2026-09-18, H2).
1123            // EVERY citation this barrier adds was read or written with no
1124            // lease held. Another writer's commit may since have uncited
1125            // the path and collected the object: its GC deletes by the etag
1126            // it integrated. That etag names an ADOPTED object (the model's
1127            // LeanBarrierLeaseAdoptBlind) — and, since an S3 whole-PUT etag
1128            // is the MD5 of the bytes, it also names this barrier's own PUT
1129            // of IDENTICAL bytes (finding 13, the writers drill's A3: a
1130            // same-content rewrite re-uploaded, the peer's GC deleted it,
1131            // the commit cited a hole). Collection runs only inside a
1132            // commit section, so a re-read HERE, holding the fence, cannot
1133            // be overtaken before this CAS — held, that is, until the read
1134            // of the cell after the load: a deposal before it is seen
1135            // there, and after it the successor's rotation moves the
1136            // pointer the CAS expects (H2). Whatever is gone or replaced is
1137            // withheld: parked, recorded, and left dirty for the next
1138            // barrier to publish with a PUT of its own.
1139            let heads: Vec<(String, LeanResult<bool>)> = {
1140                use futures::stream::{self, StreamExt};
1141                let this: &Syncer = &*self;
1142                stream::iter(upserts.iter().map(|(path, cited)| async move {
1143                    let still = match this.store.head(&cited.key).await {
1144                        Ok(meta) => Ok(meta.etag == cited.etag),
1145                        Err(StoreError::NotFound(_)) => Ok(false),
1146                        Err(e) => Err(e.into()),
1147                    };
1148                    (path.clone(), still)
1149                }))
1150                .buffer_unordered(self.cfg.upload_fanout.max(1))
1151                .collect()
1152                .await
1153            };
1154            let mut heads = heads;
1155            heads.sort_by(|a, b| a.0.cmp(&b.0));
1156            for (path, still) in heads {
1157                let still_there = still?;
1158                let was_observed = observed.contains(&path);
1159                let cited = &upserts[&path];
1160                self.trace("observed", serde_json::json!({"flush": flush_uuid, "path": path, "etag": cited.etag, "still": still_there, "own_put": !was_observed}));
1161                if still_there {
1162                    continue;
1163                }
1164                let gone = upserts.remove(&path).expect("looked up above");
1165                new_baseline_entries.remove(&path);
1166                report.uploaded.retain(|p| p != &path);
1167                report.published_bytes = report.published_bytes.saturating_sub(gone.size);
1168                // ADOPTED bytes that are the baseline's own (a file touched
1169                // without a change, re-uploaded by a restarted barrier under
1170                // the same flush) that went before the commit: nothing new
1171                // is lost, so the path is not parked, and the next consume
1172                // takes the manifest's version. (Once this was the citation
1173                // repair's arm; storm drill S0, 2026-09-15, showed parking
1174                // it kept a writer on withheld bytes for good.)
1175                let integrated = was_observed
1176                    && baseline.entries.get(&path).is_some_and(|be| be.etag == gone.etag);
1177                if integrated {
1178                    self.state.append_conflict(&ConflictRecord {
1179                        path: path.clone(),
1180                        foreign_etag: gone.etag,
1181                        preserved_key: None,
1182                        kind: "repair-withheld: the object this barrier re-cited was replaced or \
1183                               collected before its commit; nothing cited, and the next consume \
1184                               installs the manifest's version"
1185                            .into(),
1186                        at_unix: now_unix(),
1187                    })?;
1188                    continue;
1189                }
1190                parked.insert(path.clone());
1191                let kind = if was_observed {
1192                    "adopt-withheld: the object this barrier found already at its key was \
1193                     replaced or collected before its commit; nothing cited, the path stays \
1194                     dirty and publishes next barrier"
1195                } else {
1196                    "upload-withheld: the object this barrier PUT was replaced or collected \
1197                     before its commit (a peer's GC recognizes the etag of identical bytes); \
1198                     nothing cited, the path stays dirty and publishes next barrier"
1199                };
1200                self.state.append_conflict(&ConflictRecord {
1201                    path: path.clone(),
1202                    foreign_etag: gone.etag,
1203                    preserved_key: None,
1204                    kind: kind.into(),
1205                    at_unix: now_unix(),
1206                })?;
1207                report.parked.push(path.clone());
1208            }
1209            clock.lap("verify");
1210            let mut foreign_entries: Vec<(String, LeanEntry)> = vec![];
1211            // Set when the merge added nothing to the document: nothing is
1212            // installed, and theirs becomes the merge base as it stands.
1213            let mut installed_nothing = false;
1214            let mut attempt = 0;
1215            // The last value carried out: the adoptions this install
1216            // declined for a move, whose entries stay in the cell.
1217            let (mut docs, installed_etag, overridden, recreated, over_theirs, replaced_etag) = loop {
1218                attempt += 1;
1219                if attempt > CAS_ATTEMPTS {
1220                    return Err(LeanError::State(format!(
1221                        "manifest CAS lost {CAS_ATTEMPTS} merge races — refusing this barrier"
1222                    )));
1223                }
1224                // Lever G (latency plan §12): the document chunk by chunk,
1225                // unless it is on another layout or the view is off.
1226                let opened = if self.cfg.chunk_view {
1227                    DocView::open(self.store.clone(), &self.cfg, None).await?
1228                } else {
1229                    Opened::Full(manifest::load(self.store.as_ref(), &self.cfg).await?)
1230                };
1231                clock.lap("load");
1232                // Review 2026-09-18, H2: THE fence — the cell read AFTER the
1233                // load this CAS merges onto. The read at the top of step 5
1234                // comes before the HEAD fan-out and the window, and a holder
1235                // deposed in between loaded its successor's document,
1236                // rotation included, and CASed onto it: the rotation fences
1237                // only a load that PRECEDES it. Read here, the order closes
1238                // both ways: deposed before this load, and this read sees
1239                // it; deposed after, and the successor's rotation has moved
1240                // the pointer this CAS expects.
1241                //
1242                // Review 2026-09-18, M3: renew first if the cell has stood
1243                // still for half the takeover threshold. Nothing else moves
1244                // the token between the claim and the end of the section, so
1245                // a live holder whose verify, merge or CAS retries ran past
1246                // it was deposed by a waiter, and two such writers could
1247                // depose each other every floor.
1248                super::lease::renew_if_due(self).await?;
1249                self.verify_not_deposed().await?;
1250                clock.lap("fence");
1251                let mut theirs = match opened {
1252                    Opened::View(v) => v,
1253                    Opened::Full(current) => {
1254                        // The baseline IS the merge base (P1-lite).
1255                        let base = merge_base(&baseline);
1256                        let MergeOnto { theirs, expected, merged, foreign, overridden, recreated, over_theirs } = self.merge_onto(
1257                            current.as_ref(),
1258                            &base,
1259                            &upserts,
1260                            &classified.deletes,
1261                            &parked,
1262                            &flush_uuid,
1263                        );
1264                        // A DOCUMENT MUST BE MATERIALISABLE. No filesystem can hold
1265                        // a file `a` and a file `a/x`, so no document may cite
1266                        // both — a fresh checkout would have to create a directory
1267                        // where its own manifest says a file goes. The agent makes
1268                        // exactly that shape by replacing a published file with a
1269                        // directory (or the reverse): the scan sees the old path
1270                        // absent and the new one new, the two-scan guard withholds a
1271                        // FIRST absence, and the upload lands beside the citation it
1272                        // was meant to replace (review 2026-09-18, M5's agent-side
1273                        // twin). Judged here because this is where the document's
1274                        // final shape is known — theirs, my upserts and the deletes
1275                        // that actually applied. Mine is the side that yields: the
1276                        // path stays dirty and publishes at the next barrier, by
1277                        // which time the withheld absence is confirmed and its
1278                        // citation gone. This re-merges without spending a CAS
1279                        // attempt; the clash is decided, not raced.
1280                        let clash = path_clashes(&merged.entries, &upserts);
1281                        if !clash.is_empty() {
1282                            self.withhold_clashes(clash, &mut upserts, &mut new_baseline_entries, &mut report, &mut parked, &flush_uuid)?;
1283                            clock.lap("merge");
1284                            attempt -= 1;
1285                            continue;
1286                        }
1287                        clock.lap("merge");
1288                        // Nothing of ours changes the document — a barrier that only
1289                        // found the manifest moved by another writer. Installing it
1290                        // anyway was an empty generation, and the peer's next tick
1291                        // then found the manifest moved and did the same: two idle
1292                        // writers traded generations (and cell claims) for as long
1293                        // as both ran. Theirs becomes the merge base as it stands.
1294                        if let Some(handle) = expected.as_ref() {
1295                            if merged.entries == theirs.entries && merged.sole_writer == theirs.sole_writer {
1296                                foreign_entries = foreign;
1297                                installed_nothing = true;
1298                                break (
1299                                    CommitDocs::Full { installed: theirs.clone(), theirs },
1300                                    handle.etag.clone(),
1301                                    vec![],
1302                                    vec![],
1303                                    vec![],
1304                                    Some(handle.etag.clone()),
1305                                );
1306                            }
1307                        }
1308                        match manifest::cas_write_retiring(
1309                            self.store.as_ref(),
1310                            &self.cfg,
1311                            &merged,
1312                            Some(&theirs),
1313                            expected.as_ref(),
1314                            epoch,
1315                            &flush_uuid,
1316                            Some(source),
1317                        )
1318                        .await
1319                        {
1320                            Ok(meta) => {
1321                                // Before the deletes and before step 7: this is the
1322                                // only record that survives a crash in that window.
1323                                self.trace("cas", serde_json::json!({"flush": flush_uuid, "seq": merged.seq,
1324                                    "expected": expected.as_ref().map(|h| h.etag.clone()), "etag": meta.etag, "result": "ok"}));
1325                                // H10: what this install carried of the standing
1326                                // declaration survives a restart, for the ack.
1327                                if let Some(id) = carries.as_ref() {
1328                                    note_carried(&mut intent, id, &classified, &upserts, &report, |p| merged.entries.contains_key(p));
1329                                }
1330                                self.state.save_intent(&intent)?;
1331                                clock.lap("cas");
1332                                foreign_entries = foreign;
1333                                let replaced = expected.as_ref().map(|h| h.etag.clone());
1334                                break (CommitDocs::Full { theirs, installed: merged }, meta.etag, overridden, recreated, over_theirs, replaced);
1335                            }
1336                            Err(LeanError::Store(StoreError::PreconditionFailed(_)))
1337                            | Err(LeanError::Store(StoreError::Conflict(_))) => {
1338                                self.trace("cas", serde_json::json!({"flush": flush_uuid, "seq": merged.seq,
1339                                    "expected": expected.as_ref().map(|h| h.etag.clone()), "result": "lost"}));
1340                                clock.lap("cas");
1341                                // A rotation is exactly this 412, and re-merging past
1342                                // it would be the straggler install: the next
1343                                // attempt's read after its load is the fence.
1344                                // Back off first: under P2 a UI save never waits on
1345                                // the lease, so the race is usually lost to saves,
1346                                // and the gap between two of them is the way in (G2).
1347                                tokio::time::sleep(cas_backoff(attempt)).await;
1348                                clock.lap("backoff");
1349                                continue;
1350                            }
1351                            Err(e) => return Err(e),
1352                        }
1353                    }
1354                };
1355                // Lever G (latency plan §12): the merge, the clash check, the
1356                // re-chunk and the retire set read only the chunks the
1357                // commit's paths fall in, and the new chunk list re-encodes
1358                // only what the change and its cascade reach.
1359                let paths: Vec<String> = upserts.keys().chain(classified.deletes.iter()).cloned().collect();
1360                theirs.prefetch(paths.iter().map(|p| p.as_str())).await?;
1361                let mut at: BTreeMap<String, LeanEntry> = BTreeMap::new();
1362                for p in &paths {
1363                    if let Some(e) = theirs.get(p).await? {
1364                        at.insert(p.clone(), e);
1365                    }
1366                }
1367                let base_of = |p: &str| baseline.entries.get(p).map(|e| e.etag.as_str());
1368                let mc = manifest::merge_changes(
1369                    &base_of,
1370                    &at,
1371                    &theirs.tombstones,
1372                    theirs.seq(),
1373                    &upserts,
1374                    &classified.deletes,
1375                    &parked,
1376                );
1377                // The event trace's merge record (its foreign and gone counts
1378                // are about every path) and the shadow check read the whole
1379                // documents.
1380                let full = if self.cfg.event_trace.is_some() || self.cfg.g_shadow {
1381                    let doc = theirs.assemble().await?;
1382                    Some(self.merge_onto(Some(&doc), &merge_base(&baseline), &upserts, &classified.deletes, &parked, &flush_uuid))
1383                } else {
1384                    None
1385                };
1386                let clash = clashes_view(&mut theirs, &mc.overlay, &upserts).await?;
1387                let shadow = full.as_ref().filter(|_| self.cfg.g_shadow);
1388                if let Some(f) = shadow {
1389                    shadow_merge(f, &mc, &clash, &upserts);
1390                }
1391                if !clash.is_empty() {
1392                    self.withhold_clashes(clash, &mut upserts, &mut new_baseline_entries, &mut report, &mut parked, &flush_uuid)?;
1393                    clock.lap("merge");
1394                    attempt -= 1;
1395                    continue;
1396                }
1397                clock.lap("merge");
1398                let expected = theirs.handle();
1399                // Nothing of ours changes the document: the full path's
1400                // check, over the paths the overlay names.
1401                let adds_nothing = self.cfg.sole_writer == theirs.pointer.sole_writer
1402                    && mc.overlay.iter().all(|(p, v)| at.get(p) == v.as_ref());
1403                if let Some(f) = shadow {
1404                    assert_eq!(adds_nothing, f.adds_nothing(), "lever G: adds-nothing is not the whole-document merge's");
1405                }
1406                if adds_nothing {
1407                    foreign_entries = full.map(|f| f.foreign).unwrap_or_default();
1408                    installed_nothing = true;
1409                    let (seq, tombstones) = (theirs.seq(), theirs.tombstones.clone());
1410                    let (sole_writer, boundary_source) = (theirs.pointer.sole_writer, theirs.pointer.boundary_source.clone());
1411                    break (
1412                        CommitDocs::View { theirs, overlay: BTreeMap::new(), retired: vec![], seq, tombstones, sole_writer, boundary_source },
1413                        expected.etag.clone(),
1414                        vec![],
1415                        vec![],
1416                        vec![],
1417                        Some(expected.etag),
1418                    );
1419                }
1420                let chunks = theirs.rechunk(&mc.overlay).await?;
1421                let retired = theirs.retired(&mc.overlay).await?;
1422                if let Some(f) = shadow {
1423                    shadow_write(f, &chunks, &retired, &self.cfg);
1424                }
1425                let doc = manifest::ChunkedDoc {
1426                    seq: mc.seq,
1427                    sole_writer: self.cfg.sole_writer,
1428                    boundary_source: Some(source.to_string()),
1429                    tombstones: &mc.tombstones,
1430                };
1431                match manifest::cas_write_view(
1432                    self.store.as_ref(),
1433                    &self.cfg,
1434                    &chunks,
1435                    &doc,
1436                    &expected,
1437                    epoch,
1438                    &flush_uuid,
1439                    retired.clone(),
1440                )
1441                .await
1442                {
1443                    Ok(meta) => {
1444                        // Before the deletes and before step 7: this is the
1445                        // only record that survives a crash in that window.
1446                        self.trace("cas", serde_json::json!({"flush": flush_uuid, "seq": mc.seq,
1447                            "expected": Some(expected.etag.clone()), "etag": meta.etag, "result": "ok"}));
1448                        if let Some(id) = carries.as_ref() {
1449                            note_carried(&mut intent, id, &classified, &upserts, &report, |p| match mc.overlay.get(p) {
1450                                Some(v) => v.is_some(),
1451                                None => at.contains_key(p),
1452                            });
1453                        }
1454                        self.state.save_intent(&intent)?;
1455                        clock.lap("cas");
1456                        foreign_entries = full.map(|f| f.foreign).unwrap_or_default();
1457                        let live: std::collections::HashSet<&str> = chunks.iter().map(|(r, _)| r.addr.as_str()).collect();
1458                        theirs.retain_aliases(&live);
1459                        let replaced = Some(expected.etag.clone());
1460                        break (
1461                            CommitDocs::View {
1462                                theirs,
1463                                overlay: mc.overlay,
1464                                retired,
1465                                seq: mc.seq,
1466                                tombstones: mc.tombstones,
1467                                sole_writer: self.cfg.sole_writer,
1468                                boundary_source: Some(source.to_string()),
1469                            },
1470                            meta.etag,
1471                            mc.overridden,
1472                            mc.recreated,
1473                            mc.over_theirs,
1474                            replaced,
1475                        );
1476                    }
1477                    Err(LeanError::Store(StoreError::PreconditionFailed(_)))
1478                    | Err(LeanError::Store(StoreError::Conflict(_))) => {
1479                        self.trace("cas", serde_json::json!({"flush": flush_uuid, "seq": mc.seq,
1480                            "expected": Some(expected.etag.clone()), "result": "lost"}));
1481                        clock.lap("cas");
1482                        // As the full path: the next attempt's read after its
1483                        // load is the fence, after a backoff (G2).
1484                        tokio::time::sleep(cas_backoff(attempt)).await;
1485                        clock.lap("backoff");
1486                        continue;
1487                    }
1488                    Err(e) => return Err(e),
1489                }
1490            };
1491            let installed_seq = docs.installed_seq();
1492            report.seq = Some(installed_seq);
1493            report.no_change = installed_nothing;
1494            // A fused install IS a coherent point, and cadence/hybrid have
1495            // exactly one source. The gauges must not report "no boundary
1496            // ever" on a workspace that publishes every minute. A barrier
1497            // that installed nothing marks no boundary of its own.
1498            if !installed_nothing {
1499                self.note_boundary("cadence", installed_seq)?;
1500            }
1501            report.manifest_etag = Some(installed_etag.clone());
1502            report.observed_seq = Some(installed_seq);
1503            report.observed_etag = Some(installed_etag.clone());
1504            report.foreign_queued = foreign_entries.len();
1505
1506            // Step 6: RETIREMENT and collection (design 2026-09-19, R3 and
1507            // R7). What the document this CAS merged onto cited and the
1508            // installed document does not — a modified path's old handle,
1509            // a deleted path's — is retired, minus every handle the
1510            // installed document still cites at another path (a rename
1511            // cites the source's handle at the destination) and every
1512            // handle an inbox entry names (a rename whose destination is
1513            // still only in the cell). A retired handle can never be cited
1514            // again: nothing discovers handles from paths, and the merge
1515            // carries forward only what the base cites. So the collector
1516            // deletes it UNCONDITIONALLY, in one batch — no HEAD, no
1517            // If-Match, no fence, no renew (a straggler deposed after its
1518            // CAS has an exact retired set too; the model's
1519            // `LeanImmutableStragglerGC`). The HEAD-then-If-Match dance,
1520            // the collector that gave way on a store without a conditional
1521            // DELETE (review H1's leak) and the C2 renew-before-delete
1522            // clock all went with the slot.
1523            //
1524            // And what this barrier published OVER without ever integrating
1525            // it — a peer's publish, a UI write consumed elsewhere — is
1526            // preserved by a server-side copy and a record BEFORE the batch
1527            // takes the handle. The slot's 412 caught that at the PUT and
1528            // `Upload412Preserves` did the same; a fresh handle has no slot
1529            // to fail on, so the CAS, the one place the citation is read,
1530            // takes the decision (R7; the model's first HitlOverAny run
1531            // under handles cited an acked write over blind without it).
1532            let retired: Vec<String> = docs.retired();
1533            clock.lap("retire_set");
1534            for (path, was) in &overridden {
1535                let integrated = baseline
1536                    .entries
1537                    .get(path)
1538                    .is_some_and(|be| be.key.as_deref() == Some(was.key.as_str()));
1539                if integrated || parked.contains(path) {
1540                    continue;
1541                }
1542                let preserved = self.preserve_conflict_copy(path, &was.key, &was.etag).await?;
1543                self.state.append_conflict(&ConflictRecord {
1544                    path: path.clone(),
1545                    foreign_etag: was.etag.clone(),
1546                    preserved_key: Some(preserved),
1547                    kind: "commit-surfaced-foreign: this barrier published over a version it never \
1548                           integrated (another writer's publish, or a UI write consumed elsewhere); \
1549                           that version is preserved under this record and superseded knowingly"
1550                        .into(),
1551                    at_unix: now_unix(),
1552                })?;
1553                report.surfaced.push(path.clone());
1554                self.trace("surface", serde_json::json!({"flush": flush_uuid, "path": path, "etag": was.etag, "key": was.key}));
1555            }
1556            for (path, deleted) in &recreated {
1557                self.state.append_conflict(&ConflictRecord {
1558                    path: path.clone(),
1559                    foreign_etag: deleted.clone(),
1560                    preserved_key: None,
1561                    kind: "commit-recreated-deleted: this barrier published over a DELETE it never \
1562                           integrated (another writer's, or the UI's); mine wins, the path is cited \
1563                           again with this tree's bytes, and the deleted version is named here"
1564                        .into(),
1565                    at_unix: now_unix(),
1566                })?;
1567                report.surfaced.push(path.clone());
1568                self.trace("surface", serde_json::json!({"flush": flush_uuid, "path": path, "etag": deleted, "deleted": true}));
1569            }
1570            // M3: what this barrier's DELETE removed that it never
1571            // integrated — theirs, changed since the baseline — is preserved
1572            // before the batch below takes the handle, and named.
1573            for (path, was) in &over_theirs {
1574                let preserved = self.preserve_conflict_copy(path, &was.key, &was.etag).await?;
1575                self.state.append_conflict(&ConflictRecord {
1576                    path: path.clone(),
1577                    foreign_etag: was.etag.clone(),
1578                    preserved_key: Some(preserved),
1579                    kind: "commit-deleted-over-theirs: this barrier deleted a path whose version it \
1580                           never integrated (another writer's publish, or a UI save); the delete \
1581                           stands, and that version is preserved under this record"
1582                        .into(),
1583                    at_unix: now_unix(),
1584                })?;
1585                report.surfaced.push(path.clone());
1586                self.trace("surface", serde_json::json!({"flush": flush_uuid, "path": path, "etag": was.etag, "key": was.key, "over_delete": true}));
1587            }
1588            for path in &classified.deletes {
1589                if docs.installed_get(path).await?.is_some() {
1590                    // Never expected: since M3 a delete applies over theirs,
1591                    // and only uploads are parked, so every delete leaves the
1592                    // document. Were one still cited it is not in this
1593                    // boundary — PARKED, which the ack names as not carried —
1594                    // and the next scan classifies it again. Traced like
1595                    // every outcome: a replay owes a GC step for every path
1596                    // in the delete set (W4 phase 2, R3-S2).
1597                    self.trace("gc", serde_json::json!({"flush": flush_uuid, "path": path, "result": "still-cited"}));
1598                    report.parked.push(path.clone());
1599                } else {
1600                    let key = docs.theirs_get(path).await?.map(|e| e.key);
1601                    self.trace("gc", serde_json::json!({"flush": flush_uuid, "path": path, "key": key, "result": "retired"}));
1602                    report.deleted.push(path.clone());
1603                }
1604            }
1605            clock.lap("surface");
1606            if self.cfg.drill_hold_gc_secs > 0 && !retired.is_empty() {
1607                eprintln!(
1608                    "flint-sync: DRILL: holding the GC for {}s before the batch delete",
1609                    self.cfg.drill_hold_gc_secs
1610                );
1611                self.trace("drill_hold", serde_json::json!({"flush": flush_uuid, "where": "gc",
1612                    "secs": self.cfg.drill_hold_gc_secs}));
1613                tokio::time::sleep(std::time::Duration::from_secs(self.cfg.drill_hold_gc_secs)).await;
1614                clock.lap("drill_hold");
1615            }
1616            // THE COLLECTION PASS (latency plan 2026-10-23, lever C): the
1617            // retire ledger, the reap, the orphan sweep, the generation and
1618            // chunk sweeps, run together and only when
1619            // `collect_every_secs` has passed since this writer last ran
1620            // them — not on every publish, where they were three LISTs and
1621            // four GETs under the lease. Still in the commit section, under
1622            // the cell (DP2): only WHEN the collector runs moves, and the
1623            // model's reap and sweeps are steps a commit section may take
1624            // or not. Skipping one costs storage for the interval, never a
1625            // citation: nothing is deleted while it is skipped.
1626            let collect_at = now_unix();
1627            let collect = self.cfg.collect_every_secs == 0 || {
1628                let last = self.state.load_collect_at();
1629                last == 0 || collect_at.saturating_sub(last) >= self.cfg.collect_every_secs
1630            };
1631            // RETIRE-AGE G (M1): what this commit retired was LOGGED with its
1632            // CAS (`manifest::cas_write_retiring`), as the gateway's commits
1633            // log theirs. It is collected once the retirement is
1634            // `retire_grace_secs` old — at once when that is 0 — by whichever
1635            // writer's collection pass comes next; until then the sweeps spare
1636            // it, so a reader that loaded the document before this CAS can
1637            // still fetch it. No ledger, no sweep: a leak, never a deletion
1638            // a lagging reader meets.
1639            let ledger = match collect {
1640                false => None,
1641                true => match self.retire_ledger(collect_at).await {
1642                    Ok(l) => Some(l),
1643                    Err(e) => {
1644                        eprintln!("flint-sync: could not read the retire logs ({e}); no sweep this time");
1645                        None
1646                    }
1647                },
1648            };
1649            if collect {
1650                clock.lap("retire_ledger");
1651            }
1652            // The collection pass reads the whole installed document (lever G
1653            // assembles it from the view only on the commits that run it).
1654            let installed_full = match &ledger {
1655                Some(_) => Some(docs.installed_full().await?),
1656                None => None,
1657            };
1658            let whole = || installed_full.as_deref().expect("assembled for the collection pass");
1659            if let Some(l) = &ledger {
1660                match self.reap_retired(l, whole(), &flush_uuid).await {
1661                    Ok(n) => report.collected = n,
1662                    Err(e) => eprintln!("flint-sync: retired handles not collected (next time): {e}"),
1663                }
1664            }
1665            if collect {
1666                clock.lap("reap");
1667            }
1668            if !retired.is_empty() {
1669                self.trace("gc", serde_json::json!({"flush": flush_uuid, "retired": retired.len(),
1670                    "collected": report.collected}));
1671            }
1672            // The orphan sweep (R4): a handle nothing cites, no young retire
1673            // log names, older than its grace — left by a writer that never
1674            // committed. Inside the commit section, under the cell, so the
1675            // commit's re-read of its own uploads above and this CAS are
1676            // race-free against it; never this barrier's own handles.
1677            if let Some(l) = ledger.as_ref().filter(|_| self.lease.is_some()) {
1678                match self.sweep_orphans_if_due(whole(), &flush_uuid, now_unix(), &l.spared).await {
1679                    Ok(n) => report.swept = n,
1680                    Err(e) => eprintln!("flint-sync: orphan sweep (retrying next time it is due): {e}"),
1681                }
1682            }
1683            drop(installed_full);
1684            if collect {
1685                clock.lap("orphan_sweep");
1686            }
1687
1688            // Step 7: the baseline follows what this barrier published. What
1689            // theirs changed is owed, and the next consume derives it: the
1690            // pointer now names a document it never derived against. UNLESS
1691            // this CAS replaced exactly the document the consume derived
1692            // against — then the installed one is that plus this tree's own
1693            // changes, nothing new is owed, and the next tick stays cheap
1694            // (a busy agent's publishes would otherwise each cost a full
1695            // derive). The paths published or deleted here are no longer the
1696            // agent's pending work.
1697            let same = |a: &str, b: &str| a.trim_matches('"') == b.trim_matches('"');
1698            // No document replaced ("") is the empty workspace's derive.
1699            let over_derived = matches!(&baseline.derived_etag,
1700                Some(d) if same(replaced_etag.as_deref().unwrap_or(""), d));
1701            if over_derived {
1702                baseline.derived_etag = Some(installed_etag.clone());
1703                for path in new_baseline_entries.keys().chain(&report.deleted) {
1704                    baseline.skipped.remove(path);
1705                }
1706            }
1707            for (path, be) in new_baseline_entries {
1708                baseline.entries.insert(path, be);
1709            }
1710            for path in &report.deleted {
1711                baseline.entries.remove(path);
1712            }
1713            let carried = self.carried_paths()?;
1714            let carried_at = docs.installed_at(&carried).await?;
1715            self.note_carrier_uncited(&carried_at)?;
1716            baseline.seq = installed_seq;
1717            baseline.manifest_etag = Some(installed_etag);
1718            baseline.prev_scan.sync_to(scanned.present(&baseline.entries));
1719            self.state.commit_baseline(baseline)?;
1720            self.state.clear_intent_keys()?;
1721            clock.lap("baseline");
1722            // The commit section's end (the model's `Finish`). No window is
1723            // written: under P2 a UI save never waits on one (G1).
1724            self.trace("window_clear", serde_json::json!({"flush": flush_uuid}));
1725            // The rest of the collection pass (see above).
1726            if collect {
1727                // Reap superseded generations. Immutable metadata that is never
1728                // collected is a leak that grows by a whole manifest per
1729                // publish, and this also collects the orphan a crash between the
1730                // entries PUT and the pointer CAS leaves behind. Best effort by
1731                // design: a publish that succeeded is not un-done by a failure
1732                // to tidy up after it.
1733                // A deposal after the CAS stops every deletion after it (M3):
1734                // what is left is a leak the next holder's sweeps take, never a
1735                // deletion under a successor's cell. The reap and the orphan
1736                // sweep renew between their batches (`delete_many_renewing`);
1737                // these two renew once before they start.
1738                let may_delete = self.lease.is_some() && super::lease::renew_if_due(self).await.is_ok();
1739                let swept_generations = match may_delete {
1740                    false => Ok(0),
1741                    true => manifest::sweep_generations(self.store.as_ref(), &self.cfg).await,
1742                };
1743                let swept_generations = match swept_generations {
1744                    Ok(0) => 0,
1745                    Ok(n) => {
1746                        eprintln!("flint-sync: reaped {n} superseded manifest generation(s)");
1747                        n
1748                    }
1749                    Err(e) => {
1750                        eprintln!("flint-sync: generation sweep: {e}");
1751                        0
1752                    }
1753                };
1754                self.trace("sweep", serde_json::json!({"flush": flush_uuid, "what": "generations", "removed": swept_generations}));
1755                clock.lap("generation_sweep");
1756                // And the CHUNK reaper, same reason, same best-effort terms.
1757                // `sweep_chunks` returns 0 without a request on a workspace that
1758                // is not chunked, so this costs nothing until the layout moves.
1759                //
1760                // It was written to four model-established rules, guarded by six
1761                // mutation configs, and called from nothing but its own tests —
1762                // so with chunking on, every publish left its superseded chunks
1763                // in the bucket forever. The rules ran in the model and in the
1764                // suite and never once in production. `the_barrier_reaps...`
1765                // below is the test that asks whether this line exists at all.
1766                let may_delete = may_delete && self.lease.is_some() && super::lease::renew_if_due(self).await.is_ok();
1767                let swept_chunks = match ledger.as_ref() {
1768                    _ if !may_delete => Ok(0),
1769                    None => Ok(0),
1770                    Some(l) => manifest::sweep_chunks_sparing(self.store.as_ref(), &self.cfg, &l.spared).await,
1771                };
1772                let swept_chunks = match swept_chunks {
1773                    Ok(0) => 0,
1774                    Ok(n) => {
1775                        eprintln!("flint-sync: reaped {n} unreferenced manifest chunk(s)");
1776                        n
1777                    }
1778                    Err(e) => {
1779                        eprintln!("flint-sync: chunk sweep: {e}");
1780                        0
1781                    }
1782                };
1783                self.trace("sweep", serde_json::json!({"flush": flush_uuid, "what": "chunks", "removed": swept_chunks}));
1784                clock.lap("chunk_sweep");
1785                // The pass ran: the next is due `collect_every_secs` from
1786                // now. A ledger that could not be read collected nothing,
1787                // so the next commit tries again.
1788                if ledger.is_some() {
1789                    if let Err(e) = self.state.save_collect_at(collect_at) {
1790                        eprintln!("flint-sync: could not record the collection pass ({e}); the next commit runs it again");
1791                    }
1792                }
1793            }
1794            Ok(())
1795        }
1796        .await;
1797        // The section's documents (theirs, the merge, the install) are
1798        // freed as its block ends: at a million entries that is time the
1799        // lease is held for, so it is its own step rather than release's.
1800        clock.lap("free");
1801        // Hand the cell on whatever happened — unless it is no longer
1802        // ours to hand on: a fence means a successor holds it, and the
1803        // barrier is simply abandoned (its manifest never installed; its
1804        // uploads stand, and the next barrier adopts them by flush_uuid).
1805        match &commit {
1806            Err(LeanError::Fenced(m)) => {
1807                self.trace("fence", serde_json::json!({"where": "commit", "detail": m}));
1808                self.lease = None
1809            }
1810            _ => {
1811                if let Err(e) = super::lease::release(self).await {
1812                    eprintln!(
1813                        "flint-sync: the publish fence could not be released ({e}); a waiter \
1814                         deposes it after the quiet threshold"
1815                    );
1816                }
1817            }
1818        }
1819        clock.lap("release");
1820        report.steps = clock.steps;
1821        commit?;
1822        self.trace_barrier_end(&report, barrier_started);
1823        Ok(report)
1824    }
1825
1826    fn trace_barrier_end(&self, report: &BarrierReport, started: std::time::Instant) {
1827        if self.cfg.event_trace.is_none() {
1828            return;
1829        }
1830        self.trace(
1831            "barrier_end",
1832            serde_json::json!({
1833                "seq": report.seq, "uploaded": report.uploaded.len(), "deleted": report.deleted.len(),
1834                "parked": report.parked.len(),
1835                "leaked": report.leaked.len(),
1836                "consumed": report.consumed, "no_change": report.no_change,
1837                "ms": started.elapsed().as_millis() as u64, "requests": self.trace_requests(),
1838                "steps": report.steps,
1839            }),
1840        );
1841    }
1842
1843    /// The > whole_put_max path: contiguous `PartSource::Local` chunks
1844    /// through `compose_generation` (streaming multipart; the store
1845    /// aborts its partial assembly on every failure path). The CRC is
1846    /// computed by a streaming pass first; a writer racing the compose
1847    /// fails server-side validation and the path DEFERS to the next
1848    /// barrier — publish-possibly-torn is put_whole's documented
1849    /// dilemma, not this path's.
1850    #[allow(clippy::too_many_arguments)]
1851    #[allow(clippy::too_many_arguments)]
1852    async fn upload_compose(
1853        &self,
1854        path: &str,
1855        key: &str,
1856        // The ONE descriptor `upload_one` opened by a walk that follows
1857        // no link at any depth. The store reads its parts from this and
1858        // never reopens the path (review 2026-09-18, H5's residual).
1859        file: std::sync::Arc<std::fs::File>,
1860        size: u64,
1861        scanned: &scan::ScanEntry,
1862        stamps: GenerationStamps,
1863        generation: u64,
1864        epoch: u64,
1865    ) -> LeanResult<UploadOutcome> {
1866        // NO PRE-PASS. The store accumulates the full-object CRC from
1867        // the parts as it reads them for upload, so this file is read
1868        // ONCE (the 2026-09-10 door drill measured the second read at
1869        // roughly 11 s of the `mixed` workload's 24.3 s).
1870        // Part grid: within [min_part_size, ...], at most max_parts,
1871        // contiguous from 0.
1872        let min_part = self.store.min_part_size().max(1);
1873        let max_parts = self.store.max_parts().max(1) as u64;
1874        let mut chunk = self.cfg.whole_put_max.max(min_part);
1875        let need = size.div_ceil(chunk);
1876        if need > max_parts {
1877            chunk = size.div_ceil(max_parts).div_ceil(min_part) * min_part;
1878        }
1879        let mut parts = vec![];
1880        let mut off = 0u64;
1881        while off < size {
1882            let len = chunk.min(size - off);
1883            parts.push(flint_store::PartSource::Local { offset: off, len });
1884            off += len;
1885        }
1886
1887
1888        // A fresh handle: the Complete lands on a key nobody else writes
1889        // (design 2026-09-19, R1). The one thing that can already sit
1890        // there is this barrier's OWN Complete whose response was lost —
1891        // a torn response — and the recogniser is the bytes' checksum:
1892        // the same, cite them (re-read under the fence first, like every
1893        // citation); different, nothing minted this key but us, so the
1894        // barrier refuses rather than guess. The 412 arms this replaced
1895        // — adopt-own through the crash journal, preserve-a-foreign-
1896        // version, park — arbitrated a slot that no longer exists.
1897        let spec = flint_store::ComposeSpec {
1898            progress: None,
1899            key,
1900            local: Some(file),
1901            parts: parts.clone(),
1902            base_key: None,
1903            base_etag: None,
1904            condition: PutCondition::IfNoneMatchAny,
1905            stamps: stamps.clone(),
1906            crc64: None,
1907        };
1908        match self.store.compose_generation(&spec).await {
1909            Ok(meta) => {
1910                // The store computed it; refuse to cite bytes nothing
1911                // vouched for rather than invent a number for the
1912                // manifest. A backend that returns no checksum has not
1913                // validated the publish server-side either.
1914                let crc = meta
1915                    .crc64_b64
1916                    .as_deref()
1917                    .and_then(flint_store::crc64_from_b64)
1918                    .ok_or_else(|| {
1919                        LeanError::State(format!(
1920                            "compose of {key} returned no full-object checksum — refusing to \
1921                             cite bytes nothing vouched for"
1922                        ))
1923                    })?;
1924                Ok(UploadOutcome::published(
1925                    path, key.to_string(), meta.etag, crc, size, scanned, generation, epoch,
1926                ))
1927            }
1928            Err(StoreError::ChecksumMismatch(_)) | Err(StoreError::NoSuchUpload(_)) => {
1929                // Drift mid-compose, or the operator sweep aborted us:
1930                // nothing published; the next barrier re-queues.
1931                Ok(UploadOutcome::Deferred)
1932            }
1933            Err(StoreError::PreconditionFailed(_)) => {
1934                // The torn-response recogniser compares OUR bytes' checksum
1935                // against what landed, and without the pre-pass we do not
1936                // have one — so the file is read HERE, on the rare recovery
1937                // path, instead of on every publish.
1938                let crc = file_crc(&self.cfg.root, path)?;
1939                let head = self.store.head(key).await?;
1940                if head.crc64_b64.as_deref() == Some(crc64_to_b64(crc).as_str()) {
1941                    let g = GenerationStamps::from_meta(&head.meta).map(|s| s.generation).unwrap_or(generation);
1942                    return Ok(UploadOutcome::adopted(
1943                        path, key.to_string(), head.etag, crc, head.size, scanned, g, epoch,
1944                    ));
1945                }
1946                Err(LeanError::State(format!(
1947                    "handle {key} already holds different bytes (etag {}): a handle is minted once \
1948                     per flush, so something other than this barrier wrote it; refusing",
1949                    head.etag
1950                )))
1951            }
1952            Err(e) => Err(e.into()),
1953        }
1954    }
1955
1956
1957    async fn upload_one(
1958        &self,
1959        path: &str,
1960        scanned: &scan::ScanEntry,
1961        base: Option<&BaselineEntry>,
1962        epoch: u64,
1963        flush_uuid: &str,
1964    ) -> LeanResult<UploadOutcome> {
1965        let key = self.cfg.handle_key(path, flush_uuid);
1966        let local_path = self.cfg.root.join(path);
1967        let generation = base.map(|b| b.generation + 1).unwrap_or(1);
1968        // Review 2026-09-12, atomicity-6: the scan skipped symlinks; this
1969        // read followed them. A regular file swapped for a symlink between
1970        // the two published the link's TARGET — a file outside the
1971        // workspace, the syncer's own /proc/self/environ included. Every
1972        // stat here is an lstat and the read opens O_NOFOLLOW; a path that
1973        // is no longer a regular file is refused, recorded, and deferred
1974        // (the next scan skips it, and the two-scan rule retires it).
1975        //
1976        // Review 2026-09-18, H5: and not only the last component. A
1977        // DIRECTORY on the path swapped for a link (`mv d d.bak; ln -s
1978        // /proc/self d`) made `d/environ` a regular file outside the
1979        // workspace; the lstat and the O_NOFOLLOW open guarded the last
1980        // component only. The file is opened by a walk that follows no
1981        // link at any depth, and everything below — the stamps, the
1982        // size, the bytes — comes from that one descriptor.
1983        consume_window("before-upload", path);
1984        let file = match super::safefs::open_beneath_nofollow(&self.cfg.root, path) {
1985            Ok(f) => f,
1986            Err(e)
1987                if e.kind() == std::io::ErrorKind::InvalidInput
1988                    || matches!(e.raw_os_error(), Some(libc::ELOOP) | Some(libc::ENOTDIR)) =>
1989            {
1990                self.state.append_conflict(&ConflictRecord {
1991                    path: path.to_string(),
1992                    foreign_etag: String::new(),
1993                    preserved_key: None,
1994                    kind: format!(
1995                        "upload-refused-not-regular: no longer a regular file inside the workspace after \
1996                         the scan (a symlink or special file replaced it or a directory above it: {e}); \
1997                         nothing published"
1998                    ),
1999                    at_unix: now_unix(),
2000                })?;
2001                return Ok(UploadOutcome::Deferred);
2002            }
2003            Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => return Ok(UploadOutcome::Unreadable),
2004            Err(e) => return Err(LeanError::State(format!("stat {}: {e}", local_path.display()))),
2005        };
2006        let lmeta = file
2007            .metadata()
2008            .map_err(|e| LeanError::State(format!("stat {}: {e}", local_path.display())))?;
2009        let posix = Some(PosixStamps::from_metadata(&lmeta));
2010        let stamps = GenerationStamps {
2011            generation,
2012            epoch,
2013            flush_uuid: flush_uuid.to_string(),
2014            boundary_source: None,
2015            posix,
2016        };
2017        let size = lmeta.len();
2018        if size > self.cfg.whole_put_max {
2019            // Streaming multipart compose: put_whole is never fed past
2020            // whole_put_max (unbounded memory + S3's 5 GiB wall).
2021            return self
2022                .upload_compose(path, &key, std::sync::Arc::new(file), size, scanned, stamps, generation, epoch)
2023                .await;
2024        }
2025        // The upload byte gate — the store's own, shared with the compose
2026        // arm above, which charges its parts inside the store. This body
2027        // is charged from before it is read (the read is the allocation)
2028        // until its PUT, retried or not, has returned: the permit lives
2029        // to the end of this function. `None` = a store with no gate.
2030        let _upload_permit = match self.store.upload_gate() {
2031            Some(g) => Some(g.acquire(size).await),
2032            None => None,
2033        };
2034        // `Bytes::from(Vec<u8>)` TAKES OWNERSHIP without copying, so the
2035        // old `Bytes::from(body.clone())` was a full memcpy of every
2036        // published file body — bought solely to leave `body` intact for
2037        // the 412 retry below, which is reached almost never. Build the
2038        // `Bytes` once; the retry gets a refcount clone.
2039        let body = Bytes::from({
2040            use std::io::Read;
2041            let mut v = Vec::with_capacity(size as usize);
2042            (&file).read_to_end(&mut v)?;
2043            v
2044        });
2045        let uploaded_len = body.len() as u64;
2046        let crc = crc64_nvme(&body);
2047        // A fresh handle (design 2026-09-19, R1): no If-Match, because
2048        // nothing else ever writes this key. The one thing that can sit
2049        // there is this barrier's OWN PUT whose response was lost, and
2050        // the bytes' checksum recognises it: the same, cite it (re-read
2051        // under the fence first, like every citation); different, nothing
2052        // minted this key but us, so refuse rather than guess. The 412
2053        // policy this replaced — adopt-own, preserve-then-supersede a
2054        // foreign version, park, the 404-on-If-Match create arm —
2055        // arbitrated a slot that no longer exists; a foreign version is
2056        // now met at the CAS, the one place a citation is read (R7).
2057        match self
2058            .store
2059            .put_whole(&key, body, &PutCondition::IfNoneMatchAny, &stamps, crc)
2060            .await
2061        {
2062            Ok(meta) => Ok(UploadOutcome::published(
2063                path, key, meta.etag, crc, uploaded_len, scanned, generation, epoch,
2064            )),
2065            Err(StoreError::PreconditionFailed(_)) => {
2066                let head = self.store.head(&key).await?;
2067                if head.crc64_b64.as_deref() == Some(crc64_to_b64(crc).as_str()) {
2068                    let g = GenerationStamps::from_meta(&head.meta).map(|s| s.generation).unwrap_or(generation);
2069                    return Ok(UploadOutcome::adopted(
2070                        path, key, head.etag, crc, head.size, scanned, g, epoch,
2071                    ));
2072                }
2073                Err(LeanError::State(format!(
2074                    "handle {key} already holds different bytes (etag {}): a handle is minted once \
2075                     per flush, so something other than this barrier wrote it; refusing",
2076                    head.etag
2077                )))
2078            }
2079            Err(e) => Err(e.into()),
2080        }
2081    }
2082}
2083
2084enum UploadOutcome {
2085    /// `adopted`: the etag was OBSERVED at the handle (bytes already
2086    /// there, no PUT of ours produced them — a torn response), so the
2087    /// commit section re-reads it before citing, as it does every
2088    /// citation.
2089    Published { entry: LeanEntry, baseline_entry: BaselineEntry, adopted: bool },
2090    /// The source drifted mid-transfer (checksum refused server-side)
2091    /// or the assembly was swept: publish nothing, advance nothing —
2092    /// the next scan re-queues the path.
2093    Deferred,
2094    /// The syncer may not open the file (EACCES or EPERM): held
2095    /// (`scan::Scan::held`), and nothing advanced, so the next scan asks
2096    /// again.
2097    Unreadable,
2098}
2099
2100impl UploadOutcome {
2101    #[allow(clippy::too_many_arguments)]
2102    fn published(
2103        path: &str,
2104        key: String,
2105        etag: String,
2106        crc: u64,
2107        uploaded_len: u64,
2108        scanned: &scan::ScanEntry,
2109        generation: u64,
2110        epoch: u64,
2111    ) -> UploadOutcome {
2112        let _ = path;
2113        UploadOutcome::Published {
2114            adopted: false,
2115            entry: LeanEntry {
2116                key: key.clone(),
2117                etag: etag.clone(),
2118                crc64_b64: crc64_to_b64(crc),
2119                // The length the object HAS (review 2026-09-12,
2120                // atomicity-1): the scanned size was cited while the
2121                // body was read fresh, so a file that grew between the
2122                // two was cited short and every fresh checkout of it
2123                // failed its CRC fold — the successor never started.
2124                size: uploaded_len,
2125                mode: scanned.mode,
2126                mtime_unix: scanned.mtime_unix,
2127                generation,
2128                epoch,
2129            },
2130            baseline_entry: BaselineEntry {
2131                etag,
2132                key: Some(key),
2133                generation,
2134                // The PRE-read stat: if the agent wrote during our read,
2135                // the next scan sees the drift and re-queues (the
2136                // re-stat/re-queue valve).
2137                size: scanned.size,
2138                mtime_unix: scanned.mtime_unix,
2139                mtime_nanos: Some(scanned.mtime_nanos),
2140                // The mode the entry above cites.
2141                mode: Some(scanned.mode & scan::PERM_BITS),
2142                crc64_b64: Some(crc64_to_b64(crc)),
2143                // Measured from the SCAN's stat, which precedes the read:
2144                // an upload can take longer than the window, and the
2145                // moment that matters is when the bytes were read.
2146                racy: scan::racy(scanned.mtime_unix, scanned.mtime_nanos, scanned.seen_nanos),
2147            },
2148        }
2149    }
2150}
2151
2152impl UploadOutcome {
2153    /// `published`, for bytes this barrier found already at the key and
2154    /// cites without a PUT of its own.
2155    #[allow(clippy::too_many_arguments)]
2156    fn adopted(
2157        path: &str,
2158        key: String,
2159        etag: String,
2160        crc: u64,
2161        uploaded_len: u64,
2162        scanned: &scan::ScanEntry,
2163        generation: u64,
2164        epoch: u64,
2165    ) -> UploadOutcome {
2166        match UploadOutcome::published(path, key, etag, crc, uploaded_len, scanned, generation, epoch) {
2167            UploadOutcome::Published { entry, baseline_entry, .. } => {
2168                UploadOutcome::Published { entry, baseline_entry, adopted: true }
2169            }
2170            other => other,
2171        }
2172    }
2173}
2174
2175/// The paths THIS barrier must withhold because the document would cite a
2176/// file and something under it (a published file replaced by a directory,
2177/// or the reverse). Whichever side is this barrier's upsert yields; if both
2178/// are, so does the parent.
2179///
2180/// Every key under `p/` sits in one contiguous run of the sorted map, so a
2181/// range lookup finds them: O(N log N). The first version scanned every
2182/// key for every key, and a 200k-path commit spent 80 s here (bench
2183/// 2026-09-25). The run does NOT start right after `p`: `p-b` and `p.txt`
2184/// sort between `p` and `p/`.
2185pub(super) fn path_clashes<V, U>(
2186    cited: &BTreeMap<String, V>,
2187    upserts: &BTreeMap<String, U>,
2188) -> Vec<String> {
2189    let mut mine: Vec<String> = vec![];
2190    for p in cited.keys() {
2191        let pfx = format!("{p}/");
2192        let mut under = cited.range::<str, _>((std::ops::Bound::Included(pfx.as_str()), std::ops::Bound::Unbounded))
2193            .map(|(k, _)| k)
2194            .take_while(|q| q.starts_with(&pfx))
2195            .peekable();
2196        if under.peek().is_none() {
2197            continue;
2198        }
2199        if upserts.contains_key(p) {
2200            mine.push(p.clone());
2201        } else if let Some(c) = under.find(|q| upserts.contains_key(*q)) {
2202            mine.push(c.clone());
2203        }
2204    }
2205    mine.sort();
2206    mine.dedup();
2207    mine
2208}
2209
2210/// `path_clashes` over a view and the overlay a merge made of it (lever G,
2211/// latency plan §12): the same answer, from each upsert's own neighbourhood
2212/// rather than every cited path. An upsert with anything cited under it is
2213/// mine to withhold; so, for each cited ancestor that is not an upsert, is
2214/// the first upsert under it (the whole-document rule takes one a round,
2215/// and the caller re-merges for the rest).
2216pub(super) async fn clashes_view(
2217    theirs: &mut DocView,
2218    overlay: &BTreeMap<String, Option<LeanEntry>>,
2219    upserts: &BTreeMap<String, LeanEntry>,
2220) -> LeanResult<Vec<String>> {
2221    async fn cites(theirs: &mut DocView, overlay: &BTreeMap<String, Option<LeanEntry>>, p: &str) -> LeanResult<bool> {
2222        match overlay.get(p) {
2223            Some(v) => Ok(v.is_some()),
2224            None => Ok(theirs.get(p).await?.is_some()),
2225        }
2226    }
2227    let under = |pfx: &str| {
2228        overlay
2229            .range::<str, _>((std::ops::Bound::Included(pfx), std::ops::Bound::Unbounded))
2230            .take_while(|(k, _)| k.starts_with(pfx))
2231            .any(|(_, v)| v.is_some())
2232    };
2233    let mut mine: Vec<String> = vec![];
2234    for u in upserts.keys() {
2235        if !cites(theirs, overlay, u).await? {
2236            continue;
2237        }
2238        let pfx = format!("{u}/");
2239        if under(&pfx) || theirs.first_under(&pfx, &|k| overlay.contains_key(k)).await?.is_some() {
2240            mine.push(u.clone());
2241        }
2242        let mut cut = 0;
2243        while let Some(i) = u[cut..].find('/') {
2244            let a = &u[..cut + i];
2245            cut += i + 1;
2246            if upserts.contains_key(a) || !cites(theirs, overlay, a).await? {
2247                continue;
2248            }
2249            let apfx = format!("{a}/");
2250            let first = upserts
2251                .range::<str, _>((std::ops::Bound::Included(apfx.as_str()), std::ops::Bound::Unbounded))
2252                .take_while(|(k, _)| k.starts_with(&apfx))
2253                .map(|(k, _)| k)
2254                .find(|k| matches!(overlay.get(k.as_str()), Some(Some(_))));
2255            if let Some(q) = first {
2256                mine.push(q.clone());
2257            }
2258        }
2259    }
2260    mine.sort();
2261    mine.dedup();
2262    Ok(mine)
2263}
2264
2265/// The shadow check of a chunk-view merge (tests; latency plan §12,
2266/// "Verification"): the overlay over theirs is the whole-document merge,
2267/// and so are the tombstones, every per-path decision and the clashes.
2268fn shadow_merge(
2269    full: &MergeOnto,
2270    mc: &manifest::MergedChanges,
2271    clash: &[String],
2272    upserts: &BTreeMap<String, LeanEntry>,
2273) {
2274    let mut applied = full.theirs.entries.clone();
2275    for (p, v) in &mc.overlay {
2276        match v {
2277            Some(e) => {
2278                applied.insert(p.clone(), e.clone());
2279            }
2280            None => {
2281                applied.remove(p);
2282            }
2283        }
2284    }
2285    assert!(applied == full.merged.entries, "lever G: the overlay over theirs is not the whole-document merge");
2286    assert_eq!(mc.seq, full.merged.seq, "lever G: the merged seq");
2287    assert_eq!(mc.tombstones, full.merged.tombstones, "lever G: the merged tombstones");
2288    assert_eq!(mc.overridden, full.overridden, "lever G: overridden");
2289    assert_eq!(mc.recreated, full.recreated, "lever G: recreated");
2290    assert_eq!(mc.over_theirs, full.over_theirs, "lever G: over_theirs");
2291    assert_eq!(clash, path_clashes(&full.merged.entries, upserts).as_slice(), "lever G: the path clashes");
2292}
2293
2294/// The shadow check of a chunk-view write: the chunk list is `split_with`
2295/// of the merged entries, ref for ref and byte for byte, and the retire set
2296/// is the whole-document one.
2297fn shadow_write(full: &MergeOnto, chunks: &[(super::chunk::ChunkRef, Option<Vec<u8>>)], retired: &[String], cfg: &super::LeanConfig) {
2298    let want = super::chunk::split_with(&full.merged.entries, cfg.chunk_target, cfg.chunk_min, cfg.chunk_max)
2299        .expect("the shadow split");
2300    let got: Vec<&super::chunk::ChunkRef> = chunks.iter().map(|(r, _)| r).collect();
2301    let want_refs: Vec<&super::chunk::ChunkRef> = want.iter().map(|(r, _)| r).collect();
2302    assert_eq!(got, want_refs, "lever G: the re-chunk is not split_with of the merged entries");
2303    for ((r, body), (_, w)) in chunks.iter().zip(&want) {
2304        if let Some(b) = body {
2305            assert!(b == w, "lever G: chunk {} encodes differently from split_with", r.addr);
2306        }
2307    }
2308    let whole = CommitDocs::Full { theirs: full.theirs.clone(), installed: full.merged.clone() }.retired();
2309    assert_eq!(retired, whole.as_slice(), "lever G: the retire set");
2310}
2311
2312/// H10: what an install carried of the standing declaration, kept for the
2313/// ack: the paths it published and deleted, the bytes it published, and
2314/// what it dropped. `cites(p)`: the installed document cites `p`. Both
2315/// commit paths, the whole document's and lever G's view, record here.
2316fn note_carried(
2317    intent: &mut IntentJournal,
2318    id: &String,
2319    classified: &scan::Classified,
2320    upserts: &BTreeMap<String, LeanEntry>,
2321    report: &BarrierReport,
2322    cites: impl Fn(&str) -> bool,
2323) {
2324    let mut c = intent
2325        .carrier
2326        .take()
2327        .filter(|c| &c.pending_id == id)
2328        .unwrap_or_else(|| Carrier { pending_id: id.clone(), ..Default::default() });
2329    c.paths.extend(classified.uploads.iter().chain(&classified.deletes).cloned());
2330    // L-136: an earlier carry's delete that this one
2331    // uploads is an upload again, so a peer's delete
2332    // of it is named (`note_carrier_uncited`).
2333    // L-137: the bytes it published, so a peer's
2334    // replacement of them is named too.
2335    for p in &classified.uploads {
2336        c.deletes.remove(p);
2337        match upserts.get(p) {
2338            Some(e) => c.published.insert(p.clone(), e.crc64_b64.clone()),
2339            None => c.published.remove(p),
2340        };
2341    }
2342    for p in &classified.deletes {
2343        c.published.remove(p);
2344    }
2345    c.deletes.extend(classified.deletes.iter().cloned());
2346    // `honor_publish`'s dropped set, decided here:
2347    // parks and deferrals are settled before the
2348    // CAS, and an outranked delete is one the merged
2349    // document still cites. Since M3 no delete is
2350    // outranked (only uploads park, and the merge
2351    // removes every unparked delete: `merge_applies_
2352    // a_local_delete_and_names_the_version_it_
2353    // deleted_over`), so the second clause is a
2354    // backstop no barrier reaches.
2355    c.dropped.extend(report.parked.iter().chain(&report.deferred).chain(&report.held).cloned());
2356    c.dropped.extend(classified.deletes.iter().filter(|p| cites(p.as_str())).cloned());
2357    intent.carrier = Some(c);
2358}
2359
2360/// Says what a barrier held, on every barrier that holds anything: until
2361/// the tree's owner makes it readable (or renames a name that is not
2362/// UTF-8), nothing there reaches the bucket and no boundary is complete,
2363/// and the operator has to be told which paths. Ten at most; the report
2364/// carries them all.
2365fn note_held(held: &[String]) {
2366    const SHOWN: usize = 10;
2367    let more = held.len().saturating_sub(SHOWN);
2368    eprintln!(
2369        "flint-sync: held {} path(s) the syncer may not read (EACCES or EPERM) or name (not UTF-8, \
2370         shown with U+FFFD): {:?}{} — nothing at or \
2371         under them is published or deleted until they are readable and named in UTF-8; the rest of the tree is",
2372        held.len(),
2373        &held[..held.len().min(SHOWN)],
2374        if more > 0 { format!(" and {more} more") } else { String::new() },
2375    );
2376}
2377
2378pub(super) fn local_dirty(local: &Path, base: Option<&BaselineEntry>) -> bool {
2379    match (std::fs::metadata(local), base) {
2380        (Err(_), None) => false,                   // both absent: clean
2381        (Err(_), Some(_)) => true,                 // locally deleted vs baseline
2382        (Ok(_), None) => true,                     // local exists, never published
2383        (Ok(m), Some(b)) => {
2384            scan::stat_changed(b, m.len(), mtime_of(&m), mtime_nanos_of(&m), mode_of(&m))
2385                // RACY CLEAN: an unchanged stat recorded inside the window
2386                // is believed only if the bytes agree (`scan::racy`).
2387                || (b.racy && racy_bytes_differ(local, b))
2388        }
2389    }
2390}
2391
2392/// `local_dirty`'s content check for a racy entry: dirty unless `local`
2393/// is a regular file (not a link) whose CRC is the one the baseline
2394/// integrated. Anything that cannot be read to tell is dirty.
2395fn racy_bytes_differ(local: &Path, b: &BaselineEntry) -> bool {
2396    use std::io::Read;
2397    if !std::fs::symlink_metadata(local).is_ok_and(|m| m.is_file()) {
2398        return true;
2399    }
2400    let Ok(mut f) = std::fs::File::open(local) else { return true };
2401    let mut crc = flint_store::Crc64Nvme::new();
2402    let mut buf = vec![0u8; 1 << 20];
2403    loop {
2404        match f.read(&mut buf) {
2405            Ok(0) => break,
2406            Ok(n) => crc.update(&buf[..n]),
2407            Err(_) => return true,
2408        }
2409    }
2410    b.crc64_b64.as_deref() != Some(crc64_to_b64(crc.finalize()).as_str())
2411}
2412
2413pub(super) fn file_crc(root: &Path, rel: &str) -> LeanResult<u64> {
2414    use std::io::Read;
2415    let mut crc = flint_store::Crc64Nvme::new();
2416    // No link anywhere on the path (H5): this hashes what a 412 recovery
2417    // is about to call "already there".
2418    let mut f = super::safefs::open_beneath_nofollow(root, rel)?;
2419    let mut buf = vec![0u8; 4 << 20];
2420    loop {
2421        let n = f.read(&mut buf)?;
2422        if n == 0 {
2423            break;
2424        }
2425        crc.update(&buf[..n]);
2426    }
2427    Ok(crc.finalize())
2428}
2429
2430/// Whether a stat taken just now is racy (`scan::racy`): every site that
2431/// records a baseline entry from a fresh stat of bytes it just read or
2432/// wrote stamps the entry with this.
2433pub(super) fn racy_stat(m: &std::fs::Metadata) -> bool {
2434    scan::racy(mtime_of(m), mtime_nanos_of(m), scan::now_nanos())
2435}
2436
2437pub(super) fn mtime_nanos_of(m: &std::fs::Metadata) -> u32 {
2438    m.modified()
2439        .ok()
2440        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
2441        .map(|d| d.subsec_nanos())
2442        .unwrap_or(0)
2443}
2444
2445pub(super) fn mtime_of(m: &std::fs::Metadata) -> i64 {
2446    m.modified()
2447        .ok()
2448        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
2449        .map(|d| d.as_secs() as i64)
2450        .unwrap_or(0)
2451}
2452
2453/// A file's `st_mode`, as the walk reports it (`scan::stat_changed`).
2454pub(super) fn mode_of(m: &std::fs::Metadata) -> u32 {
2455    #[cfg(unix)]
2456    {
2457        use std::os::unix::fs::MetadataExt;
2458        m.mode()
2459    }
2460    #[cfg(not(unix))]
2461    {
2462        let _ = m;
2463        0o644
2464    }
2465}
2466
2467/// Validate `rel` under `root` WITHOUT creating anything — for callers
2468/// that must decide whether a path is writable before doing any work
2469/// (inbox consume refuses a refused path outright rather than routing it
2470/// through conflict-preserve, which would cost a GET+PUT and leave the
2471/// path in the baseline).
2472pub(super) fn check_contained(root: &Path, rel: &str) -> LeanResult<()> {
2473    resolve_contained(root, rel, false).map(|_| ())
2474}
2475
2476/// Resolve `rel` under `root`, refusing escapes, creating missing parent
2477/// directories. Walks the parent chain component by component: a
2478/// component that EXISTS as a symlink is a refusal (never followed).
2479pub(super) fn contained_path(root: &Path, rel: &str) -> LeanResult<std::path::PathBuf> {
2480    resolve_contained(root, rel, true).map(|(p, _)| p)
2481}
2482
2483/// As `contained_path`, and also what the walk already learned about
2484/// the FINAL component: its `lstat` if it exists, `None` if it does
2485/// not. The walk stats every component to refuse a symlink; a caller
2486/// that then asked `exists()` and `metadata()` paid two more stats for
2487/// an answer it was already holding (checkout's resume check did, on
2488/// every one of 20,000 files).
2489pub(super) fn contained_path_stat(
2490    root: &Path,
2491    rel: &str,
2492) -> LeanResult<(std::path::PathBuf, Option<std::fs::Metadata>)> {
2493    resolve_contained(root, rel, true)
2494}
2495
2496fn resolve_contained(
2497    root: &Path,
2498    rel: &str,
2499    create_dirs: bool,
2500) -> LeanResult<(std::path::PathBuf, Option<std::fs::Metadata>)> {
2501    use std::path::Component;
2502    let refuse = |why: &str| {
2503        Err(LeanError::State(format!(
2504            "refusing write to {rel:?}: {why} (containment)"
2505        )))
2506    };
2507    if rel.is_empty() {
2508        return refuse("empty path");
2509    }
2510    if super::scan::is_control_path(rel) {
2511        // The reserved namespace is the syncer's own; a citation or
2512        // inbox entry naming it is surfaced, never materialized (D0.3).
2513        return refuse("reserved control namespace");
2514    }
2515    if rel.split('/').any(|seg| seg.ends_with(super::scan::TMP_SUFFIX)) {
2516        // Review 2026-09-18, C1: the walk skips this suffix at every
2517        // depth (the consume's own temp sibling, atomicity-7), so a file
2518        // materialised under it is absent from every scan — cited once
2519        // by the repair as a first absence, then classified a delete and
2520        // its object collected. Surfaced and never materialised, like the
2521        // control namespace; the gateway refuses the name at the door.
2522        return refuse("a name reserved for the syncer's temporary files");
2523    }
2524    let relp = Path::new(rel);
2525    let mut cur = root.to_path_buf();
2526    let mut comps: Vec<&std::ffi::OsStr> = vec![];
2527    for c in relp.components() {
2528        match c {
2529            Component::Normal(n) => comps.push(n),
2530            Component::CurDir => {}
2531            Component::ParentDir => return refuse("`..` component"),
2532            Component::RootDir | Component::Prefix(_) => return refuse("absolute path"),
2533        }
2534    }
2535    if comps.is_empty() {
2536        return refuse("no path components");
2537    }
2538    let last = comps.len() - 1;
2539    let mut final_meta = None;
2540    for (i, c) in comps.iter().enumerate() {
2541        cur.push(c);
2542        match std::fs::symlink_metadata(&cur) {
2543            Ok(m) if m.file_type().is_symlink() => {
2544                return refuse("path traverses a symlink");
2545            }
2546            Ok(m) if i < last && !m.is_dir() => {
2547                return refuse("parent component is not a directory");
2548            }
2549            Ok(m) => {
2550                if i == last {
2551                    final_meta = Some(m);
2552                }
2553            }
2554            Err(_) if i < last => {
2555                if create_dirs {
2556                    if let Err(e) = std::fs::create_dir(&cur) {
2557                        // A SIBLING may have created it between the stat
2558                        // above and this mkdir: fetches run on their own
2559                        // tasks, so the first files of one directory
2560                        // race here, and the loser's EEXIST is not a
2561                        // containment refusal. It is fine only if what
2562                        // exists now is a plain directory — a symlink
2563                        // that appeared in the window is refused exactly
2564                        // as one the stat found.
2565                        match std::fs::symlink_metadata(&cur) {
2566                            Ok(m) if m.file_type().is_symlink() => {
2567                                return refuse("path traverses a symlink");
2568                            }
2569                            Ok(m) if m.is_dir() => {}
2570                            _ => {
2571                                return Err(LeanError::State(format!(
2572                                    "mkdir {}: {e}",
2573                                    cur.display()
2574                                )))
2575                            }
2576                        }
2577                    }
2578                }
2579            }
2580            Err(_) => {}
2581        }
2582    }
2583    Ok((cur, final_meta))
2584}
2585
2586/// Write `bytes` to `path` through an exclusive temp sibling and a
2587/// rename. The parent already exists — containment created it — and
2588/// one that vanished since is recreated on the retry path in `safefs`,
2589/// so the common path pays no `mkdir` and no `stat` for it. Returns
2590/// the written file's metadata.
2591pub(super) fn write_file_atomic(
2592    path: &Path,
2593    bytes: &[u8],
2594    mode: Option<u32>,
2595) -> LeanResult<std::fs::Metadata> {
2596    super::safefs::check_parent(path)?;
2597    // NOT with_extension(): that REPLACES the final extension, so
2598    // "a.txt" and "a.md" would collide on one tmp name.
2599    let tmp = path.with_file_name(format!(
2600        "{}.flint-sync-tmp",
2601        path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default()
2602    ));
2603    // The temp sibling is computed AFTER containment ran, so the walk
2604    // never saw it: it needs its own refusal, not a plain write.
2605    // FAST (no per-file fsync): materialisations are made durable by
2606    // `sync_tree` before the marker or baseline that vouches for them.
2607    super::safefs::write_via_tmp_fast(path, &tmp, bytes, mode)
2608}
2609
2610/// `write_file_atomic`, renamed over `path` only if `proceed()` holds once
2611/// the temp is written (`safefs::write_via_tmp_fast_if`).
2612pub(super) fn write_file_atomic_if(
2613    path: &Path,
2614    bytes: &[u8],
2615    mode: Option<u32>,
2616    proceed: &dyn Fn() -> bool,
2617) -> LeanResult<Option<std::fs::Metadata>> {
2618    super::safefs::check_parent(path)?;
2619    let tmp = path.with_file_name(format!(
2620        "{}.flint-sync-tmp",
2621        path.file_name().map(|n| n.to_string_lossy()).unwrap_or_default()
2622    ));
2623    super::safefs::write_via_tmp_fast_if(path, &tmp, bytes, mode, proceed)
2624}
2625
2626// Test-only: run a closure inside a write window for one path —
2627// `"before-write"` (the licence to overwrite checked, the temp not yet
2628// written), `"after-rename"` (before the baseline records the write), or
2629// `"before-delete"` (`sync`'s remote-delete arm). The consume and `sync`
2630// both open them. Nothing in either window calls the store, so the hooked double
2631// cannot reach them (review 2026-09-18, H7).
2632#[cfg(test)]
2633thread_local! {
2634    pub(super) static CONSUME_WINDOW_HOOK: std::cell::RefCell<Option<(String, &'static str, Box<dyn Fn()>)>> =
2635        std::cell::RefCell::new(None);
2636}
2637
2638#[cfg(test)]
2639pub(super) fn consume_window(stage: &'static str, path: &str) {
2640    let hook = CONSUME_WINDOW_HOOK.with(|h| {
2641        let mut h = h.borrow_mut();
2642        if matches!(&*h, Some((p, s, _)) if p == path && *s == stage) {
2643            h.take().map(|(_, _, f)| f)
2644        } else {
2645            None
2646        }
2647    });
2648    if let Some(f) = hook {
2649        f();
2650    }
2651}
2652
2653#[cfg(not(test))]
2654#[inline(always)]
2655pub(super) fn consume_window(_: &'static str, _: &str) {}
2656
2657/// How many merge races a commit may lose before its barrier gives up (the
2658/// next tick tries again). Under P2 the UI saves without the lease (G1), so
2659/// the races are lost to saves, and backing off finds the gaps between them.
2660const CAS_ATTEMPTS: u32 = 12;
2661
2662/// The wait after the `attempt`-th lost race: 10 ms, doubling, capped at
2663/// 640 ms, plus up to half again of jitter from the clock's nanoseconds, so
2664/// a writer and a stream of saves do not fall into lockstep.
2665fn cas_backoff(attempt: u32) -> std::time::Duration {
2666    let base = 10u64 << attempt.saturating_sub(1).min(6);
2667    let nanos = std::time::SystemTime::now()
2668        .duration_since(std::time::UNIX_EPOCH)
2669        .map(|d| d.subsec_nanos() as u64)
2670        .unwrap_or(0);
2671    std::time::Duration::from_millis(base + nanos % (base / 2 + 1))
2672}
2673
2674
2675/// The merge base, P1-lite: the baseline's own citations. A tree holds no
2676/// version the document never cited (P2), so what it integrated IS what it
2677/// merges from; one base serves the merge and every reader of its outcome
2678/// (M5, L-124).
2679fn merge_base(baseline: &super::state::Baseline) -> BTreeMap<String, String> {
2680    baseline.entries.iter().map(|(p, e)| (p.clone(), e.etag.clone())).collect()
2681}
2682
2683/// One three-way merge of a barrier's changes onto the bucket's manifest.
2684struct MergeOnto {
2685    theirs: manifest::LeanManifest,
2686    /// The CAS handle over `theirs`; `None` when the bucket has no manifest.
2687    expected: Option<manifest::ManifestHandle>,
2688    merged: manifest::LeanManifest,
2689    /// Other writers' changes since the merge base that the tree lacks.
2690    foreign: Vec<(String, LeanEntry)>,
2691    /// Theirs' version at a path this barrier uploaded, changed since the
2692    /// merge base: what mine publishes OVER. The caller surfaces those it
2693    /// never integrated (design 2026-09-19, R7).
2694    overridden: Vec<(String, LeanEntry)>,
2695    /// Theirs' DELETES this barrier's uploads publish over (`merge`).
2696    recreated: Vec<(String, String)>,
2697    /// Theirs' version at a path this barrier DELETES, changed since the
2698    /// merge base (M3): the caller preserves and records each.
2699    over_theirs: Vec<(String, LeanEntry)>,
2700}
2701
2702impl MergeOnto {
2703    /// The merge adds nothing to the document: installing it would be an
2704    /// empty generation.
2705    fn adds_nothing(&self) -> bool {
2706        self.merged.entries == self.theirs.entries && self.merged.sole_writer == self.theirs.sole_writer
2707    }
2708}
2709
2710impl Syncer {
2711    /// H10: the carried uploads the document this barrier ended at does not
2712    /// cite (a peer deleted them since). The ack names that document, so it
2713    /// reads this; no request is made for it.
2714    /// The paths the standing carrier record names (H10): all that
2715    /// `note_carrier_uncited` reads of a document.
2716    fn carried_paths(&self) -> LeanResult<Vec<String>> {
2717        Ok(self.state.load_intent()?.carrier.map(|c| c.paths.into_iter().collect()).unwrap_or_default())
2718    }
2719
2720    /// A DOCUMENT MUST BE MATERIALISABLE: each upload `clash` names is
2721    /// withheld this barrier, recorded, and stays dirty for the next (the
2722    /// caller's comment at `path_clashes`). Both commit paths withhold here.
2723    #[allow(clippy::too_many_arguments)]
2724    fn withhold_clashes(
2725        &self,
2726        clash: Vec<String>,
2727        upserts: &mut BTreeMap<String, LeanEntry>,
2728        new_baseline_entries: &mut BTreeMap<String, BaselineEntry>,
2729        report: &mut BarrierReport,
2730        parked: &mut BTreeSet<String>,
2731        flush_uuid: &str,
2732    ) -> LeanResult<()> {
2733        for path in clash {
2734            let gone = upserts.remove(&path).expect("named from upserts above");
2735            new_baseline_entries.remove(&path);
2736            report.uploaded.retain(|p| p != &path);
2737            report.published_bytes = report.published_bytes.saturating_sub(gone.size);
2738            parked.insert(path.clone());
2739            report.parked.push(path.clone());
2740            self.state.append_conflict(&ConflictRecord {
2741                path: path.clone(),
2742                foreign_etag: gone.etag,
2743                preserved_key: None,
2744                kind: "upload-withheld-path-clash: the document still cites a path \
2745                       this one would sit inside (or around) — a published file \
2746                       replaced by a directory, or the reverse. No workspace can \
2747                       hold both, so nothing is cited here until that citation \
2748                       goes; the path stays dirty and publishes next barrier"
2749                    .into(),
2750                at_unix: now_unix(),
2751            })?;
2752            self.trace("path_clash", serde_json::json!({"flush": flush_uuid, "path": path}));
2753        }
2754        Ok(())
2755    }
2756
2757    fn note_carrier_uncited(&self, doc: &manifest::LeanManifest) -> LeanResult<()> {
2758        let mut intent = self.state.load_intent()?;
2759        let Some(c) = intent.carrier.as_mut() else { return Ok(()) };
2760        let uncited: BTreeSet<String> = c
2761            .paths
2762            .iter()
2763            .filter(|p| {
2764                !c.deletes.contains(*p)
2765                    && match doc.entries.get(p.as_str()) {
2766                        None => true,
2767                        // L-137: other bytes at the path are not the upload
2768                        // this writer carried.
2769                        Some(e) => c.published.get(p.as_str()).is_some_and(|crc| *crc != e.crc64_b64),
2770                    }
2771            })
2772            .cloned()
2773            .collect();
2774        if uncited != c.uncited {
2775            c.uncited = uncited;
2776            self.state.save_intent(&intent)?;
2777        }
2778        Ok(())
2779    }
2780
2781    fn merge_onto(
2782        &self,
2783        current: Option<&manifest::LoadedManifest>,
2784        // `merge_base`'s answer, which the caller also gives every other
2785        // reader of this merge's outcome.
2786        base: &BTreeMap<String, String>,
2787        upserts: &BTreeMap<String, LeanEntry>,
2788        deletes: &BTreeSet<String>,
2789        parked: &BTreeSet<String>,
2790        flush_uuid: &str,
2791    ) -> MergeOnto {
2792        let (theirs, expected) = match current {
2793            // The handle carries the LAYOUT as well as the tag: a
2794            // workspace still on the legacy single object CASes a
2795            // pointer that must not exist yet, not one it read.
2796            Some(l) => (l.manifest.clone(), Some(l.handle())),
2797            None => (Default::default(), None),
2798        };
2799        let manifest::Merged { doc: mut merged, foreign, overridden, deletes: outcomes, recreated, over_theirs } =
2800            manifest::merge(base, &theirs, upserts, deletes, parked);
2801        // The deletes theirs outranked, named: the trace's record of the
2802        // merge's per-path decision (`TraceCore.tla` checks it against
2803        // `Install`'s `Foreign(s, p)`).
2804        // Since M3 no delete is outranked; the trace keeps the field (empty)
2805        // and names the deletes that went over theirs.
2806        let outranked: Vec<&String> = vec![];
2807        let deleted_over: Vec<&String> =
2808            outcomes.iter().filter(|(_, o)| **o == manifest::DeleteOutcome::OverTheirs).map(|(p, _)| p).collect();
2809        // Deletions another writer made since this workspace's
2810        // merge base: in the base, gone from theirs, and not this
2811        // barrier's own upsert, delete or park. `merge` has no use
2812        // for them — theirs already lacks the path — but the TREE
2813        // does: they reach it through the local queue, as the
2814        // foreign upserts do.
2815        let gone: Vec<(String, String)> = base
2816            .iter()
2817            .filter(|(p, _)| {
2818                !theirs.entries.contains_key(*p)
2819                    && !upserts.contains_key(*p)
2820                    && !deletes.contains(*p)
2821                    && !parked.contains(*p)
2822            })
2823            .map(|(p, retired)| (p.clone(), retired.clone()))
2824            .collect();
2825        // `merge` clears it; the installing pass owns it. A mirror
2826        // is a property of how this workspace is DEPLOYED, so it
2827        // comes from config on every publish rather than being
2828        // inherited from whatever wrote last.
2829        merged.sole_writer = self.cfg.sole_writer;
2830        self.trace("merge", serde_json::json!({"flush": flush_uuid, "theirs_seq": theirs.seq, "upserts": upserts.len(),
2831            "deletes": deletes.len(), "foreign": foreign.len(), "gone": gone.len(), "overridden": overridden.len(),
2832            "outranked": outranked, "over_theirs": deleted_over, "adds_nothing": merged.entries == theirs.entries}));
2833        MergeOnto { theirs, expected, merged, foreign, overridden, recreated, over_theirs }
2834    }
2835}