refactor(cli): rename amy wot -> amy fof (follows-of-follows)

`wot` claimed the whole web-of-trust concept for what is actually a cheap
single-hop metric — the count of your follows who also follow X. The real
computed web of trust is `graperank`. Renamed the command (and WotCommand ->
FofCommand) to `fof`, reframing its KDoc/usage away from "trust," and kept
`wot` as a deprecation alias that warns and points at both `fof` and
`graperank`.

Also documents the command for the first time: `amy wot` had no --help block
and no README row. Added both (help block + three README rows for
get/list/sync), so the follows-of-follows score and its relationship to
graperank are now discoverable. JSON output shape is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013WSzVX9RoUxyV3nT3dfc56
This commit is contained in:
Claude
2026-07-14 22:43:44 +00:00
parent 1a8a4012a6
commit f628783186
3 changed files with 35 additions and 11 deletions
+3
View File
@@ -400,6 +400,9 @@ HTTP endpoint. Reuses quartz's `Nip86Client` and the shared `Nip86Retriever`
| `amy graperank register [PROVIDER] [--service KIND:TAG] [--relay URL]` | Declare a NIP-85 provider in your kind:10040 so clients can discover it (default: self as the `30382:rank` provider). |
| `amy graperank unregister PROVIDER [--service KIND:TAG] [--relay URL]` | The inverse of `register`: remove matching entries (public + private) from your kind:10040 and re-publish it. `--service`/`--relay` narrow the match; without them every entry for that provider key is dropped. |
| `amy graperank providers [USER]` | List a user's declared NIP-85 trusted providers (public + your own private entries). |
| `amy fof get USER` | Follows-of-follows social proof: how many accounts you follow also follow USER. Single-hop, cheap — **not** the computed web of trust (that's `graperank`). Read from the local store; run `fof sync` first to freshen it. |
| `amy fof list [--threshold N] [--limit N]` | Rank accounts by that social-proof score — who's most-followed inside your network (discovery). Defaults: `--threshold 1`, `--limit 50`. |
| `amy fof sync [--timeout SECS]` | Pull your follows' latest kind:3 from the index relays so the next `get`/`list` is current. (`amy wot …` remains as a deprecation alias for all three.) |
#### GrapeRank scores are persisted locally, then published (NIP-85)
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.cli.commands.EncryptCommand
import com.vitorpamplona.amethyst.cli.commands.EventCommand
import com.vitorpamplona.amethyst.cli.commands.FetchCommand
import com.vitorpamplona.amethyst.cli.commands.FilterCommand
import com.vitorpamplona.amethyst.cli.commands.FofCommand
import com.vitorpamplona.amethyst.cli.commands.FollowCommand
import com.vitorpamplona.amethyst.cli.commands.GiftCommands
import com.vitorpamplona.amethyst.cli.commands.GitCommands
@@ -70,7 +71,6 @@ import com.vitorpamplona.amethyst.cli.commands.SubscribeCommand
import com.vitorpamplona.amethyst.cli.commands.SyncCommand
import com.vitorpamplona.amethyst.cli.commands.UseCommand
import com.vitorpamplona.amethyst.cli.commands.VerifyCommand
import com.vitorpamplona.amethyst.cli.commands.WotCommand
import com.vitorpamplona.amethyst.cli.commands.ZapCommand
import com.vitorpamplona.amethyst.cli.commands.cashu.CashuCommands
import com.vitorpamplona.amethyst.cli.commands.cashu.CashuMintCommands
@@ -292,7 +292,14 @@ private suspend fun dispatch(argv: Array<String>): Int {
"podcast" -> PodcastCommands.dispatch(dataDir, tail)
"podcast20" -> Podcast20Commands.dispatch(dataDir, tail)
"bunker" -> BunkerCommand.run(dataDir, tail)
"wot" -> WotCommand.dispatch(dataDir, tail)
"fof" -> FofCommand.dispatch(dataDir, tail)
// `wot` overclaimed the whole web-of-trust concept for a cheap
// single-hop follower count; renamed to `fof` (follows-of-follows).
// Kept as a warning alias — the real WoT engine is `graperank`.
"wot" -> {
System.err.println("[amy] `wot` is deprecated — use `fof` (follows-of-follows). The computed web of trust is `graperank`.")
FofCommand.dispatch(dataDir, tail)
}
else -> {
System.err.println("unknown subcommand: $head")
printUsage()
@@ -645,6 +652,14 @@ private fun printUsage() {
| -> service-key. (default: status)
| graperank probe alias for `relay probe` (the relay census).
|
|Follows-of-follows (social proof — the cheap counterpart to graperank):
| fof get USER USER's score: how many accounts you follow also
| follow them (single-hop social proof, not trust).
| fof list [--threshold N] [--limit N] accounts ranked by that score — who's most
| followed inside your network (default N: 1 / 50).
| fof sync [--timeout SECS] refresh your follows' kind:3 from the index relays
| so the next get/list is current (`wot` = alias).
|
|Zaps (NIP-57):
| zap user USER SATS build a profile zap-request, fetch a BOLT11
| [--comment X] [--anon|--private] invoice from the recipient's LN service
@@ -38,15 +38,21 @@ import kotlinx.coroutines.cancel
import java.util.Collections
/**
* `amy wot <get|list|sync>` — Web-of-Trust score queries.
* `amy fof <get|list|sync>` — follows-of-follows social-proof scores.
*
* The score for a pubkey X is the count of accounts in the active user's
* kind-3 follow set who also follow X. `get` and `list` are read-only —
* they hydrate the score map from whatever kind-3 events already live in
* the local event store. `sync` pulls fresh kind-3 events from the
* kind:3 follow set who also follow X — cheap single-hop social proof, NOT
* the computed web of trust (that's `amy graperank`). `get` and `list` are
* read-only — they hydrate the score map from whatever kind:3 events already
* live in the local event store. `sync` pulls fresh kind:3 events from the
* configured relay pool so the next `get` / `list` is up to date.
*
* - `fof get USER` — X's score (how many of your follows follow X).
* - `fof list` — accounts ranked by score (discovery: who's most
* followed inside your network).
* - `fof sync` — refresh your follows' kind:3 from relays.
*/
object WotCommand {
object FofCommand {
suspend fun dispatch(
dataDir: DataDir,
rest: Array<String>,
@@ -61,13 +67,13 @@ object WotCommand {
}
}
private fun usage(): Int = Output.error("bad_args", "wot <get|list|sync>")
private fun usage(): Int = Output.error("bad_args", "fof <get|list|sync>")
private suspend fun get(
dataDir: DataDir,
rest: Array<String>,
): Int {
if (rest.isEmpty()) return Output.error("bad_args", "wot get <pubkey|npub>")
if (rest.isEmpty()) return Output.error("bad_args", "fof get <pubkey|npub>")
val userArg = rest[0]
Context.open(dataDir).use { ctx ->
ctx.prepare()
@@ -148,8 +154,8 @@ object WotCommand {
// Amy's store lookup is suspending; can't do
// it here. The dispatcher then falls through
// to Phase 1 discovery for every author, which
// matches the old `amy wot sync` behaviour of
// always re-asking. A future optimisation
// matches the `fof sync` behaviour of always
// re-asking. A future optimisation
// could pre-populate a `Map<HexKey,
// AdvertisedRelayListEvent>` before dispatch.
null