docs: consolidate design documentation and standardize crate English

This commit is contained in:
2026-10-12 14:58:00 +09:00
parent a633a53274
commit 4efb0ace1f
404 changed files with 4556 additions and 11654 deletions
+10
View File
@@ -0,0 +1,10 @@
# workspace-drive
Stores the latest Workspace-owned document tree using SQLite metadata and immutable blobs.
Does not provide a POSIX filesystem, document version history, or Worker/Session-owned attachments. The Host handles HTTP authentication and grants.
## Main entry points
- [`Drive`]: document tree, reads, collection, and deletion
- [`Mutation`], [`Drive::mutate`]: updates based on observed mutations
- [`RequestStatus`], [`Drive::request_status`]: committed operation results
+8 -6
View File
@@ -1,10 +1,12 @@
//! Synchronous, immutable local blobs for the SQLite drive authority.
//! Synchronous, immutable local blobs for SQLite Drive authority.
//!
//! Call these methods on blocking threads, while holding the authority's SQLite
//! IMMEDIATE transaction. That transaction, not this adapter, serializes reads,
//! uploads and GC across processes. The root and its ancestors must be owned by
//! trusted workspace code: path checks reject existing symlinks, but the
//! path-based LocalFileSystem API cannot prevent hostile concurrent replacement.
//! The caller's IMMEDIATE transaction, not this adapter, provides inter-process exclusion for readers, uploads, and GC.
//! `LocalFileSystem` Create publication alone does not guarantee file/directory sync. Sync the published file,
//! then its directory, before returning success to the metadata transaction. Unreferenced blobs left by failures are eligible for GC.
//!
//! The Host protects the root and ancestors. Existing symlinks are rejected, but this path-based API is not
//! a sandbox against concurrent path replacement by a hostile local actor. Requires a local persistent filesystem
//! with working file/directory sync and SQLite WAL locks; sync failures never silently weaken durability.
use std::collections::BTreeSet;
use std::fs::{self, File};
+54 -40
View File
@@ -1,6 +1,5 @@
//! Workspace-owned latest-version storage. Call these blocking operations on a
//! blocking thread, never on an async executor. SQL IMMEDIATE transactions are
//! the cross-process authority for uploads, tree changes, reads, and collection.
#![doc = include_str!("../README.md")]
mod blob;
mod migrations;
use migrations::MIGRATIONS;
@@ -83,13 +82,16 @@ pub enum Kind {
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
/// Public projection of a node with a stable Workspace-local ID.
/// IDs are never reused after deletion and use decimal strings in JSON to preserve JavaScript precision.
/// Blob keys and physical paths are excluded from the public projection.
pub struct Node {
pub id: NodeId,
pub workspace_id: String,
pub parent_id: Option<NodeId>,
pub name: String,
pub kind: Kind,
/// Request ID of the last committed mutation; empty only for the immutable root.
/// Request ID of the last committed mutation. Only the immutable root has an empty value.
pub last_mutation_id: String,
pub size: u64,
pub content_type: Option<String>,
@@ -99,6 +101,9 @@ pub struct Node {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "operation", rename_all = "snake_case", deny_unknown_fields)]
/// Document mutations based on an observed commit request ID.
/// Update/Relocate/Delete compare the expected mutation ID with the current value in the same transaction.
/// Conflicts use neither counters nor timestamps; only one concurrent mutation based on the same observation commits.
pub enum Mutation {
CreateFolder {
parent: NodeId,
@@ -137,8 +142,8 @@ pub struct MutationResult {
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum RequestStatus {
/// No committed result in this authority snapshot. A concurrently running
/// request may still commit; retry with the SAME identity and fingerprint.
/// No committed receipt exists at this observation point.
/// Does not prove an in-flight concurrent request will not commit; retry with the same identity and payload.
Uncommitted,
Committed {
result: MutationResult,
@@ -180,6 +185,14 @@ pub struct CollectionPage {
type Authorizer = dyn Fn(&Connection, bool) -> Result<()> + Send + Sync;
/// Workspace-scoped storage of the latest document tree.
/// SQLite is authoritative for the tree and latest blob references; names/parents never determine blob paths.
/// The root is immutable, names undergo no normalization or case folding, and ordinary folder deletion requires an empty folder.
///
/// Uploads, tree mutations, reads, and GC serialize through the same IMMEDIATE transaction.
/// Processes must share the same metadata DB and blob root; distributed writes through independent DBs are unsupported.
/// Operations are synchronous, so async Hosts use blocking threads.
/// No mechanism snapshots DB and blobs together; metadata backup alone cannot restore content.
#[derive(Clone)]
pub struct Drive {
authorizer: Option<Arc<Authorizer>>,
@@ -217,10 +230,11 @@ impl Drive {
_storage: storage.clone(),
})
}
/// Trusted Server attachment: SQLite IMMEDIATE also locks this authority,
/// so a cached Drive handle cannot race deletion reservation in server.db.
/// This is Host configuration, never a model-selected database path. The
/// attached authority is read-only by convention; Drive never writes it.
/// Attaches a trusted Host's Workspace authority DB to the connection.
/// IMMEDIATE transactions lock authority too, rejecting mutations through cached handles after deletion is reserved.
/// Drive does not modify attached authority or guarantee atomic writes across DBs.
/// Blob I/O also serializes with Host DB writes, so long operations delay other Host writes.
/// The DB path is Host construction input, never model input.
pub fn open_with_workspace_authority(
storage: &ScopedFeatureStorage,
registration: &RegisteredFeature,
@@ -257,10 +271,9 @@ impl Drive {
})?;
Ok(drive)
}
/// Bind immutable caller authority to this handle and its clones. The callback
/// runs within every client operation's IMMEDIATE transaction, never at bind
/// time; attached authority stays locked through blob I/O and publication.
/// Host maintenance must use an unbound handle.
/// Binds the caller's authorization callback to the handle and its clones.
/// Runs inside every client operation transaction, not at binding, holding the authority lock through blob I/O and publication.
/// Rechecks receipt replay and result queries too. Host maintenance uses unbound handles.
pub fn with_authorizer<F>(&self, authorizer: F) -> Self
where
F: Fn(&Connection, bool) -> Result<()> + Send + Sync + 'static,
@@ -276,8 +289,7 @@ impl Drive {
None => Ok(()),
}
}
/// Early admission check, not a lease: every subsequent operation still
/// invokes its authorizer in that operation's own transaction.
/// Early admission check, not a lease. Subsequent operations recheck authorization in their own transactions.
pub fn check_authorized(&self, write: bool) -> Result<()> {
self.database.try_transaction(|tx| {
self.authorize(tx, write)?;
@@ -302,8 +314,8 @@ impl Drive {
})
}
/// Keyset pages are ordered by stable ID, not name. Concurrent mutations may
/// appear/disappear between pages. There is no snapshot promise across calls.
/// Keyset page in stable ID order, not name order or a snapshot across calls.
/// Concurrent changes between pages may add, delete, or move entries.
pub fn list(&self, parent: NodeId, after: Option<NodeId>, limit: usize) -> Result<Page> {
validate_limit(limit, MAX_PAGE)?;
self.database.try_transaction(|tx| {
@@ -329,9 +341,9 @@ impl Drive {
})
}
/// Recover the committed result by request identity, including pre-cutover
/// receipts whose payload cannot be verified for replay. Committed status is
/// not proof that a different actor/payload matches that request.
/// Reads the original committed result by request identity. Receipts survive later node deletion.
/// Results remain readable even if the legacy receipt's payload cannot be proven for replay,
/// but their existence never authorizes an operation with a different actor/payload.
pub fn request_status(&self, request_id: &str) -> Result<RequestStatus> {
validate_identity(request_id, "request_id", 128)?;
self.database.try_transaction(|c| {
@@ -352,12 +364,11 @@ impl Drive {
})
}
/// DB commit is publication. The blob and receipt are never independently
/// treated as a successful mutation. Successful request IDs are retained for
/// the Workspace lifetime; reuse with a different actor/payload is conflict.
/// Pre-cutover replay checks the original typed-payload digest using proven
/// receipt correspondence. If that evidence is unavailable, returns Conflict
/// without executing; recover the committed result through request_status.
/// Publishes metadata updates and a receipt in one DB commit after syncing blobs.
/// Saving a blob alone is not success. New blobs left by sync/commit failures stay unreferenced and eligible for GC.
/// Request IDs persist for the Workspace lifetime; replay with the same actor/payload returns the original result.
/// The actor is trusted input from the authentication adapter; reusing an ID with a different actor/payload conflicts.
/// Legacy replay uses only the original payload digest and proven correspondence; unknown correspondence conflicts without execution.
pub fn mutate(
&self,
request_id: &str,
@@ -428,9 +439,9 @@ impl Drive {
})
}
/// Each chunk requires the originally observed committed mutation. Old blobs are
/// not retained: after update/delete return conflict/not-found, never switch
/// silently to the latest blob. GC cannot run while this read holds SQL lock.
/// Pins each chunk to the observed commit request, serializing with uploads/GC via SQL locks.
/// Old blobs are not retained as history; updates/deletions return conflict/not-found rather than switch to latest content.
/// Checks referenced blob existence and total length even for empty files; missing blobs are not normal empty content.
pub fn read(
&self,
id: NodeId,
@@ -475,8 +486,8 @@ impl Drive {
})
}
/// UTF-8 text over byte offsets. Callers must select character-aligned
/// ranges; invalid UTF-8 (including a split character) is an explicit error.
/// Reads a byte-offset range as UTF-8.
/// Ranges splitting character boundaries or invalid UTF-8 return explicit errors, never implicit correction.
pub fn read_text(
&self,
id: NodeId,
@@ -497,9 +508,9 @@ impl Drive {
})
}
/// Search examines at most `budget` nodes after the cursor. Text inspection
/// is limited to UTF-8 text/* files <=64KiB, and matches are bounded by budget.
/// An empty matching page may still carry a continuation cursor.
/// Search with a budget limiting nodes examined after the cursor.
/// Text inspection covers only small UTF-8 text/* blobs, never unbounded reads of large blobs.
/// The cursor is the last examined ID, so continuation may exist even with no matches.
pub fn search(
&self,
query: &str,
@@ -535,9 +546,10 @@ impl Drive {
})
}
/// Collection shares the exact database lock with uploads/readers. No grace
/// period or process-local mutex is used as a safety proof. Repeat full sweeps
/// from None, since new random keys can sort before a previous cursor.
/// Holds the same IMMEDIATE lock as readers/uploads and collects only unreferenced blobs and interrupted staging files.
/// Includes blobs left by rollback after a crash; relies on neither grace periods nor in-process mutexes alone.
/// Local listing scans the whole directory, but responses and retained candidates are bounded.
/// New UUIDs may sort before the cursor, so future sweeps start at the beginning.
pub fn collect(&self, after: Option<&str>, limit: usize) -> Result<CollectionPage> {
validate_limit(limit, MAX_PAGE)?;
self.database.try_transaction(|tx| {
@@ -564,14 +576,16 @@ impl Drive {
})
})
}
/// Commits a persistent tombstone rejecting new operations. Receipt reads/replays never authorize new mutations.
pub fn fence(&self) -> Result<()> {
self.database.try_transaction(|tx| {
tx.execute("UPDATE drive_state SET deleting=1 WHERE singleton=1", [])?;
Ok(())
})
}
/// Retryable purge. Fence is committed first. Root and deletion tombstone
/// remain until Host FeatureStorage deletes the metadata scope.
/// Commits the fence first, then removes this Workspace's blobs, nodes, and receipts in a retryable manner.
/// Retains the root and tombstone until the Host deletes the FeatureStorage scope.
/// A Host lifecycle operation distinct from ordinary folder deletion; never processes other Workspaces' storage roots.
pub fn purge(&self) -> Result<()> {
self.fence()?;
self.database.try_transaction(|tx| {
+5 -1
View File
@@ -1,4 +1,8 @@
//! Frozen schema upgrades. Old column/JSON names occur only to preserve existing data.
//! Frozen schema conversions preserving existing records.
//!
//! Binds current nodes to receipt-derived commit requests, preserving original fingerprints,
//! receipts for deleted nodes, and ID allocation history. Missing or ambiguous current-state correspondence rolls back the entire migration.
//! Legacy representations are used only for exact replay validation against original requests, never as authority for new mutations.
use crate::{Mutation, NodeId};
use feature_storage::FeatureMigration;
use rusqlite::{OptionalExtension, Transaction};