Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Typed Path Provenance

Status: Current Last updated: 2026-05-19 20:22 EDT

Paths in batchalign3 cross machine boundaries: a client submits paths from their filesystem, the server resolves media on its own filesystem (potentially different mount points), and media mappings translate logical repo names to physical volume roots. Untyped String/PathBuf allowed mixing client and server paths, causing repeated media resolution failures. The path newtype system makes provenance explicit so the compiler prevents these bugs.

The Problem

The batchalign3 server accepts job submissions from both local daemons (same machine, shared filesystem) and remote clients (different machines, different mount points). Before path newtypes, both cases used bare String or PathBuf:

#![allow(unused)]
fn main() {
// BEFORE: which machine is this path on?
pub struct JobSubmission {
    pub source_dir: String,       // Client's input dir, but server can't read it
    pub media_mapping: String,    // Logical name, not a path at all
    pub source_paths: Vec<String>, // Client paths, must not be opened on server
}
}

This caused bugs where the server attempted filesystem I/O on a client’s path (which did not exist on the server), or where a logical mapping key was accidentally used as a filesystem path.

The Solution: Four Path Newtypes

All defined in crates/batchalign-types/src/paths.rs:

classDiagram
    class ClientPath {
        -String inner
        +as_str() &str
        +assume_shared_filesystem() ServerPath
        +contains_component(component) bool
        +suffix_after_component(component) Option~&str~
    }
    note for ClientPath "Does NOT impl AsRef~Path~\nPrevents accidental filesystem I/O"

    class ServerPath {
        -PathBuf inner
        +as_path() &Path
        +join(component) ServerPath
        +as_str() &str
    }
    note for ServerPath "Impls AsRef~Path~\nSafe for tokio::fs, std::fs"

    class RepoRelativePath {
        -String inner
        +resolve_on_server(root: &ServerPath) ServerPath
        +join(sub) RepoRelativePath
        +as_str() &str
    }
    note for RepoRelativePath "Relative to a data repo root\nMachine-independent"

    class MediaMappingKey {
        -String inner
        +as_str() &str
    }
    note for MediaMappingKey "Logical name, not a path\ne.g. 'slabank-data'"

    ClientPath ..> ServerPath : assume_shared_filesystem()
    RepoRelativePath ..> ServerPath : resolve_on_server()
    MediaMappingKey ..> ServerPath : lookup in config

ClientPath

A path on the submitting client’s filesystem. The server receives this as metadata but must not do filesystem I/O on it directly.

Key invariant: ClientPath deliberately does NOT implement AsRef<Path>. This means passing a ClientPath to tokio::fs::read_to_string() or std::fs::metadata() is a compile error. The compiler enforces the boundary.

The only sanctioned conversion to ServerPath is:

pub fn assume_shared_filesystem(&self) -> ServerPath

This asserts that the server shares the client’s filesystem (true when the server is a local daemon on the same machine). Callers must verify this precondition – using it on a remote client’s path produces a ServerPath that points to a nonexistent location.

ClientPath also provides string-level inspection methods for media mapping inference:

  • contains_component("slabank-data") – checks if a repo name appears as a path component
  • suffix_after_component("slabank-data") – extracts the repo-relative portion (e.g., "French/Newcastle/Photos")

These are pure string operations that never touch the filesystem.

ServerPath

A path on the server’s filesystem, safe for I/O. Implements AsRef<Path> so it can be passed directly to filesystem operations:

let server_path: ServerPath = /* ... */;
let contents = tokio::fs::read_to_string(&server_path).await?;

Created from:

  • ClientPath::assume_shared_filesystem() (shared-filesystem assertion)
  • RepoRelativePath::resolve_on_server(&root) (combining a relative path with a server root)
  • ServerPath::new(pathbuf) (direct construction from a known server path)
  • Media mapping config deserialization (volume roots in server.yaml)

RepoRelativePath

A path relative to a data repository root (e.g., "French/Newcastle/Photos/13"). This is machine-independent – it is valid on any machine that has the repository cloned.

Must be combined with a ServerPath root to produce an absolute server path:

let root = ServerPath::new("/srv/talkbank/slabank");
let rel = RepoRelativePath::new("French/Newcastle/Photos/13");
let abs = rel.resolve_on_server(&root);
// → /srv/talkbank/slabank/French/Newcastle/Photos/13

MediaMappingKey

A logical name that maps to a ServerPath via the server’s media_mappings configuration. Not a filesystem path at all – it is an index into a BTreeMap<MediaMappingKey, ServerPath> in ServerConfig.

Examples: "slabank-data", "childes-eng-na-data", "aphasia-data".

Data Flow: Media Resolution

The FA pipeline (runner/dispatch/fa_pipeline.rs) resolves audio files through a multi-step cascade. The path newtypes make each step’s provenance explicit.

flowchart TD
    subgraph Client["Client Machine"]
        submit["JobSubmission\n(source_dir: ClientPath)"]
    end

    subgraph Server["Server (fa_pipeline.rs)"]
        step1{"--media-dir\nprovided?"}
        step2{"paths_mode?\n(shared filesystem)"}
        step3{"media_mapping key\nin config?"}
        step4{"auto-infer mapping\nfrom ClientPath?"}
        step5["media_roots\nfallback search"]

        convert["assume_shared_filesystem()\nClientPath → ServerPath"]
        infer["infer_media_mapping()\n(paths.rs)"]
        resolve["resolve_on_server()\nRepoRelativePath + ServerPath"]

        found["ServerPath\n(safe for I/O)"]
    end

    submit --> step1
    step1 -->|"yes"| found
    step1 -->|"no"| step2
    step2 -->|"yes"| convert --> found
    step2 -->|"no"| step3
    step3 -->|"yes"| resolve --> found
    step3 -->|"no"| step4
    step4 -->|"match"| infer --> resolve
    step4 -->|"no match"| step5 --> found

Auto-Inference: infer_media_mapping()

When no explicit media_mapping key is provided, the server auto-infers the mapping from the client’s source directory path. This is the function in crates/batchalign-types/src/paths.rs:

pub fn infer_media_mapping<'a>(
    client_dir: &ClientPath,
    mappings: impl IntoIterator<Item = (&'a MediaMappingKey, &'a ServerPath)>,
) -> Option<(MediaMappingKey, ServerPath, RepoRelativePath)>

It checks whether any key in mappings appears as a path component in client_dir. Example:

  • client_dir: /Users/operator/chat-data/slabank-data/French/Newcastle/Photos
  • mappings: {"slabank-data" → "/srv/talkbank/slabank"}
  • Returns: ("slabank-data", "/srv/talkbank/slabank", "French/Newcastle/Photos")

This is a pure string operation on ClientPath – it never touches the filesystem. The suffix_after_component() method extracts the repo-relative portion, which becomes a RepoRelativePath.

Where Path Types Are Used

TypeUsed inField / Parameter
ClientPathJobSubmissionsource_dir, source_paths, output_paths, before_paths
ClientPathJobMetadatasource_dir
ClientPathJobFilesystemConfigsource_dir
MediaMappingKeyJobSubmissionmedia_mapping
MediaMappingKeyServerConfigmedia_mappings (key)
ServerPathServerConfigmedia_roots, media_mappings (value)
RepoRelativePathJobSubmissionmedia_subdir
RepoRelativePathFA pipelineInferred corpus subdirectory

Design Decisions

Why ClientPath stores String, not PathBuf

Client paths arrive as JSON strings over HTTP. They may reference Windows paths (C:\Users\...) on a macOS server. PathBuf on the server would normalize path separators, potentially corrupting the client’s path. String preserves the exact bytes the client sent.

Why ServerPath stores PathBuf, not String

Server paths are used for actual filesystem I/O. PathBuf integrates with std::fs, tokio::fs, and the Path trait ecosystem. The AsRef<Path> implementation makes ServerPath a drop-in for any function that accepts paths.

Why From<&str> on ClientPath is not TryFrom

ClientPath has no validation invariants – any string a client sends is a valid client path (it might not exist, but that is discovered at resolution time). The From<&str> impl is genuinely infallible.

MediaMappingKey follows the same reasoning: any string is a valid key (it might not match any config entry, but that is a lookup miss, not a construction error).

Relationship to type-driven-design.md

The path newtypes are an instance of Pattern 3 (Provenance Newtypes) from the type-driven design catalog. The difference from text provenance types (ChatRawText, AsrNormalizedText) is that path types also encode machine boundaries – which side of the client/server divide a value lives on – and enforce this through the presence or absence of AsRef<Path>.

Key Source Files

FileRole
crates/batchalign-types/src/paths.rsAll four newtypes + infer_media_mapping()
crates/batchalign/src/types/request.rsJobSubmission uses ClientPath, MediaMappingKey, RepoRelativePath
crates/batchalign/src/types/config.rsServerConfig.media_mappings: BTreeMap<MediaMappingKey, ServerPath>
crates/batchalign/src/runner/dispatch/fa_pipeline.rsMedia resolution cascade using all four types
crates/batchalign/src/submission.rsClientPath to PathBuf bridge in materialize_submission_job()
crates/batchalign/src/store/job/types.rsJobFilesystemConfig stores ClientPath for source_dir

This page last changed: 2026-07-29 (commit f4f12680). The whole book last changed: 2026-09-16 (commit 34d249d8).