diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..1ebfac7
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,24 @@
+name: Publish
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: systemic-engineering/ci/actions/nix-setup@main
+
+ - name: Download deps
+ run: nix develop -c gleam deps download
+
+ - name: Test
+ run: nix develop -c gleam test
+
+ - name: Publish to Hex.pm
+ run: nix develop -c gleam hex publish --yes
+ env:
+ HEXPM_API_KEY: ${{ secrets.HEX_API_KEY }}
diff --git a/gleam.toml b/gleam.toml
index f51bbe2..bf2c597 100644
--- a/gleam.toml
+++ b/gleam.toml
@@ -7,6 +7,7 @@ repository = { type = "github", user = "systemic-engineering", repo = "fragmenta
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
+simplifile = ">= 2.0.0 and < 3.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
diff --git a/manifest.toml b/manifest.toml
index ba887ee..91354dc 100644
--- a/manifest.toml
+++ b/manifest.toml
@@ -2,10 +2,13 @@
# You typically do not need to edit this file
packages = [
+ { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "gleam_stdlib", version = "0.69.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AAB0962BEBFAA67A2FBEE9EEE218B057756808DC9AF77430F5182C6115B3A315" },
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
+ { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
]
[requirements]
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
+simplifile = { version = ">= 2.0.0 and < 3.0.0" }
diff --git a/src/fragmentation/git.gleam b/src/fragmentation/git.gleam
new file mode 100644
index 0000000..c6d2c28
--- /dev/null
+++ b/src/fragmentation/git.gleam
@@ -0,0 +1,28 @@
+/// git: content-addressed fragment persistence.
+///
+/// Writes fragments to disk named by their SHA.
+/// The store is a directory. Each fragment becomes a file.
+/// File name = SHA-256 of canonical serialization.
+/// Idempotent: same SHA, same content, same file.
+import fragmentation
+import simplifile
+
+// ---------------------------------------------------------------------------
+// Operations
+// ---------------------------------------------------------------------------
+
+/// Write a fragment to disk under `dir`, named by its content-addressed SHA.
+///
+/// Computes the SHA via `fragmentation.hash_fragment`, serializes via
+/// `fragmentation.serialize`, then writes to `
/`.
+/// Returns Ok(Nil) on success, Error(FileError) on failure.
+/// Idempotent: writing the same fragment twice produces the same file.
+pub fn write(
+ fragment: fragmentation.Fragment,
+ dir: String,
+) -> Result(Nil, simplifile.FileError) {
+ let sha = fragmentation.hash_fragment(fragment)
+ let content = fragmentation.serialize(fragment)
+ let path = dir <> "/" <> sha
+ simplifile.write(path, content)
+}
diff --git a/test/fragmentation_git_test.gleam b/test/fragmentation_git_test.gleam
new file mode 100644
index 0000000..86f76d5
--- /dev/null
+++ b/test/fragmentation_git_test.gleam
@@ -0,0 +1,88 @@
+import fragmentation
+import fragmentation/git
+import gleeunit/should
+import simplifile
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+fn test_witnessed() -> fragmentation.Witnessed {
+ fragmentation.witnessed(
+ fragmentation.author("alex"),
+ fragmentation.committer("reed"),
+ fragmentation.timestamp("2026-03-01T00:00:00Z"),
+ fragmentation.message("test"),
+ )
+}
+
+fn make_shard(data: String) -> fragmentation.Fragment {
+ let r = fragmentation.ref(fragmentation.hash(data), "self")
+ fragmentation.shard(r, test_witnessed(), data)
+}
+
+// ---------------------------------------------------------------------------
+// write_fragment_creates_file_test
+//
+// write_fragment writes a file named by SHA containing serialized fragment.
+// ---------------------------------------------------------------------------
+
+pub fn write_fragment_creates_file_test() {
+ let dir = "/tmp/fragmentation_git_test_write"
+ let _ = simplifile.create_directory(dir)
+ let frag = make_shard("hello-world")
+ let sha = fragmentation.hash_fragment(frag)
+
+ let result = git.write(frag, dir)
+ result |> should.be_ok()
+
+ let path = dir <> "/" <> sha
+ simplifile.is_file(path) |> should.equal(Ok(True))
+}
+
+// ---------------------------------------------------------------------------
+// write_fragment_idempotent_test
+//
+// write_fragment is idempotent — same SHA, same content, no error on repeat.
+// ---------------------------------------------------------------------------
+
+pub fn write_fragment_idempotent_test() {
+ let dir = "/tmp/fragmentation_git_test_idempotent"
+ let _ = simplifile.create_directory(dir)
+ let frag = make_shard("idempotent-shard")
+ let sha = fragmentation.hash_fragment(frag)
+
+ let r1 = git.write(frag, dir)
+ let r2 = git.write(frag, dir)
+
+ r1 |> should.be_ok()
+ r2 |> should.be_ok()
+
+ // Only one file: idempotent means same path, same content
+ let path = dir <> "/" <> sha
+ simplifile.is_file(path) |> should.equal(Ok(True))
+}
+
+// ---------------------------------------------------------------------------
+// write_two_fragments_test
+//
+// Writing two different fragments creates two files.
+// ---------------------------------------------------------------------------
+
+pub fn write_two_fragments_test() {
+ let dir = "/tmp/fragmentation_git_test_two"
+ let _ = simplifile.create_directory(dir)
+ let frag_a = make_shard("fragment-alpha")
+ let frag_b = make_shard("fragment-beta")
+ let sha_a = fragmentation.hash_fragment(frag_a)
+ let sha_b = fragmentation.hash_fragment(frag_b)
+
+ git.write(frag_a, dir) |> should.be_ok()
+ git.write(frag_b, dir) |> should.be_ok()
+
+ // SHAs must be distinct
+ sha_a |> should.not_equal(sha_b)
+
+ simplifile.is_file(dir <> "/" <> sha_a) |> should.equal(Ok(True))
+ simplifile.is_file(dir <> "/" <> sha_b) |> should.equal(Ok(True))
+}