draftline 0.2.4

Git-native versioning for creative content workflows.
Documentation

Draftline

Git-native versioning for creative content workflows.

Draftline is a Rust library for apps that need safe version history for folders full of creative content. It exposes business-friendly concepts like workspaces, versions, variations, change sets, preflight reports, and recovery state while keeping Git as a storage implementation detail.

Content policy

Use a ContentPolicy to describe which workspace files are user content and which files are app/runtime state. Paths are workspace-relative, extensions are normalized case-insensitively, and .draftline state is excluded by default.

use draftline::{ContentPolicy, Workspace};

fn main() -> Result<(), draftline::DraftlineError> {
    let policy = ContentPolicy::new()
        .include_paths(["content", "assets"])?
        .include_extensions(["md", "txt"])?
        .exclude_paths(["content/private"])?;

    let workspace = Workspace::init_with_policy("my-content", policy)?;
    Ok(())
}

Variation metadata

Variations have stable Draftline names backed by Git refs. Hosts can attach display metadata without changing those names.

use draftline::{VariationMetadata, Workspace};

fn main() -> Result<(), draftline::DraftlineError> {
    let workspace = Workspace::init("my-content")?;
    let version = workspace.save_version("Initial draft")?;
    let variation = workspace.create_variation_from_with_metadata(
        version.id(),
        "draft-a",
        VariationMetadata::new()
            .with_label("Draft A")
            .with_slug("draft-a"),
    )?;

    assert_eq!(variation.display_label(), "Draft A");
    Ok(())
}

label is user-facing display text. slug is host-owned metadata for URLs, routing, or integration. Neither field changes the underlying variation name or Git branch.

Remote credentials

Remote operations accept credential callbacks so host apps can fetch, clone, and publish through their own authentication flow.

use draftline::{RemoteCredential, RemoteOptions, Workspace};

fn main() -> Result<(), draftline::DraftlineError> {
    let token = std::env::var("GITHUB_TOKEN").unwrap();
    let mut options = RemoteOptions::new().with_credentials(move |request| {
        if request.allows_username_password {
            Ok(RemoteCredential::UsernamePassword {
                username: "x-access-token".to_string(),
                password: token.clone(),
            })
        } else {
            Ok(RemoteCredential::Default)
        }
    });

    let workspace = Workspace::open("my-content")?;
    workspace.fetch_remote_with_options("origin", &mut options)?;
    Ok(())
}

Tauri Workbench contract

The draftline::tauri_contract module exposes dependency-free command adapter functions for Workbench and other Tauri hosts. Hosts can wrap these functions with #[tauri::command] while keeping Draftline's Rust APIs as the source of truth for preflight, execution, verification, and serializable error shapes.

The contract includes read-only diagnostics (inspect_workspace, verify_workspace, list_variations, list_support_refs), selected-file mutations (selected_save, selected_shelve, selected_discard), and remote collaboration commands (fetch_remote, preflight_apply_incoming, apply_incoming, preflight_merge_incoming, merge_incoming, merge_incoming_with_resolutions, publish_current_variation). Collaboration commands refresh remote-tracking state before reporting preflight results so host UIs can render current SyncState values and then execute through Draftline's tokenized apply, merge, and publish paths.

Conflicted merge preflight returns conflicts plus a token when the workspace and remote heads are safe to merge, while can_merge_cleanly remains false. Hosts should collect explicit user choices and call merge_incoming_with_resolutions with the preflight token and one MergeConflictResolution per conflict. The token binds execution to the local, remote, and merge-base commits the user reviewed, so stale resolution submissions fail instead of resolving unseen remote content. Whole-file choices support use_ours, use_theirs, use_base, delete, or use_content; semantic field_path conflicts currently require a host-produced use_content result for the resolved file.

The Workbench contract keeps credential handling out of frontend DTOs. Hosts route commands through DraftlineCommandContext to configure content policy, host-provided contributor attribution, backend-only remote credentials, and redaction-safe workspace events in one place. Clone, open, adopt, fetch, publish, apply, and merge command adapters all use the context so product frontends never need to pass clone or fetch tokens over IPC.

use draftline::tauri_contract::{inspect_workspace, WorkspaceRequest};

#[tauri::command]
fn inspect_workspace_command(
    workspace_path: std::path::PathBuf,
) -> draftline::tauri_contract::TauriCommandResult<
    draftline::tauri_contract::WorkspaceDiagnostics,
> {
    draftline::tauri_contract::into_tauri_result(inspect_workspace(WorkspaceRequest {
        workspace_path,
    }))
}

For product hosts, prefer context-aware wrappers:

use draftline::{
    tauri_contract::{selected_save_with_context, DraftlineCommandContext, SelectedSaveRequest},
    ContentPolicy, Contributor, ContributorProfile,
};

let policy = ContentPolicy::new()
    .include("content")?
    .exclude(".chats")?
    .exclude("runtime")?;
let profile = ContributorProfile::new(
    Contributor {
        name: "Product User".to_string(),
        email: Some("user@example.invalid".to_string()),
    },
    Contributor {
        name: "Draftline Service".to_string(),
        email: Some("service@example.invalid".to_string()),
    },
);
let mut context = DraftlineCommandContext::new()
    .with_content_policy(policy)
    .with_contributor_profile(profile)
    .with_event_sink(|event| {
        // Tauri hosts can emit this as `draftline://workspace_event`.
        let _ = event.sequence;
    });
# let request = SelectedSaveRequest {
#     workspace_path: std::path::PathBuf::from("my-content"),
#     paths: vec![std::path::PathBuf::from("content/post.md")],
#     label: "Save".to_string(),
# };
let _ = selected_save_with_context(&mut context, request);
# Ok::<(), draftline::DraftlineError>(())

Treat ContributorProfile as product identity and service attribution, not as a Git configuration screen. The host should derive author from the signed-in product user, saved_by from the actor that initiated the save, and optional service from the backend automation performing the operation. Draftline writes normal Git author/committer metadata from that profile, but users should not need to understand or edit user.name / user.email for product saves:

use draftline::{Contributor, ContributorProfile};

let profile = ContributorProfile::new(
    Contributor {
        name: "Avery Writer".to_string(),
        email: Some("avery@example.invalid".to_string()),
    },
    Contributor {
        name: "CutReady Sync Service".to_string(),
        email: Some("sync@example.invalid".to_string()),
    },
)
.with_service(Contributor {
    name: "CutReady".to_string(),
    email: None,
});
# let _ = profile;