Skip to main content

kernel/
layout.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Where a city keeps each kind of file on disk: one derivation from
7//! the city root and an address (`crates/kernel/spec/Layout.lean` §8-56).
8//!
9//! Every directory name and file name the city writes is declared here
10//! once, and every path it writes them to is spelled here once, so a
11//! rename reaches every caller and none is left reading an empty
12//! directory.
13//!
14//! Nothing here touches a disk. A `CityLayout` is a value: it answers
15//! where a file would be, and the caller that owns the I/O decides
16//! whether to create it, read it, or refuse. That keeps the layout
17//! inside `kernel`, where the address grammar it rests on already is.
18//!
19//! One rule governs the placement itself: **what governs a scope lives
20//! in that scope's reserved subtree, and no write domain reaches it**
21//! (`Address::is_reserved`). The rules of a building, its configuration
22//! layer and its filters are therefore under [`RESERVED_PREFIX`], while
23//! the documents residents write — the job, the handoff, the urbanite
24//! page, the archive — sit in the open where residents can edit them.
25
26use std::path::{Path, PathBuf};
27
28use crate::{Address, RESERVED_PREFIX};
29
30/// The append-only history of a city, under the city's reserved subtree.
31pub const LEDGER_DIR: &str = "ledger";
32/// The content-addressed object store, under the city's reserved subtree.
33pub const CAS_DIR: &str = "cas";
34/// What the views held after one ledger line, under the city's reserved
35/// subtree; a projection a start may discard.
36pub const SNAPSHOT_DIR: &str = "snapshot";
37/// The city's shelves of skills, under the city's reserved subtree.
38pub const LIBRARY_DIR: &str = "library";
39/// A building's own shelf of skills, under that building's reserved
40/// subtree: what this building knows and no other building is given.
41pub const BUILDING_SHELF: &str = "skills";
42/// The one document of a skill filed as a directory: a package on a
43/// section shelf, and every skill on a shelf the city mounts from
44/// elsewhere. It is the layout pi, claude and agents all use; the
45/// catalog lists this file's first line, and `read` opens the files
46/// beside it by `<name>/<path>`.
47pub const SKILL_FILE: &str = "SKILL.md";
48/// What one configuration layer declares. The same file name at every
49/// layer, because the layer is the scope it sits in rather than a name.
50pub const CONFIG_FILE: &str = "CONFIG.toml";
51/// What a scope keeps out of the transcript it sends to a model.
52pub const FILTERS_FILE: &str = "FILTERS.toml";
53/// Where a building files work it has finished with, by kind and day.
54pub const ARCHIVE_DIR: &str = "Archive";
55/// The task of one session, in the room it is run from.
56pub const JOB_FILE: &str = "JOB.md";
57/// What the next agent needs before it starts, per room.
58pub const HANDOFF_FILE: &str = "Handoff.md";
59/// Who a standing resident is, in that resident's own directory.
60pub const URBANITE_FILE: &str = "URBANITE.md";
61/// How far the person has got through the first-run guide, under the
62/// city's reserved subtree.
63pub const GUIDE_FILE: &str = "GUIDE.toml";
64/// The extension of what one run saw, written in its room as
65/// `<run>.jsonl`.
66pub const TRANSCRIPT_EXT: &str = "jsonl";
67/// A run id as a git ignore pattern: one `?` for each character of the
68/// hyphenated form a [`crate::RunId`] displays, so a building can keep
69/// every run's transcript out of its history by name.
70pub const RUN_ID_PATTERN: &str = "????????-????-????-????-????????????";
71/// What the remote gate keeps on disk, under the city's reserved subtree.
72pub const REMOTE_DIR: &str = "remote";
73/// The devices paired to reach the city from outside the machine.
74pub const DEVICES_FILE: &str = "devices.toml";
75/// Where residents' playback exports land, under the city's reserved
76/// subtree, one directory per building.
77pub const PLAYBACK_DIR: &str = "playback";
78
79/// The disk layout of one city, derived from its root.
80///
81/// Construct it once per city root and ask it for paths; the segments
82/// are never joined by hand at a call site.
83#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct CityLayout {
85    root: PathBuf,
86}
87
88impl CityLayout {
89    /// The layout of the city rooted at `root`.
90    #[must_use]
91    pub fn new(root: &Path) -> Self {
92        Self {
93            root: root.to_path_buf(),
94        }
95    }
96
97    /// The city root this layout derives from.
98    #[must_use]
99    pub fn root(&self) -> &Path {
100        &self.root
101    }
102
103    /// The directory an address names: the city root with the address's
104    /// segments below it.
105    ///
106    /// Segment by segment rather than one joined string, because a
107    /// city served on Windows and a city served on Linux must lay the
108    /// same address down as the same directory.
109    #[must_use]
110    pub fn scope(&self, addr: &Address) -> PathBuf {
111        let mut path = self.root.clone();
112        for segment in addr.as_str().split('/') {
113            path.push(segment);
114        }
115        path
116    }
117
118    /// The whole city's history.
119    #[must_use]
120    pub fn ledger(&self) -> PathBuf {
121        self.governed_root().join(LEDGER_DIR)
122    }
123
124    /// The snapshot a start resumes the views from.
125    #[must_use]
126    pub fn snapshot(&self) -> PathBuf {
127        self.governed_root().join(SNAPSHOT_DIR)
128    }
129
130    /// The whole city's object store.
131    #[must_use]
132    pub fn cas(&self) -> PathBuf {
133        self.governed_root().join(CAS_DIR)
134    }
135
136    /// The city's shelves of skills.
137    #[must_use]
138    pub fn library(&self) -> PathBuf {
139        self.governed_root().join(LIBRARY_DIR)
140    }
141
142    /// The configuration layer a scope declares.
143    #[must_use]
144    pub fn config(&self, addr: &Address) -> PathBuf {
145        self.governed(addr).join(CONFIG_FILE)
146    }
147
148    /// The configuration layer the city itself declares.
149    ///
150    /// The same rule as [`config`](Self::config) at the root, and a
151    /// method of its own because the root is not an address, and a
152    /// caller without one would otherwise join the two names by hand.
153    #[must_use]
154    pub fn city_config(&self) -> PathBuf {
155        self.governed_root().join(CONFIG_FILE)
156    }
157
158    /// What the city as a whole keeps out of what it sends a model.
159    #[must_use]
160    pub fn city_filters(&self) -> PathBuf {
161        self.governed_root().join(FILTERS_FILE)
162    }
163
164    /// A building's own shelf of skills.
165    #[must_use]
166    pub fn building_skills(&self, addr: &Address) -> PathBuf {
167        self.governed(addr).join(BUILDING_SHELF)
168    }
169
170    /// What a scope keeps out of what it sends a model.
171    #[must_use]
172    pub fn filters(&self, addr: &Address) -> PathBuf {
173        self.governed(addr).join(FILTERS_FILE)
174    }
175
176    /// Where a building files finished work.
177    ///
178    /// In the open rather than under the reserved subtree: residents
179    /// write these documents and read them back.
180    #[must_use]
181    pub fn archive(&self, building: &Address) -> PathBuf {
182        self.scope(building).join(ARCHIVE_DIR)
183    }
184
185    /// The task written for the session run at this address.
186    #[must_use]
187    pub fn job(&self, addr: &Address) -> PathBuf {
188        self.scope(addr).join(JOB_FILE)
189    }
190
191    /// What one room hands to whoever picks it up next.
192    ///
193    /// The room's rather than the building's: two sessions of one
194    /// building can freeze at once, and one file for both would be two
195    /// contents fighting over one name.
196    #[must_use]
197    pub fn handoff(&self, room: &Address) -> PathBuf {
198        self.scope(room).join(HANDOFF_FILE)
199    }
200
201    /// Who the standing resident at this address is.
202    #[must_use]
203    pub fn urbanite(&self, addr: &Address) -> PathBuf {
204        self.scope(addr).join(URBANITE_FILE)
205    }
206
207    /// How far the person has got through this city's first-run guide
208    /// (`crates/wire/Spec.lean` §8-68).
209    ///
210    /// Under the city's reserved subtree, because it decides what this
211    /// city shows a person when it opens, and no write domain reaches it.
212    #[must_use]
213    pub fn guide(&self) -> PathBuf {
214        self.governed_root().join(GUIDE_FILE)
215    }
216
217    /// The devices paired to reach this city from outside the machine.
218    ///
219    /// Under the city's reserved subtree, because the table decides who
220    /// may reach the city and no write domain may change that
221    /// (`crates/kernel/spec/Layout.lean` §8-76).
222    #[must_use]
223    pub fn devices(&self) -> PathBuf {
224        self.governed_root().join(REMOTE_DIR).join(DEVICES_FILE)
225    }
226
227    /// Where residents' playback exports land, one directory per
228    /// building (`crates/accounting/Spec.lean` §8-13, `crates/sprawling/Spec.lean` §8-132).
229    ///
230    /// Under the city's reserved subtree, because no write domain reaches
231    /// it, so a written report is changed only by exporting another; the
232    /// city root's `.gitignore` keeps it out of history; and no worktree
233    /// holds it, so sweeping one loses no report.
234    #[must_use]
235    pub fn playback_exports(&self) -> PathBuf {
236        self.governed_root().join(PLAYBACK_DIR)
237    }
238
239    /// The layout whose ledger is `dir`, when `dir` is a city's.
240    ///
241    /// The inverse of [`ledger`](Self::ledger), for the writer that is
242    /// handed the ledger directory and must find the root every other
243    /// path is derived from. `None` for a directory that is not a ledger
244    /// under a reserved prefix: a fixture, a bundle being verified, or a
245    /// store somebody opened directly, none of which is a city.
246    #[must_use]
247    pub fn of_ledger(dir: &Path) -> Option<CityLayout> {
248        if dir.file_name()? != LEDGER_DIR {
249            return None;
250        }
251        let governed = dir.parent()?;
252        if governed.file_name()? != RESERVED_PREFIX {
253            return None;
254        }
255        let root = governed.parent()?;
256        if root.as_os_str().is_empty() {
257            return None;
258        }
259        Some(CityLayout::new(root))
260    }
261
262    /// The city's own name, read from the directory it lives in.
263    ///
264    /// The one address that names the city rather than a room: the
265    /// genesis record carries it so a city can say who it is, and the
266    /// session projection must file it nowhere, because no building is
267    /// named after the city inside itself. Not every directory name is
268    /// an address - a path can hold characters an address may not - and
269    /// a city whose directory cannot be spelled as an address simply has
270    /// no name to show, which is honest and rare.
271    #[must_use]
272    pub fn city_address(&self) -> Option<Address> {
273        self.root
274            .file_name()
275            .and_then(|name| name.to_str())
276            .and_then(|name| Address::parse(name).ok())
277    }
278
279    /// The city root's own reserved subtree.
280    fn governed_root(&self) -> PathBuf {
281        self.root.join(RESERVED_PREFIX)
282    }
283
284    /// A scope's own reserved subtree: what governs that scope, where no
285    /// write domain reaches it.
286    fn governed(&self, addr: &Address) -> PathBuf {
287        self.scope(addr).join(RESERVED_PREFIX)
288    }
289}
290
291#[cfg(test)]
292#[allow(
293    clippy::unwrap_used,
294    clippy::expect_used,
295    clippy::panic,
296    clippy::indexing_slicing,
297    reason = "test code"
298)]
299mod tests;