Something went wrong. Try again.
unified development activity
Something went wrong. Try again.
30 kB · 838 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839//! The global denominator's rule set: which repositories' commits count as//! Tangled activity, from evidence about each one.//!//! Every repository gets an evidence vector. Each signal says something about//! whether its commits are work done on Tangled, and each has a tier://!//! | tier | for | against |//! |--------|---------------------------------------------|---------------------------------|//! | manual | — | excludes file, fork record, description says copy |//! | strong | Tangled-identity authors; history born on Tangled | a large author crowd with no Tangled identity; stopped at the page cap |//! | medium | small team; owner keeps other repos; an original description | many authors before the repo existed |//! | weak | complete walk; plausible size (shown only) | history before the repo existed (shown only) |//!//! The verdict, in order: a manual signal excludes. A strong negative//! excludes, or holds the repo for review when a strong positive contradicts//! it. A page-cap stop holds. Otherwise the repo counts with one strong//! positive, two medium ones, or one medium and no medium negative, and is//! excluded for want of evidence otherwise. A repo with nothing counted is//! `empty`. Separately, many authors before the repo existed (an imported//! community history) takes away the repo's grace period, so only its//! commits from its Tangled creation date count; and at the merge a repo//! over [`RuleParams::hold_share_pct`] of any window is held unless reviewed.//!//! Everything is recomputed from the rules on every run; nothing sticks. The//! parameters, the excludes file and the reviewed list hash to one value that//! every summary carries, so a changed rule set is visible and can be refused.
use std::collections::BTreeSet;use std::path::Path;
use anyhow::{Context, Result};use serde::{Deserialize, Serialize};use sha2::{Digest, Sha256};
use crate::config::Config;use crate::db::Db;use crate::described;
/// Bump with any change to a threshold, a signal or the verdict order.pub const RULES_VERSION: &str = "evidence-v1";
/// The rule set's parameters. The evidence thresholds are frozen in code for/// a version; only the older crawl filters come from the environment.#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct RuleParams { /// Tangled's start: nothing earlier counts (TALLY_GLOBAL_IMPORTED_BEFORE). pub imported_before: String, /// Commits up to this many days before a repo's creation count /// (TALLY_GLOBAL_REPO_AGE_GRACE_DAYS), unless the repo loses its grace. pub repo_age_grace_days: i64, pub skip_forks: bool, pub skip_described_copies: bool, /// `foreign_crowd`: at least this many distinct authors ... pub crowd_min_authors: u64, /// ... and under this percentage of commits by Tangled identities. pub crowd_max_tangled_pct: f64, /// `imported_burst`: this many distinct authors before the repo existed. pub import_min_authors: u64, /// `small_team`: at most this many distinct authors. pub small_team_max_authors: u64, /// `born_on_tangled`: under this percentage of commits before creation. pub born_max_pre_creation_pct: f64, /// `plausible_size` (weak): under this many counted commits. pub plausible_max_commits: u64, /// Held for review at the merge: over this percentage of any window ... pub hold_share_pct: f64, /// ... and at least this many changes in it, so a small window or a test /// database doesn't hold everything. pub hold_share_min_changes: u64, /// Count a rewritten commit without a Change-Id once: same author email, /// author time to the second and subject. pub collapse_rebased: bool, /// Commits dated more than this many days after the run don't count. pub future_slack_days: i64,}
impl RuleParams { pub fn v1(imported_before: &str, grace_days: i64, forks: bool, described: bool) -> Self { Self { imported_before: imported_before.to_string(), repo_age_grace_days: grace_days, skip_forks: forks, skip_described_copies: described, crowd_min_authors: 50, crowd_max_tangled_pct: 0.5, import_min_authors: 10, small_team_max_authors: 10, born_max_pre_creation_pct: 50.0, plausible_max_commits: 20_000, hold_share_pct: 5.0, hold_share_min_changes: 1_000, collapse_rebased: true, future_slack_days: 1, } }}
/// The rules one run applies: parameters, the excludes file and the list of/// held repositories the owner has reviewed and released.#[derive(Debug, Clone, PartialEq)]pub struct RuleSet { pub params: RuleParams, pub excludes: BTreeSet<String>, pub reviewed: BTreeSet<String>,}
/// What a summary records about the rules that made it.#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct RulesManifest { pub version: String, /// SHA-256 of the version, parameters, excludes and reviewed list. pub hash: String, pub params: RuleParams, pub excludes: usize, pub reviewed: Vec<String>,}
impl RuleSet { pub fn from_config(config: &Config) -> Result<Self> { Ok(Self { params: RuleParams::v1( &config.global_imported_before, config.global_repo_age_grace_days, config.global_skip_forks, config.global_skip_described_copies, ), excludes: load_did_list(&config.global_excludes)?, reviewed: load_did_list(&config.global_reviewed)?, }) }
#[cfg(test)] pub fn for_tests() -> Self { Self { params: RuleParams::v1("2025-01-01", 30, true, true), excludes: BTreeSet::new(), reviewed: BTreeSet::new(), } }
pub fn hash(&self) -> String { let canonical = serde_json::json!({ "version": RULES_VERSION, "params": self.params, "excludes": self.excludes, "reviewed": self.reviewed, }); hex::encode(Sha256::digest(canonical.to_string().as_bytes())) }
pub fn manifest(&self) -> RulesManifest { RulesManifest { version: RULES_VERSION.to_string(), hash: self.hash(), params: self.params.clone(), excludes: self.excludes.len(), reviewed: self.reviewed.iter().cloned().collect(), } }
/// The SQLite date modifier for a repo's grace period. pub fn grace_modifier(&self) -> String { format!("-{} days", self.params.repo_age_grace_days) }
/// The latest author time that counts, `now` plus the slack. pub fn future_cap(&self, now: chrono::DateTime<chrono::Utc>) -> String { (now + chrono::Duration::days(self.params.future_slack_days)) .to_rfc3339_opts(chrono::SecondsFormat::Secs, true) }}
/// The windows a share is shown for, ending on `end`: a label and the first/// day, the way the site counts them (`end` and the days before it, so 30d/// is 30 dates), and all time from `imported_before`.pub fn windows(end: chrono::NaiveDate, imported_before: &str) -> [(&'static str, String); 4] { let back = |days: i64| (end - chrono::Duration::days(days - 1)).to_string(); [ ("30d", back(30)), ("90d", back(90)), ("1y", back(365)), ("all", imported_before.to_string()), ]}
/// What the merge needs to decide holds: the share threshold and the/// owner's reviewed list.#[derive(Debug, Clone)]pub struct HoldPolicy { pub share_pct: f64, pub min_changes: u64, pub imported_before: String, pub reviewed: BTreeSet<String>,}
impl RuleSet { pub fn hold_policy(&self) -> HoldPolicy { HoldPolicy { share_pct: self.params.hold_share_pct, min_changes: self.params.hold_share_min_changes, imported_before: self.params.imported_before.clone(), reviewed: self.reviewed.clone(), } }}
/// A repository the published denominator leaves out until the owner/// reviews it, with what it would add.#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct HeldRepo { /// The repository DID. #[serde(alias = "repo_did")] pub repo: String, /// Changes it would add to the published (default-branch) count, all /// time. pub changes: u64, /// `page_cap`, `conflict` or `share`. pub reason: String, pub name: String, pub url: String, /// Its largest share of a window, for a `share` hold. #[serde(default, skip_serializing_if = "Option::is_none")] pub share_pct: Option<f64>, /// Changes it would add to each window, default branch and every branch. pub changes_by_window: std::collections::BTreeMap<String, u64>, pub changes_all_branches: std::collections::BTreeMap<String, u64>, pub signals: Vec<String>,}
/// One repository's evidence and verdict, for a summary.#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]pub struct RepoVerdict { pub repo_did: String, pub name: String, pub url: String, pub verdict: String, pub reason: String, pub commits: u64, pub authors: u64, pub tangled_commits: u64, pub pre_creation: u64, pub pre_creation_authors: u64, pub history_before: u64, pub signals: Vec<String>,}
/// One DID per line; `#` comments and a `tangled:` prefix allowed. A missing/// file is an empty list.pub fn load_did_list(path: &Path) -> Result<BTreeSet<String>> { let text = match std::fs::read_to_string(path) { Ok(text) => text, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(BTreeSet::new()), Err(err) => return Err(err).with_context(|| format!("reading {}", path.display())), }; Ok(text .lines() .map(str::trim) .filter(|line| !line.is_empty() && !line.starts_with('#')) .map(|line| line.strip_prefix("tangled:").unwrap_or(line).to_string()) .collect())}
/// What the crawl knows about one repository: its record, and its own/// commits from the grace floor on (every branch, nothing past the cap).#[derive(Debug, Clone, Default, PartialEq)]pub struct RepoFacts { pub repo_did: String, pub owner_did: String, pub fork: bool, pub description: Option<String>, /// The last walk stopped at the page cap. pub truncated: bool, /// Non-fork repositories with the same owner, this one included. pub owner_repos: u64, pub commits: u64, pub authors: u64, /// Commits by a Tangled identity: a DID author email, or an author the /// crawl resolved to one of the owner's identities or agents. pub tangled_commits: u64, /// Commits authored before the repo's creation day (UTC), and by how many /// distinct authors. pub pre_creation: u64, pub pre_creation_authors: u64, /// Commits seen from before the floor: stored ones, or the most a walk /// has passed over. pub history_before: u64,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]#[serde(rename_all = "snake_case")]pub enum Signal { ExcludesFile, ForkRecord, DescribedCopy, TangledIdentity, BornOnTangled, ForeignCrowd, PageCap, SmallTeam, OwnerActive, DescribedOriginal, ImportedBurst, CompleteWalk, PlausibleSize, PreTangledHistory,}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]pub enum Tier { Manual, Strong, Medium, Weak,}
impl Signal { pub fn tier(self) -> Tier { match self { Signal::ExcludesFile | Signal::ForkRecord | Signal::DescribedCopy => Tier::Manual, Signal::TangledIdentity | Signal::BornOnTangled | Signal::ForeignCrowd | Signal::PageCap => Tier::Strong, Signal::SmallTeam | Signal::OwnerActive | Signal::DescribedOriginal | Signal::ImportedBurst => Tier::Medium, Signal::CompleteWalk | Signal::PlausibleSize | Signal::PreTangledHistory => Tier::Weak, } }
/// Evidence that the repo's commits are Tangled work. pub fn positive(self) -> bool { matches!( self, Signal::TangledIdentity | Signal::BornOnTangled | Signal::SmallTeam | Signal::OwnerActive | Signal::DescribedOriginal | Signal::CompleteWalk | Signal::PlausibleSize ) }
pub fn as_str(self) -> &'static str { match self { Signal::ExcludesFile => "excludes_file", Signal::ForkRecord => "fork_record", Signal::DescribedCopy => "described_copy", Signal::TangledIdentity => "tangled_identity", Signal::BornOnTangled => "born_on_tangled", Signal::ForeignCrowd => "foreign_crowd", Signal::PageCap => "page_cap", Signal::SmallTeam => "small_team", Signal::OwnerActive => "owner_active", Signal::DescribedOriginal => "described_original", Signal::ImportedBurst => "imported_burst", Signal::CompleteWalk => "complete_walk", Signal::PlausibleSize => "plausible_size", Signal::PreTangledHistory => "pre_tangled_history", } }}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]#[serde(rename_all = "snake_case")]pub enum Verdict { Include, Exclude, Hold, Empty,}
impl Verdict { pub fn as_str(self) -> &'static str { match self { Verdict::Include => "include", Verdict::Exclude => "exclude", Verdict::Hold => "hold", Verdict::Empty => "empty", } }}
#[derive(Debug, Clone, PartialEq)]pub struct Evidence { pub signals: Vec<Signal>, pub verdict: Verdict, /// The rule that decided: a signal's name, or `strong_positive`, /// `medium_positive`, `insufficient_evidence`, `conflict`, `reviewed`, /// `no_counted_commits`. pub reason: &'static str, /// Only commits from the creation day on count. pub no_grace: bool,}
impl Evidence { pub fn has(&self, signal: Signal) -> bool { self.signals.contains(&signal) }}
/// The evidence about one repository and the verdict it leads to.pub fn evaluate(facts: &RepoFacts, rules: &RuleSet) -> Evidence { let p = &rules.params; let pct = |part: u64, whole: u64| part as f64 * 100.0 / whole.max(1) as f64; let mut signals = Vec::new(); let mut add = |on: bool, signal: Signal| { if on { signals.push(signal); } }; add( rules.excludes.contains(&facts.repo_did), Signal::ExcludesFile, ); add(p.skip_forks && facts.fork, Signal::ForkRecord); add( p.skip_described_copies && facts .description .as_deref() .and_then(described::copy_reason) .is_some(), Signal::DescribedCopy, ); add(facts.tangled_commits > 0, Signal::TangledIdentity); add( facts.commits > 0 && facts.history_before == 0 && !facts.truncated && pct(facts.pre_creation, facts.commits) < p.born_max_pre_creation_pct, Signal::BornOnTangled, ); add( facts.authors >= p.crowd_min_authors && pct(facts.tangled_commits, facts.commits) < p.crowd_max_tangled_pct, Signal::ForeignCrowd, ); add(facts.truncated, Signal::PageCap); add( facts.authors > 0 && facts.authors <= p.small_team_max_authors, Signal::SmallTeam, ); add(facts.owner_repos >= 2, Signal::OwnerActive); add( facts .description .as_deref() .is_some_and(|text| !text.trim().is_empty() && described::copy_reason(text).is_none()), Signal::DescribedOriginal, ); add( facts.pre_creation_authors >= p.import_min_authors, Signal::ImportedBurst, ); add(!facts.truncated, Signal::CompleteWalk); add( facts.commits < p.plausible_max_commits, Signal::PlausibleSize, ); add(facts.history_before > 0, Signal::PreTangledHistory);
let count = |tier: Tier, positive: bool| { signals .iter() .filter(|signal| signal.tier() == tier && signal.positive() == positive) .count() }; let strong_for = count(Tier::Strong, true); let medium_for = count(Tier::Medium, true); let medium_against = count(Tier::Medium, false); let manual = signals.iter().find(|signal| signal.tier() == Tier::Manual); let (verdict, reason) = if let Some(signal) = manual { (Verdict::Exclude, signal.as_str()) } else if facts.commits == 0 { (Verdict::Empty, "no_counted_commits") } else if signals.contains(&Signal::ForeignCrowd) { if strong_for > 0 { (Verdict::Hold, "conflict") } else { (Verdict::Exclude, Signal::ForeignCrowd.as_str()) } } else if facts.truncated { (Verdict::Hold, Signal::PageCap.as_str()) } else if strong_for > 0 { (Verdict::Include, "strong_positive") } else if medium_for >= 2 || (medium_for == 1 && medium_against == 0) { (Verdict::Include, "medium_positive") } else { (Verdict::Exclude, "insufficient_evidence") }; // The owner's review releases a hold; it never overrides a manual rule. let (verdict, reason) = if verdict == Verdict::Hold && rules.reviewed.contains(&facts.repo_did) { (Verdict::Include, "reviewed") } else { (verdict, reason) }; let no_grace = signals.contains(&Signal::ImportedBurst); Evidence { signals, verdict, reason, no_grace, }}
/// How many repositories and counted commits ended in each verdict.#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]pub struct Tally { pub repos: u64, pub commits: u64,}
#[derive(Debug, Default)]pub struct Applied { pub hash: String, /// `verdict:reason` → tally. pub verdicts: std::collections::BTreeMap<String, Tally>,}
/// Recompute every repository's evidence and verdict from `rules` and store/// them: the exclusions (repos a manual rule removes; the crawl skips them)/// are replaced, not added to, so removing a rule or an exclude brings a/// repo back. `cap` is the latest author time that counts.pub fn apply(db: &Db, rules: &RuleSet, cap: &str) -> Result<Applied> { let facts = db.repo_facts(&rules.params.imported_before, &rules.grace_modifier(), cap)?; let hash = rules.hash(); let mut applied = Applied { hash: hash.clone(), ..Default::default() }; let decisions: Vec<(RepoFacts, Evidence)> = facts .into_iter() .map(|facts| { let evidence = evaluate(&facts, rules); (facts, evidence) }) .collect(); for (facts, evidence) in &decisions { let tally = applied .verdicts .entry(format!("{}:{}", evidence.verdict.as_str(), evidence.reason)) .or_default(); tally.repos += 1; tally.commits += facts.commits; } db.store_evidence(&decisions, &hash)?; Ok(applied)}
#[cfg(test)]mod tests { use super::*;
fn rules() -> RuleSet { RuleSet::for_tests() }
/// Facts measured on the 22:05Z global database (2026-10-25), counted /// commits from each repo's grace floor on. fn nixpkgs_mirror() -> RepoFacts { // did:plc:ruj3bdjdfuathhxwapp45nxw, knot.pyrox.dev, description "lol". RepoFacts { repo_did: "did:plc:ruj3bdjdfuathhxwapp45nxw".into(), owner_did: "did:plc:5cqzysioqzttihsnbsaxrggu".into(), description: Some("lol".into()), truncated: true, owner_repos: 30, commits: 39_866, authors: 1_640, tangled_commits: 0, pre_creation: 34, pre_creation_authors: 18, ..Default::default() } }
fn tangled_core() -> RepoFacts { // did:plc:…/core (tangled.org/core): 142 authors, 264 DID commits. RepoFacts { repo_did: "did:plc:core".into(), description: Some("tightly-knit social coding".into()), owner_repos: 12, commits: 11_484, authors: 142, tangled_commits: 264, pre_creation: 413, pre_creation_authors: 9, history_before: 0, ..Default::default() } }
fn owners_repo() -> RepoFacts { // permadeath.com/didbot: every commit by a DID author. RepoFacts { repo_did: "did:plc:didbot".into(), owner_did: "did:plc:nlzmjyfv6loqtxyzvdcznwgf".into(), owner_repos: 9, commits: 1_872, authors: 1, tangled_commits: 1_872, ..Default::default() } }
#[test] fn the_nixpkgs_mirror_is_excluded_by_its_authors_not_its_name() { let evidence = evaluate(&nixpkgs_mirror(), &rules()); assert_eq!(evidence.verdict, Verdict::Exclude); assert_eq!(evidence.reason, "foreign_crowd"); assert!(evidence.has(Signal::PageCap)); // A walk that finished is still excluded: the page cap isn't why. It // would have passed over upstream history from before the floor. let whole = RepoFacts { truncated: false, history_before: 1_000, ..nixpkgs_mirror() }; assert_eq!(evaluate(&whole, &rules()).reason, "foreign_crowd"); }
#[test] fn a_crowd_with_tangled_identities_is_counted() { let evidence = evaluate(&tangled_core(), &rules()); assert_eq!(evidence.verdict, Verdict::Include); assert!(evidence.has(Signal::TangledIdentity) && evidence.has(Signal::BornOnTangled)); assert!(!evidence.has(Signal::ForeignCrowd)); assert!(!evidence.no_grace); }
#[test] fn the_owners_repos_are_counted() { let evidence = evaluate(&owners_repo(), &rules()); assert_eq!( (evidence.verdict, evidence.reason), (Verdict::Include, "strong_positive") ); // vibescrobble.com: its first commit predates its record by six // days, the case the blog post's native rule dropped. let early = RepoFacts { commits: 937, pre_creation: 120, ..owners_repo() }; assert_eq!(evaluate(&early, &rules()).verdict, Verdict::Include); }
#[test] fn an_imported_upstream_loses_its_grace_or_its_place() { // codex: 1,378 of 1,393 commits from 162 authors before creation. let codex = RepoFacts { repo_did: "did:plc:codex".into(), owner_repos: 4, commits: 1_393, authors: 163, pre_creation: 1_378, pre_creation_authors: 162, ..Default::default() }; let evidence = evaluate(&codex, &rules()); assert_eq!(evidence.verdict, Verdict::Exclude); assert!(evidence.no_grace); // ghostty-like: 28 authors, imported, owner keeps other repos. let ghostty = RepoFacts { authors: 28, pre_creation_authors: 27, commits: 211, pre_creation: 209, history_before: 16_141, ..codex }; let evidence = evaluate(&ghostty, &rules()); assert_eq!( (evidence.verdict, evidence.reason), (Verdict::Exclude, "insufficient_evidence") ); }
#[test] fn a_moved_personal_repo_counts() { // dotfiles moved to Tangled: history from elsewhere, one author. let dotfiles = RepoFacts { repo_did: "did:plc:dotfiles".into(), owner_repos: 1, commits: 338, authors: 2, pre_creation: 48, pre_creation_authors: 1, history_before: 828, ..Default::default() }; let evidence = evaluate(&dotfiles, &rules()); assert_eq!( (evidence.verdict, evidence.reason), (Verdict::Include, "medium_positive") ); assert!(evidence.has(Signal::PreTangledHistory)); }
#[test] fn conflicts_and_page_caps_are_held_until_reviewed() { // A mirror the owner also commits to through Tangled. let mixed = RepoFacts { authors: 60, commits: 10_000, tangled_commits: 8, ..nixpkgs_mirror() }; let evidence = evaluate(&mixed, &rules()); assert_eq!( (evidence.verdict, evidence.reason), (Verdict::Hold, "conflict") ); // An agent repo that stopped at the cap. let capped = RepoFacts { truncated: true, ..owners_repo() }; assert_eq!(evaluate(&capped, &rules()).reason, "page_cap"); let mut reviewed = rules(); reviewed.reviewed.insert(capped.repo_did.clone()); let evidence = evaluate(&capped, &reviewed); assert_eq!( (evidence.verdict, evidence.reason), (Verdict::Include, "reviewed") ); }
#[test] fn manual_rules_come_first_and_follow_the_switches() { let fork = RepoFacts { fork: true, ..owners_repo() }; assert_eq!(evaluate(&fork, &rules()).reason, "fork_record"); let mut off = rules(); off.params.skip_forks = false; assert_eq!(evaluate(&fork, &off).verdict, Verdict::Include); let mirror = RepoFacts { description: Some("[READ-ONLY] Mirror of https://github.com/x/y".into()), ..owners_repo() }; assert_eq!(evaluate(&mirror, &rules()).reason, "described_copy"); let mut listed = rules(); listed.excludes.insert(owners_repo().repo_did); listed.reviewed.insert(owners_repo().repo_did); assert_eq!(evaluate(&owners_repo(), &listed).reason, "excludes_file"); let empty = RepoFacts { commits: 0, authors: 0, tangled_commits: 0, ..owners_repo() }; assert_eq!(evaluate(&empty, &rules()).verdict, Verdict::Empty); }
#[test] fn the_hash_follows_every_input() { let base = rules(); let mut seen = std::collections::HashSet::new(); assert!(seen.insert(base.hash())); let mut changed = base.clone(); changed.params.crowd_min_authors = 49; assert!(seen.insert(changed.hash())); let mut changed = base.clone(); changed.excludes.insert("did:plc:x".into()); assert!(seen.insert(changed.hash())); let mut changed = base.clone(); changed.reviewed.insert("did:plc:x".into()); assert!(seen.insert(changed.hash())); assert_eq!(base.hash(), rules().hash()); assert_eq!(base.manifest().version, RULES_VERSION); }
#[test] fn exclusions_follow_the_rules_of_each_run() { use crate::model::Repo; let db = Db::in_memory().unwrap(); for (did, fork) in [("did:plc:listed", false), ("did:plc:fork", true)] { db.upsert_repo(&Repo { repo_did: did.into(), owner_did: "did:plc:someone".into(), name: did.into(), knot: "knot.test".into(), url: String::new(), website: None, fork, created_at: None, active: true, last_scanned: None, last_error: None, }) .unwrap(); } let active = |db: &Db| { let mut dids: Vec<String> = db .list_active_repos() .unwrap() .into_iter() .map(|repo| repo.repo_did) .collect(); dids.sort(); dids }; let mut listed = rules(); listed.excludes.insert("did:plc:listed".into()); let applied = apply(&db, &listed, "2026-10-27T00:00:00Z").unwrap(); assert!(active(&db).is_empty()); assert_eq!(applied.verdicts["exclude:excludes_file"].repos, 1); assert_eq!(applied.verdicts["exclude:fork_record"].repos, 1); // Take the DID out of the list and stop skipping forks: both return. let mut none = rules(); none.params.skip_forks = false; apply(&db, &none, "2026-10-27T00:00:00Z").unwrap(); assert_eq!(active(&db), ["did:plc:fork", "did:plc:listed"]); }
#[test] fn did_lists_accept_comments_and_the_tangled_prefix() { let mut file = tempfile::NamedTempFile::new().unwrap(); use std::io::Write; writeln!(file, "# keep\ndid:plc:one\ntangled:did:plc:two\n").unwrap(); let values = load_did_list(file.path()).unwrap(); assert_eq!( values.into_iter().collect::<Vec<_>>(), ["did:plc:one", "did:plc:two"] ); assert!( load_did_list(Path::new("/nonexistent/list.txt")) .unwrap() .is_empty() ); }}