From 3f503f0d0473bed85318aa425884ec4373095620 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 18:51:01 +0000 Subject: [PATCH] refactor(cli): drop graperank --max-attempts, hardcode 3 retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-user outbox retry bound doesn't need to be tunable — replace the --max-attempts flag with a MAX_OUTBOX_ATTEMPTS = 3 constant. Same behaviour, one fewer knob. Updates usage text, README, and the parity doc. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RWk2ZMrGBSr4WenKgwqmbB --- .../2026-07-06-graperank-brainstorm-parity.md | 6 +++--- .../com/vitorpamplona/amethyst/cli/Main.kt | 11 +++++------ .../amethyst/cli/commands/GrapeRankCommand.kt | 18 ++++++++++-------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/cli/plans/2026-07-06-graperank-brainstorm-parity.md b/cli/plans/2026-07-06-graperank-brainstorm-parity.md index b31e51975f..52f09056d9 100644 --- a/cli/plans/2026-07-06-graperank-brainstorm-parity.md +++ b/cli/plans/2026-07-06-graperank-brainstorm-parity.md @@ -89,7 +89,7 @@ It is **data**, not math: exactly the users our crawl discovers and whose kind 3/10000/1984 we fetch. So the effective scoring input is the same, as long as the crawl actually checks every discovered user's outbox — which it now does exhaustively (no - user cap, retrying an unreachable outbox up to `--max-attempts` times). + user cap, retrying an unreachable outbox a few times). 2. **Fringe users / crawl gaps.** A relay timeout that drops a contact list removes edges and shifts nearby scores. The injector mitigates this with a two-stage model mirroring the app's `pickRelaysToLoadUsers`, plus a @@ -104,7 +104,7 @@ It is **data**, not math: when the outbox is unknown/down. **Indexers are not used for content** — they don't serve those kinds; kind:3/mutes/reports live only on the user's outbox. The crawl loops round by round, retrying any member whose contact list still - didn't arrive (up to `--max-attempts`), until every discovered user's outbox + didn't arrive (a few times), until every discovered user's outbox has been checked. Remaining mitigation lever: a generous `--timeout`. 3. **Convergence precision.** Both stop at delta 0.0001; residual error is < ~0.0001 in influence ⇒ < ~0.01 rank points ⇒ identical integer `rank`. @@ -116,7 +116,7 @@ It is **data**, not math: - **Keep the current DEFAULT params** — they are byte-for-byte the Brainstorm DEFAULT preset. No change needed for parity. - **The crawl is exhaustive by default** (no user cap; every reachable user's - outbox is checked, unreachable outboxes retried up to `--max-attempts`). An + outbox is checked, unreachable outboxes retried a few times). An incomplete crawl is the single biggest source of drift, so avoid capping it. - **Optional, for fuller parity (not required for close scores):** - Add `--preset default|permissive|restrictive`. DEFAULT is confirmed; the diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt index 92f73a8a3e..38bd66bb2d 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Main.kt @@ -529,12 +529,11 @@ private fun printUsage() { |Web of Trust (GrapeRank): | graperank [OBSERVER] compute subjective trust scores (0..1) for every | [--limit N] [--min-score X] user reachable in the follow/mute/report graph. - | [--rigor X] [--attenuation X] Crawls each user's kind:10002 outbox for their - | [--max-attempts N] [--max-rounds N] latest kind:3/10000/1984 until every discovered - | [--offline] [--timeout SECS] user has been checked (no user cap; --max-attempts - | [--publish] [--min-rank N] bounds retries of an unreachable outbox, default 3). - | [--publish-limit N] [--publish-relay URL] OBSERVER: npub|nprofile|hex|name@domain (default: - | active account). --offline scores from the local + | [--rigor X] [--attenuation X] Exhaustively crawls each user's kind:10002 outbox + | [--max-rounds N] for their latest kind:3/10000/1984 until every + | [--offline] [--timeout SECS] discovered user has been checked (no user cap). + | [--publish] [--min-rank N] OBSERVER: npub|nprofile|hex|name@domain (default: + | [--publish-limit N] [--publish-relay URL] active account). --offline scores from the local | store only. --publish writes NIP-85 kind:30382 | cards (rank = round(score*100)) for each user at | or above --min-rank (unchanged ranks skipped). diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt index 78eaebb322..86294079f0 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/GrapeRankCommand.kt @@ -60,8 +60,8 @@ import kotlin.math.roundToInt * their kind:3 / kind:10000 / kind:1984 events are fetched from *their own* * relays. The crawl is exhaustive: it keeps going, with no user cap, until every * discovered user's outbox has been checked and their contact list pulled (an - * unreachable outbox is retried up to `--max-attempts` times), then runs the - * scoring engine in `commons/wot`. + * unreachable outbox is retried a few times), then runs the scoring engine in + * `commons/wot`. * * Prints a ranked list (text, or one JSON object under `--json`). With * `--publish`, results are also published as NIP-85 kind:30382 `ContactCardEvent` @@ -84,6 +84,10 @@ object GrapeRankCommand { // Emit a scoring-progress line every this many worklist visits. private const val SCORE_PROGRESS_STEP = 5_000 + // Times we re-query an unreachable user's outbox before giving up on it, so + // the crawl still terminates on a finite graph. + private const val MAX_OUTBOX_ATTEMPTS = 3 + suspend fun dispatch( dataDir: DataDir, tail: Array, @@ -103,10 +107,8 @@ object GrapeRankCommand { val args = Args(rest) val observerArg = args.positionalOrNull(0) // Crawl to full convergence by default (every reachable user's outbox - // checked). --max-rounds is only a safety backstop; --max-attempts bounds - // how many times we re-try an unreachable user's outbox before giving up. + // checked). --max-rounds is only a safety backstop. val maxRounds = args.intFlag("max-rounds", Int.MAX_VALUE) - val maxAttempts = args.intFlag("max-attempts", 3) val limit = args.intFlag("limit", 100) val minScore = args.flag("min-score")?.toDoubleOrNull() ?: 0.0 val offline = args.bool("offline") @@ -153,8 +155,8 @@ object GrapeRankCommand { // Loop until every discovered user has had their outbox checked and // their kind:3/10000/1984 pulled from it — no user cap. A user whose - // outbox stays unreachable is dropped after --max-attempts tries so - // the crawl still terminates. + // outbox stays unreachable is dropped after MAX_OUTBOX_ATTEMPTS tries + // so the crawl still terminates. while (rounds < maxRounds) { val pending = discovered.filterNot { it in done } if (pending.isEmpty()) break @@ -187,7 +189,7 @@ object GrapeRankCommand { attempts[pk] = tries // Give up once we've exhausted retries: either the user has // no contact list, or their outbox is unreachable. - if (tries >= maxAttempts) done += pk + if (tries >= MAX_OUTBOX_ATTEMPTS) done += pk } } System.err.println(