From 7807ed66f625ec1a0fa4da1ecdadee924f986137 Mon Sep 17 00:00:00 2001
From: jenya
Date: Wed, 30 Sep 2026 17:43:23 +0300
Subject: [PATCH 1/4] Library API: portable core crate with CLI and WASM
adapters
Cargo workspace:
- eigentrust (root): the library, rlib, builds on every target, no wasm gating
- eigentrust-cli (cli/): the `eigentrust` binary, uses only the public API
- eigentrust-wasm (wasm/): JS bindings, the only cdylib, publish = false
Public API, all re-exported from lib.rs, every module private:
- eigentrust(local_trust, pre_trust) and eigentrust_with_options(.., &options)
- TrustEdge { from, to, weight }, PreTrust { peer, weight }, From tuple impls
- EigenTrustOptions: alpha, epsilon, max_iterations; private fields, builder,
validated before any work
- TrustScores: scores, get, ranking, iterations, residual
- EigenTrustError (#[non_exhaustive]) and Input; no String errors
- csv feature: csv::Network::from_csv with its own Error / ErrorKind
- parallel feature: rayon; default features: none
Behavior and numbers are unchanged: CLI output is byte-identical to the
previous version on 10 reference inputs (up to 5M lines), WASM output is
byte-identical, CLI is ~15% faster, WASM the same.
- crate docs with input rules, convergence and examples; missing_docs on
- examples/basic.rs, examples/csv.rs
- tests/api.rs and tests/csv.rs use the crate like an external consumer
- README Rust example runs as a doc test
- crates.io metadata, include list, docs.rs config; Cargo.lock committed
- removed the unused sparse helpers (CSC, merge, sorting utils) and the
unwired distrust functions; their history stays in git
---
.gitignore | 2 -
Cargo.lock | 298 +++++++++++++++----
Cargo.toml | 61 ++--
build.sh | 11 +-
cli/Cargo.toml | 21 ++
cli/LICENSE-APACHE | 1 +
cli/LICENSE-MIT | 1 +
{src => cli/src}/main.rs | 40 +--
examples/basic.rs | 26 ++
examples/csv.rs | 17 ++
src/algorithm/matrix.rs | 105 +++++++
src/algorithm/mod.rs | 128 +++++++++
src/algorithm/power.rs | 202 +++++++++++++
src/basic/eigentrust.rs | 600 ---------------------------------------
src/basic/engine.rs | 134 ---------
src/basic/input.rs | 212 --------------
src/basic/localtrust.rs | 149 ----------
src/basic/mod.rs | 6 -
src/basic/trustvector.rs | 118 --------
src/basic/util.rs | 54 ----
src/csv.rs | 395 ++++++++++++++++++++++++++
src/error.rs | 145 ++++++----
src/lib.rs | 211 +++++++++-----
src/options.rs | 108 +++++++
src/scores.rs | 62 ++++
src/sparse/entry.rs | 310 --------------------
src/sparse/matrix.rs | 591 --------------------------------------
src/sparse/mod.rs | 4 -
src/sparse/util.rs | 91 ------
src/sparse/vector.rs | 245 ----------------
src/types.rs | 64 +++++
tests/api.rs | 325 +++++++++++++++++++++
tests/csv.rs | 110 +++++++
wasm/Cargo.toml | 32 +++
wasm/src/bindings.rs | 34 +++
wasm/src/convert.rs | 71 +++++
wasm/src/lib.rs | 15 +
37 files changed, 2245 insertions(+), 2754 deletions(-)
create mode 100644 cli/Cargo.toml
create mode 120000 cli/LICENSE-APACHE
create mode 120000 cli/LICENSE-MIT
rename {src => cli/src}/main.rs (55%)
create mode 100644 examples/basic.rs
create mode 100644 examples/csv.rs
create mode 100644 src/algorithm/matrix.rs
create mode 100644 src/algorithm/mod.rs
create mode 100644 src/algorithm/power.rs
delete mode 100644 src/basic/eigentrust.rs
delete mode 100644 src/basic/engine.rs
delete mode 100644 src/basic/input.rs
delete mode 100644 src/basic/localtrust.rs
delete mode 100644 src/basic/mod.rs
delete mode 100644 src/basic/trustvector.rs
delete mode 100644 src/basic/util.rs
create mode 100644 src/csv.rs
create mode 100644 src/options.rs
create mode 100644 src/scores.rs
delete mode 100644 src/sparse/entry.rs
delete mode 100644 src/sparse/matrix.rs
delete mode 100644 src/sparse/mod.rs
delete mode 100644 src/sparse/util.rs
delete mode 100644 src/sparse/vector.rs
create mode 100644 src/types.rs
create mode 100644 tests/api.rs
create mode 100644 tests/csv.rs
create mode 100644 wasm/Cargo.toml
create mode 100644 wasm/src/bindings.rs
create mode 100644 wasm/src/convert.rs
create mode 100644 wasm/src/lib.rs
diff --git a/.gitignore b/.gitignore
index 8daf6d3..8240dcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,9 +19,7 @@ firebase.json
debug/
target/
-# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
-Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
diff --git a/Cargo.lock b/Cargo.lock
index b99bdc3..5bf2f3e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,56 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "async-trait"
version = "0.1.92"
@@ -19,7 +69,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.6",
]
[[package]]
@@ -28,6 +78,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
[[package]]
name = "bumpalo"
version = "3.16.0"
@@ -56,6 +112,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@@ -73,7 +135,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f"
dependencies = [
"log",
- "wasm-bindgen",
"web-sys",
]
@@ -132,17 +193,63 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror",
+]
+
[[package]]
name = "eigentrust"
-version = "0.1.0"
+version = "0.2.0"
dependencies = [
- "console_error_panic_hook",
- "console_log",
"csv",
- "env_logger",
"log",
"rayon",
- "serde",
+]
+
+[[package]]
+name = "eigentrust-cli"
+version = "0.2.0"
+dependencies = [
+ "csv",
+ "eigentrust",
+ "env_logger",
+]
+
+[[package]]
+name = "eigentrust-wasm"
+version = "0.2.0"
+dependencies = [
+ "console_error_panic_hook",
+ "console_log",
+ "eigentrust",
+ "log",
"serde_json",
"wasm-bindgen",
"wasm-bindgen-rayon",
@@ -156,16 +263,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
-name = "env_logger"
-version = "0.10.2"
+name = "env_filter"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
- "humantime",
- "is-terminal",
"log",
"regex",
- "termcolor",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
]
[[package]]
@@ -199,33 +316,53 @@ dependencies = [
]
[[package]]
-name = "hermit-abi"
-version = "0.4.0"
+name = "is_terminal_polyfill"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
-name = "humantime"
-version = "2.1.0"
+name = "itoa"
+version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
-name = "is-terminal"
-version = "0.4.13"
+name = "jiff"
+version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
+checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb"
dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.52.0",
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
]
[[package]]
-name = "itoa"
-version = "1.0.11"
+name = "jiff-core"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c"
+dependencies = [
+ "defmt",
+ "log",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
[[package]]
name = "js-sys"
@@ -238,12 +375,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "libc"
-version = "0.2.158"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
-
[[package]]
name = "libm"
version = "0.2.16"
@@ -252,9 +383,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "log"
-version = "0.4.22"
+version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "memchr"
@@ -297,6 +428,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
[[package]]
name = "oorandom"
version = "11.1.5"
@@ -309,6 +446,21 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.107"
@@ -320,9 +472,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.37"
+version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
@@ -351,9 +503,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.10.6"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
@@ -363,9 +515,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.7"
+version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
@@ -374,9 +526,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.4"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustversion"
@@ -426,7 +578,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.6",
]
[[package]]
@@ -453,6 +605,17 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
[[package]]
name = "syn"
version = "3.0.6"
@@ -465,12 +628,23 @@ dependencies = [
]
[[package]]
-name = "termcolor"
-version = "1.4.1"
+name = "thiserror"
+version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
- "winapi-util",
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.6",
]
[[package]]
@@ -488,6 +662,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -541,7 +721,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.6",
"wasm-bindgen-shared",
]
@@ -596,7 +776,7 @@ checksum = "3b84b5ac638bfb168196a1a461fcc8f46a294a18b1b6be52133b4e0db122cc9f"
dependencies = [
"proc-macro2",
"quote",
- "syn",
+ "syn 3.0.6",
]
[[package]]
@@ -635,22 +815,28 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[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.52.0"
+version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
-version = "0.59.0"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-targets",
+ "windows-link",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index cc63a15..da1b21d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,43 +1,50 @@
+[workspace]
+members = ["cli", "wasm"]
+resolver = "2"
+
[package]
name = "eigentrust"
-version = "0.1.0"
+version = "0.2.0"
edition = "2021"
authors = ["Jenya "]
license = "MIT OR Apache-2.0"
-description = "Fast EigenTrust reputation algorithm: global trust scores for peer-to-peer networks and social graphs, native and WebAssembly"
+description = "EigenTrust reputation algorithm: global trust scores for peer-to-peer networks and social graphs, with Sybil resistance"
+repository = "https://github.com/hypnagonia/eigentrust-rust"
homepage = "https://eigentrust.jenyadoesapps.com"
+documentation = "https://docs.rs/eigentrust"
readme = "README.md"
-keywords = ["eigentrust", "reputation", "trust", "sybil", "wasm"]
-categories = ["algorithms", "wasm", "science"]
-repository = "https://github.com/hypnagonia/eigentrust-rust"
+keywords = ["eigentrust", "reputation", "trust", "sybil", "pagerank"]
+categories = ["algorithms", "mathematics", "science"]
+include = [
+ "/src/**/*.rs",
+ "/tests/**/*.rs",
+ "/examples/**/*.rs",
+ "/example/*.csv",
+ "/README.md",
+ "/LICENSE-MIT",
+ "/LICENSE-APACHE",
+]
[features]
-# Multithreaded WASM (wasm-bindgen-rayon). Needs nightly, +atomics and a cross-origin
-# isolated page, see build.sh
-parallel = ["dep:wasm-bindgen-rayon", "dep:rayon"]
-
-[target.'cfg(target_arch = "wasm32")'.dependencies]
-wasm-bindgen = "0.2.100"
-console_error_panic_hook = "0.1"
-console_log = { version = "1.0", features = ["color"]}
-wasm-bindgen-rayon = { version = "1.3", optional = true }
-rayon = { version = "1.8", optional = true }
-
-[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
-wasm-bindgen-test = "0.3"
-
-[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
-env_logger = "0.10"
-rayon = "1.8"
+default = []
+## Read named peers from CSV (`eigentrust::csv`).
+csv = ["dep:csv"]
+## Multithreaded power iteration with rayon. On wasm32 this needs a threaded build,
+## see the eigentrust-wasm crate.
+parallel = ["dep:rayon"]
[dependencies]
-csv = "1.3"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
+csv = { version = "1.3", optional = true }
log = "0.4"
+rayon = { version = "1.8", optional = true }
+
+[[example]]
+name = "csv"
+required-features = ["csv"]
-[lib]
-crate-type = ["cdylib"]
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
[profile.release]
lto = true
diff --git a/build.sh b/build.sh
index 28ffa90..4b903d2 100755
--- a/build.sh
+++ b/build.sh
@@ -1,18 +1,19 @@
#!/bin/sh
-# Builds the WASM packages and copies them into the demo.
+# Builds the WebAssembly packages from wasm/ and copies them into the demo.
# pkg/ single-threaded, stable toolchain, works everywhere
# pkg-parallel/ multithreaded (rayon on Web Workers), nightly + atomics,
# needs a cross-origin isolated page (COOP/COEP headers, see demo/vercel.json)
set -e
+cd "$(dirname "$0")"
-wasm-pack build --target web --release
+wasm-pack build wasm --target web --release --out-dir ../pkg --out-name eigentrust
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory,+mutable-globals' \
- rustup run nightly wasm-pack build --target web --release --out-dir pkg-parallel \
- -- --features parallel -Z build-std=panic_abort,std
+ rustup run nightly wasm-pack build wasm --target web --release \
+ --out-dir ../pkg-parallel --out-name eigentrust \
+ -- --features threads -Z build-std=panic_abort,std
rm -rf demo/pkg demo/pkg-parallel
-mkdir -p demo
cp -r pkg demo/pkg
cp -r pkg-parallel demo/pkg-parallel
# wasm-bindgen-rayon imports its package as a directory ('../../..'), which only a bundler resolves
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
new file mode 100644
index 0000000..e150c6a
--- /dev/null
+++ b/cli/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "eigentrust-cli"
+version = "0.2.0"
+edition = "2021"
+authors = ["Jenya "]
+license = "MIT OR Apache-2.0"
+description = "Command-line EigenTrust: rank peers from CSV trust statements"
+repository = "https://github.com/hypnagonia/eigentrust-rust"
+homepage = "https://eigentrust.jenyadoesapps.com"
+readme = "../README.md"
+keywords = ["eigentrust", "reputation", "trust", "cli", "csv"]
+categories = ["command-line-utilities", "algorithms"]
+
+[[bin]]
+name = "eigentrust"
+path = "src/main.rs"
+
+[dependencies]
+eigentrust = { version = "0.2.0", path = "..", features = ["csv", "parallel"] }
+csv = "1.3"
+env_logger = { version = "0.11", default-features = false, features = ["auto-color", "humantime"] }
diff --git a/cli/LICENSE-APACHE b/cli/LICENSE-APACHE
new file mode 120000
index 0000000..965b606
--- /dev/null
+++ b/cli/LICENSE-APACHE
@@ -0,0 +1 @@
+../LICENSE-APACHE
\ No newline at end of file
diff --git a/cli/LICENSE-MIT b/cli/LICENSE-MIT
new file mode 120000
index 0000000..76219eb
--- /dev/null
+++ b/cli/LICENSE-MIT
@@ -0,0 +1 @@
+../LICENSE-MIT
\ No newline at end of file
diff --git a/src/main.rs b/cli/src/main.rs
similarity index 55%
rename from src/main.rs
rename to cli/src/main.rs
index fcce6d2..c02fb22 100644
--- a/src/main.rs
+++ b/cli/src/main.rs
@@ -1,18 +1,22 @@
+//! eigentrust [alpha]
+//!
+//! Prints `peer,score` for every peer with a non-zero score, highest first.
+
+use eigentrust::csv::Network;
+use eigentrust::EigenTrustOptions;
use std::env;
use std::fs;
use std::io;
use std::process::ExitCode;
-use crate::basic::engine::calculate_from_csv;
-use crate::basic::util::init_logger;
-pub mod basic;
-pub mod error;
-pub mod sparse;
-
const USAGE: &str = "usage: eigentrust [alpha]";
fn main() -> ExitCode {
- init_logger();
+ // RUST_LOG overrides, e.g. RUST_LOG=trace
+ env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
+ .format_timestamp_millis()
+ .init();
+
match run(env::args().skip(1).collect()) {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
@@ -23,13 +27,13 @@ fn main() -> ExitCode {
}
fn run(args: Vec) -> Result<(), String> {
- let (localtrust_path, pretrust_path, alpha) = match args.as_slice() {
- [lt, pt] => (lt, pt, None),
+ let (localtrust_path, pretrust_path, options) = match args.as_slice() {
+ [lt, pt] => (lt, pt, EigenTrustOptions::default()),
[lt, pt, alpha] => {
let alpha = alpha
.parse::()
.map_err(|_| format!("alpha {:?} is not a number\n{}", alpha, USAGE))?;
- (lt, pt, Some(alpha))
+ (lt, pt, EigenTrustOptions::default().with_alpha(alpha))
}
_ => return Err(USAGE.to_string()),
};
@@ -37,17 +41,17 @@ fn run(args: Vec) -> Result<(), String> {
let read = |path: &String| {
fs::read_to_string(path).map_err(|e| format!("cannot read {}: {}", path, e))
};
- let localtrust_csv = read(localtrust_path)?;
- let pretrust_csv = read(pretrust_path)?;
-
- let result =
- calculate_from_csv(&localtrust_csv, &pretrust_csv, alpha).map_err(|e| e.to_string())?;
+ let network = Network::from_csv(&read(localtrust_path)?, &read(pretrust_path)?)
+ .map_err(|e| e.to_string())?;
+ let result = network.eigentrust(&options).map_err(|e| e.to_string())?;
- // proper CSV so peer ids with commas or quotes round-trip
+ // proper CSV so peer names with commas or quotes round-trip
let write = || -> csv::Result<()> {
let mut out = csv::Writer::from_writer(io::stdout().lock());
- for (name, score) in &result {
- out.write_record([name.as_str(), &score.to_string()])?;
+ for (name, score) in network.ranking(&result) {
+ if score > 0.0 {
+ out.write_record([name, &score.to_string()])?;
+ }
}
out.flush()?;
Ok(())
diff --git a/examples/basic.rs b/examples/basic.rs
new file mode 100644
index 0000000..f9a9e96
--- /dev/null
+++ b/examples/basic.rs
@@ -0,0 +1,26 @@
+//! cargo run --example basic
+
+use eigentrust::{eigentrust, PreTrust, TrustEdge};
+
+fn main() -> Result<(), Box> {
+ let names = ["alice", "bob", "carol", "mallory"];
+
+ // who trusts whom, and how much (weights are relative per truster)
+ let local_trust = [
+ TrustEdge::new(0, 1, 3.0), // alice -> bob
+ TrustEdge::new(0, 2, 1.0), // alice -> carol
+ TrustEdge::new(1, 2, 2.0), // bob -> carol
+ TrustEdge::new(2, 0, 1.0), // carol -> alice
+ TrustEdge::new(3, 3, 9.0), // mallory only vouches for herself
+ ];
+ // alice is trusted from the start
+ let pre_trust = [PreTrust::new(0, 1.0)];
+
+ let result = eigentrust(local_trust, pre_trust)?;
+
+ for (peer, score) in result.ranking() {
+ println!("{:<8} {:.4}", names[peer], score);
+ }
+ println!("converged after {} iterations", result.iterations());
+ Ok(())
+}
diff --git a/examples/csv.rs b/examples/csv.rs
new file mode 100644
index 0000000..2ab6a3f
--- /dev/null
+++ b/examples/csv.rs
@@ -0,0 +1,17 @@
+//! cargo run --example csv --features csv
+
+use eigentrust::csv::Network;
+use eigentrust::EigenTrustOptions;
+
+fn main() -> Result<(), Box> {
+ let local_trust = "from,to,weight\nalice,bob,3\nalice,carol,1\nbob,carol,2\ncarol,alice,1\n";
+ let pre_trust = "peer\nalice\n";
+
+ let network = Network::from_csv(local_trust, pre_trust)?;
+ let result = network.eigentrust(&EigenTrustOptions::default().with_alpha(0.2))?;
+
+ for (name, score) in network.ranking(&result) {
+ println!("{:<8} {:.4}", name, score);
+ }
+ Ok(())
+}
diff --git a/src/algorithm/matrix.rs b/src/algorithm/matrix.rs
new file mode 100644
index 0000000..d82848a
--- /dev/null
+++ b/src/algorithm/matrix.rs
@@ -0,0 +1,105 @@
+// Compressed sparse rows: `rows[i]` holds the non-zero entries of row i, sorted by column.
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub(crate) struct Entry {
+ pub(crate) index: usize,
+ pub(crate) value: f64,
+}
+
+impl Entry {
+ pub(crate) fn new(index: usize, value: f64) -> Self {
+ Entry { index, value }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct CsrMatrix {
+ pub(crate) rows: Vec>,
+}
+
+impl CsrMatrix {
+ // Sorts each row by column. Repeated columns keep the last value in input order and
+ // zero values are dropped.
+ pub(crate) fn from_rows(mut rows: Vec>) -> Self {
+ for row in &mut rows {
+ // stable sort keeps input order among duplicates, so the last one wins
+ row.sort_by_key(|e| e.index);
+ dedup_keep_last(row);
+ row.retain(|e| e.value != 0.0);
+ }
+ CsrMatrix { rows }
+ }
+
+ // Column lists come out sorted by row, which fixes the summation order of the
+ // matrix-vector product.
+ pub(crate) fn transpose(&self) -> CsrMatrix {
+ let mut counts = vec![0usize; self.rows.len()];
+ for row in &self.rows {
+ for entry in row {
+ counts[entry.index] += 1;
+ }
+ }
+ let mut rows: Vec> = counts.into_iter().map(Vec::with_capacity).collect();
+ for (i, row) in self.rows.iter().enumerate() {
+ for entry in row {
+ rows[entry.index].push(Entry::new(i, entry.value));
+ }
+ }
+ CsrMatrix { rows }
+ }
+}
+
+fn dedup_keep_last(row: &mut Vec) {
+ if row.len() < 2 {
+ return;
+ }
+ let mut write = 0;
+ for read in 1..row.len() {
+ if row[read].index == row[write].index {
+ row[write].value = row[read].value;
+ } else {
+ write += 1;
+ row[write] = row[read];
+ }
+ }
+ row.truncate(write + 1);
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn e(index: usize, value: f64) -> Entry {
+ Entry::new(index, value)
+ }
+
+ #[test]
+ fn from_rows_sorts_dedups_and_drops_zeros() {
+ let m = CsrMatrix::from_rows(vec![
+ vec![e(1, 1.0), e(0, 3.0), e(1, 5.0), e(2, 0.0)],
+ vec![e(1, 2.0), e(0, 4.0), e(0, 0.0)],
+ ]);
+ assert_eq!(m.rows, vec![vec![e(0, 3.0), e(1, 5.0)], vec![e(1, 2.0)]]);
+ }
+
+ #[test]
+ fn transpose_round_trips() {
+ let m = CsrMatrix::from_rows(vec![
+ vec![e(0, 100.0), e(1, 200.0), e(2, 300.0)],
+ vec![e(1, 400.0), e(3, 500.0)],
+ vec![],
+ vec![e(0, 600.0), e(1, 700.0), e(2, 800.0), e(3, 900.0)],
+ ]);
+ let t = m.transpose();
+ assert_eq!(
+ t.rows,
+ vec![
+ vec![e(0, 100.0), e(3, 600.0)],
+ vec![e(0, 200.0), e(1, 400.0), e(3, 700.0)],
+ vec![e(0, 300.0), e(3, 800.0)],
+ vec![e(1, 500.0), e(3, 900.0)],
+ ]
+ );
+ assert_eq!(t.transpose(), m);
+ }
+}
diff --git a/src/algorithm/mod.rs b/src/algorithm/mod.rs
new file mode 100644
index 0000000..df36fd3
--- /dev/null
+++ b/src/algorithm/mod.rs
@@ -0,0 +1,128 @@
+// The one EigenTrust implementation behind the public API, the CSV reader, the CLI and
+// the WebAssembly bindings.
+
+mod matrix;
+mod power;
+
+use crate::error::Input;
+use crate::{EigenTrustError, EigenTrustOptions, PreTrust, TrustEdge, TrustScores};
+use matrix::{CsrMatrix, Entry};
+
+pub(crate) fn run(
+ local_trust: impl IntoIterator,
+ pre_trust: impl IntoIterator,
+ options: &EigenTrustOptions,
+) -> Result {
+ options.validate()?;
+
+ let mut peers = 0;
+ let mut rows: Vec> = Vec::new();
+ for (position, edge) in local_trust.into_iter().enumerate() {
+ check_weight(Input::LocalTrust, position, edge.weight)?;
+ let bound = peer_bound(Input::LocalTrust, position, edge.from.max(edge.to))?;
+ peers = peers.max(bound);
+ if edge.from >= rows.len() {
+ rows.resize_with(edge.from + 1, Vec::new);
+ }
+ rows[edge.from].push(Entry::new(edge.to, edge.weight));
+ }
+
+ let mut seeds: Vec = Vec::new();
+ for (position, seed) in pre_trust.into_iter().enumerate() {
+ check_weight(Input::PreTrust, position, seed.weight)?;
+ peers = peers.max(peer_bound(Input::PreTrust, position, seed.peer)?);
+ seeds.push(seed);
+ }
+
+ if peers == 0 {
+ return Err(EigenTrustError::EmptyNetwork);
+ }
+ rows.resize_with(peers, Vec::new);
+
+ let p = normalize_pre_trust(&seeds, peers)?;
+ let c = normalize_local_trust(CsrMatrix::from_rows(rows), &p)?;
+ let dense_p = to_dense(&p, peers);
+
+ let out = power::iterate(
+ &c,
+ &dense_p,
+ options.alpha(),
+ options.epsilon_for(peers),
+ options.max_iterations(),
+ )?;
+ Ok(TrustScores::new(out.scores, out.iterations, out.residual))
+}
+
+fn check_weight(input: Input, position: usize, weight: f64) -> Result<(), EigenTrustError> {
+ if !weight.is_finite() {
+ return Err(EigenTrustError::NonFiniteWeight { input, position });
+ }
+ if weight < 0.0 {
+ return Err(EigenTrustError::NegativeWeight {
+ input,
+ position,
+ weight,
+ });
+ }
+ Ok(())
+}
+
+// Number of peers needed to include `peer`.
+fn peer_bound(input: Input, position: usize, peer: usize) -> Result {
+ peer.checked_add(1)
+ .ok_or(EigenTrustError::InvalidPeer { input, position })
+}
+
+// Sparse distribution over peers, sorted by peer. A repeated peer keeps its last weight;
+// all-zero or empty pre-trust becomes uniform.
+fn normalize_pre_trust(seeds: &[PreTrust], peers: usize) -> Result, EigenTrustError> {
+ let mut last: Vec
-
+
## What is EigenTrust?
-EigenTrust turns local trust ("alice trusts bob") into a global reputation score for every peer. Trust spreads from a few seed peers you already trust, so fake accounts that only vouch for each other get almost nothing.
+EigenTrust turns local trust ("alice trusts bob") into a global reputation score for every peer. Trust spreads from a few seed peers you already trust, so fake accounts that only vouch for each other get almost nothing. With no seeds it behaves like PageRank.
-- **Input:** who trusts whom (`alice,bob,3`), plus seed peers.
-- **Output:** a trust score for every peer. The scores add up to 1.
-- **Algorithm:** power iteration on a sparse trust matrix, as in the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar, Schlosser, Garcia-Molina, 2003). With no seeds it behaves like PageRank.
+It is the algorithm from the [EigenTrust paper](https://nlp.stanford.edu/pubs/eigentrust.pdf) (Kamvar, Schlosser, Garcia-Molina, 2003), used for reputation in peer-to-peer and decentralized networks, Sybil and spam resistance, and ranking accounts or contributors by who vouches for them.
-## Use cases
+**Try it: [eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**, a live playground in 10 languages that can also rank 250,000 peers in the browser.
-- Reputation systems for peer-to-peer and decentralized networks
-- Sybil and spam resistance in social graphs and communities
-- Ranking accounts, contributors or nodes by who vouches for them
-- Web of trust and endorsement graphs
+## Rust library
-## Live demo
+```toml
+[dependencies]
+eigentrust = { git = "https://github.com/hypnagonia/eigentrust-rust" }
+```
+
+```rust
+use eigentrust::{eigentrust, PreTrust, TrustEdge};
+
+fn main() -> Result<(), eigentrust::EigenTrustError> {
+ let local_trust = [
+ TrustEdge::new(0, 1, 2.0), // peer 0 trusts peer 1 with weight 2
+ TrustEdge::new(0, 2, 1.0),
+ TrustEdge::new(1, 2, 1.0),
+ TrustEdge::new(2, 0, 1.0),
+ ];
+ let pre_trust = [PreTrust::new(0, 1.0)]; // peer 0 is the seed
+
+ let result = eigentrust(local_trust, pre_trust)?;
+ for (peer, score) in result.ranking() {
+ println!("{peer}: {score:.4}");
+ }
+ Ok(())
+}
+```
-**[eigentrust.jenyadoesapps.com](https://eigentrust.jenyadoesapps.com)**: build a trust network, move the α slider and watch the ranking update. You can also rank 250,000 peers in the browser.
+- `eigentrust_with_options` takes `EigenTrustOptions` for `alpha` (default 0.5), the convergence threshold and the iteration limit.
+- Results come back as `TrustScores`: one score per peer, summing to 1, plus the iteration count and residual.
+- Errors are a typed `EigenTrustError`.
+- Optional features:
+ - `csv` adds `eigentrust::csv::Network` for named peers from CSV.
+ - `parallel` runs the iteration on all cores with rayon.
+- There are no required dependencies beyond `log`, and the same code builds for `wasm32`.
-Available in English, Español, 中文, हिन्दी, العربية, Português, Français, Deutsch, Русский and 日本語.
+The crate documentation (`cargo doc --open`) covers the exact input rules and convergence behavior. See also [`examples/`](examples).
-## Quick start (CLI)
+## Command line
```sh
-cargo run --release -- ./example/localtrust.csv ./example/pretrust.csv [alpha]
+cargo install --git https://github.com/hypnagonia/eigentrust-rust eigentrust-cli
+eigentrust localtrust.csv pretrust.csv [alpha]
```
-```
+```text
alice,0.6666666865348816
bob,0.3333333134651184
```
-α defaults to `0.5`. A higher α keeps trust closer to the seeds.
+It prints `peer,score` for every peer with a non-zero score, highest first. Errors go to stderr with exit code 1.
-## Use in the browser (WebAssembly)
+## Browser (WebAssembly)
```js
import init, { run } from './pkg/eigentrust.js'
@@ -62,20 +87,22 @@ const result = JSON.parse(run(enc.encode('alice,bob,2\nbob,carol,1\n'), enc.enco
// { Ok: [["alice", ...], ["bob", ...], ["carol", ...]] } or { Err: "..." }
```
-Build with `./build.sh`. For a ready-made Web Worker, see [`demo/worker.js`](demo/worker.js).
+- `./build.sh` builds `pkg/` and a multithreaded `pkg-parallel/` from the [`wasm`](wasm) crate.
+- The multithreaded build needs a cross-origin isolated page; see [`demo/vercel.json`](demo/vercel.json).
+- [`demo/worker.js`](demo/worker.js) runs the engine in a Web Worker and picks the right build.
-## Input format
+## Input rules
-| File | Line | Example |
+| Input | CSV line | Rust type |
| --- | --- | --- |
-| Local trust | `from,to[,weight]` | `alice,bob,2` |
-| Seeds (pre-trust) | `peer[,weight]` | `alice,1` |
+| Local trust | `from,to[,weight]` | `TrustEdge { from, to, weight }` |
+| Pre-trust (seeds) | `peer[,weight]` | `PreTrust { peer, weight }` |
-- Standard CSV: quoted fields (`"Smith, J"`), a header row, spaces, CRLF and a UTF-8 BOM are fine.
-- Weights default to 1 and must be finite and non-negative. Negative trust (distrust) is not supported yet.
-- A repeated `from,to` pair or seed peer: the last line wins.
-- α must be in [0, 1]. At α = 0 some networks oscillate instead of converging; the engine stops after 10,000 iterations with an error.
-- Errors name the file and line, e.g. `local trust CSV, line 5: weight "NaN" must be finite`.
+- **Weights:** finite and non-negative, default 1. Zero means no trust. Negative trust (distrust) is rejected.
+- **Normalization:** each truster's weights are scaled to sum to 1, and so is pre-trust. With no pre-trust, every peer starts equal.
+- **Duplicates:** a repeated edge or seed keeps its last weight.
+- **Convergence:** at α = 0 some networks oscillate instead of converging. The engine stops after 10,000 iterations with an error.
+- **CSV:** standard CSV, so quoted fields (`"Smith, J"`), a header row, spaces, CRLF and a UTF-8 BOM are fine. Errors name the file and line.
## Performance
@@ -84,18 +111,28 @@ Random trust graphs, 10 links per peer, CSV parsing included.
| Peers | Links | CLI | Browser |
| ---: | ---: | ---: | ---: |
| 20,000 | 200,000 | 0.06 s | 58 ms |
-| 100,000 | 1,000,000 | 0.4 s | 0.2 s |
+| 100,000 | 1,000,000 | 0.3 s | 0.2 s |
| 250,000 | 2,500,000 | | 0.6 s |
-The browser build is multithreaded when the page is cross-origin isolated (see [`demo/vercel.json`](demo/vercel.json)).
+Results are reproducible bit for bit, with or without threads.
+
+## Repository layout
+
+| Path | Crate | Role |
+| --- | --- | --- |
+| [`src/`](src) | `eigentrust` | the library: one implementation of the algorithm |
+| [`cli/`](cli) | `eigentrust-cli` | the `eigentrust` command, built on the library's public API |
+| [`wasm/`](wasm) | `eigentrust-wasm` | JavaScript bindings, built on the same API |
+| [`demo/`](demo) | | the web playground |
## Development
```sh
-cargo test --release # tests
-./build.sh # WASM builds, copied into demo/
-python3 -m http.server -d demo # run the playground locally
-git config core.hooksPath .githooks # once per clone
+cargo test --workspace --all-features # tests, including doc tests
+cargo run --example basic # library example
+./build.sh # WASM builds, copied into demo/
+python3 -m http.server -d demo # run the playground locally
+git config core.hooksPath .githooks # once per clone
```
## License
From 3da8c5a47205854881ebcc8fb8f4ebd33c822cbc Mon Sep 17 00:00:00 2001
From: jenya
Date: Wed, 30 Sep 2026 17:43:23 +0300
Subject: [PATCH 3/4] CI: feature matrix, docs, packaging, machete,
informational semver check
---
.github/workflows/ci.yml | 74 +++++++++++++++++++++++++++++++++++-----
1 file changed, 65 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3cf5940..2dd6aa0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,7 +15,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- - run: cargo fmt --check
+ - run: cargo fmt --all --check
clippy:
runs-on: ubuntu-latest
@@ -26,10 +26,12 @@ jobs:
components: clippy
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- - name: Native
- run: cargo clippy --release --all-targets -- -D warnings
+ - name: Native, all features
+ run: cargo clippy --workspace --all-targets --all-features -- -D warnings
+ - name: Native, no features
+ run: cargo clippy -p eigentrust --all-targets -- -D warnings
- name: WebAssembly
- run: cargo clippy --release --target wasm32-unknown-unknown --all-targets -- -D warnings
+ run: cargo clippy -p eigentrust -p eigentrust-wasm --target wasm32-unknown-unknown --all-targets -- -D warnings
test:
runs-on: ubuntu-latest
@@ -37,7 +39,31 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- - run: cargo test --release
+ - name: Every feature combination of the library compiles
+ run: |
+ cargo check -p eigentrust --all-targets
+ cargo check -p eigentrust --all-targets --features csv
+ cargo check -p eigentrust --all-targets --features parallel
+ cargo check -p eigentrust --all-targets --all-features
+ - name: Library without features
+ run: cargo test -p eigentrust
+ - name: Workspace, all features (unit, integration and doc tests)
+ run: cargo test --workspace --all-features
+ - name: Examples
+ run: |
+ cargo run --example basic
+ cargo run --example csv --features csv
+
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@nightly
+ - uses: Swatinem/rust-cache@v2
+ - name: Build docs as docs.rs does
+ run: cargo doc -p eigentrust --all-features --no-deps
+ env:
+ RUSTDOCFLAGS: --cfg docsrs -D warnings
wasm:
runs-on: ubuntu-latest
@@ -59,18 +85,48 @@ jobs:
with:
node-version: 22
- name: Tests in Node
- run: wasm-pack test --node --release
+ run: wasm-pack test --node --release wasm
- name: Single-threaded and multithreaded builds
run: ./build.sh
- audit:
+ package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+ - uses: dtolnay/rust-toolchain@stable
+ - uses: Swatinem/rust-cache@v2
+ - name: Package and build the published crates from their tarballs
+ run: cargo package -p eigentrust -p eigentrust-cli
+ - name: Library package contents
+ run: cargo package -p eigentrust --list
+
+ dependencies:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: taiki-e/install-action@v2
+ with:
+ tool: cargo-audit,cargo-machete
+ - name: Security advisories
+ run: cargo audit
+ - name: Unused dependencies
+ run: cargo machete
+
+ # Reports public API changes that need a version bump, compared with main.
+ # Informational until the first crates.io release; then remove continue-on-error.
+ semver:
+ if: github.event_name == 'pull_request'
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@v2
with:
- tool: cargo-audit
- - run: cargo audit
+ tool: cargo-semver-checks
+ - run: cargo semver-checks check-release -p eigentrust --baseline-rev origin/${{ github.base_ref }}
attribution:
runs-on: ubuntu-latest
From 277744a4ba7e6eda2971e781be1d0ce33a7ff864 Mon Sep 17 00:00:00 2001
From: jenya
Date: Wed, 30 Sep 2026 17:43:23 +0300
Subject: [PATCH 4/4] CLAUDE.md: workspace layout and commands
---
CLAUDE.md | 52 +++++++++++++++++++++++++++++++---------------------
1 file changed, 31 insertions(+), 21 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 81c76fa..9138ae3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,36 +8,46 @@
- This is enforced by `.githooks/commit-msg` and by CI. Enable the hook once per clone:
`git config core.hooksPath .githooks`
- Never bypass it with `--no-verify`.
+- PRs get squash-merged quickly: start follow-up work from a fresh branch off `main`.
## Project
-EigenTrust in Rust, runs natively and as WASM in the browser.
+EigenTrust in Rust. Cargo workspace:
-- `src/basic/engine.rs` - `calculate_from_csv`: CSV in, sorted `(peer, score)` out
-- `src/basic/input.rs` - CSV reading (csv crate), header detection, weight validation
-- `src/basic/eigentrust.rs` - power iteration (`compute`), runs on dense vectors
-- `src/error.rs` - `Error` enum used everywhere; `Display` gives user-facing messages
-- `src/sparse/` - CSR matrix / sparse vector
-- `src/lib.rs` - wasm-bindgen entry `run(localtrust, pretrust, alpha)` (wasm32 only)
-- `src/main.rs` - native CLI, reports errors as `error: ...` with exit code 1
-- `demo/` - static interactive web demo deployed to Vercel
+- `eigentrust` (root, `src/`) - the library. The only implementation of the algorithm.
+ - `lib.rs` - crate docs, the public API (`eigentrust`, `eigentrust_with_options`, re-exports)
+ - `types.rs` (`TrustEdge`, `PreTrust`), `options.rs`, `scores.rs` (`TrustScores`), `error.rs`
+ - `algorithm/` - private: input validation and normalization (`mod.rs`), CSR matrix,
+ power iteration (`power.rs`)
+ - `csv.rs` - `csv` feature: named peers from CSV (`csv::Network`), its own error type
+ - features: `csv`, `parallel` (rayon); default none
+- `eigentrust-cli` (`cli/`) - binary `eigentrust`, uses only the public API
+- `eigentrust-wasm` (`wasm/`) - JS bindings; `convert.rs` is plain Rust, `bindings.rs` wasm32 only;
+ `threads` feature for the multithreaded build; `publish = false`
+- `demo/` - static web playground deployed to Vercel
+
+Keep the public surface small: modules private by default, export through `lib.rs`,
+`#![warn(missing_docs)]` is on.
## Commands
-- Test: `cargo test --release`
-- WASM tests: `wasm-pack test --node --release`
-- Lint: `cargo fmt --check`, `cargo clippy --release --all-targets -- -D warnings`
- (also with `--target wasm32-unknown-unknown`)
-- CLI: `cargo run --release -- ./example/localtrust.csv ./example/pretrust.csv [alpha]`
-- WASM: `./build.sh` (builds `pkg/` and `pkg-parallel/` and copies them into `demo/`)
+- Test: `cargo test --workspace --all-features` (unit, integration, doc tests incl. README)
+- Library alone: `cargo test -p eigentrust`
+- WASM tests: `wasm-pack test --node --release wasm`
+- Lint: `cargo fmt --all --check`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`,
+ and `cargo clippy -p eigentrust -p eigentrust-wasm --target wasm32-unknown-unknown --all-targets -- -D warnings`
+- Docs: `RUSTDOCFLAGS="--cfg docsrs -D warnings" cargo +nightly doc -p eigentrust --all-features --no-deps`
+- Package: `cargo package -p eigentrust -p eigentrust-cli`, check `cargo package -p eigentrust --list`
+- CLI: `cargo run --release -p eigentrust-cli -- ./example/localtrust.csv ./example/pretrust.csv [alpha]`
+- WASM: `./build.sh` (builds `pkg/` and `pkg-parallel/` from `wasm/` and copies them into `demo/`)
- Demo locally: `python3 -m http.server -d demo` then open http://localhost:8000
- Deploy: `vercel deploy --prod` from `demo/`
## Notes
-- Tests compare floats with exact equality. The iteration uses Kahan-Babuska-Neumaier summation
- in row order; keep the summation order if you touch `compute` / `mul_dense`.
-- Duplicate `(i, j)` local trust records and duplicate pre-trust peers: the last one wins.
-- Weights must be finite and non-negative. Distrust (negative weights) is rejected until its
- effect on the scores is defined; `extract_distrust` / `discount_trust_vector` are kept for that.
-- `compute` stops after `DEFAULT_MAX_ITERATIONS` (10,000) with `Error::NotConverged`.
+- Results must stay bit-identical: compensated (KBN) sums in a fixed order in `power.rs`, plain
+ sums for normalization, stable sorts. Tests compare floats exactly.
+- Duplicate edges and duplicate pre-trust peers: the last one wins.
+- Weights must be finite and non-negative; distrust (negative weights) is rejected.
+- The iteration stops after `max_iterations` (default 10,000) with `EigenTrustError::NotConverged`.
+- Version 0.2.0 is not on crates.io yet; the CI semver job is informational until it is.