From 6595dbed856eec31669beb6db74b456faa4be9a1 Mon Sep 17 00:00:00 2001 From: Sergio Date: Wed, 1 Jul 2026 18:02:43 +0000 Subject: [PATCH] M0/T0: scaffold del crate sheafsync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 1 + Cargo.lock | 612 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 21 ++ DESIGN.md | 430 ++++++++++++++++++++++++++++++++ src/cell.rs | 40 +++ src/cohomology.rs | 33 +++ src/error.rs | 22 ++ src/lib.rs | 38 +++ src/linalg.rs | 73 ++++++ src/main.rs | 6 + src/nerve.rs | 34 +++ src/oracle.rs | 16 ++ src/sheaf.rs | 46 ++++ src/verdict.rs | 43 ++++ 14 files changed, 1415 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 DESIGN.md create mode 100644 src/cell.rs create mode 100644 src/cohomology.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/linalg.rs create mode 100644 src/main.rs create mode 100644 src/nerve.rs create mode 100644 src/oracle.rs create mode 100644 src/sheaf.rs create mode 100644 src/verdict.rs 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; + + /// 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 } + +/// 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, +} + +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, + /// Para cada arista: su espacio (dimensión). + pub edge_stalks: Vec, + /// 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>, +} + +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 } // H¹ ≠ 0, con localización +} + +pub struct Knot { + pub replicas: Vec, // réplicas implicadas en el nudo + pub edges: Vec, // 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; + + /// 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, +} + +/// 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>, +} + +/// Calcula la cohomología del haz vía eliminación gaussiana (§7). +/// +/// M0: no implementado; llega en T1. +pub fn compute(_sheaf: &Sheaf) -> Result { + 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 = std::result::Result; 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, +} + +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>; +} + +/// 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> { + // 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, +} + +/// 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, +} + +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, + /// Para cada arista: la dimensión de su stalk `F(e)`. + pub edge_stalks: Vec, + /// 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 { + 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, + /// Aristas del ciclo obstruido (índices en el nervio). + pub edges: Vec, +} + +/// 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, + }, +} + +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 { + 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() }) + } + } +}