Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/tests/money/judge.rs

//! **The one judge of money words**, Gunnar's verbatim (`gunnar/crates/gunnar-server/tests/
//! open_build_speaks_no_money.rs`), shared by Holger's two guards: the server's literals
//! (`server/lib/tests/open_build_speaks_no_money.rs`) and the command line
//! (`server/cli/tests/open_cli_speaks_no_money.rs`). When Gunnar's list changes, this changes
//! in the same breath. Holger adds `unpaid` and a few technical words of its own, marked.
#![allow(dead_code)]

/// Words the open product has no use for. See the module docs.
const MONEY: &[&str] = &[
    "price",
    "prices",
    "priced",
    "pricing",
    "pay",
    "pays",
    "paying",
    "paid",
    "payable",
    "payment",
    "payments",
    "paywall",
    "buy",
    "buys",
    "buying",
    "bought",
    "buyer",
    "buyers",
    "purchase",
    "purchases",
    "purchased",
    "purchasing",
    "sell",
    "sells",
    "selling",
    "sold",
    "seller",
    "sellers",
    "bill",
    "bills",
    "billed",
    "billing",
    "billable",
    "invoice",
    "invoices",
    "invoicing",
    "checkout",
    "checkouts",
    "money",
    "currency",
    "currencies",
    "sek",
    "eur",
    "usd",
    "stripe",
    "plan",
    "plans",
    "quota",
    "quotas",
    "subscription",
    "subscriptions",
    "subscribe",
    "renew",
    "renews",
    "renewal",
    "renewals",
    "refund",
    "refunds",
    "revenue",
    // Holger's own (its first guard's list, 2026-09-15).
    "unpaid",
];

/// Words that are money only when nothing in the same text makes them
/// technical. See the module docs for the rule.
const CONTEXTUAL: &[&str] = &[
    "plan",
    "plans",
    "checkout",
    "checkouts",
    "renew",
    "renews",
    "renewal",
    "renewals",
    "subscription",
    "subscriptions",
    "subscribe",
];

/// What makes a [`CONTEXTUAL`] word technical: networking and DHCP, git's
/// worktree, the event bus (a `Watch` subscription, a replication or webhook
/// subscriber), the data-set grow and the console reset.
const TECHNICAL_CONTEXT: &[&str] = &[
    // network / DHCP
    "interface",
    "interfaces",
    "lease",
    "leases",
    "dhcp",
    "dhcprequest",
    "address",
    "addresses",
    "route",
    "routes",
    "network",
    "net",
    // the event bus
    "event",
    "events",
    "bus",
    "watch",
    "watchrequest",
    "replication",
    "webhook",
    "runner",
    "subscriber",
    "stream",
    // git
    "git",
    "tree",
    "worktree",
    "commit",
    "branch",
    "clone",
    "entry",
    "build",
    // grow / install / reset
    "grow",
    "growth",
    "sectors",
    "blocks",
    "resize",
    "layout",
    "install",
    "reset",
    "passkeys",
    // Holger's (A28 M13): a RETENTION plan, a MIGRATION plan and its dry run, the download
    // ADMISSION plan, a CERTIFICATE renewal, a box's DOOR.
    "retention",
    "migration",
    "dry",
    "admission",
    "admit",
    "certificate",
    "cert",
    "door",
];

/// `"a lapsed tenant renews"` -> `["a", "lapsed", "tenant", "renews"]`.
pub fn words(text: &str) -> Vec<String> {
    text.split(|c: char| !c.is_ascii_alphanumeric())
        .filter(|w| !w.is_empty())
        .map(|w| w.to_ascii_lowercase())
        .collect()
}

/// **The one judge**: the money words in `text`, after the contextual rule.
pub fn leaks_in(text: &str) -> Vec<String> {
    let ws = words(text);
    let technical = ws.iter().any(|w| TECHNICAL_CONTEXT.contains(&w.as_str()));
    let mut out = Vec::new();
    for w in &ws {
        if !MONEY.contains(&w.as_str()) {
            continue;
        }
        if CONTEXTUAL.contains(&w.as_str()) && technical {
            continue;
        }
        if w == "stripe"
            && !(text.contains("Stripe") || text.contains("STRIPE") || text.contains("stripe.com"))
        {
            continue;
        }
        out.push(w.clone());
    }
    out.sort();
    out.dedup();
    out
}