gioser

tawasuyu/hifas

Motor en Rust para decidir, con demostración y no a ojo, cuándo un conjunto de réplicas local-first puede operar sin coordinación y cuándo no — y, cuando la respuesta es "sí, dentro de límites", proveer el mecanismo que lo garantiza.

Rust 24Markdown 15TOML 1358 Sutichaykuna

M0/T0: scaffold del crate sheafsync

Estructura de módulos (§5) sin lógica todavía: cell, nerve, sheaf,
cohomology, verdict, oracle, más error y linalg (trait LinAlg con impl
f64 vacía). Deps petgraph/nalgebra/thiserror + proptest dev-dep.
main imprime "hello sheaf"; test dummy pasa. DESIGN.md (antes sdd.txt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

6595dbed856eec31669beb6db74b456faa4be9a1 · ruwaq Sergio · (3 killa ñawpaq) · kay commitpi sach'ata qhaway

Semántico sayk'uy · 39

src/cell.rs

src/cohomology.rs

src/error.rs

src/lib.rs

src/linalg.rs

src/main.rs

src/nerve.rs

src/oracle.rs

src/sheaf.rs

src/verdict.rs

14 qillqakuna · +1415 −0

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..174bbad
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,612 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "bit-set"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fixedbitset"
+version = "0.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+]
+
+[[package]]
+name = "glam"
+version = "0.30.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
+
+[[package]]
+name = "glam"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7"
+
+[[package]]
+name = "glam"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0"
+
+[[package]]
+name = "glam"
+version = "0.33.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f22fb22f065b308be0d8724e3706c7fa3fc2a6c7d6899df4cad7860e7a75436"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "matrixmultiply"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
+[[package]]
+name = "nalgebra"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc43a60c217b0c6ff46e47f26911015ad8d2e5a8be1af668c67e370d99a4346"
+dependencies = [
+ "approx",
+ "glam 0.30.10",
+ "glam 0.31.1",
+ "glam 0.32.1",
+ "glam 0.33.2",
+ "matrixmultiply",
+ "nalgebra-macros",
+ "num-complex",
+ "num-rational",
+ "num-traits",
+ "simba",
+ "typenum",
+]
+
+[[package]]
+name = "nalgebra-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "973e7178a678cfd059ccec50887658d482ce16b0aa9da3888ddeab5cd5eb4889"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "petgraph"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
+dependencies = [
+ "fixedbitset",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "serde",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proptest"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
+dependencies = [
+ "bit-set",
+ "bit-vec",
+ "bitflags",
+ "num-traits",
+ "rand",
+ "rand_chacha",
+ "rand_xorshift",
+ "regex-syntax",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_xorshift"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rusty-fork"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
+dependencies = [
+ "fnv",
+ "quick-error",
+ "tempfile",
+ "wait-timeout",
+]
+
+[[package]]
+name = "safe_arch"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "sheafsync"
+version = "0.1.0"
+dependencies = [
+ "nalgebra",
+ "petgraph",
+ "proptest",
+ "thiserror",
+]
+
+[[package]]
+name = "simba"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f45c644a9f3a386f9288625d9f0c1e999e1acf07a37df35d0516c7f199d9cb2"
+dependencies = [
+ "approx",
+ "num-complex",
+ "num-traits",
+ "wide",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.3",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wide"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..ec91ad7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "sheafsync"
+version = "0.1.0"
+edition = "2021"
+description = "Motor de consistencia sin coordinación vía cohomología de haces celulares"
+
+[lib]
+name = "sheafsync"
+path = "src/lib.rs"
+
+[[bin]]
+name = "sheafsync"
+path = "src/main.rs"
+
+[dependencies]
+nalgebra = "0.35.0"
+petgraph = "0.8.3"
+thiserror = "2.0.18"
+
+[dev-dependencies]
+proptest = "1.11.0"
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..848c1b7
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,430 @@
+# SDD — Motor de Consistencia sin Coordinación
+
+**Nombre en clave:** `sheafsync`
+**Lenguaje:** Rust (edición 2021)
+**Autor del diseño:** (tú)
+**Estado:** Diseño v1 — listo para scaffolding con Claude Code
+**Última edición:** 2026-07-01
+
+---
+
+## 0. Cómo usar este documento con Claude Code
+
+Este SDD está escrito para ser el contexto raíz de un proyecto en Claude Code (CLI).
+La forma recomendada de trabajarlo:
+
+1. Coloca este archivo en la raíz del repo como `DESIGN.md`.
+2. Arranca Claude Code en esa carpeta y dile: *"Lee DESIGN.md completo. No escribas código todavía; primero resúmeme el plan de módulos y confírmame que entendiste el hito M1."*
+3. Trabaja **un módulo por sesión**, en el orden de la sección 8. No dejes que genere todo de golpe: pídele un módulo, corre `cargo test`, revisa, y solo entonces avanza.
+4. La sección 12 (Apéndice) trae los "tickets" ya redactados para copiar/pegar como tareas.
+
+Regla de oro para dirigirlo: **el criterio de éxito de cada pieza es un test que pasa, no que "se ve bien".** Este proyecto vive o muere por la corrección matemática, no por la estética.
+
+---
+
+## 1. Resumen ejecutivo
+
+`sheafsync` es un motor en Rust que responde, con demostración y no a ojo, una pregunta central de los sistemas soberanos / local-first:
+
+> Dado un conjunto de réplicas que editan copias locales de un estado compartido,
+> ¿cuándo se pueden fundir esas copias en un estado global coherente **sin coordinación**,
+> y cuándo existe una obstrucción que obliga a sincronizar?
+
+La herramienta modela el estado distribuido como un **haz celular** (cellular sheaf) sobre el
+grafo de comunicación entre réplicas (el "nervio"), y calcula su **cohomología** `H⁰` y `H¹`:
+
+- `H⁰` = secciones globales = los estados globales consistentes que existen.
+- `H¹` = obstrucciones = los "nudos" donde lo local es consistente por pares pero no pega globalmente.
+
+El motor cruza ese resultado topológico con dos anclas conocidas del mundo de sistemas:
+
+- **CRDTs** (tipos de datos que ya convergen solos; retículos join-semilattice).
+- **Teorema CALM** (un cómputo tiene implementación libre de coordinación **si y solo si** es monótono).
+
+**Salida práctica:** dada una configuración de réplicas + datos, el motor imprime
+`H¹ = 0 → corre libre (sin jefe)` o `H¹ ≠ 0 → nudo localizado entre réplicas X e Y → coordina aquí`.
+
+---
+
+## 2. Fundamento teórico (preciso pero legible)
+
+### 2.1 Las tres piezas
+
+**CRDTs (Shapiro et al., 2011).** Estructuras de datos que se pegan solas porque su operación
+de fusión es un `join` sobre un retículo (conmutativa, asociativa, idempotente). El ejemplo mínimo
+es el *G-Counter*: cada réplica solo incrementa su propia casilla; fusionar = tomar el máximo casilla
+por casilla. Nunca hay conflicto porque nunca se resta.
+
+**Teorema CALM (Hellerstein, "Keeping CALM").** Formaliza la intuición anterior:
+un programa admite una implementación consistente **sin coordinación** exactamente cuando es
+*monótono* (solo agrega conocimiento, nunca lo retracta). En cuanto aparece una operación no monótona
+(borrar, resetear, sobrescribir, negar), la coordinación se vuelve necesaria.
+
+**Haces celulares y cohomología (Hansen & Ghrist, "Toward a spectral theory of cellular sheaves").**
+Un haz celular sobre un grafo asigna:
+- a cada **vértice** `v` (una réplica) un espacio de estado local, la *stalk* `F(v)`;
+- a cada **arista** `e` (dos réplicas que comparten datos) un espacio `F(e)`;
+- a cada incidencia vértice→arista un *mapa de restricción* lineal `F(v ⊴ e): F(v) → F(e)`
+  (la proyección del estado de la réplica sobre el dato que comparte en esa arista).
+
+Con eso se construye un complejo de cocadenas y su **coboundary** `δ: C⁰ → C¹`.
+La cohomología es:
+- `H⁰ = ker δ` — asignaciones locales que coinciden en TODA arista compartida = estados globales coherentes.
+- `H¹ = coker δ = C¹ / im δ` — las obstrucciones a pegar. `dim H¹ = dim C¹ − rank δ`.
+
+### 2.2 El puente (la hipótesis central del proyecto)
+
+La apuesta intelectual de `sheafsync` es que **la cohomología del haz de sincronización coincide
+con lo que CALM predice sobre la coordinación**:
+
+> **Hipótesis H:** para un estado compartido codificado linealmente,
+> `H¹ = 0` sobre el grafo de réplicas ⟺ la configuración admite fusión libre de coordinación
+> (i.e. el caso monótono / CALM-libre).
+
+**Esto NO es un teorema publicado.** Es la conjetura que el proyecto explora y valida empíricamente.
+Por eso el corazón de la validación (sección 9) es: en los casos donde YA conocemos la respuesta por
+CRDT/CALM, comprobar que el `H¹` calculado concuerda. Si concuerda en los casos fáciles, ganamos
+confianza para usarlo en los casos difíciles donde ya no es obvio a ojo. Ese es el valor de la herramienta,
+y también su honestidad: no vendemos un teorema, construimos y probamos una correspondencia.
+
+### 2.3 La brecha de modelado que hay que respetar
+
+La cohomología de haces es **lineal** (sobre un cuerpo o anillo). Los retículos de los CRDTs son
+**de orden** (join-semilattices), no espacios vectoriales. Reconciliar ambos es la parte de frontera.
+
+Estrategia:
+- **MVP:** trabajar con haces *valuados en espacios vectoriales*, donde la cohomología es álgebra
+  lineal estándar. Esto cubre limpiamente los datos que embeben en un grupo (un G-Counter embebe en
+  `ℤⁿ`; la pertenencia a conjuntos sobre `GF(2)`), y las restricciones son proyecciones lineales.
+- **Extensión de investigación:** haces valuados en retículos / posets (la generalización "de Tarski"),
+  necesaria para capturar operaciones que no embeben en un grupo. Fuera del alcance del MVP; ver §11.
+
+---
+
+## 3. Objetivos y no-objetivos
+
+### 3.1 Objetivos (MVP)
+- G1. Representar réplicas, datos compartidos y su grafo de sincronización (nervio).
+- G2. Construir el haz celular y su matriz de coboundary `δ`.
+- G3. Calcular `rank δ`, `dim H⁰`, `dim H¹` de forma exacta.
+- G4. Localizar la obstrucción: no solo decir *que* hay un nudo, sino *entre qué aristas/réplicas*.
+- G5. Reportar el veredicto en lenguaje de sistemas: "corre libre" vs "coordina aquí".
+- G6. Validar contra un oráculo CRDT/CALM en una batería de casos conocidos.
+
+### 3.2 No-objetivos (explícitos, para no dispersarse)
+- N1. NO es un motor de replicación en producción. No hay red real, ni persistencia, ni wire protocol.
+      Es un *analizador* de configuraciones.
+- N2. NO implementa haces sobre retículos generales en el MVP (solo espacios vectoriales).
+- N3. NO resuelve la coordinación cuando hace falta; solo *detecta y localiza* dónde hace falta.
+- N4. NO busca rendimiento a gran escala en v1. Corrección > velocidad. Grafos de decenas de réplicas.
+- N5. NO integra con Tawasuyu/Hammer todavía. Eso es un hito posterior (M4), con API estable primero.
+
+---
+
+## 4. Modelo de dominio
+
+Vocabulario que usa todo el código y este documento:
+
+| Término | Significado |
+|---|---|
+| **Réplica** | Un nodo que tiene una copia local del estado. Vértice del grafo. |
+| **Nervio** | El grafo de qué réplicas comparten datos con cuáles. Aristas. |
+| **Stalk** | El espacio de estado local de una réplica (o de una arista). |
+| **Dato compartido** | La porción del estado que dos réplicas deben mantener coherente en su arista. |
+| **Mapa de restricción** | Proyección lineal del estado de una réplica sobre el dato de una arista. |
+| **Coboundary `δ`** | Operador lineal que mide desacuerdos en todas las aristas a la vez. |
+| **Sección global (`H⁰`)** | Un estado global coherente: todos concuerdan en todo lo compartido. |
+| **Obstrucción (`H¹`)** | Un nudo: coherencia por pares que no se puede globalizar. |
+
+---
+
+## 5. Arquitectura
+
+Crate único (`sheafsync`) con módulos claros. Es un binario + librería (`src/lib.rs` + `src/main.rs`).
+
+```
+Réplicas + datos            ──►  módulo `cell`     (qué es un dato local que sabe fundirse)
+Quién comparte con quién    ──►  módulo `nerve`    (el grafo)
+Pegar lo local              ──►  módulo `sheaf`    (haz + coboundary δ)
+Detector de choques         ──►  módulo `cohomology` (rank, H⁰, H¹, localización)
+Veredicto                   ──►  módulo `verdict`  (traducción a lenguaje de sistemas)
+Oráculo de validación       ──►  módulo `oracle`   (CRDT/CALM de referencia)
+```
+
+Cada módulo corresponde exactamente a una caja del diagrama de tubería del diseño conceptual.
+
+### 5.1 Principios de diseño de código
+- Núcleo matemático **puro y determinista**: sin I/O, sin aleatoriedad, sin estado global.
+- Los tipos de datos compartidos entran por un `trait`; el motor no sabe (ni le importa) si son
+  contadores, conjuntos o registros.
+- El álgebra lineal se aísla tras una interfaz `LinAlg`, con dos implementaciones intercambiables:
+  `f64` (rápida, para arrancar) y `GF(2)` exacta (para el sabor topológico puro).
+- Errores explícitos con `thiserror`. Nada de `unwrap()` en rutas de librería.
+
+---
+
+## 6. Modelo de tipos (bocetos de Rust)
+
+Estos son bocetos de *forma*, no código final. Claude Code debe completarlos y ajustarlos.
+
+### 6.1 `cell` — el dato local
+
+```rust
+/// Un pedacito de estado local que sabe cómo intentar fundirse con otro,
+/// y cómo proyectarse al espacio de una arista compartida.
+pub trait Cell: Clone + PartialEq {
+    /// El vector que representa este estado en el espacio de la stalk.
+    fn to_vector(&self) -> Vec<Scalar>;
+
+    /// Fusión de retículo (join). Debe ser conmutativa, asociativa, idempotente.
+    fn join(&self, other: &Self) -> Self;
+
+    /// ¿Esta operación es monótona? (marca para el oráculo CALM)
+    fn is_monotone() -> bool;
+}
+
+/// CRDT mínimo del MVP: cuenta por réplica, fusiona por máximo. Siempre monótono.
+pub struct GCounter { pub slots: Vec<u64> }
+
+/// CRDT no monótono para el caso "difícil": permite reset/borrado.
+/// Sirve para provocar H¹ ≠ 0 deliberadamente.
+pub struct ResettableRegister { /* ... */ }
+```
+
+`Scalar` es un alias que resuelve la implementación de álgebra lineal elegida (ver §7).
+
+### 6.2 `nerve` — el grafo
+
+```rust
+use petgraph::graph::UnGraph;
+
+pub struct Nerve {
+    /// Grafo no dirigido: vértices = réplicas, aristas = pares que comparten datos.
+    pub graph: UnGraph<ReplicaId, SharedLink>,
+}
+
+pub struct ReplicaId(pub usize);
+pub struct SharedLink { /* qué coordenadas se comparten en esta arista */ }
+```
+
+### 6.3 `sheaf` — el haz y el coboundary
+
+```rust
+pub struct Sheaf {
+    /// Para cada réplica: su espacio local (dimensión de la stalk).
+    pub vertex_stalks: Vec<usize>,
+    /// Para cada arista: su espacio (dimensión).
+    pub edge_stalks: Vec<usize>,
+    /// Mapas de restricción: por arista, dos matrices (una por extremo).
+    pub restrictions: Vec<(RestrictionMap, RestrictionMap)>,
+}
+
+impl Sheaf {
+    /// Ensambla la matriz global de coboundary δ: C⁰ → C¹.
+    /// Convención: para arista e=(u→v), (δx)_e = R_{u⊴e}·x_u − R_{v⊴e}·x_v.
+    pub fn coboundary(&self) -> Matrix { /* ... */ }
+}
+```
+
+### 6.4 `cohomology` — la única cuenta que importa
+
+```rust
+pub struct Cohomology {
+    pub dim_c0: usize,
+    pub dim_c1: usize,
+    pub rank_delta: usize,
+    pub dim_h0: usize, // = dim_c0 - rank_delta
+    pub dim_h1: usize, // = dim_c1 - rank_delta
+    /// Si dim_h1 > 0, una base del cokernel: cada vector señala aristas
+    /// que participan en la obstrucción (para localizar el nudo).
+    pub obstruction_basis: Vec<Vec<Scalar>>,
+}
+
+pub fn compute(sheaf: &Sheaf) -> Cohomology { /* rank vía eliminación gaussiana */ }
+```
+
+### 6.5 `verdict` — traducción a lenguaje humano
+
+```rust
+pub enum Verdict {
+    RunsFree,                              // H¹ = 0
+    NeedsCoordination { knots: Vec<Knot> } // H¹ ≠ 0, con localización
+}
+
+pub struct Knot {
+    pub replicas: Vec<ReplicaId>, // réplicas implicadas en el nudo
+    pub edges: Vec<usize>,        // aristas del ciclo obstruido
+}
+```
+
+---
+
+## 7. La cuenta de cohomología, paso a paso
+
+Esto es lo que Claude Code debe implementar en `cohomology::compute`, y es TODO el corazón:
+
+1. Ensamblar `δ` como una matriz de tamaño `(Σ dim F(e)) × (Σ dim F(v))`.
+   Bloque de la arista `e=(u,v)`: `+R_{u⊴e}` en las columnas de `u`, `−R_{v⊴e}` en las de `v`.
+2. Calcular `rank(δ)` por eliminación gaussiana.
+3. `dim H⁰ = dim C⁰ − rank(δ)`  (núcleo del coboundary).
+4. `dim H¹ = dim C¹ − rank(δ)`  (cokernel).
+5. Si `dim H¹ > 0`, extraer una base del cokernel (vectores de `C¹` que no están en `im δ`).
+   Las aristas con soporte no nulo en esos vectores son el nudo → poblar `Knot`.
+
+### 7.1 Dos aritméticas, misma interfaz
+- `Scalar = f64` con `nalgebra`: rápido, para tener algo corriendo ya. Riesgo: rango numérico
+  aproximado (tolerancia epsilon). Suficiente para el MVP y demos.
+- `Scalar = GF2` (bit-packed, eliminación gaussiana sobre `𝔽₂`): **exacto**, sin epsilon, y es la
+  versión "topológica pura". Recomendado en cuanto el MVP funcione. Se implementa a mano con `u64`
+  como filas de bits; es código corto y muy robusto.
+
+Ambas detrás de un `trait LinAlg { fn rank(&Matrix) -> usize; fn cokernel_basis(&Matrix) -> ...; }`.
+
+### 7.2 Prueba de cordura obligatoria (¡implementar primero!)
+Para el **haz constante** (todas las stalks iguales, todas las restricciones = identidad), la cohomología
+DEBE coincidir con la topología del grafo:
+- `dim H⁰ = (#componentes conexas) × d`
+- `dim H¹ = (#ciclos independientes, i.e. primer número de Betti) × d`
+donde `d` es la dimensión de la stalk. Si esto no cuadra, el coboundary está mal ensamblado.
+Este test es el andamio: hazlo pasar antes de tocar CRDTs.
+
+---
+
+## 8. Plan de hitos
+
+### M0 — Scaffolding (día 1)
+`cargo new`, estructura de módulos vacía, `LinAlg` con la impl `f64`, `petgraph` y `thiserror` como deps.
+Un `main` que imprime "hello sheaf". Test dummy que pasa.
+
+### M1 — EL HITO DE DOS SEMANAS (el que prueba el núcleo entero)
+Objetivo: dos corridas, una que da cero y otra que no.
+
+- Implementar `cohomology::compute` + la prueba de cordura del haz constante (§7.2).
+- **Corrida A (monótona):** 3 réplicas, un `GCounter` compartido, cada una incrementa por su lado.
+  El motor construye el haz, calcula `H¹`, y **debe imprimir `H¹ = 0 → corre libre`**.
+- **Corrida B (no monótona):** mismas 3 réplicas en ciclo, pero con `ResettableRegister` y una
+  configuración de constraints cíclica inconsistente. El motor **debe imprimir
+  `H¹ ≠ 0 → nudo entre réplicas 2 y 3 → coordina aquí`**, con localización correcta.
+
+Criterio de éxito: A da 0, B da ≠0, y la localización de B apunta al ciclo correcto.
+Si logras esto, el núcleo está probado. Todo lo demás es crecer, no reinventar.
+
+### M2 — Aritmética exacta y localización robusta
+Añadir la impl `GF(2)`. Migrar los tests a exacto (sin epsilon). Mejorar `Knot` para reportar todos
+los ciclos obstruidos, no solo el primero. Property-based tests con `proptest`.
+
+### M3 — Oráculo y batería de validación
+Implementar `oracle`: un evaluador CRDT/CALM de referencia que, para cada caso de prueba, dice
+independientemente si es coordination-free. Batería de ≥20 casos (monótonos y no monótonos, con y sin
+ciclos). Test maestro: **el veredicto del haz coincide con el del oráculo en todos**. Esto valida la
+Hipótesis H (§2.2) empíricamente. Documentar cualquier caso donde discrepen — esos son oro científico.
+
+### M4 — API estable + puente a Tawasuyu/Hammer (posterior)
+Congelar la API pública de `verdict`. Adaptador que tome un estado real de tus sistemas soberanos y lo
+codifique como haz. Recién aquí se conecta a lo tuyo.
+
+### Extensión de investigación (sin fecha)
+Haces valuados en retículos/posets para cubrir datos no embebibles en grupos (§2.3). Aquí es donde el
+proyecto deja de replicar lo conocido y aporta algo nuevo de verdad.
+
+---
+
+## 9. Estrategia de pruebas
+
+Tres capas, de más barata a más valiosa:
+
+1. **Unitarias de álgebra lineal.** `rank`, `cokernel_basis` sobre matrices con respuesta conocida a mano.
+2. **Cordura topológica.** Haz constante sobre grafos con Betti conocido (árbol → H¹=0; triángulo →
+   H¹ de dimensión 1; dos triángulos pegados → dimensión 2). No negociable (§7.2).
+3. **Oráculo CRDT/CALM.** El corazón de la validación (M3). Cada caso trae su respuesta "verdadera"
+   calculada por un camino independiente (monotonicidad de las operaciones + estructura del grafo), y el
+   motor debe coincidir. `proptest` para generar configuraciones aleatorias y verificar invariantes
+   (p.ej. "toda configuración puramente monótona sobre grafo arbitrario ⟹ H¹=0").
+
+Invariantes que `proptest` debe intentar romper:
+- Solo-suma sobre cualquier grafo ⟹ `H¹ = 0`.
+- Añadir una arista nunca reduce `dim H⁰` (más restricciones, no menos).
+- El haz constante siempre reproduce los números de Betti del grafo.
+
+---
+
+## 10. Dependencias
+
+| Crate | Para qué | Nota |
+|---|---|---|
+| `petgraph` | El nervio (grafo de réplicas) | Maduro, estándar. |
+| `nalgebra` | Álgebra lineal `f64` del MVP | Para arrancar; rango con tolerancia. |
+| `thiserror` | Errores de librería tipados | Sin `unwrap` en rutas públicas. |
+| `proptest` | Tests basados en propiedades | Clave para la validación de invariantes. |
+| (propio) | `GF(2)` bit-packed | Implementación a mano, ~1 archivo. Sin dependencia externa. |
+
+Deliberadamente mínimo. Los CRDTs del MVP (`GCounter`, `ResettableRegister`) se implementan inline para
+controlar su semántica exacta; no se depende de un crate de CRDTs en v1.
+
+---
+
+## 11. Riesgos y preguntas abiertas (honestas)
+
+- **R1 (el grande).** La Hipótesis H puede fallar en casos sutiles. La codificación lineal de datos de
+  retículo puede perder información (§2.3), haciendo que el haz "vea" obstrucciones que CALM no, o
+  viceversa. *Mitigación:* M3 está diseñado justo para cazar esas discrepancias; son el resultado
+  científico, no un bug a esconder.
+- **R2.** La localización del nudo (`obstruction_basis` → `Knot`) es interpretable de forma limpia solo
+  cuando la base del cokernel es "esparcida". En casos densos, señalar "las réplicas culpables" puede ser
+  ambiguo. *Mitigación:* empezar con localización por ciclos del grafo, refinar después.
+- **R3.** Rango en `f64` es aproximado. *Mitigación:* migrar a `GF(2)` exacto en M2; no confiar en el
+  MVP `f64` para conclusiones finas.
+- **Q1.** ¿Qué codificación lineal de cada CRDT preserva exactamente la monotonicidad? Es una decisión de
+  diseño por tipo de dato, no automática.
+- **Q2.** Para datos no monótonos, ¿el haz sobre `GF(2)` basta, o hace falta ya la generalización a
+  retículos? Probable que baste para el MVP; confirmar empíricamente en M3.
+
+---
+
+## 12. Apéndice — Tickets para Claude Code
+
+Copia/pega estos como tareas, en orden. Uno por sesión. Corre `cargo test` entre cada uno.
+
+**T0.** "Crea el scaffold del crate `sheafsync` según §5: módulos `cell`, `nerve`, `sheaf`,
+`cohomology`, `verdict`, `oracle`, más `lib.rs` y `main.rs`. Añade deps `petgraph`, `nalgebra`,
+`thiserror`, y `proptest` como dev-dependency. Define el trait `LinAlg` con una impl `f64` vacía.
+Deja todo compilando con un test dummy que pase. No implementes lógica aún."
+
+**T1.** "Implementa `cohomology::compute` según §7, sobre la impl `f64` de `LinAlg`. Implementa la
+prueba de cordura del haz constante de §7.2 como tests: árbol (H¹=0), triángulo (H¹ dim 1), dos
+triángulos pegados (H¹ dim 2). Que pasen."
+
+**T2.** "Implementa `GCounter` y `ResettableRegister` en `cell` según §6.1, con `is_monotone`
+correcto. Escribe el ejemplo del hito M1 (§8): Corrida A (GCounter, 3 réplicas) que debe dar H¹=0, y
+Corrida B (ResettableRegister en ciclo inconsistente) que debe dar H¹≠0 con nudo localizado.
+Muéstralos desde `main`."
+
+**T3.** "Añade la impl `GF(2)` de `LinAlg` (bit-packed con u64, eliminación gaussiana exacta). Migra
+los tests a exacto. Extrae `Knot` con localización de todos los ciclos obstruidos según §6.5."
+
+**T4.** "Implementa `oracle` (§8 M3): un evaluador CRDT/CALM de referencia independiente. Crea una
+batería de ≥20 casos y un test maestro que verifique que el veredicto del haz coincide con el del
+oráculo. Añade los invariantes `proptest` de §9. Documenta cualquier discrepancia en un archivo
+`DISCREPANCIES.md`."
+
+---
+
+## 13. Glosario mínimo
+
+- **Coordination-free / libre de coordinación:** que las réplicas pueden converger sin un protocolo
+  de consenso ni un coordinador central.
+- **Monótono:** una operación que solo agrega información y nunca la retracta (base de CALM).
+- **Cohomología (`H⁰`, `H¹`):** invariantes lineales del haz; aquí, "estados globales coherentes" y
+  "obstrucciones a la coherencia global".
+- **Cokernel:** `C¹ / im δ`; lo que "sobra" en el espacio de aristas y no proviene de ningún estado
+  global — la firma matemática de una obstrucción.
+- **Betti (primer número de):** cantidad de ciclos independientes de un grafo; sirve de patrón de
+  cordura para el haz constante.
+
+## 14. Referencias
+
+- M. Shapiro, N. Preguiça, C. Baquero, M. Zawirski — *A Comprehensive Study of Convergent and
+  Commutative Replicated Data Types* (2011).
+- J. M. Hellerstein, P. Alvaro — *Keeping CALM: When Distributed Consistency is Easy* (CACM).
+- J. Hansen, R. Ghrist — *Toward a Spectral Theory of Cellular Sheaves* (2019).
+- R. Ghrist — *Elementary Applied Topology* (para el andamiaje de cohomología celular).
diff --git a/src/cell.rs b/src/cell.rs
new file mode 100644
index 0000000..f2d94d9
--- /dev/null
+++ b/src/cell.rs
@@ -0,0 +1,40 @@
+//! `cell` — el dato local que sabe fundirse y proyectarse (§6.1).
+//!
+//! M0: solo la *forma* del trait y los tipos. Los CRDTs concretos
+//! (`GCounter`, `ResettableRegister`) y sus semánticas llegan en T2.
+
+use crate::linalg::Scalar;
+
+/// Un pedacito de estado local que sabe cómo intentar fundirse con otro,
+/// y cómo proyectarse al espacio de una arista compartida.
+pub trait Cell: Clone + PartialEq {
+    /// El vector que representa este estado en el espacio de la stalk.
+    fn to_vector(&self) -> Vec<Scalar>;
+
+    /// Fusión de retículo (join). Debe ser conmutativa, asociativa, idempotente.
+    fn join(&self, other: &Self) -> Self;
+
+    /// ¿Esta operación es monótona? (marca para el oráculo CALM)
+    fn is_monotone() -> bool;
+}
+
+/// CRDT mínimo del MVP: cuenta por réplica, fusiona por máximo. Siempre monótono.
+///
+/// M0: forma únicamente; `impl Cell` llega en T2.
+#[derive(Debug, Clone, PartialEq)]
+pub struct GCounter {
+    /// Una casilla por réplica; fusionar = máximo casilla por casilla.
+    pub slots: Vec<u64>,
+}
+
+/// CRDT no monótono para el caso "difícil": permite reset/borrado.
+/// Sirve para provocar `H¹ ≠ 0` deliberadamente (§8, corrida B).
+///
+/// M0: forma únicamente; `impl Cell` llega en T2.
+#[derive(Debug, Clone, PartialEq)]
+pub struct ResettableRegister {
+    /// Valor actual del registro.
+    pub value: i64,
+    /// Contador de generación; un reset lo incrementa (rompe la monotonicidad).
+    pub generation: u64,
+}
diff --git a/src/cohomology.rs b/src/cohomology.rs
new file mode 100644
index 0000000..f03a312
--- /dev/null
+++ b/src/cohomology.rs
@@ -0,0 +1,33 @@
+//! `cohomology` — la única cuenta que importa (§6.4, §7).
+//!
+//! `rank δ`, `dim H⁰`, `dim H¹` y una base del cokernel para localizar el nudo.
+//! M0: forma del resultado y firma de `compute`. La cuenta llega en T1.
+
+use crate::error::{Result, SheafError};
+use crate::linalg::Scalar;
+use crate::sheaf::Sheaf;
+
+/// Resultado del cálculo cohomológico sobre un haz.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Cohomology {
+    /// `dim C⁰ = Σ dim F(v)`.
+    pub dim_c0: usize,
+    /// `dim C¹ = Σ dim F(e)`.
+    pub dim_c1: usize,
+    /// `rank δ`.
+    pub rank_delta: usize,
+    /// `dim H⁰ = dim_c0 − rank_delta` (secciones globales coherentes).
+    pub dim_h0: usize,
+    /// `dim H¹ = dim_c1 − rank_delta` (obstrucciones).
+    pub dim_h1: usize,
+    /// Si `dim_h1 > 0`, una base del cokernel: cada vector señala aristas
+    /// que participan en la obstrucción (para localizar el nudo).
+    pub obstruction_basis: Vec<Vec<Scalar>>,
+}
+
+/// Calcula la cohomología del haz vía eliminación gaussiana (§7).
+///
+/// M0: no implementado; llega en T1.
+pub fn compute(_sheaf: &Sheaf) -> Result<Cohomology> {
+    Err(SheafError::NotImplemented("cohomology::compute llega en T1"))
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..362e806
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,22 @@
+//! Errores tipados de la librería (§5.1). Nada de `unwrap()` en rutas públicas.
+
+use thiserror::Error;
+
+/// Error raíz de `sheafsync`.
+#[derive(Debug, Error)]
+pub enum SheafError {
+    /// Dimensiones incompatibles al ensamblar el coboundary o multiplicar matrices.
+    #[error("dimensiones incompatibles: {0}")]
+    DimensionMismatch(String),
+
+    /// El haz referencia una réplica o arista que no existe en el nervio.
+    #[error("entidad inexistente en el nervio: {0}")]
+    MissingEntity(String),
+
+    /// Marcador de funcionalidad aún no implementada (scaffolding M0).
+    #[error("no implementado todavía: {0}")]
+    NotImplemented(&'static str),
+}
+
+/// Alias de resultado usado en toda la librería.
+pub type Result<T> = std::result::Result<T, SheafError>;
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..9a2e039
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,38 @@
+//! # sheafsync
+//!
+//! Motor de consistencia sin coordinación. Modela el estado distribuido como un
+//! haz celular sobre el grafo de réplicas (el nervio) y calcula su cohomología
+//! `H⁰`/`H¹` para decidir, con demostración, cuándo se puede fundir sin
+//! coordinación y cuándo hay una obstrucción localizada.
+//!
+//! Ver `DESIGN.md` para el diseño completo. Este es el scaffold de M0: la forma
+//! de todos los módulos, sin lógica de negocio todavía.
+//!
+//! Pipeline (§5):
+//! `cell` → `nerve` → `sheaf` → `cohomology` → `verdict`, validado por `oracle`.
+
+pub mod cell;
+pub mod cohomology;
+pub mod error;
+pub mod linalg;
+pub mod nerve;
+pub mod oracle;
+pub mod sheaf;
+pub mod verdict;
+
+pub use error::{Result, SheafError};
+
+#[cfg(test)]
+mod tests {
+    use crate::linalg::Matrix;
+
+    /// Test dummy de M0: el andamio compila y los tipos básicos existen.
+    /// La lógica real (cohomología, cordura topológica) llega en T1.
+    #[test]
+    fn scaffold_compila() {
+        let m = Matrix::zeros(2, 3);
+        assert_eq!(m.rows, 2);
+        assert_eq!(m.cols, 3);
+        assert_eq!(m.data.len(), 6);
+    }
+}
diff --git a/src/linalg.rs b/src/linalg.rs
new file mode 100644
index 0000000..02d2e6e
--- /dev/null
+++ b/src/linalg.rs
@@ -0,0 +1,73 @@
+//! Aislamiento del álgebra lineal tras un `trait` (§5.1, §7.1).
+//!
+//! Dos aritméticas, misma interfaz:
+//! - `F64Backend`: rápido, con tolerancia epsilon. Para arrancar el MVP.
+//! - `Gf2Backend` (M2): exacto, bit-packed sobre 𝔽₂. Aún no implementado.
+//!
+//! En M0 solo dejamos la *forma*: la impl `f64` existe pero sus operaciones
+//! todavía no calculan nada. La lógica llega en T1.
+
+/// Escalar del backend activo. Alias que resuelve la impl elegida (§6.1, §7).
+pub type Scalar = f64;
+
+/// Matriz densa mínima almacenada por filas. Placeholder de M0; en T1 puede
+/// migrarse a `nalgebra::DMatrix` o mantenerse propia para el backend `GF(2)`.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Matrix {
+    pub rows: usize,
+    pub cols: usize,
+    /// Datos row-major: `data[r * cols + c]`.
+    pub data: Vec<Scalar>,
+}
+
+impl Matrix {
+    /// Matriz de ceros de tamaño `rows × cols`.
+    pub fn zeros(rows: usize, cols: usize) -> Self {
+        Self {
+            rows,
+            cols,
+            data: vec![0.0; rows * cols],
+        }
+    }
+}
+
+/// Interfaz de álgebra lineal que el motor usa sin conocer la aritmética concreta.
+///
+/// Las dos operaciones que la cohomología necesita (§7): rango del coboundary y
+/// una base del cokernel para localizar la obstrucción.
+pub trait LinAlg {
+    /// `rank(m)` — número de filas/columnas independientes.
+    fn rank(&self, m: &Matrix) -> usize;
+
+    /// Base del cokernel `C¹ / im δ`: vectores de longitud `m.rows` que no
+    /// provienen de ninguna sección global. Vacío si `H¹ = 0`.
+    fn cokernel_basis(&self, m: &Matrix) -> Vec<Vec<Scalar>>;
+}
+
+/// Backend `f64` del MVP (§7.1). Rango con tolerancia epsilon.
+///
+/// M0: shell vacío. La eliminación gaussiana llega en T1.
+#[derive(Debug, Default, Clone, Copy)]
+pub struct F64Backend {
+    /// Tolerancia para considerar un pivote como cero.
+    pub epsilon: Scalar,
+}
+
+impl F64Backend {
+    /// Backend con la tolerancia por defecto.
+    pub fn new() -> Self {
+        Self { epsilon: 1e-9 }
+    }
+}
+
+impl LinAlg for F64Backend {
+    fn rank(&self, _m: &Matrix) -> usize {
+        // TODO(T1): eliminación gaussiana con pivoteo, contar pivotes > epsilon.
+        unimplemented!("F64Backend::rank llega en T1")
+    }
+
+    fn cokernel_basis(&self, _m: &Matrix) -> Vec<Vec<Scalar>> {
+        // TODO(T1): extraer base del cokernel a partir de la forma escalonada.
+        unimplemented!("F64Backend::cokernel_basis llega en T1")
+    }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..14aa160
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,6 @@
+//! Binario de `sheafsync`. En M0 solo saluda; las corridas A/B del hito M1
+//! (§8) se conectan aquí en T2.
+
+fn main() {
+    println!("hello sheaf");
+}
diff --git a/src/nerve.rs b/src/nerve.rs
new file mode 100644
index 0000000..e5eac74
--- /dev/null
+++ b/src/nerve.rs
@@ -0,0 +1,34 @@
+//! `nerve` — el grafo de qué réplicas comparten datos con cuáles (§6.2).
+//!
+//! Vértices = réplicas, aristas = pares que comparten un dato.
+
+use petgraph::graph::UnGraph;
+
+/// Identificador de una réplica (vértice del nervio).
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct ReplicaId(pub usize);
+
+/// Qué coordenadas del estado se comparten en una arista.
+///
+/// M0: forma únicamente; el detalle de la proyección se fija en `sheaf` (T1/T2).
+#[derive(Debug, Clone, Default, PartialEq)]
+pub struct SharedLink {
+    /// Índices de las coordenadas compartidas en esta arista.
+    pub shared_coords: Vec<usize>,
+}
+
+/// El nervio: grafo no dirigido de réplicas y sus enlaces compartidos.
+#[derive(Debug, Default)]
+pub struct Nerve {
+    /// Grafo no dirigido: vértices = réplicas, aristas = pares que comparten datos.
+    pub graph: UnGraph<ReplicaId, SharedLink>,
+}
+
+impl Nerve {
+    /// Nervio vacío.
+    pub fn new() -> Self {
+        Self {
+            graph: UnGraph::new_undirected(),
+        }
+    }
+}
diff --git a/src/oracle.rs b/src/oracle.rs
new file mode 100644
index 0000000..e88996e
--- /dev/null
+++ b/src/oracle.rs
@@ -0,0 +1,16 @@
+//! `oracle` — evaluador CRDT/CALM de referencia, independiente del haz (§8 M3).
+//!
+//! Para cada caso de prueba dice, por un camino independiente (monotonicidad de
+//! las operaciones + estructura del grafo), si es coordination-free. El test
+//! maestro de M3 comprueba que el veredicto del haz coincide con este.
+//!
+//! M0: solo la forma. La batería de ≥20 casos y la lógica llegan en T4.
+
+/// Predicción independiente del oráculo para una configuración.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum OracleVerdict {
+    /// El oráculo cree que la configuración es libre de coordinación.
+    CoordinationFree,
+    /// El oráculo cree que hace falta coordinar.
+    NeedsCoordination,
+}
diff --git a/src/sheaf.rs b/src/sheaf.rs
new file mode 100644
index 0000000..64a41b3
--- /dev/null
+++ b/src/sheaf.rs
@@ -0,0 +1,46 @@
+//! `sheaf` — el haz celular y el ensamblado del coboundary `δ` (§6.3, §7).
+//!
+//! M0: forma de los tipos y firma de `coboundary`. El ensamblado real
+//! (bloque por arista, `+R_{u⊴e}` / `−R_{v⊴e}`) llega en T1.
+
+use crate::error::{Result, SheafError};
+use crate::linalg::Matrix;
+
+/// Mapa de restricción `F(v ⊴ e): F(v) → F(e)`: proyección lineal del estado
+/// de una réplica sobre el dato de una arista. Matriz `edge_dim × vertex_dim`.
+pub type RestrictionMap = Matrix;
+
+/// Un haz celular sobre el nervio.
+#[derive(Debug, Default)]
+pub struct Sheaf {
+    /// Para cada réplica: la dimensión de su stalk `F(v)`.
+    pub vertex_stalks: Vec<usize>,
+    /// Para cada arista: la dimensión de su stalk `F(e)`.
+    pub edge_stalks: Vec<usize>,
+    /// Mapas de restricción: por arista, dos matrices (una por extremo).
+    pub restrictions: Vec<(RestrictionMap, RestrictionMap)>,
+    /// Extremos `(u, v)` de cada arista, como índices en `vertex_stalks`.
+    pub edge_endpoints: Vec<(usize, usize)>,
+}
+
+impl Sheaf {
+    /// Dimensión de `C⁰ = ⊕_v F(v)` (espacio de cocadenas de vértice).
+    pub fn dim_c0(&self) -> usize {
+        self.vertex_stalks.iter().sum()
+    }
+
+    /// Dimensión de `C¹ = ⊕_e F(e)` (espacio de cocadenas de arista).
+    pub fn dim_c1(&self) -> usize {
+        self.edge_stalks.iter().sum()
+    }
+
+    /// Ensambla la matriz global de coboundary `δ: C⁰ → C¹`.
+    ///
+    /// Convención (§6.3): para arista `e=(u,v)`,
+    /// `(δx)_e = R_{u⊴e}·x_u − R_{v⊴e}·x_v`.
+    ///
+    /// M0: no implementado; llega en T1.
+    pub fn coboundary(&self) -> Result<Matrix> {
+        Err(SheafError::NotImplemented("Sheaf::coboundary llega en T1"))
+    }
+}
diff --git a/src/verdict.rs b/src/verdict.rs
new file mode 100644
index 0000000..7bf5b8e
--- /dev/null
+++ b/src/verdict.rs
@@ -0,0 +1,43 @@
+//! `verdict` — traducción del resultado topológico a lenguaje de sistemas (§6.5).
+//!
+//! `H¹ = 0 → corre libre` vs `H¹ ≠ 0 → coordina aquí`, con localización.
+
+use crate::cohomology::Cohomology;
+use crate::error::Result;
+use crate::nerve::ReplicaId;
+
+/// Un nudo: una obstrucción localizada entre réplicas/aristas concretas.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Knot {
+    /// Réplicas implicadas en el nudo.
+    pub replicas: Vec<ReplicaId>,
+    /// Aristas del ciclo obstruido (índices en el nervio).
+    pub edges: Vec<usize>,
+}
+
+/// El veredicto final en lenguaje de sistemas.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Verdict {
+    /// `H¹ = 0`: fusión libre de coordinación.
+    RunsFree,
+    /// `H¹ ≠ 0`: hace falta coordinar, con los nudos localizados.
+    NeedsCoordination {
+        /// Los nudos detectados.
+        knots: Vec<Knot>,
+    },
+}
+
+impl Verdict {
+    /// Deriva el veredicto a partir de la cohomología calculada.
+    ///
+    /// M0: cuando `dim_h1 == 0` devuelve `RunsFree`; la localización de nudos
+    /// (poblar `Knot` desde `obstruction_basis`) llega en T2/T3.
+    pub fn from_cohomology(cohomology: &Cohomology) -> Result<Verdict> {
+        if cohomology.dim_h1 == 0 {
+            Ok(Verdict::RunsFree)
+        } else {
+            // TODO(T2/T3): mapear obstruction_basis → Knot con réplicas y aristas.
+            Ok(Verdict::NeedsCoordination { knots: Vec::new() })
+        }
+    }
+}