Skip to main content

atelier_sdk/
workspace.rs

1use std::env::{self, VarError};
2use std::fs;
3use std::panic::{AssertUnwindSafe, catch_unwind};
4use std::path::{Component, Path, PathBuf};
5use std::sync::mpsc::RecvTimeoutError;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use atelier_sdk_remote::RemoteFolder;
9use sha2::{Digest, Sha256};
10
11use atelier_sdk_diff::{
12    Address, Delta, DeltaKind, Diff, Fidelity, FormatPackage, PackageId, as_text, detect_package,
13    diff_lines,
14};
15use atelier_sdk_docx::DocxPackage;
16use notify::{Event, RecursiveMode, Watcher};
17
18use crate::config::{
19    Actor, InstructionFidelity, ROOT_MOUNT, Source, SourceKind, SyncPolicy, WorkspaceConfig,
20    read_workspace_config, resolve_actor, write_workspace_config,
21};
22use crate::coordination::{Coordination, LeaseClaim, RequestRow, SessionRow};
23use crate::engine::{
24    DiffSides, Engine, FileBlob, LADDER_FILE_SIZE_MAX, LandOutcome, Side, StepBack,
25};
26use crate::error::{Error, config_err, engine_err};
27use crate::journal::{Act, Journal, JournalEntry};
28use crate::landing::{
29    Approval, GateOutcome, Landing, LandingRequest, RequestId, RequestState, Restore,
30};
31use crate::projection::ProjectionCache;
32use crate::read::{ReadResult, window_size, window_text};
33use crate::session::{Instruction, Session, SessionId, SessionState, SourceChange};
34use crate::watch::{
35    STOP_TICK, WatchEvent, WatchStop, event_is_content, settle, watcher_failed, watcher_gone,
36};
37
38pub use crate::engine::Snapshot;
39
40const CONTROL_DIR: &str = ".atelier";
41const JOURNAL_FILE: &str = "journal.sqlite3";
42const SESSIONS_DIR: &str = "sessions";
43pub(crate) const SKIP_NAMES: [&str; 3] = [".atelier", ".jj", ".git"];
44
45/// The one scarce point of a workspace in v1: its landing point.
46const LANDING_LEASE_POINT: &str = "landing";
47/// The bookmark a landing moves when no adopted branch names one; exported
48/// as a git branch so plain `git push` carries the shared line.
49const LANDED_BOOKMARK: &str = "atelier";
50/// The outcome of one sync-back attempt (ADR-0010).
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub enum SyncOutcome {
53    /// The origin now mirrors the landed snapshot.
54    Synced {
55        /// The snapshot the origin now mirrors.
56        snapshot: String,
57    },
58    /// The origin changed out-of-band since the last recorded sync;
59    /// nothing was written. `atelier sync --force` overwrites deliberately.
60    Parked {
61        /// The landed snapshot that still waits to sync.
62        snapshot: String,
63    },
64}
65
66/// The remote handle was not opened for a remote target: unreachable by
67/// construction in `sync_source`, surfaced as an error because corrupt
68/// control flow must not write anywhere.
69fn unreachable_remote<T>() -> Result<T, Error> {
70    Err(Error::Engine(
71        "sync_source lost its remote handle; this is a bug".to_owned(),
72    ))
73}
74
75/// The outcome of one pull attempt (ADR-0012, R2).
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum PullOutcome {
78    /// The bucket's changes folded into the line as this snapshot.
79    Pulled {
80        /// The snapshot the fold produced.
81        snapshot: String,
82    },
83    /// The bucket already matches the last sync; nothing to fold.
84    Current,
85}
86
87/// Where a sync-back writes: a folder origin on this machine, or a
88/// bucket prefix behind the remote adapter (ADR-0012).
89enum SyncTarget {
90    Folder(PathBuf),
91    Remote(String),
92}
93
94/// How long a landing lease lives; a holder that dies mid-apply frees the
95/// point when this passes.
96const LANDING_LEASE_TTL_MS: i64 = 30_000;
97
98/// A named, versioned body of work content with its own histories and
99/// journal. The root engine is source zero; each mounted source carries
100/// its own engine and history (ADR-0009).
101pub struct Workspace {
102    root: PathBuf,
103    actor: Actor,
104    engine: Engine,
105    /// Mounted sources in mount-name order — the deterministic order every
106    /// aggregate read model and the landing fan-out walk them in.
107    mounts: Vec<MountedSource>,
108    journal: Journal,
109    coordination: Coordination,
110    packages: Vec<Box<dyn FormatPackage>>,
111    projections: ProjectionCache,
112}
113
114/// One mounted source: its name and the engine carrying its history.
115struct MountedSource {
116    name: String,
117    engine: Engine,
118    /// The adopted branch landings move; `None` falls back to
119    /// [`LANDED_BOOKMARK`].
120    branch: Option<String>,
121}
122
123impl Workspace {
124    /// Turn `path` into a workspace: control dir, journal, and engine store.
125    pub fn init(path: impl AsRef<Path>) -> Result<Self, Error> {
126        let root = path.as_ref().to_path_buf();
127        let actor = resolve_actor()?;
128
129        let control = root.join(CONTROL_DIR);
130        if control.exists() {
131            return Err(Error::WorkspaceExists(root));
132        }
133        if let Some(ancestor) = enclosing_workspace(&root) {
134            return Err(Error::NestedWorkspace(ancestor));
135        }
136
137        fs::create_dir_all(&control)?;
138        let engine = Engine::init(&root, &actor, &[])?;
139
140        let config = WorkspaceConfig::new(workspace_name(&root));
141        write_workspace_config(&control, &config)?;
142
143        let journal = Journal::open(&control.join(JOURNAL_FILE))?;
144        let coordination = Coordination::open(&control.join(JOURNAL_FILE))?;
145        let mounts = Vec::new();
146        let workspace = Self {
147            root,
148            actor,
149            engine,
150            mounts,
151            journal,
152            coordination,
153            packages: builtin_packages(),
154            projections: ProjectionCache::new(&control),
155        };
156        let entry = workspace.entry(Act::WorkspaceInit, None)?;
157        workspace.journal.append(&entry)?;
158        Ok(workspace)
159    }
160
161    /// Open the workspace already present at `path`.
162    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
163        let root = path.as_ref().to_path_buf();
164        let actor = resolve_actor()?;
165
166        let control = root.join(CONTROL_DIR);
167        if !control.exists() {
168            return Err(Error::NotAWorkspace(root));
169        }
170
171        let config = read_workspace_config(&control)?;
172        let mount_names = mount_names(&config);
173        let engine = Engine::open(&root, &actor, &mount_names)?;
174        let mut mounts = Vec::new();
175        for name in &mount_names {
176            let branch = config
177                .sources
178                .iter()
179                .find(|source| source.mount == *name)
180                .and_then(|source| source.branch.clone());
181            mounts.push(MountedSource {
182                name: name.clone(),
183                engine: Engine::open(&root.join(name), &actor, &[])?,
184                branch,
185            });
186        }
187        let journal = Journal::open(&control.join(JOURNAL_FILE))?;
188        let coordination = Coordination::open(&control.join(JOURNAL_FILE))?;
189        Ok(Self {
190            root,
191            actor,
192            engine,
193            mounts,
194            journal,
195            coordination,
196            packages: builtin_packages(),
197            projections: ProjectionCache::new(&control),
198        })
199    }
200
201    /// The actor this workspace handle acts as.
202    #[must_use]
203    pub fn actor(&self) -> &Actor {
204        &self.actor
205    }
206
207    /// Attach a local folder, importing its content into the root — source
208    /// zero. One root import per workspace; mounted sources go through
209    /// [`Workspace::attach_mount`].
210    pub fn attach(&mut self, folder: impl AsRef<Path>) -> Result<Source, Error> {
211        let folder = folder.as_ref();
212        if !folder.is_dir() {
213            return Err(Error::Io(std::io::Error::new(
214                std::io::ErrorKind::NotFound,
215                format!("source folder not found: {}", folder.display()),
216            )));
217        }
218
219        let control = self.root.join(CONTROL_DIR);
220        let mut config = read_workspace_config(&control)?;
221        if config.sources.iter().any(|s| s.mount == ROOT_MOUNT) {
222            return Err(Error::AlreadyAttached);
223        }
224        if folder_uses_lfs(folder)? {
225            return Err(Error::LfsSourceUnsupported);
226        }
227
228        self.auto_snapshot()?;
229
230        copy_tree(folder, &self.root, &SKIP_NAMES)?;
231        let source = Source {
232            kind: SourceKind::LocalFolder,
233            path: folder.to_path_buf(),
234            sync: SyncPolicy::TwoWay,
235            mount: ROOT_MOUNT.to_owned(),
236            branch: None,
237        };
238        config.sources.push(source.clone());
239        write_workspace_config(&control, &config)?;
240
241        let snapshot = self.engine.snapshot()?;
242        // The origin equals the import at this instant; record the
243        // fingerprint the first sync-back checks against (ADR-0010).
244        let fingerprint = folder_fingerprint(folder)?;
245        self.coordination
246            .record_sync_state(ROOT_MOUNT, &fingerprint, &self.engine.head()?)?;
247        let entry = self.entry(Act::SourceAttach, snapshot)?;
248        self.journal.append(&entry)?;
249        Ok(source)
250    }
251
252    /// Attach a local folder as a mounted source: its own engine, its own
253    /// history, at `root/<name>` (ADR-0009).
254    pub fn attach_mount(&mut self, folder: impl AsRef<Path>, name: &str) -> Result<Source, Error> {
255        let folder = folder.as_ref();
256        if !folder.is_dir() {
257            return Err(Error::Io(std::io::Error::new(
258                std::io::ErrorKind::NotFound,
259                format!("source folder not found: {}", folder.display()),
260            )));
261        }
262        let control = self.root.join(CONTROL_DIR);
263        let mut config = read_workspace_config(&control)?;
264        let mount_dir = self.root.join(name);
265        mount_refusals(name, &config, &mount_dir)?;
266        if folder_uses_lfs(folder)? {
267            return Err(Error::LfsSourceUnsupported);
268        }
269
270        // Settle every engine before the boundary moves.
271        self.auto_snapshot()?;
272
273        let (kind, engine, snapshot) = match self.import_folder(folder, &mount_dir) {
274            Ok(imported) => imported,
275            Err(error) => {
276                // A half-made mount must not squat the name: the refusal
277                // already speaks; a cleanup failure resurfaces as the
278                // collision refusal on retry.
279                let _ = fs::remove_dir_all(&mount_dir);
280                return Err(error);
281            }
282        };
283        // The branch is read from the source itself: the engine detaches
284        // the copy's HEAD as lines move, so only the origin's HEAD names
285        // what the source had checked out.
286        let branch = match kind {
287            SourceKind::LocalGit => adopted_branch(folder)?,
288            // Remote never occurs here: buckets attach through
289            // attach_remote. Folders carry no branch.
290            SourceKind::LocalFolder | SourceKind::Remote => None,
291        };
292
293        let source = Source {
294            kind,
295            path: folder.to_path_buf(),
296            sync: SyncPolicy::TwoWay,
297            mount: name.to_owned(),
298            branch: branch.clone(),
299        };
300        config.sources.push(source.clone());
301        write_workspace_config(&control, &config)?;
302
303        // The root engine's boundary now excludes the new mount; reopen it
304        // so its ignores see the world as configured.
305        let mount_names = mount_names(&config);
306        self.engine = Engine::open(&self.root, &self.actor, &mount_names)?;
307        let position = self
308            .mounts
309            .binary_search_by(|mount| mount.name.as_str().cmp(name))
310            .unwrap_or_else(|position| position);
311        self.mounts.insert(
312            position,
313            MountedSource {
314                name: name.to_owned(),
315                engine,
316                branch,
317            },
318        );
319
320        match kind {
321            // The origin equals the import at this instant; record the
322            // fingerprint the first sync-back checks against (ADR-0010).
323            SourceKind::LocalFolder => {
324                let fingerprint = folder_fingerprint(folder)?;
325                let head = self.mounts[position].engine.head()?;
326                self.coordination
327                    .record_sync_state(name, &fingerprint, &head)?;
328            }
329            // Remote never occurs here: buckets attach through
330            // attach_remote, which records the listing fingerprint.
331            SourceKind::LocalGit | SourceKind::Remote => {}
332        }
333
334        let reference = snapshot.map(|id| format!("{name} {id}"));
335        let entry = self.entry(Act::SourceAttach, reference)?;
336        self.journal.append(&entry)?;
337        Ok(source)
338    }
339
340    /// Copy or adopt `folder` into `mount_dir` with its own engine: a
341    /// folder that is already a git repository is adopted, never imported —
342    /// its history is preserved and the mount stays a real repo plain git
343    /// pushes (ADR-0009).
344    fn import_folder(
345        &self,
346        folder: &Path,
347        mount_dir: &Path,
348    ) -> Result<(SourceKind, Engine, Option<String>), Error> {
349        fs::create_dir_all(mount_dir)?;
350        let adopts_git = folder.join(".git").is_dir();
351        let (kind, mut engine) = if adopts_git {
352            copy_tree(folder, mount_dir, &[".atelier", ".jj"])?;
353            (
354                SourceKind::LocalGit,
355                Engine::adopt_git(mount_dir, &self.actor, &[])?,
356            )
357        } else {
358            let engine = Engine::init(mount_dir, &self.actor, &[])?;
359            copy_tree(folder, mount_dir, &SKIP_NAMES)?;
360            (SourceKind::LocalFolder, engine)
361        };
362        let snapshot = engine.snapshot()?;
363        Ok((kind, engine, snapshot))
364    }
365
366    /// Download the bucket into `mount_dir` with its own engine.
367    fn import_remote(
368        &self,
369        remote: &RemoteFolder,
370        mount_dir: &Path,
371    ) -> Result<(Engine, Option<String>), Error> {
372        fs::create_dir_all(mount_dir)?;
373        let mut engine = Engine::init(mount_dir, &self.actor, &[])?;
374        remote.download_all(mount_dir).map_err(engine_err)?;
375        let snapshot = engine.snapshot()?;
376        Ok((engine, snapshot))
377    }
378
379    /// Attach a bucket prefix as a mounted source (ADR-0012): the objects
380    /// import into the mount, which carries its own engine and history;
381    /// the listing's fingerprint guards every later mirror home.
382    pub fn attach_remote(&mut self, url: &str, name: &str) -> Result<Source, Error> {
383        let control = self.root.join(CONTROL_DIR);
384        let mut config = read_workspace_config(&control)?;
385        let mount_dir = self.root.join(name);
386        mount_refusals(name, &config, &mount_dir)?;
387
388        // Settle every engine before the boundary moves.
389        self.auto_snapshot()?;
390
391        let remote = RemoteFolder::open(url).map_err(engine_err)?;
392        let (engine, snapshot) = match self.import_remote(&remote, &mount_dir) {
393            Ok(imported) => imported,
394            Err(error) => {
395                // A half-made mount must not squat the name: the refusal
396                // already speaks; a cleanup failure resurfaces as the
397                // collision refusal on retry.
398                let _ = fs::remove_dir_all(&mount_dir);
399                return Err(error);
400            }
401        };
402        // The bucket equals the import at this instant; record the
403        // listing fingerprint the first mirror checks against.
404        let fingerprint = remote.fingerprint().map_err(engine_err)?;
405        let head = engine.head()?;
406        self.coordination
407            .record_sync_state(name, &fingerprint, &head)?;
408
409        let source = Source {
410            kind: SourceKind::Remote,
411            path: PathBuf::from(url),
412            sync: SyncPolicy::TwoWay,
413            mount: name.to_owned(),
414            branch: None,
415        };
416        config.sources.push(source.clone());
417        write_workspace_config(&control, &config)?;
418
419        // The root engine's boundary now excludes the new mount; reopen it
420        // so its ignores see the world as configured.
421        let mount_names = mount_names(&config);
422        self.engine = Engine::open(&self.root, &self.actor, &mount_names)?;
423        let position = self
424            .mounts
425            .binary_search_by(|mount| mount.name.as_str().cmp(name))
426            .unwrap_or_else(|position| position);
427        self.mounts.insert(
428            position,
429            MountedSource {
430                name: name.to_owned(),
431                engine,
432                branch: None,
433            },
434        );
435
436        let reference = snapshot.map(|id| format!("{name} {id}"));
437        let entry = self.entry(Act::SourceAttach, reference)?;
438        self.journal.append(&entry)?;
439        Ok(source)
440    }
441
442    /// The shared lines' snapshots: the root's, then each mount's in name
443    /// order, each newest first, `limit` applying per source.
444    pub fn log(&mut self, limit: usize) -> Result<Vec<SourceSnapshot>, Error> {
445        self.refresh_engines()?;
446        self.auto_snapshot()?;
447        let mut entries = Vec::new();
448        for snapshot in self.engine.log(limit)? {
449            entries.push(SourceSnapshot {
450                source: None,
451                snapshot,
452            });
453        }
454        for mount in &self.mounts {
455            for snapshot in mount.engine.log(limit)? {
456                entries.push(SourceSnapshot {
457                    source: Some(mount.name.clone()),
458                    snapshot,
459                });
460            }
461        }
462        Ok(entries)
463    }
464
465    /// Diff each source's latest snapshot against its first parent, root
466    /// first then mounts in name order, every delta raised to the highest
467    /// rung the ladder allows and mounted addresses scoped by mount.
468    pub fn diff_latest(&mut self) -> Result<Diff, Error> {
469        self.refresh_engines()?;
470        self.auto_snapshot()?;
471        let (diff, sides) = self.engine.diff_latest()?;
472        let mut deltas = self.raised(&self.engine, diff, &sides, None)?.deltas;
473        for mount in &self.mounts {
474            let (mount_diff, sides) = mount.engine.diff_latest()?;
475            let raised = self.raised(&mount.engine, mount_diff, &sides, Some(&mount.name))?;
476            deltas.extend(raised.deltas);
477        }
478        Ok(Diff { deltas })
479    }
480
481    /// Render the read model an actor consumes first: identity, sources,
482    /// discipline, live state, and the loop this workspace expects. Every
483    /// face returns this text verbatim (ADR-0006: one render, three faces).
484    pub fn manifest(&mut self) -> Result<String, Error> {
485        self.refresh_engines()?;
486        self.auto_snapshot()?;
487        let config = read_workspace_config(&self.root.join(CONTROL_DIR))?;
488        let mut lines = vec![
489            format!("workspace: {}", config.workspace.name),
490            format!("schema: {}", config.schema),
491            String::new(),
492            "sources:".to_owned(),
493        ];
494        if config.sources.is_empty() {
495            lines.push("  (none)".to_owned());
496        }
497        for source in &config.sources {
498            lines.push(format!(
499                "  {}  {}  {}  {}",
500                source.mount,
501                source.kind,
502                source.path.display(),
503                source.sync
504            ));
505        }
506        lines.push(String::new());
507        lines.push("discipline:".to_owned());
508        let landing = config.landing;
509        let self_approve = if landing.allow_self_approve {
510            "allowed"
511        } else {
512            "forbidden"
513        };
514        let dismiss = if landing.dismiss_approvals_on_new_snapshots {
515            "yes"
516        } else {
517            "no"
518        };
519        lines.push(format!(
520            "  approvals: {}  self-approval: {self_approve}  snapshots dismiss approvals: {dismiss}",
521            landing.approvals
522        ));
523        let fidelity = match config.journal.instruction_fidelity {
524            InstructionFidelity::Summary => "summary",
525            InstructionFidelity::Verbatim => "verbatim",
526        };
527        lines.push(format!("  instructions: {fidelity}"));
528        lines.push(String::new());
529        lines.push("state:".to_owned());
530        for line in self.state_lines()? {
531            lines.push(format!("  {line}"));
532        }
533        lines.push(String::new());
534        lines.push("the loop:".to_owned());
535        lines
536            .push("  open_session -> write -> diff -> land (or request_land + approve)".to_owned());
537        lines.push(
538            "  mount-scoped paths address sources; editing never takes the landing lease"
539                .to_owned(),
540        );
541        Ok(lines.join("\n"))
542    }
543
544    /// The live-state read model: what the manifest's state section says,
545    /// standing alone. Every face returns this text verbatim (ADR-0006).
546    pub fn status(&mut self) -> Result<String, Error> {
547        self.refresh_engines()?;
548        self.auto_snapshot()?;
549        Ok(self.state_lines()?.join("\n"))
550    }
551
552    /// The live state every read model shares: per-source heads, open
553    /// sessions, live requests.
554    fn state_lines(&mut self) -> Result<Vec<String>, Error> {
555        let mut lines = vec![format!("head: {}", self.engine.head()?)];
556        for mount in &self.mounts {
557            lines.push(format!("head {}: {}", mount.name, mount.engine.head()?));
558        }
559        let mut open_sessions: Vec<String> = self
560            .sessions()?
561            .into_iter()
562            .filter(|session| match session.state {
563                SessionState::Open => true,
564                SessionState::Landed | SessionState::Abandoned => false,
565            })
566            .map(|session| session.id.to_string())
567            .collect();
568        open_sessions.reverse();
569        lines.push(if open_sessions.is_empty() {
570            "open sessions: none".to_owned()
571        } else {
572            format!("open sessions: {}", open_sessions.join(", "))
573        });
574        let mut live_requests: Vec<String> = self
575            .landing_requests()?
576            .into_iter()
577            .filter(|request| match request.state {
578                RequestState::Open | RequestState::Approved | RequestState::Parked => true,
579                RequestState::Landed | RequestState::Rejected | RequestState::Abandoned => false,
580            })
581            .map(|request| format!("{} ({})", request.id, request.state))
582            .collect();
583        live_requests.reverse();
584        lines.push(if live_requests.is_empty() {
585            "live requests: none".to_owned()
586        } else {
587            format!("live requests: {}", live_requests.join(", "))
588        });
589        Ok(lines)
590    }
591
592    /// Diff two of the root line's snapshots by id: `before` against
593    /// `after`, each delta raised to the highest rung the ladder allows.
594    /// Mounted lines' snapshot pairs arrive with the session fan-out.
595    pub fn diff_between(&mut self, before: &str, after: &str) -> Result<Diff, Error> {
596        self.refresh_engines()?;
597        self.auto_snapshot()?;
598        let (diff, sides) = self.engine.diff_between(before, after)?;
599        self.raised(&self.engine, diff, &sides, None)
600    }
601
602    /// Read up to `limit` journal entries, newest first.
603    pub fn journal(&mut self, limit: usize) -> Result<Vec<JournalEntry>, Error> {
604        self.refresh_engines()?;
605        self.auto_snapshot()?;
606        self.journal.entries(limit)
607    }
608
609    /// Open a session for `actor`: its own working copy holding the shared
610    /// head, its own change. Isolation is not optional — every session
611    /// starts isolated, and only landing serializes.
612    pub fn open_session(
613        &mut self,
614        actor: &Actor,
615        instruction: &Instruction,
616    ) -> Result<Session, Error> {
617        self.refresh_engines()?;
618        self.auto_snapshot()?;
619        let verbatim = match self.config()?.journal.instruction_fidelity {
620            InstructionFidelity::Summary => None,
621            InstructionFidelity::Verbatim => instruction.verbatim.clone(),
622        };
623        let row = self.coordination.create_session(
624            actor,
625            &instruction.summary,
626            instruction.run_ref.as_deref(),
627            verbatim.as_deref(),
628            now_ms()?,
629        )?;
630        let id = SessionId(row);
631        let change_id = match self.engine.create_session_workspace(
632            &self.session_root(id),
633            &format!("session-{id}"),
634            actor,
635        ) {
636            Ok(change_id) => change_id,
637            Err(error) => {
638                self.coordination.delete_session(row)?;
639                return Err(error);
640            }
641        };
642        self.coordination.set_session_change(row, &change_id)?;
643        // The session spans every source: one working copy and one change
644        // per mount, mirroring the workspace's shape (ADR-0009).
645        let session_root = self.session_root(id);
646        for index in 0..self.mounts.len() {
647            let name = self.mounts[index].name.clone();
648            let mount_change = match self.mounts[index].engine.create_session_workspace(
649                &session_root.join(&name),
650                &format!("session-{id}"),
651                actor,
652            ) {
653                Ok(change_id) => change_id,
654                Err(error) => {
655                    self.coordination.delete_session(row)?;
656                    return Err(error);
657                }
658            };
659            self.coordination
660                .set_session_source_change(row, &name, &mount_change)?;
661        }
662        self.journal.append(&JournalEntry {
663            at_ms: now_ms()?,
664            actor_name: actor.name.clone(),
665            actor_kind: actor.kind,
666            act: Act::SessionOpen,
667            session: Some(id.to_string()),
668            instruction_summary: Some(instruction.summary.clone()),
669            instruction_run_ref: instruction.run_ref.clone(),
670            instruction_verbatim: verbatim,
671            reference: None,
672        })?;
673        self.session(id)
674    }
675
676    /// Every session, newest first. Sessions are durable rows plus real
677    /// directories: they survive process restarts, and nothing deletes them.
678    pub fn sessions(&mut self) -> Result<Vec<Session>, Error> {
679        let rows = self.coordination.sessions()?;
680        rows.into_iter().map(|row| self.session_from(row)).collect()
681    }
682
683    /// The session named `id`.
684    pub fn session(&mut self, id: SessionId) -> Result<Session, Error> {
685        match self.coordination.session(id.0)? {
686            Some(row) => self.session_from(row),
687            None => Err(Error::SessionNotFound(id.to_string())),
688        }
689    }
690
691    /// Write `content` at `path` inside the session's working copy — a
692    /// mount-scoped path lands in that source's working copy — and
693    /// snapshot every source; the id of the written source's tip snapshot.
694    pub fn session_write(
695        &mut self,
696        id: SessionId,
697        path: &str,
698        content: &str,
699    ) -> Result<String, Error> {
700        self.engine.refresh()?;
701        let session = self.open_session_only(id)?;
702        let (source, directory, inner) = self.session_target(&session, path);
703        let file = session_file(&directory, &inner)?;
704        if let Some(parent) = file.parent() {
705            fs::create_dir_all(parent)?;
706        }
707        fs::write(&file, content)?;
708        let tips = self.snapshot_session(&session)?;
709        Ok(tips.tip_of(source.as_deref()))
710    }
711
712    /// Read `path` inside the session's working copy, windowed. A document
713    /// a package projects reads as its projection; plain text reads as
714    /// itself; anything else refuses — raw byte views arrive with a later
715    /// slice.
716    pub fn session_read(
717        &mut self,
718        id: SessionId,
719        path: &str,
720        start: usize,
721        max_bytes: Option<usize>,
722    ) -> Result<ReadResult, Error> {
723        let session = self.open_session_only(id)?;
724        let size = window_size(max_bytes)?;
725        let (_, directory, inner) = self.session_target(&session, path);
726        let file = session_file(&directory, &inner)?;
727        let bytes = fs::read(&file)?;
728        if let Some(package) = self.detected(path, &bytes)? {
729            let text = self.project_for_read(package, &bytes)?;
730            return Ok(window_text(&text, start, size, Some(package.id())));
731        }
732        match as_text(&bytes) {
733            Some(text) => Ok(window_text(text, start, size, None)),
734            None => Err(Error::NotText(path.to_owned())),
735        }
736    }
737
738    /// Each source's session change against the shared-line snapshot it
739    /// forked from, raised through the ladder like any diff, mounted
740    /// addresses scoped by mount. An untouched source contributes nothing.
741    pub fn session_diff(&mut self, id: SessionId) -> Result<Diff, Error> {
742        self.refresh_engines()?;
743        let session = self.open_session_only(id)?;
744        let tips = self.snapshot_session(&session)?;
745        let base = self.engine.parent_of(&tips.root)?;
746        let (diff, sides) = self.engine.diff_between(&base, &tips.root)?;
747        let mut deltas = self.raised(&self.engine, diff, &sides, None)?.deltas;
748        for (name, tip) in &tips.mounts {
749            let mount = self.mount(name)?;
750            let base = mount.engine.parent_of(tip)?;
751            let (diff, sides) = mount.engine.diff_between(&base, tip)?;
752            let raised = self.raised(&mount.engine, diff, &sides, Some(name))?;
753            deltas.extend(raised.deltas);
754        }
755        Ok(Diff { deltas })
756    }
757
758    /// Open the session's landing request — the gate's object, never a
759    /// direct write (ADR-0007). Asking again returns the request already
760    /// holding the gate.
761    pub fn request_land(&mut self, id: SessionId) -> Result<LandingRequest, Error> {
762        self.refresh_engines()?;
763        let session = self.open_session_only(id)?;
764        self.snapshot_session(&session)?;
765        if let Some(row) = self.coordination.gated_request_for_session(id.0)? {
766            return self.request_from(row);
767        }
768        let row = self
769            .coordination
770            .create_request(id.0, &session.actor, now_ms()?)?;
771        let request_id = RequestId(row);
772        self.append_session_entry(
773            &session.actor,
774            Act::LandRequest,
775            id,
776            Some(request_id.to_string()),
777        )?;
778        self.request(request_id)
779    }
780
781    /// Every landing request, newest first.
782    pub fn landing_requests(&mut self) -> Result<Vec<LandingRequest>, Error> {
783        let rows = self.coordination.requests()?;
784        rows.into_iter().map(|row| self.request_from(row)).collect()
785    }
786
787    /// The landing request named `id`.
788    pub fn request(&mut self, id: RequestId) -> Result<LandingRequest, Error> {
789        match self.coordination.request(id.0)? {
790            Some(row) => self.request_from(row),
791            None => Err(Error::RequestNotFound(id.to_string())),
792        }
793    }
794
795    /// Record `approver`'s approval on the request; when the gate is
796    /// satisfied the apply runs — lease, rebase, advance — landing the
797    /// change or parking the request on a conflict.
798    pub fn approve(&mut self, id: RequestId, approver: &Actor) -> Result<GateOutcome, Error> {
799        self.refresh_engines()?;
800        let row = self.gated_request(id)?;
801        let session = self.open_session_only(SessionId(row.session_id))?;
802        let policy = self.config()?.landing;
803        let requester = Actor {
804            name: row.requester_name.clone(),
805            kind: row.requester_kind,
806        };
807        if !policy.allow_self_approve && *approver == requester {
808            return Err(Error::SelfApprovalForbidden);
809        }
810        let tips = self.snapshot_session(&session)?;
811        // The approval covers the change as its root tip names it; a new
812        // snapshot on any source dismisses approvals through the gate's
813        // side effects, so a stale approval never carries later work.
814        let tip = tips.root.clone();
815        // The snapshot may have dismissed approvals and re-opened the gate;
816        // judge the gate on what the store holds now.
817        let row = self.gated_request(id)?;
818        if let RequestState::Open = row.state {
819            self.coordination
820                .add_approval(row.id, approver, &tip, now_ms()?)?;
821            self.append_session_entry(
822                approver,
823                Act::Approve,
824                session.id,
825                Some(format!("{id} {tip}")),
826            )?;
827        }
828        let approvals = self.coordination.live_approvals(row.id)?;
829        let approvers: std::collections::BTreeSet<(&str, &str)> = approvals
830            .iter()
831            .map(|approval| (approval.actor_name.as_str(), approval.actor_kind.as_str()))
832            .collect();
833        if (approvers.len() as u64) < u64::from(policy.approvals) {
834            return Ok(GateOutcome::Pending {
835                request: self.request(id)?,
836                required: policy.approvals,
837            });
838        }
839        // The gate was judged satisfied on Open; another process may have
840        // moved the request since. Losing the move means re-judging, not
841        // overwriting: an already-approved request proceeds to its apply,
842        // a closed one refuses by name through the re-fetch above.
843        if !self.coordination.move_request_state(
844            row.id,
845            &[RequestState::Open],
846            RequestState::Approved,
847        )? {
848            let row = self.gated_request(id)?;
849            if let RequestState::Open = row.state {
850                // The gate re-opened (a new snapshot dismissed approvals):
851                // this approval no longer satisfies it.
852                return Ok(GateOutcome::Pending {
853                    request: self.request(id)?,
854                    required: policy.approvals,
855                });
856            }
857        }
858        self.apply(&session, id, &tips, approver)
859    }
860
861    /// Reject the request: the gate closes, the session stays open.
862    pub fn reject(
863        &mut self,
864        id: RequestId,
865        actor: &Actor,
866        reason: Option<&str>,
867    ) -> Result<LandingRequest, Error> {
868        // A rejection closes a gate still deciding: Open or Approved.
869        // Losing the move means the gate settled first — refuse by name.
870        let row = self.gated_request(id)?;
871        while !self.coordination.move_request_state(
872            row.id,
873            &[RequestState::Open, RequestState::Approved],
874            RequestState::Rejected,
875        )? {
876            self.gated_request(id)?;
877        }
878        let reference = match reason {
879            Some(reason) => format!("{id} {reason}"),
880            None => id.to_string(),
881        };
882        self.append_session_entry(
883            actor,
884            Act::Reject,
885            SessionId(row.session_id),
886            Some(reference),
887        )?;
888        self.request(id)
889    }
890
891    /// Land the session's change: sugar for request plus self-approval.
892    /// Where policy forbids self-approval the request stays pending for
893    /// other approvers.
894    pub fn land(&mut self, id: SessionId) -> Result<GateOutcome, Error> {
895        let request = self.request_land(id)?;
896        let session = self.session(id)?;
897        let policy = self.config()?.landing;
898        if !policy.allow_self_approve {
899            return Ok(GateOutcome::Pending {
900                request,
901                required: policy.approvals,
902            });
903        }
904        self.approve(request.id, &session.actor)
905    }
906
907    /// Close the session without landing; its work stays in history and
908    /// its working copy stays on disk.
909    pub fn abandon(&mut self, id: SessionId) -> Result<Session, Error> {
910        self.engine.refresh()?;
911        let session = self.open_session_only(id)?;
912        self.snapshot_session(&session)?;
913        let mut reference = None;
914        if let Some(request) = self.coordination.gated_request_for_session(id.0)? {
915            // Abandonment closes any still-gated request; losing the move
916            // means the gate settled concurrently (landed or rejected),
917            // and that outcome stands — the session still closes.
918            let _ = self.coordination.move_request_state(
919                request.id,
920                &[
921                    RequestState::Open,
922                    RequestState::Approved,
923                    RequestState::Parked,
924                ],
925                RequestState::Abandoned,
926            )?;
927            reference = Some(RequestId(request.id).to_string());
928        }
929        if !self.coordination.move_session_state(
930            id.0,
931            SessionState::Open,
932            SessionState::Abandoned,
933        )? {
934            // A concurrent apply landed the session between the open check
935            // above and this write; the landing stands.
936            let session = self.session(id)?;
937            return Err(Error::SessionClosed {
938                id: id.to_string(),
939                state: session.state.to_string(),
940            });
941        }
942        self.append_session_entry(&session.actor, Act::SessionAbandon, id, reference)?;
943        self.session(id)
944    }
945
946    /// Snapshot outstanding edits in every engine — root first, mounts in
947    /// name order — through the one snapshot path every operation shares;
948    /// each recorded snapshot with the source that took it.
949    fn auto_snapshot(&mut self) -> Result<Vec<(Option<String>, String)>, Error> {
950        let mut recorded = Vec::new();
951        if let Some(id) = self.engine.snapshot()? {
952            let entry = self.entry(Act::Snapshot, Some(id.clone()))?;
953            self.journal.append(&entry)?;
954            recorded.push((None, id));
955        }
956        for mount in &mut self.mounts {
957            if let Some(id) = mount.engine.snapshot()? {
958                recorded.push((Some(mount.name.clone()), id));
959            }
960        }
961        for (mount, id) in &recorded {
962            if let Some(mount) = mount {
963                let entry = self.entry(Act::Snapshot, Some(format!("{mount} {id}")))?;
964                self.journal.append(&entry)?;
965            }
966        }
967        Ok(recorded)
968    }
969
970    /// Reload every engine at its current operation head, folding in what
971    /// other processes committed since this handle loaded.
972    fn refresh_engines(&mut self) -> Result<(), Error> {
973        self.engine.refresh()?;
974        for mount in &mut self.mounts {
975            mount.engine.refresh()?;
976        }
977        Ok(())
978    }
979
980    /// Watch the workspace root: external edits become attributed
981    /// snapshots through the same snapshot path every operation uses.
982    /// Blocks until `stop` asks it to return; edits made while no watcher
983    /// runs are caught up by the scan at start. Each snapshot — and the
984    /// armed watcher itself — reaches the caller through `on_event`.
985    pub fn watch(
986        &mut self,
987        debounce: Duration,
988        mut on_event: impl FnMut(&WatchEvent),
989        stop: &WatchStop,
990    ) -> Result<(), Error> {
991        // notify reports canonical paths; the filter's prefix check needs
992        // the root in the same form.
993        let root = fs::canonicalize(&self.root)?;
994        let (pulses, storm) = std::sync::mpsc::channel();
995        let filter_root = root.clone();
996        let mut watcher =
997            notify::recommended_watcher(move |event: Result<Event, notify::Error>| {
998                let pulse = match event {
999                    Ok(event) => {
1000                        if !event_is_content(&filter_root, &event) {
1001                            return;
1002                        }
1003                        Ok(())
1004                    }
1005                    Err(error) => Err(error),
1006                };
1007                // A send after the loop returned has no listener; that is the
1008                // watcher being dropped, not a failure.
1009                let _ = pulses.send(pulse);
1010            })
1011            .map_err(|error| watcher_failed(&error))?;
1012        watcher
1013            .watch(&root, RecursiveMode::Recursive)
1014            .map_err(|error| watcher_failed(&error))?;
1015        on_event(&WatchEvent::Started);
1016        self.snapshot_watched(&mut on_event)?;
1017        while !stop.stopped() {
1018            match storm.recv_timeout(STOP_TICK) {
1019                Ok(Ok(())) => {
1020                    settle(&storm, debounce, stop)?;
1021                    self.snapshot_watched(&mut on_event)?;
1022                }
1023                Ok(Err(error)) => return Err(watcher_failed(&error)),
1024                Err(RecvTimeoutError::Timeout) => {}
1025                Err(RecvTimeoutError::Disconnected) => return Err(watcher_gone()),
1026            }
1027        }
1028        Ok(())
1029    }
1030
1031    /// One watched snapshot: fold in operations other processes committed,
1032    /// then snapshot; a recorded snapshot reaches the watcher's caller.
1033    fn snapshot_watched(&mut self, on_event: &mut impl FnMut(&WatchEvent)) -> Result<(), Error> {
1034        self.refresh_engines()?;
1035        for (_, snapshot) in self.auto_snapshot()? {
1036            on_event(&WatchEvent::Snapshotted { snapshot });
1037        }
1038        Ok(())
1039    }
1040
1041    /// The gate-satisfied apply, fanned out per source (ADR-0009): the
1042    /// root first, then mounts in name order; each touched line takes its
1043    /// own lease, rebases, and advances — or parks. A landing already
1044    /// recorded for this request is never repeated, so a retry after a
1045    /// park or a lost lease finishes what remains. Editing never takes a
1046    /// lease; only landing does.
1047    fn apply(
1048        &mut self,
1049        session: &Session,
1050        id: RequestId,
1051        tips: &SessionTips,
1052        approver: &Actor,
1053    ) -> Result<GateOutcome, Error> {
1054        let already = self.coordination.landings(id.0)?;
1055        let mut parked = Vec::new();
1056        // The root always lands — the v1 line, even when untouched, so a
1057        // zero-mount workspace keeps its exact behavior. Mounts land only
1058        // when their session change carries work.
1059        let mut plan: Vec<(Option<String>, String)> = vec![(None, tips.root.clone())];
1060        for (name, tip) in &tips.mounts {
1061            if self.mount(name)?.engine.tree_changed(tip)? {
1062                plan.push((Some(name.clone()), tip.clone()));
1063            }
1064        }
1065        for (source, tip) in plan {
1066            if already.iter().any(|(landed, _)| *landed == source) {
1067                continue;
1068            }
1069            match self.apply_source(session, id, source.as_deref(), &tip, approver)? {
1070                LandOutcome::Landed { .. } => {}
1071                LandOutcome::Conflicted => parked.push(source),
1072            }
1073        }
1074        let landings: Vec<Landing> = self
1075            .coordination
1076            .landings(id.0)?
1077            .into_iter()
1078            .map(|(source, snapshot)| Landing { source, snapshot })
1079            .collect();
1080        if parked.is_empty() {
1081            // Losing the request move means the gate re-opened for a newer
1082            // snapshot or the session was abandoned mid-apply — the
1083            // winner's state stands and the session stays open for its
1084            // remaining work; the landings are recorded either way.
1085            if self.coordination.move_request_state(
1086                id.0,
1087                &[RequestState::Approved],
1088                RequestState::Landed,
1089            )? {
1090                let _ = self.coordination.move_session_state(
1091                    session.id.0,
1092                    SessionState::Open,
1093                    SessionState::Landed,
1094                )?;
1095            }
1096            return Ok(GateOutcome::Landed { landings });
1097        }
1098        // A parked line closes the gate until a new snapshot; what landed
1099        // stands (ADR-0009 — never pretended atomicity).
1100        let _ = self.coordination.move_request_state(
1101            id.0,
1102            &[RequestState::Approved],
1103            RequestState::Parked,
1104        )?;
1105        Ok(GateOutcome::Parked {
1106            request: self.request(id)?,
1107            landings,
1108            parked,
1109        })
1110    }
1111
1112    /// Land one source's tip under its own lease; the outcome of that one
1113    /// line. The landing journals and records with its source, so nothing
1114    /// repeats it and nothing mistakes it for another line's.
1115    fn apply_source(
1116        &mut self,
1117        session: &Session,
1118        id: RequestId,
1119        source: Option<&str>,
1120        tip: &str,
1121        approver: &Actor,
1122    ) -> Result<LandOutcome, Error> {
1123        let point = match source {
1124            Some(name) => format!("{LANDING_LEASE_POINT}/{name}"),
1125            None => LANDING_LEASE_POINT.to_owned(),
1126        };
1127        let holder = format!("{}:{}", self.actor.name, std::process::id());
1128        let now = now_ms()?;
1129        match self
1130            .coordination
1131            .claim_lease(&point, &holder, now, LANDING_LEASE_TTL_MS)?
1132        {
1133            LeaseClaim::HeldByOther {
1134                holder,
1135                expires_at_ms,
1136            } => {
1137                return Err(Error::LeaseHeld {
1138                    holder,
1139                    expires_at_ms,
1140                });
1141            }
1142            LeaseClaim::Held => {}
1143        }
1144        let outcome = self.apply_source_holding_lease(session, id, source, tip, approver);
1145        let released = self.coordination.release_lease(&point, &holder);
1146        let outcome = outcome?;
1147        released?;
1148        Ok(outcome)
1149    }
1150
1151    fn apply_source_holding_lease(
1152        &mut self,
1153        session: &Session,
1154        id: RequestId,
1155        source: Option<&str>,
1156        tip: &str,
1157        approver: &Actor,
1158    ) -> Result<LandOutcome, Error> {
1159        // Test seam: the cross-process lease test needs the winner to hold
1160        // the point long enough for the loser to observe `LeaseHeld`.
1161        if let Some(hold) = land_hold_ms()? {
1162            std::thread::sleep(Duration::from_millis(hold));
1163        }
1164        // Another process may have advanced this line since the gate
1165        // check; the lease is held, so the head stays put through the
1166        // apply.
1167        self.refresh_engines()?;
1168        self.auto_snapshot()?;
1169        let outcome = match source {
1170            None => self.engine.land(tip, LANDED_BOOKMARK)?,
1171            Some(name) => {
1172                let index = self
1173                    .mounts
1174                    .iter()
1175                    .position(|mount| mount.name == name)
1176                    .ok_or_else(|| Error::Engine(format!("no source is mounted at {name:?}")))?;
1177                let bookmark = self.mounts[index]
1178                    .branch
1179                    .clone()
1180                    .unwrap_or_else(|| LANDED_BOOKMARK.to_owned());
1181                self.mounts[index].engine.land(tip, &bookmark)?
1182            }
1183        };
1184        let scoped = |text: &str| match source {
1185            Some(name) => format!("{name} {text}"),
1186            None => text.to_owned(),
1187        };
1188        match &outcome {
1189            LandOutcome::Conflicted => {
1190                self.append_session_entry(
1191                    approver,
1192                    Act::LandParked,
1193                    session.id,
1194                    Some(scoped(&id.to_string())),
1195                )?;
1196            }
1197            LandOutcome::Landed { snapshot } => {
1198                self.coordination.record_landing(id.0, source, snapshot)?;
1199                self.append_session_entry(
1200                    approver,
1201                    Act::Land,
1202                    session.id,
1203                    Some(scoped(&format!("{id} {snapshot}"))),
1204                )?;
1205                self.sync_after_line_move(session, source, approver)?;
1206            }
1207        }
1208        Ok(outcome)
1209    }
1210
1211    /// Step a landed request back off every line it landed (ADR-0011):
1212    /// reverse landing order, each line under its landing lease,
1213    /// idempotent per line. The request re-opens with its approvals
1214    /// dismissed — an undo is a new decision point — and the session
1215    /// re-opens with its change intact, immediately re-landable.
1216    pub fn undo(&mut self, id: RequestId) -> Result<Vec<Restore>, Error> {
1217        self.refresh_engines()?;
1218        self.auto_snapshot()?;
1219        let Some(row) = self.coordination.request(id.0)? else {
1220            return Err(Error::RequestNotFound(id.to_string()));
1221        };
1222        match row.state {
1223            RequestState::Landed => {}
1224            RequestState::Open
1225            | RequestState::Approved
1226            | RequestState::Parked
1227            | RequestState::Rejected
1228            | RequestState::Abandoned => {
1229                return Err(Error::Config(format!(
1230                    "{id} is {}; only a landed request undoes - snapshots amend forward, gate acts move forward, syncs reconcile with atelier sync",
1231                    row.state
1232                )));
1233            }
1234        }
1235        let session = self.session(SessionId(row.session_id))?;
1236        let mut landings = self.coordination.landings(id.0)?;
1237        landings.reverse();
1238        let mut restores = Vec::new();
1239        for (source, landed) in &landings {
1240            if let Some(head) = self.undo_source(&session, id, source.as_deref(), landed)? {
1241                restores.push(Restore {
1242                    source: source.clone(),
1243                    head,
1244                });
1245            }
1246        }
1247        if self.coordination.move_request_state(
1248            id.0,
1249            &[RequestState::Landed],
1250            RequestState::Open,
1251        )? {
1252            let actor = self.actor.clone();
1253            if self.coordination.dismiss_approvals(id.0)? > 0 {
1254                self.append_session_entry(
1255                    &actor,
1256                    Act::ApprovalsDismissed,
1257                    session.id,
1258                    Some(id.to_string()),
1259                )?;
1260            }
1261            let _ = self.coordination.move_session_state(
1262                session.id.0,
1263                SessionState::Landed,
1264                SessionState::Open,
1265            )?;
1266        }
1267        Ok(restores)
1268    }
1269
1270    /// One line's undo under its landing lease — the same scarce point a
1271    /// landing holds, so undos and applies never interleave on a line.
1272    fn undo_source(
1273        &mut self,
1274        session: &Session,
1275        id: RequestId,
1276        source: Option<&str>,
1277        landed: &str,
1278    ) -> Result<Option<String>, Error> {
1279        let point = match source {
1280            Some(name) => format!("{LANDING_LEASE_POINT}/{name}"),
1281            None => LANDING_LEASE_POINT.to_owned(),
1282        };
1283        let holder = format!("{}:{}", self.actor.name, std::process::id());
1284        let now = now_ms()?;
1285        match self
1286            .coordination
1287            .claim_lease(&point, &holder, now, LANDING_LEASE_TTL_MS)?
1288        {
1289            LeaseClaim::HeldByOther {
1290                holder,
1291                expires_at_ms,
1292            } => {
1293                return Err(Error::LeaseHeld {
1294                    holder,
1295                    expires_at_ms,
1296                });
1297            }
1298            LeaseClaim::Held => {}
1299        }
1300        let outcome = self.undo_source_holding_lease(session, id, source, landed);
1301        let released = self.coordination.release_lease(&point, &holder);
1302        let outcome = outcome?;
1303        released?;
1304        Ok(outcome)
1305    }
1306
1307    /// The step-back plus its records: the undo act and the origin
1308    /// re-mirror. `None` when a prior attempt already stepped this line.
1309    fn undo_source_holding_lease(
1310        &mut self,
1311        session: &Session,
1312        id: RequestId,
1313        source: Option<&str>,
1314        landed: &str,
1315    ) -> Result<Option<String>, Error> {
1316        let step = match source {
1317            None => self.engine.step_back(landed, LANDED_BOOKMARK)?,
1318            Some(name) => {
1319                let index = self
1320                    .mounts
1321                    .iter()
1322                    .position(|mount| mount.name == name)
1323                    .ok_or_else(|| Error::Engine(format!("no source is mounted at {name:?}")))?;
1324                let bookmark = self.mounts[index]
1325                    .branch
1326                    .clone()
1327                    .unwrap_or_else(|| LANDED_BOOKMARK.to_owned());
1328                self.mounts[index].engine.step_back(landed, &bookmark)?
1329            }
1330        };
1331        match step {
1332            StepBack::Stepped { restored } => {
1333                // The landing is no longer a fact: a re-apply of this
1334                // request must land the line anew, not skip it.
1335                self.coordination.delete_landing(id.0, source)?;
1336                let reference = match source {
1337                    Some(name) => format!("{name} {id} {restored}"),
1338                    None => format!("{id} {restored}"),
1339                };
1340                let actor = self.actor.clone();
1341                self.append_session_entry(&actor, Act::Undo, session.id, Some(reference))?;
1342                self.sync_after_line_move(session, source, &actor)?;
1343                Ok(Some(restored))
1344            }
1345            StepBack::AlreadyStepped => {
1346                // The step happened on a prior attempt that died before
1347                // un-recording; repair the record, journal nothing more.
1348                self.coordination.delete_landing(id.0, source)?;
1349                Ok(None)
1350            }
1351            StepBack::LineMoved { head } => {
1352                let line = source.unwrap_or("the root");
1353                Err(Error::Config(format!(
1354                    "{line} moved past {id}: {head} sits on the line now; undo that landing first"
1355                )))
1356            }
1357        }
1358    }
1359
1360    /// Fold bucket-side changes into a mounted remote source's line as one
1361    /// attributed snapshot (ADR-0012, R2). A line that moved locally since
1362    /// its last sync refuses by name - land or sync it first; nothing is
1363    /// pulled over unlanded movement, and the pull's own auto-snapshot
1364    /// means outstanding edits count as movement, never as loss.
1365    pub fn pull(&mut self, source: Option<&str>) -> Result<PullOutcome, Error> {
1366        self.refresh_engines()?;
1367        self.auto_snapshot()?;
1368        let mount = source.unwrap_or(ROOT_MOUNT);
1369        let config = read_workspace_config(&self.root.join(CONTROL_DIR))?;
1370        let Some(entry) = config.sources.iter().find(|s| s.mount == mount) else {
1371            return Err(Error::Config(format!("no source is attached at {mount:?}")));
1372        };
1373        match entry.kind {
1374            SourceKind::Remote => {}
1375            SourceKind::LocalFolder | SourceKind::LocalGit => {
1376                return Err(Error::Config(format!(
1377                    "{mount:?} is not a remote source; folders reconcile with atelier sync and git sources pull with plain git"
1378                )));
1379            }
1380        }
1381        let index = self
1382            .mounts
1383            .iter()
1384            .position(|m| m.name == mount)
1385            .ok_or_else(|| Error::Engine(format!("no source is mounted at {mount:?}")))?;
1386        let remote = RemoteFolder::open(&entry.path.display().to_string()).map_err(engine_err)?;
1387        let Some((recorded, last_synced)) = self.coordination.sync_state(mount)? else {
1388            return Err(Error::Config(format!(
1389                "{mount:?} has no sync record; atelier sync --force seeds one"
1390            )));
1391        };
1392        if remote.fingerprint().map_err(engine_err)? == recorded {
1393            return Ok(PullOutcome::Current);
1394        }
1395        let head = self.mounts[index].engine.head()?;
1396        if head != last_synced {
1397            return Err(Error::Config(format!(
1398                "{mount:?} moved locally since its last sync ({head}); land or sync it first, then pull"
1399            )));
1400        }
1401        let mount_dir = self.root.join(mount);
1402        remote.download_mirror(&mount_dir).map_err(engine_err)?;
1403        let Some(snapshot) = self.mounts[index].engine.snapshot()? else {
1404            // The listing changed without the content changing (an ETag
1405            // rewrite); the record catches up, the line stays put.
1406            let fingerprint = remote.fingerprint().map_err(engine_err)?;
1407            self.coordination
1408                .record_sync_state(mount, &fingerprint, &head)?;
1409            return Ok(PullOutcome::Current);
1410        };
1411        // The window between mirror and this listing is ADR-0012's; the
1412        // record names what the pull believes the bucket held.
1413        let fingerprint = remote.fingerprint().map_err(engine_err)?;
1414        self.coordination
1415            .record_sync_state(mount, &fingerprint, &snapshot)?;
1416        let entry = self.entry(Act::Pull, Some(format!("{mount} {snapshot}")))?;
1417        self.journal.append(&entry)?;
1418        Ok(PullOutcome::Pulled { snapshot })
1419    }
1420
1421    /// Mirror a folder source's shared line back to its origin (ADR-0010):
1422    /// guarded by the recorded fingerprint unless `force`. Git sources
1423    /// refuse by name - bookmark motion is their out-flow. The act is
1424    /// journaled either way.
1425    pub fn sync(&mut self, source: Option<&str>, force: bool) -> Result<SyncOutcome, Error> {
1426        self.refresh_engines()?;
1427        self.auto_snapshot()?;
1428        let mount = source.unwrap_or(ROOT_MOUNT);
1429        let config = read_workspace_config(&self.root.join(CONTROL_DIR))?;
1430        let Some(entry) = config.sources.iter().find(|s| s.mount == mount) else {
1431            return Err(Error::Config(format!("no source is attached at {mount:?}")));
1432        };
1433        let target = match entry.kind {
1434            SourceKind::LocalGit => {
1435                return Err(Error::Config(format!(
1436                    "{mount:?} is a git source; landed work publishes with plain git push"
1437                )));
1438            }
1439            SourceKind::LocalFolder => SyncTarget::Folder(self.origin_path(&entry.path)),
1440            SourceKind::Remote => SyncTarget::Remote(entry.path.display().to_string()),
1441        };
1442        let outcome = self.sync_source(source, &target, force)?;
1443        let (act, detail) = match &outcome {
1444            SyncOutcome::Synced { snapshot } => (Act::Sync, snapshot.clone()),
1445            SyncOutcome::Parked { snapshot } => {
1446                (Act::SyncParked, format!("{snapshot} origin changed"))
1447            }
1448        };
1449        let reference = match source {
1450            Some(name) => format!("{name} {detail}"),
1451            None => detail,
1452        };
1453        let entry = self.entry(act, Some(reference))?;
1454        self.journal.append(&entry)?;
1455        Ok(outcome)
1456    }
1457
1458    /// An origin path as configured: absolute stays; relative anchors at
1459    /// the workspace root, where attach commands run.
1460    fn origin_path(&self, configured: &Path) -> PathBuf {
1461        if configured.is_absolute() {
1462            configured.to_path_buf()
1463        } else {
1464            self.root.join(configured)
1465        }
1466    }
1467
1468    /// Export the line's head to the target under the fingerprint guard;
1469    /// no journaling - the caller records the act in its own context.
1470    fn sync_source(
1471        &mut self,
1472        source: Option<&str>,
1473        target: &SyncTarget,
1474        force: bool,
1475    ) -> Result<SyncOutcome, Error> {
1476        let mount = source.unwrap_or(ROOT_MOUNT);
1477        let index = match source {
1478            None => None,
1479            Some(name) => Some(
1480                self.mounts
1481                    .iter()
1482                    .position(|m| m.name == name)
1483                    .ok_or_else(|| Error::Engine(format!("no source is mounted at {name:?}")))?,
1484            ),
1485        };
1486        let snapshot = match index {
1487            None => self.engine.head()?,
1488            Some(index) => self.mounts[index].engine.head()?,
1489        };
1490        let remote = match target {
1491            SyncTarget::Folder(_) => None,
1492            SyncTarget::Remote(url) => Some(RemoteFolder::open(url).map_err(engine_err)?),
1493        };
1494        if !force {
1495            let recorded = self.coordination.sync_state(mount)?;
1496            let current = match (target, &remote) {
1497                (SyncTarget::Folder(origin), _) => folder_fingerprint(origin)?,
1498                (SyncTarget::Remote(_), Some(remote)) => {
1499                    remote.fingerprint().map_err(engine_err)?
1500                }
1501                (SyncTarget::Remote(_), None) => unreachable_remote()?,
1502            };
1503            if recorded.map(|(fingerprint, _)| fingerprint) != Some(current) {
1504                return Ok(SyncOutcome::Parked { snapshot });
1505            }
1506        }
1507        let engine = match index {
1508            None => &self.engine,
1509            Some(index) => &self.mounts[index].engine,
1510        };
1511        let fingerprint = match (target, &remote) {
1512            (SyncTarget::Folder(origin), _) => {
1513                engine.export_tree(&snapshot, origin)?;
1514                folder_fingerprint(origin)?
1515            }
1516            (SyncTarget::Remote(_), Some(remote)) => {
1517                // The landed tree materializes in a scratch directory and
1518                // the adapter reconciles the bucket against it (ADR-0012).
1519                let scratch = tempfile::tempdir()?;
1520                engine.export_tree(&snapshot, scratch.path())?;
1521                remote.mirror(scratch.path()).map_err(engine_err)?;
1522                remote.fingerprint().map_err(engine_err)?
1523            }
1524            (SyncTarget::Remote(_), None) => unreachable_remote()?,
1525        };
1526        self.coordination
1527            .record_sync_state(mount, &fingerprint, &snapshot)?;
1528        Ok(SyncOutcome::Synced { snapshot })
1529    }
1530
1531    /// After a line moved — a landing advanced it or an undo stepped it
1532    /// back — mirror a folder source home. The move already stood: a dirty
1533    /// or unwritable origin parks the sync in the journal and never fails
1534    /// the caller (ADR-0010).
1535    fn sync_after_line_move(
1536        &mut self,
1537        session: &Session,
1538        source: Option<&str>,
1539        approver: &Actor,
1540    ) -> Result<(), Error> {
1541        let mount = source.unwrap_or(ROOT_MOUNT);
1542        let config = read_workspace_config(&self.root.join(CONTROL_DIR))?;
1543        let Some(entry) = config.sources.iter().find(|s| s.mount == mount) else {
1544            return Ok(());
1545        };
1546        let target = match entry.kind {
1547            SourceKind::LocalGit => return Ok(()),
1548            SourceKind::LocalFolder => SyncTarget::Folder(self.origin_path(&entry.path)),
1549            SourceKind::Remote => SyncTarget::Remote(entry.path.display().to_string()),
1550        };
1551        let (act, detail) = match self.sync_source(source, &target, false) {
1552            Ok(SyncOutcome::Synced { snapshot }) => (Act::Sync, snapshot),
1553            Ok(SyncOutcome::Parked { snapshot }) => {
1554                (Act::SyncParked, format!("{snapshot} origin changed"))
1555            }
1556            Err(error) => (Act::SyncParked, error.to_string()),
1557        };
1558        let reference = match source {
1559            Some(name) => format!("{name} {detail}"),
1560            None => detail,
1561        };
1562        self.append_session_entry(approver, act, session.id, Some(reference))?;
1563        Ok(())
1564    }
1565
1566    /// Snapshot every source's session working copy; each source's tip.
1567    /// A new snapshot — on any source — is journaled and runs the gate's
1568    /// side effects: it dismisses approvals (policy-decided) and re-opens
1569    /// an approved or parked request.
1570    fn snapshot_session(&mut self, session: &Session) -> Result<SessionTips, Error> {
1571        // The session's root working copy shares the root's boundary: a
1572        // mount name never lands on the shared line as root content.
1573        let boundary = self.mount_boundary();
1574        let mut engine = Engine::open(&session.working_copy, &session.actor, &boundary)?;
1575        let new_snapshot = engine.snapshot_amend()?;
1576        let root_tip = engine.head()?;
1577        let mut recorded: Vec<(Option<String>, String)> = Vec::new();
1578        if let Some(new_snapshot) = new_snapshot {
1579            recorded.push((None, new_snapshot));
1580        }
1581        let mut mounts = Vec::new();
1582        for name in boundary {
1583            let mut engine = Engine::open(&session.working_copy.join(&name), &session.actor, &[])?;
1584            if let Some(new_snapshot) = engine.snapshot_amend()? {
1585                recorded.push((Some(name.clone()), new_snapshot));
1586            }
1587            mounts.push((name, engine.head()?));
1588        }
1589        for (source, new_snapshot) in &recorded {
1590            let reference = match source {
1591                Some(source) => format!("{source} {new_snapshot}"),
1592                None => new_snapshot.clone(),
1593            };
1594            self.append_session_entry(&session.actor, Act::Snapshot, session.id, Some(reference))?;
1595            self.gate_reacts_to_snapshot(session, new_snapshot)?;
1596        }
1597        if !recorded.is_empty() {
1598            // The landing engines read this handle's view; fold the
1599            // sessions' operations in.
1600            self.refresh_engines()?;
1601        }
1602        Ok(SessionTips {
1603            root: root_tip,
1604            mounts,
1605        })
1606    }
1607
1608    /// The mounted source called `name`.
1609    fn mount(&self, name: &str) -> Result<&MountedSource, Error> {
1610        self.mounts
1611            .iter()
1612            .find(|mount| mount.name == name)
1613            .ok_or_else(|| Error::Engine(format!("no source is mounted at {name:?}")))
1614    }
1615
1616    /// Where a session path lives: the mount whose name leads it, or the
1617    /// session's root working copy.
1618    fn session_target(&self, session: &Session, path: &str) -> (Option<String>, PathBuf, String) {
1619        if let Some((first, rest)) = path.split_once('/')
1620            && !rest.is_empty()
1621            && self.mounts.iter().any(|mount| mount.name == first)
1622        {
1623            return (
1624                Some(first.to_owned()),
1625                session.working_copy.join(first),
1626                rest.to_owned(),
1627            );
1628        }
1629        (None, session.working_copy.clone(), path.to_owned())
1630    }
1631
1632    fn gate_reacts_to_snapshot(
1633        &mut self,
1634        session: &Session,
1635        new_snapshot: &str,
1636    ) -> Result<(), Error> {
1637        let Some(request) = self.coordination.gated_request_for_session(session.id.0)? else {
1638            return Ok(());
1639        };
1640        let id = RequestId(request.id);
1641        match request.state {
1642            RequestState::Open | RequestState::Approved | RequestState::Parked => {
1643                if self.config()?.landing.dismiss_approvals_on_new_snapshots {
1644                    let dismissed = self.coordination.dismiss_approvals(request.id)?;
1645                    if dismissed > 0 {
1646                        self.append_session_entry(
1647                            &session.actor,
1648                            Act::ApprovalsDismissed,
1649                            session.id,
1650                            Some(format!("{id} {new_snapshot}")),
1651                        )?;
1652                    }
1653                }
1654                match request.state {
1655                    // A new snapshot re-opens the gate: an approved apply
1656                    // no longer covers the change, a parked conflict may
1657                    // now be resolved. Losing the move means the gate
1658                    // closed concurrently — a closed gate stays closed.
1659                    RequestState::Approved | RequestState::Parked => {
1660                        let _ = self.coordination.move_request_state(
1661                            request.id,
1662                            &[RequestState::Approved, RequestState::Parked],
1663                            RequestState::Open,
1664                        )?;
1665                    }
1666                    RequestState::Open
1667                    | RequestState::Landed
1668                    | RequestState::Rejected
1669                    | RequestState::Abandoned => {}
1670                }
1671            }
1672            RequestState::Landed | RequestState::Rejected | RequestState::Abandoned => {}
1673        }
1674        Ok(())
1675    }
1676
1677    /// The request while its gate is still deciding; closed states refuse
1678    /// by name, and a parked request points at its way back (a new
1679    /// snapshot).
1680    fn gated_request(&mut self, id: RequestId) -> Result<RequestRow, Error> {
1681        let Some(row) = self.coordination.request(id.0)? else {
1682            return Err(Error::RequestNotFound(id.to_string()));
1683        };
1684        match row.state {
1685            RequestState::Open | RequestState::Approved => Ok(row),
1686            RequestState::Parked => Err(Error::RequestParked(id.to_string())),
1687            RequestState::Landed | RequestState::Rejected | RequestState::Abandoned => {
1688                Err(Error::RequestClosed {
1689                    id: id.to_string(),
1690                    state: row.state.to_string(),
1691                })
1692            }
1693        }
1694    }
1695
1696    /// The session when it is still open for work.
1697    fn open_session_only(&mut self, id: SessionId) -> Result<Session, Error> {
1698        let session = self.session(id)?;
1699        match session.state {
1700            SessionState::Open => Ok(session),
1701            SessionState::Landed | SessionState::Abandoned => Err(Error::SessionClosed {
1702                id: id.to_string(),
1703                state: session.state.to_string(),
1704            }),
1705        }
1706    }
1707
1708    fn session_from(&self, row: SessionRow) -> Result<Session, Error> {
1709        let id = SessionId(row.id);
1710        let change_id = row.change_id.ok_or_else(|| {
1711            Error::Engine(format!("session {id} has no change; its bootstrap failed"))
1712        })?;
1713        let mut changes = vec![SourceChange {
1714            source: None,
1715            change_id: change_id.clone(),
1716        }];
1717        for (source, mount_change) in self.coordination.session_source_changes(row.id)? {
1718            changes.push(SourceChange {
1719                source: Some(source),
1720                change_id: mount_change,
1721            });
1722        }
1723        Ok(Session {
1724            id,
1725            actor: Actor {
1726                name: row.actor_name,
1727                kind: row.actor_kind,
1728            },
1729            state: row.state,
1730            change_id,
1731            changes,
1732            working_copy: self.session_root(id),
1733            instruction_summary: row.instruction_summary,
1734            instruction_run_ref: row.instruction_run_ref,
1735            opened_at_ms: row.opened_at_ms,
1736        })
1737    }
1738
1739    fn request_from(&self, row: RequestRow) -> Result<LandingRequest, Error> {
1740        let approvals = self
1741            .coordination
1742            .live_approvals(row.id)?
1743            .into_iter()
1744            .map(|approval| Approval {
1745                actor: Actor {
1746                    name: approval.actor_name,
1747                    kind: approval.actor_kind,
1748                },
1749                snapshot: approval.snapshot_id,
1750                at_ms: approval.at_ms,
1751            })
1752            .collect();
1753        Ok(LandingRequest {
1754            id: RequestId(row.id),
1755            session_id: SessionId(row.session_id),
1756            requester: Actor {
1757                name: row.requester_name,
1758                kind: row.requester_kind,
1759            },
1760            state: row.state,
1761            approvals,
1762            created_at_ms: row.created_at_ms,
1763        })
1764    }
1765
1766    fn session_root(&self, id: SessionId) -> PathBuf {
1767        self.root
1768            .join(CONTROL_DIR)
1769            .join(SESSIONS_DIR)
1770            .join(id.to_string())
1771    }
1772
1773    fn config(&self) -> Result<WorkspaceConfig, Error> {
1774        read_workspace_config(&self.root.join(CONTROL_DIR))
1775    }
1776
1777    fn append_session_entry(
1778        &self,
1779        actor: &Actor,
1780        act: Act,
1781        session: SessionId,
1782        reference: Option<String>,
1783    ) -> Result<(), Error> {
1784        self.journal.append(&JournalEntry {
1785            at_ms: now_ms()?,
1786            actor_name: actor.name.clone(),
1787            actor_kind: actor.kind,
1788            act,
1789            session: Some(session.to_string()),
1790            instruction_summary: None,
1791            instruction_run_ref: None,
1792            instruction_verbatim: None,
1793            reference,
1794        })
1795    }
1796
1797    /// The document's projection for a read: the cache entry when
1798    /// published, computed and published otherwise. A read has no lower
1799    /// rung to fall to, so a failing or panicking package errors.
1800    fn project_for_read(&self, package: &dyn FormatPackage, bytes: &[u8]) -> Result<String, Error> {
1801        let blob = FileBlob {
1802            id: crate::projection::content_id(bytes),
1803            bytes: bytes.to_vec(),
1804        };
1805        if let Some(text) = self.projections.read(package.id(), &blob) {
1806            return Ok(text);
1807        }
1808        match catch_unwind(AssertUnwindSafe(|| package.project(&blob.bytes))) {
1809            Ok(Ok(projection)) => {
1810                // As in the diff path: the projection is already computed,
1811                // so a failed publish must not gate the read.
1812                let _ = self
1813                    .projections
1814                    .store(package.id(), &blob, &projection.text);
1815                Ok(projection.text)
1816            }
1817            Ok(Err(error)) => Err(Error::PackageFailed {
1818                package: package.id().to_string(),
1819                reason: error.to_string(),
1820            }),
1821            Err(_) => Err(Error::PackageFailed {
1822                package: package.id().to_string(),
1823                reason: "the package panicked during projection".to_owned(),
1824            }),
1825        }
1826    }
1827
1828    /// Raise every delta the ladder can: through a package projection when
1829    /// one detects the document, as plain text when both sides are text,
1830    /// else leave it at the binary rung it arrived at. A package differ's
1831    /// rich deltas follow the file delta they refine. Deltas from a
1832    /// mounted source carry mount-scoped addresses.
1833    fn raised(
1834        &self,
1835        engine: &Engine,
1836        diff: Diff,
1837        sides: &DiffSides,
1838        mount: Option<&str>,
1839    ) -> Result<Diff, Error> {
1840        let mut deltas = Vec::new();
1841        for delta in diff.deltas {
1842            deltas.extend(self.raise(engine, delta, sides, mount)?);
1843        }
1844        Ok(Diff { deltas })
1845    }
1846
1847    /// Only `Changed` deltas raise in v1: an added or removed document is
1848    /// already told by its listing line, without dumping its whole content.
1849    fn raise(
1850        &self,
1851        engine: &Engine,
1852        delta: Delta,
1853        sides: &DiffSides,
1854        mount: Option<&str>,
1855    ) -> Result<Vec<Delta>, Error> {
1856        // The engine addresses files by the path inside its own world; the
1857        // delta the workspace reports scopes that path by mount, and every
1858        // journal entry below speaks the scoped address.
1859        let raw = delta.address.as_str().to_owned();
1860        let mut delta = delta;
1861        if let Some(mount) = mount {
1862            delta.address = Address::new(format!("{mount}/{raw}"));
1863        }
1864        if delta.kind != DeltaKind::Changed {
1865            return Ok(vec![delta]);
1866        }
1867        let (before, after) = match engine.read_file_sides(sides, &raw)? {
1868            (Side::Blob(before), Side::Blob(after)) => (before, after),
1869            (Side::TooLarge, _) | (_, Side::TooLarge) => {
1870                self.file_too_large(delta.address.as_str())?;
1871                return Ok(vec![delta]);
1872            }
1873            (Side::Absent, _) | (_, Side::Absent) => return Ok(vec![delta]),
1874        };
1875        if let Some(package) = self.detected(delta.address.as_str(), &after.bytes)? {
1876            let projections = (
1877                self.projection(package, delta.address.as_str(), &before)?,
1878                self.projection(package, delta.address.as_str(), &after)?,
1879            );
1880            let (Some(projected_before), Some(projected_after)) = projections else {
1881                return Ok(vec![delta]);
1882            };
1883            let raised = delta.at_text_rung(
1884                diff_lines(&projected_before, &projected_after),
1885                Some(package.id()),
1886            );
1887            return self.enriched(raised, package, &before, &after);
1888        }
1889        // "Fidelity drops to text or binary" (CONTEXT.md, Format Package):
1890        // a package-less document that decodes as text diffs as text —
1891        // content-based detection, the git model — because extension
1892        // allowlists would drop the source and config files agents edit
1893        // all day to the binary rung. Opaque bytes stay binary.
1894        match (as_text(&before.bytes), as_text(&after.bytes)) {
1895            (Some(before), Some(after)) => {
1896                Ok(vec![delta.at_text_rung(diff_lines(before, after), None)])
1897            }
1898            _ => Ok(vec![delta]),
1899        }
1900    }
1901
1902    /// The Rich rung, additive over the text rung: the package differ's
1903    /// deltas — formatting the projection cannot express — follow the file
1904    /// delta, their format-terms addresses scoped under its path. Text
1905    /// changes stay on the file delta's lines, so nothing the differ does
1906    /// not model can ever drop out of a diff. A failing or panicking
1907    /// differ journals `package_failed` and the text rung stands.
1908    fn enriched(
1909        &self,
1910        raised: Delta,
1911        package: &dyn FormatPackage,
1912        before: &FileBlob,
1913        after: &FileBlob,
1914    ) -> Result<Vec<Delta>, Error> {
1915        let rich = match catch_unwind(AssertUnwindSafe(|| {
1916            package.diff(&before.bytes, &after.bytes)
1917        })) {
1918            Ok(None) => return Ok(vec![raised]),
1919            Ok(Some(Ok(rich))) => rich,
1920            Ok(Some(Err(error))) => {
1921                self.differ_failed(raised.address.as_str(), package.id(), &error.to_string())?;
1922                return Ok(vec![raised]);
1923            }
1924            Err(_) => {
1925                self.differ_failed(
1926                    raised.address.as_str(),
1927                    package.id(),
1928                    "the package panicked during diffing",
1929                )?;
1930                return Ok(vec![raised]);
1931            }
1932        };
1933        if rich.is_empty() {
1934            return Ok(vec![raised]);
1935        }
1936        let path = raised.address.as_str().to_owned();
1937        let mut deltas = vec![Delta {
1938            fidelity: Fidelity::Rich,
1939            ..raised
1940        }];
1941        deltas.extend(rich.into_iter().map(|delta| Delta {
1942            address: Address::new(format!("{path} > {}", delta.address.as_str())),
1943            ..delta
1944        }));
1945        Ok(deltas)
1946    }
1947
1948    /// The package claiming the document, behind a panic boundary: a
1949    /// panicking package degrades fidelity, it never kills the process
1950    /// (its journal entry keeps the degradation loud).
1951    fn detected(&self, address: &str, bytes: &[u8]) -> Result<Option<&dyn FormatPackage>, Error> {
1952        if let Ok(package) = catch_unwind(AssertUnwindSafe(|| {
1953            detect_package(&self.packages, address, bytes)
1954        })) {
1955            Ok(package)
1956        } else {
1957            self.package_failed(address, None, "a package panicked during detection")?;
1958            Ok(None)
1959        }
1960    }
1961
1962    /// One side's projection: the cache entry when published, computed and
1963    /// published otherwise. `None` when the package failed or panicked —
1964    /// journaled as `package_failed`, so the delta's fall to the binary
1965    /// rung is never silent.
1966    fn projection(
1967        &self,
1968        package: &dyn FormatPackage,
1969        address: &str,
1970        blob: &FileBlob,
1971    ) -> Result<Option<String>, Error> {
1972        if let Some(text) = self.projections.read(package.id(), blob) {
1973            return Ok(Some(text));
1974        }
1975        match catch_unwind(AssertUnwindSafe(|| package.project(&blob.bytes))) {
1976            Ok(Ok(projection)) => {
1977                // The cache is derived and evictable: the projection is
1978                // already computed and correct, so a failed publish must
1979                // not gate the diff — it only costs a recomputation on
1980                // some later diff.
1981                let _ = self.projections.store(package.id(), blob, &projection.text);
1982                Ok(Some(projection.text))
1983            }
1984            Ok(Err(error)) => {
1985                self.package_failed(address, Some(package.id()), &error.to_string())?;
1986                Ok(None)
1987            }
1988            Err(_) => {
1989                self.package_failed(
1990                    address,
1991                    Some(package.id()),
1992                    "the package panicked during projection",
1993                )?;
1994                Ok(None)
1995            }
1996        }
1997    }
1998
1999    fn package_failed(
2000        &self,
2001        address: &str,
2002        package: Option<PackageId>,
2003        reason: &str,
2004    ) -> Result<(), Error> {
2005        let reference = match package {
2006            Some(id) => format!("{address} {id} fell_back_to=binary: {reason}"),
2007            None => format!("{address} fell_back_to=binary: {reason}"),
2008        };
2009        let entry = self.entry(Act::PackageFailed, Some(reference))?;
2010        self.journal.append(&entry)
2011    }
2012
2013    /// A differ failure costs only the rich rung: the text rung the
2014    /// projection already raised stands, and the journal keeps the
2015    /// degradation loud.
2016    fn differ_failed(&self, address: &str, package: PackageId, reason: &str) -> Result<(), Error> {
2017        let reference = format!("{address} {package} fell_back_to=text: {reason}");
2018        let entry = self.entry(Act::PackageFailed, Some(reference))?;
2019        self.journal.append(&entry)
2020    }
2021
2022    /// A file past the ladder cap keeps its binary-rung listing line; the
2023    /// journal records the degradation so it is never silent.
2024    fn file_too_large(&self, address: &str) -> Result<(), Error> {
2025        let reference = format!(
2026            "{address} exceeds the {LADDER_FILE_SIZE_MAX}-byte ladder cap; kept at the binary rung"
2027        );
2028        let entry = self.entry(Act::FileTooLarge, Some(reference))?;
2029        self.journal.append(&entry)
2030    }
2031
2032    fn entry(&self, act: Act, reference: Option<String>) -> Result<JournalEntry, Error> {
2033        Ok(JournalEntry {
2034            at_ms: now_ms()?,
2035            actor_name: self.actor.name.clone(),
2036            actor_kind: self.actor.kind,
2037            act,
2038            session: None,
2039            instruction_summary: None,
2040            instruction_run_ref: None,
2041            instruction_verbatim: None,
2042            reference,
2043        })
2044    }
2045
2046    /// The root engine's boundary: every mount name, in name order.
2047    fn mount_boundary(&self) -> Vec<String> {
2048        self.mounts.iter().map(|mount| mount.name.clone()).collect()
2049    }
2050}
2051
2052/// Every format package built into this core, in detection order.
2053fn builtin_packages() -> Vec<Box<dyn FormatPackage>> {
2054    vec![Box::new(DocxPackage)]
2055}
2056
2057/// Each source's session tip: the root's, and every mount's by name.
2058struct SessionTips {
2059    root: String,
2060    mounts: Vec<(String, String)>,
2061}
2062
2063impl SessionTips {
2064    /// The tip of `source` — the root's when `None`. A session always has
2065    /// a tip for every source it spans.
2066    fn tip_of(&self, source: Option<&str>) -> String {
2067        match source {
2068            None => self.root.clone(),
2069            Some(name) => self
2070                .mounts
2071                .iter()
2072                .find(|(mount, _)| mount == name)
2073                .map_or_else(|| self.root.clone(), |(_, tip)| tip.clone()),
2074        }
2075    }
2076}
2077
2078/// One snapshot in one source's history: the root's when `source` is
2079/// `None`, else the named mount's.
2080#[derive(Debug, Clone, PartialEq, Eq)]
2081pub struct SourceSnapshot {
2082    /// The mount the snapshot belongs to; `None` for the root.
2083    pub source: Option<String>,
2084    /// The snapshot itself.
2085    pub snapshot: Snapshot,
2086}
2087
2088/// The mounted sources a config names, in name order — the one order every
2089/// aggregate walks.
2090fn mount_names(config: &WorkspaceConfig) -> Vec<String> {
2091    let mut names: Vec<String> = config
2092        .sources
2093        .iter()
2094        .filter(|source| source.mount != ROOT_MOUNT)
2095        .map(|source| source.mount.clone())
2096        .collect();
2097    names.sort();
2098    names
2099}
2100
2101/// A mount name is one path component that cannot collide with engine
2102/// internals or escape the root.
2103fn valid_mount_name(name: &str) -> Result<(), Error> {
2104    let flat = !name.is_empty()
2105        && name != "."
2106        && name != ".."
2107        && !name.contains('/')
2108        && !name.contains('\\');
2109    if !flat || SKIP_NAMES.contains(&name) {
2110        return Err(Error::Config(format!(
2111            "mount name {name:?} must be one path component outside the engine's internals"
2112        )));
2113    }
2114    Ok(())
2115}
2116
2117fn workspace_name(root: &Path) -> String {
2118    match root.file_name().and_then(|name| name.to_str()) {
2119        Some(name) => name.to_owned(),
2120        None => "workspace".to_owned(),
2121    }
2122}
2123
2124fn enclosing_workspace(root: &Path) -> Option<PathBuf> {
2125    let mut current = root.parent();
2126    while let Some(dir) = current {
2127        if dir.join(CONTROL_DIR).exists() {
2128            return Some(dir.to_path_buf());
2129        }
2130        current = dir.parent();
2131    }
2132    None
2133}
2134
2135/// A deterministic digest of a folder's content: every file's relative
2136/// path, kind, and bytes, sorted, engine-internal names skipped at any
2137/// depth. Two folders fingerprint alike exactly when a mirror would find
2138/// them identical (ADR-0010).
2139fn folder_fingerprint(folder: &Path) -> Result<String, Error> {
2140    let mut hasher = Sha256::new();
2141    hash_folder(&mut hasher, folder)?;
2142    Ok(format!("{:x}", hasher.finalize()))
2143}
2144
2145fn hash_folder(hasher: &mut Sha256, root: &Path) -> Result<(), Error> {
2146    // An explicit work stack bounds the walk by entry count, never call
2147    // depth; entries hash sorted by relative path for determinism.
2148    let mut files: Vec<(String, PathBuf, bool)> = Vec::new();
2149    let mut pending = vec![root.to_path_buf()];
2150    while let Some(dir) = pending.pop() {
2151        for entry in fs::read_dir(&dir)? {
2152            let entry = entry?;
2153            let name = entry.file_name();
2154            let Some(name) = name.to_str() else {
2155                return Err(Error::Engine(format!(
2156                    "cannot fingerprint a non-utf8 name at {}",
2157                    entry.path().display()
2158                )));
2159            };
2160            if SKIP_NAMES.contains(&name) {
2161                continue;
2162            }
2163            let path = entry.path();
2164            let file_type = entry.file_type()?;
2165            if file_type.is_dir() {
2166                pending.push(path);
2167            } else {
2168                let rel = path
2169                    .strip_prefix(root)
2170                    .map_err(engine_err)?
2171                    .to_string_lossy()
2172                    .into_owned();
2173                files.push((rel, path, file_type.is_symlink()));
2174            }
2175        }
2176    }
2177    files.sort();
2178    for (rel, path, is_symlink) in &files {
2179        hasher.update(rel.as_bytes());
2180        if *is_symlink {
2181            hasher.update([1]);
2182            hasher.update(fs::read_link(path)?.to_string_lossy().as_bytes());
2183        } else {
2184            hasher.update([2]);
2185            hasher.update(fs::read(path)?);
2186        }
2187        hasher.update([0]);
2188    }
2189    Ok(())
2190}
2191
2192/// The branch the adopted repository has checked out: the symbolic ref in
2193/// the source's `.git/HEAD`, `None` for a detached head. Landings move this
2194/// branch so plain `git push` from the mount carries the shared line.
2195fn adopted_branch(source: &Path) -> Result<Option<String>, Error> {
2196    let head = fs::read_to_string(source.join(".git").join("HEAD"))?;
2197    Ok(head
2198        .trim()
2199        .strip_prefix("ref: refs/heads/")
2200        .map(str::to_owned))
2201}
2202/// Everything that refuses a mount attach regardless of source kind,
2203/// each by name: an invalid mount name, a source already attached, a
2204/// mount colliding with workspace content.
2205fn mount_refusals(name: &str, config: &WorkspaceConfig, mount_dir: &Path) -> Result<(), Error> {
2206    valid_mount_name(name)?;
2207    if config.sources.iter().any(|s| s.mount == name) {
2208        return Err(Error::AlreadyAttached);
2209    }
2210    if mount_dir.exists() {
2211        return Err(Error::Config(format!(
2212            "mount {name:?} collides with existing workspace content"
2213        )));
2214    }
2215    Ok(())
2216}
2217
2218fn folder_uses_lfs(folder: &Path) -> Result<bool, Error> {
2219    let gitattributes = folder.join(".gitattributes");
2220    if !gitattributes.is_file() {
2221        return Ok(false);
2222    }
2223    let text = fs::read_to_string(&gitattributes)?;
2224    Ok(text.contains("filter=lfs"))
2225}
2226
2227/// Copy `source` into `target`, skipping `skips` at any depth. The import
2228/// path skips every engine-internal name; the adoption path keeps `.git` —
2229/// the repository itself is the content. An explicit work stack bounds the
2230/// walk by entry count, never call depth.
2231fn copy_tree(source: &Path, target: &Path, skips: &[&str]) -> Result<(), Error> {
2232    let mut pending = vec![(source.to_path_buf(), target.to_path_buf())];
2233    while let Some((from_dir, to_dir)) = pending.pop() {
2234        for entry in fs::read_dir(&from_dir)? {
2235            let entry = entry?;
2236            let name = entry.file_name();
2237            if skips.iter().any(|skip| name == **skip) {
2238                continue;
2239            }
2240            let from = entry.path();
2241            let to = to_dir.join(&name);
2242            if from.is_dir() {
2243                fs::create_dir_all(&to)?;
2244                pending.push((from, to));
2245            } else {
2246                fs::copy(&from, &to)?;
2247            }
2248        }
2249    }
2250    Ok(())
2251}
2252
2253/// The file at `path` inside `working_copy`: a relative path that never
2254/// climbs out — parent and root components refuse.
2255fn session_file(working_copy: &Path, path: &str) -> Result<PathBuf, Error> {
2256    let relative = Path::new(path);
2257    let stays_inside = relative.components().all(|component| match component {
2258        Component::Normal(_) | Component::CurDir => true,
2259        Component::ParentDir | Component::RootDir | Component::Prefix(_) => false,
2260    });
2261    if path.is_empty() || !stays_inside {
2262        return Err(Error::PathOutsideWorkingCopy(path.to_owned()));
2263    }
2264    Ok(working_copy.join(relative))
2265}
2266
2267/// The `ATELIER_LAND_HOLD_MS` test seam, absent in normal runs; a set but
2268/// unparsable value refuses instead of silently not holding.
2269fn land_hold_ms() -> Result<Option<u64>, Error> {
2270    match env::var("ATELIER_LAND_HOLD_MS") {
2271        Ok(value) => value.parse().map(Some).map_err(config_err),
2272        Err(VarError::NotPresent) => Ok(None),
2273        Err(error @ VarError::NotUnicode(_)) => Err(config_err(error)),
2274    }
2275}
2276
2277fn now_ms() -> Result<i64, Error> {
2278    let elapsed = SystemTime::now()
2279        .duration_since(UNIX_EPOCH)
2280        .map_err(config_err)?;
2281    i64::try_from(elapsed.as_millis()).map_err(config_err)
2282}