Skip to content

WOV specification

1. Intent

WOV (Writer-Oriented Versioning) is a fast, C-based version control library designed specifically for creative writing applications.

WOV does not attempt to be a Git GUI. Instead, it uses Git purely as a powerful, silent storage backend to solve a specific problem: fluid manuscript and chapter iteration. By orchestrating a strictly linear, forward-moving timeline via native Git bindings (libgit2), WOV unlocks advanced chronological manipulations—like pushing new chapters backward into past drafts—that standard version control paradigms naturally resist.

Important Architectural Target: WOV is designed specifically for host applications that act as a presentation layer (whether GUI, CLI, or web) and shield the user from the raw backend filenames. It relies on immutable IDs for physical files and expects the host application to handle all human-readable metadata (like titles).

Core Problems to Solve

WOV is designed to natively afford writers the following capabilities:

  • Fluid iteration: Writers need to easily create new takes of a scene and scrap them without managing complex Git branch trees.
  • Granular time travel: Reverting a single chapter backward in time should not force the entire manuscript to revert.
  • Hierarchical versioning: When a book moves to "Draft 2", all the chapters inside it should logically inherit or bump their own versions automatically.
  • Decoupled worldbuilding: Writers need to revert a story draft without accidentally reverting their global Wiki or character notes.

2. Scope & Limitations

Explicit Boundaries

  • Payload-Agnostic: WOV only cares about immutable IDs, tree structures, and physical blobs. Titles, YAML frontmatter, last cursor positions, and UI state belong in the Host Application's project file. WOV's manifest strictly manages Time and Structure. The host manages UI and Metadata.
  • Strictly Local: WOV manages the local .git tree and manifest. Remote syncing (push/pull) and external merge conflicts are explicitly out of bounds and must be handled by the host application.
  • Immutable File Paths: Because WOV expects files to be named by their immutable IDs (e.g., chapters/chap_123.md), it does not track arbitrary user renames. The host application handles the human-readable titles in its own project files, and WOV handles the immutable physical files on disk.

Architectural Trade-offs

  • Illegible Git Graph: Because WOV uses headless injections to push changes backward, the raw Git tree becomes an unreadable mess of detached commits. WOV does not rewrite the main trunk history (rebase). If a human looks at the repository in a standard Git GUI, they will just see a chaotic linear trunk with disconnected tags everywhere. WOV relies entirely on its Manifest to make sense of the timeline.
  • Performance Cost: Managing headless injections and dynamic text replacement is computationally heavier than standard branching, meaning time-travel operations might incur slight performance costs on massive manuscripts compared to simple branch checkouts.
  • The Manifest is the Source of Truth: Because parsing thousands of linear Git commits to rebuild a complex manuscript tree would be incredibly slow, WOV relies on wov-manifest.json as the brain of the project structure. If a user maliciously edits the file system outside of the Host Application, WOV relies on the host application to use Git status to identify and heal the discrepancies.

3. The Problem Space

Physical Files First

WOV does not hide text inside a proprietary SQLite database. A core requirement is that manuscript text must remain accessible and readable as plain text without an interpreter. WOV orchestrates the physical working directory, ensuring the files on the user's hard drive always reflect the active state.

The Linear Timeline

Standard Git branches are structurally inadequate for the complex draft manipulation writers require. WOV discards the concept of branching entirely. While a parent application might use branches for macro-organization (like switching between entirely different Books), WOV operates strictly as a branchless library, enforcing a single, continuous, forward-moving timeline for manuscript drafts to unlock unbound time-travel operations.

Container Version Model

  • No v0: Text is never "unversioned". A version (like Draft 1) is not a single snapshot in time; it is an open container holding all the messy, granular history and parallel takes associated with it. An author can have multiple open containers simultaneously.
  • Settling a Version: When the author is ready, they explicitly settle a version, designating a specific state as its definitive representation. The container itself remains fully expandable and modifiable, but it now presents a clean, canonical milestone to the timeline.

Settled vs. Unsettled Containers

The difference between a settled and unsettled draft is defined by its functional behavior:

  • Unsettled Drafts (Fluid):
    • All versions (Alternates) inside the draft are immediately visible.
    • Clicking on the draft automatically opens the last opened version (it is a fluid workspace tracking the author's active attention).
  • Settled Drafts (Anchored):
    • Only a single, designated version is visible by default.
    • Opening the container to look inside does not change which version is visible by default (the canonical presentation is anchored).

Alternates

  • Starting a new take of a scene (e.g., hitting "Start Over" to clear the slate) is called an Alternate.
  • Because an Alternate is just a new track starting from the exact same point, it functionally absorbs the concept of "retroactive siblings."
  • Alternates are the messy, granular history that lives inside a Version container.

Entity Types

While all entities in WOV follow the Container model, they behave differently based on their payload. WOV distinguishes between two types of entities:

  • Leaf Entities (e.g., Scenes, granular Chapters)
    • Payload: Raw text.
    • Behavior: When an author creates an Alternate of a Leaf Entity, they are generating a new slate of text. Settling a Leaf Entity means choosing which specific text variation is the definitive read.
  • Structural Entities (e.g., Books, Series)
    • Payload: Arrangements of other entities.
    • Behavior: A Book's content isn't text; its content is Chapters. When an author creates an Alternate of a Structural Entity, they are creating a new arrangement (e.g., reordering chapters, cutting a scene, experimenting with flow). Settling a Book means anchoring a definitive structural arrangement.

4. Target Workflows

Now that the architectural vocabulary is established, here is how the host application uses WOV's framework to execute solutions for the writer.

The Formal Milestone

  • The Writer's Goal:
    • The writer decides they have finished the first major draft of their manuscript and wants to cleanly label it "Draft 1" before tearing it apart to start "Draft 2".
  • How WOV handles it:
    • The host application calls the API to settle the version.
    • WOV updates the manifest, officially sealing the container and designating the current state as the canonical representation of "Draft 1".

The Do-Over

  • Scenario:
    • "This scene isn't working, I'm going to start from scratch."
  • The Writer's Goal:
    • The writer wants a blank slate for a scene, but they want the old version tucked away safely as an alternate take so they can salvage paragraphs from it later.
  • How WOV handles it:
    • The host application calls wov_create_alternate().
    • WOV bumps the manifest, commits the existing Alternate, and clears the text payload on the hard drive to create a blank slate.
    • The host UI tracks the new Alternate. The author can seamlessly switch between the old Alternate and the new blank slate Alternate at any time.

Nested Drafts

  • Scenario:
    • Cascading Versions
  • The Writer's Goal:
    • The writer is actively iterating on a specific chapter (currently Chapter 4, Version 2.7).
    • When they eventually complete the entire book (Book Version 1), they want Chapter 4 to properly log its state, and then logically roll over to Chapter 4, Version 3.0 when Book Version 2 begins.
  • How WOV handles it:
    • The library natively supports nested, hierarchical entity versioning.
    • The host application configures a cascade_behavior.
    • When a parent entity (Book) is versioned, WOV uses that schema to mathematically calculate the next logical version for all child entities (Chapters) without the UI having to track version math.

Moving Chapters to Past Milestones (Headless Injection)

  • Scenario:
    • Updating a past Book version with a newer Chapter version, without disrupting the active workspace.
  • The Writer's Goal:
    • The writer creates Book "Draft 2" and makes several modifications to Chapter 3 (generating new granular history for it).
    • They realize these modifications actually belong in Book "Draft 1".
    • They want to move Chapter 3's new version (history and all) back into the Draft 1 milestone.
    • UX Distinction: This is an organizational move, not a select. The writer is still actively working in Draft 2; their physical workspace should not change or be interrupted.
  • How WOV handles it:
    • Logically, in an append-only model, this requires checking out Draft 1, pulling in the new Chapter 3, committing, and checking Draft 2 back out.
    • To prevent severe workspace thrashing (files flickering on the hard drive and panicking IDEs), WOV executes this sequence using Headless Tree Injection via libgit2.
    • The host application calls the API to move the chapter.
    • WOV performs the operation entirely in RAM: it grabs the Git tree for Draft 1, injects the new Chapter 3 blob, creates the new commit, and updates the Draft 1 tag.
    • The Draft 1 milestone is successfully moved forward to include the chapter's new state and history.
    • To preserve the active workspace seamlessly, Draft 2 relinquishes the moved version and WOV instantly creates a new version (e.g., v4) starting with the exact text that was just moved. The physical working directory never blinks, and the writer's next keystroke safely appends to the new version.

Freeze Global Entities

  • Scenario:
    • Decoupling Worldbuilding from the Story Timeline
  • The Writer's Goal:
    • The writer wants to jump back to edit "Book 1 - Draft 1", but they do not want their global Wiki or Notes to revert, which would erase months of recent worldbuilding.
    • However, they occasionally need to peek at what the worldbuilding rules were back when they wrote Draft 1.
  • How WOV handles it:
    • The host application keeps Wiki and Notes in a completely separate Git repository from the Story.
    • When the Story time-travels backward, the Notes repository is untouched.
    • To preserve historical context, the UI calls wov_snapshot_collection() on the Notes repository when a formal Story version is created.
    • This creates a "pinned" Git Tag in the Notes repo.
    • The UI can read from this pinned tag to display read-only past notes.

5. Application

The WOV library exposes a thread-safe context architecture. While it can be compiled with a flat C-ABI (exportc) for generic FFI consumption, it is designed to be consumed natively by a host application backend.

Project Orchestration

To support independent timelines (like separating the Story from the Wiki), WOV uses a root project configuration. This tells the library which collections share a Git repository, and what their parent folders are.

{
  "collections": {
    "MainStory": {
      "folder": "story_files",
      "timeline_id": "primary"
    },
    "Notes": {
      "folder": "notes_files",
      "timeline_id": "notes_timeline",
      "allowed_pin_targets": ["MainStory", "Wiki"]
    },
    "Wiki": {
      "folder": "wiki_files",
      "timeline_id": "wiki_timeline",
      "allowed_pin_targets": ["MainStory"]
    }
  }
}

C API Surface

WOV uses an opaque context pointer (WovContext) to prevent race conditions.

  • WovContext* wov_context_create(const char* working_dir)
  • void wov_context_destroy(WovContext* ctx)
  • const char* wov_get_last_error(WovContext* ctx)

Timeline Operations

  • const char* wov_save(WovContext* ctx, const char* collection_id, const char* entity_id): Linear granular save.
  • int wov_switch(WovContext* ctx, const char* version_tag): Swaps the working directory to a target version tag.
  • const char* wov_tag_version(WovContext* ctx, const char* entity_id, const char* custom_name, bool is_subversion): Updates the manifest's version tree for the entity, auto-calculates the next granular version number based on the schema, and creates a formal Git tag.
  • const char* wov_settle(WovContext* ctx, const char* entity_id): Locks an open version container, designating a canonical state.
  • const char* wov_create_alternate(WovContext* ctx, const char* entity_id): Bumps the manifest, clears the text payload, and commits to create a parallel track.
  • int wov_inject_past(WovContext* ctx, const char* target_version, const char* file_path): Reaches backward in Git history, creates a detached headless commit swapping a past file with a new blob, and updates the past tag.
  • int wov_snapshot_collection(WovContext* ctx, const char* collection_dir, const char* version_tag): Freezes the state of an independent collection.

The Query API

WOV abstracts raw Git formatting entirely. The host UI retrieves human-readable history by querying the library:

  • get_commit_for_version(version)
  • get_version_for_commit(commit)
  • get_commits_between_versions(v1, v2)

6. The Mechanics

WOV avoids duplicating data. Because wov-project.json defines the folder for each Collection, the manifest never needs to track physical file paths for Leaf entities.

The path is inherently derived from the entity's Collection and ID (e.g., story_files/ch_mskj07.md).

wov-manifest.json:

{
  "collections": {
    "MainStory": {
      "entities": {
        "ch_mskj07": { 
          "active_commit": "def456hash"
        },
        "sc_888": { 
          "active_commit": "ghi789hash"
        },
        "sc_999": { 
          "active_commit": "old789hash"
        }
      }
    }
  }
}

  • No file paths: WOV just knows that to get the text for ch_mskj07, it looks at story_files/ch_mskj07.md in the def456hash commit.
  • Host-Driven Organization: WOV does not track chapter order. The Host Application's own project file tracks the Table of Contents. The host must ensure its project file is tracked by the Git repository (unlike the manifest) so that structural organization time-travels alongside the text blobs.
  • Untracked by Timeline: wov-manifest.json is the "God View" of the project and represents the absolute present. It is explicitly NOT tracked by the main Git timeline. If the user time-travels backward, the manifest remains unchanged, allowing the UI to retain knowledge of future drafts.

Tries

The structure:

  • Book = containers
  • Chapters = leaves

Book level drafts:

  • Draft 2 (active)
  • Draft 1 (settled)
  • First try

Outline (draft 2):

  • Chapter 1
  • v3 (active)
  • v2
  • v1
  • Chapter 2
  • v2 (active)
  • v1
  • Chapter 3
  • v3 (active)
  • v2
  • v1

Outline (draft 1 - settled):

  • Chapter 1 (active: v2, last: v2)
  • Chapter 2 (active: v1, last: v2)
  • chapter 2 (active: v3, last: v3)

Outline (first try):

  • Chapter 1
  • version2 (active)
  • version1
  • Chapter 1.5
  • v2 (active)
  • v1

Files on disk:

  • Chapters:
  • chap_id1
  • chap_id3
  • chap_id4

(immutable ids)

project.json (committed):

"chapters" {[
  "chap_id1": {
    "title"="Chapter 1"
  },
  "chap_id3": {
    "title"="Chapter 2"
  },
  "chap_id4": {
    "title"="Chapter 3"
  }
]}

leaves.json

collection{
  "chapters"{[
    "chap_id1":{
      "alt1":{
        "version 1": "commit-xx1",
        "version 2": "commit-xx2",
        "version 3": "commit-xx3"
      }
    }
  ]}
}
containers.json
{
  "first_try":{
    "title"="First Try",
    "last" :{[
      "chap_id1": ["alt11", "version2"],
      "chap_id2": ["alt21", "v2"]
    ]},
    "settled": None
  }

}

Still need to see how to make it work symbiotically with tags. It might look like tags are uneeded at this point but that's not true because tags are what allow us to pull all that a given version contains even if we're only intereted in a give commit. This is necessary to keep commit's chronology intact (except if we sort them chronologically via their messages? I dunno).

There's also some things unclear about alts and whether they have names and whatnot. They definitely need name but i need to be careful about how to encode it because what's great about the current format is that i don't need to know what something is (a version or an alt) to get it.

I think it might also be interesting to have chapters be settlable because I think it could be really practical to settle a chapter without having to settle the whole book version. In this case the chapter is settled of the current version of it parent obviously.

I think it would also make sense for settling to actually be the way to "clear" the hitory of a chapter. By default, all of a chapter's versions are visible but if you settle the previous book draft then he chapters automatically settle to the chosen version and any ulterior version of the book have no versions for those chapters immediately accessible.

Transcription of my handwritten notes

project.json

Because project.json is committed, it always shows you the right titles (not for history-commits though since they're not loaded on disk when looked at).

leaves.json

Do I een need a name here at all for alts? The main issue with no name (so basing ourselves on order) is if one of them is deleted. But i could just have another place naming them in order and making sure it's also updated with the deletion. Should alts even be treated the same as other versions in that context? Right now I'm already treating alts like they exist by default (anything created belongs to a first alt).

Additional notes

This is designed for a simple hierarchy when there's only one container entity and one leaf entity. Needs testing: - two containers - two leaves - both (with one entity being both)

Notably: here you can only create an alt on a chapter. Because chapters are leaves it means you can only create alts on leaves but that doesn't make it true in general (I'm not saying it's not true, I'm saying it's not proof that it's true). Actually, why couldn't you crate an alt on a book? a new version and an alternate version are not the same thing.

Attempted summary that ended up being longer than the thing it was summarizing

project.json

It tracks the state of what is currently on disk. It connects chapter files to their title and posiition in the book at a given moment. As well as whether they are in the book or delete (or archived?--not sure this is tracked here as an archive is technically still here and more importantly doesn't belong to the timeline--potentially).

leaves.json

Tracks the leaves overall. It connects the files to all their alts and versions in a given collection. --> the versions are represented by a single commit.

The sidenote that went south

there is a possibility I might not even need to store the last commit because:

  • if we can tell which version a commit belongs to thanks to tags and we only ever move versions forward in their entirety, the the last commit is either actual last (in the front) or the last one before a new tag. (note: you can't tell which tag since tags are also pulled to the front, only that there is a tag). -> that being said, we might still have to store which is settled so we don't fore the commit to be pulled in the front. --> I realize the logic of that last sentence is a leap at best; I have a lot of thoughts colliding. Let's try to write it all down:

  • a settled version can present a version that is not the last one

  • for a version to be presented and not be the last commit, it has to be read-only
  • unsettled version can also show a verseion that isn't the last one... ... I'm stopping there because Im' realizing I'm treating commits as versions again. !! A random commit cannot be used wihtout being pulled to the front. It can be revisited temporarily in read-only but that's it. When you settle a version you decide on a version to represent it not a commit! -> For leaves (with only one container at least) that means settling them is just abou tchoosing a version for a given alt or a given alt for the leaf and making the rest immediately (not absolutely) inaccesisble to future versions. --> For a container it means settling all leaves

illustration of settling


Book - Draft 1

Chapter 1 alt1:

  • version 1
    • hash
    • hash
    • hash
    • hash
  • version 2
    • hash
    • hash
  • version 3
    • hash
    • hash
    • hash

alt2:

  • v1
    • hash
    • hash
    • hash
    • hash
    • hash
    • hash
    • hash

  • settling chapter 1's alts means choosing a version that represent each of them.
  • settling happens from top to bottom meaning you don't need to settle lower version of something you didn't choose when you settled
  • settling a chapter means choosing an alt (+ a version if the alt itself wasn't settled) (-> note: from wov's perspective, the granularity of the choice is essential; it's for the ui to make it more ux friendly -- by automatically settling on the currently active version for instance).
  • settling the book's draft1 means settling all of its leaves.
  • even if the book itself isn't settled any settling of its leaves will propagate to future versions
  • there is no difference between settling a book and settling its lieaves except for a global marker which allows not checking every leaf to know if it's settled

(unrelated) Notes for Stuck

The order of representation of versions and commits should be a preference option:

  • last created first
  • first created first
  • last mutated/read first

On collections

Collections are basically the containers except for the fact that i haven't yet considered how it would behave with multiple nested levels of containers.

Beyond that, I was thinking about whether collections should be in the project file (which belongs to the timeline) or the leaves file (which doesn't belong to the timeline).

Originally i was thinking how collections belong to the timeline (like, if you go in a past version, you want to see it exactly as it was). But then I started wondering if that was the case. Structural changes are not really things you want to change in most scenarios and it's fairly easy to change if you do want to change them.

For containers/leaves

I was thinking that the best structure for more complex structures (notably those that might have entities that are both containers and leaves) might actually just be:

chapters/
scenes/

Where chapters are both leaves of their own and containers of scenes.

I'm not entirely sure, but I think WOV might not actually care what the structure is if containers are black boxes to higher level containers. The project file is the one responsible for the actual structure:

Book
  chap_id1
    sceneid1
    sceneid2
    sceneid5
  chap_id2
    sceneid4
    sceneid3

The orchestration can look like this:

collection.chapters
  timeline: primary
  path: story/chapters/
  type: leaf, container[scenes]

collection.scenes
  timeline: primary
  path: story/scenes/
  type: leaf

So far collections were actually separated by things like story, wiki, notes, etc. Which all already have different timelines which defeats the purpose of the timeline parameter to begin with.