feat(trust): confidence gate on the local ladder
A lone friend-of-a-friend no longer reads as trust. useViewerGraph now counts distinct follows per friend-of-a-friend (fofCounts), and deriveLocalScore returns verified only at FOF_MIN_RATERS (2) or more. Provider assertions are unaffected; the full GrapeRank confidence model stays provider-side. Docs updated.
This commit is contained in:
parent
18a9e28f0b
commit
db7f4a2707
3 changed files with 36 additions and 12 deletions
|
|
@ -2565,14 +2565,19 @@ kind-10040 provider designation** (`useSetTrustProvider`: custom npub/hex +
|
||||||
`wss://` relay, or clear to fall back to the community provider). The local
|
`wss://` relay, or clear to fall back to the community provider). The local
|
||||||
(non-provider) ladder is computed **once per viewer** by `useViewerGraph` and
|
(non-provider) ladder is computed **once per viewer** by `useViewerGraph` and
|
||||||
shared, so `TrustBadge` no longer fires a per-card `useTrustScore` graph query.
|
shared, so `TrustBadge` no longer fires a per-card `useTrustScore` graph query.
|
||||||
|
The local ladder has a **confidence gate**: `useViewerGraph` counts distinct
|
||||||
|
follows per friend-of-a-friend and `deriveLocalScore` only verifies at
|
||||||
|
`FOF_MIN_RATERS` (2) or more, so a lone endorsement is not trust; provider
|
||||||
|
assertions are unaffected.
|
||||||
**Mute landed:** `useMuteList` + `useMuteUser` (NIP-51 kind-10000, optimistic via
|
**Mute landed:** `useMuteList` + `useMuteUser` (NIP-51 kind-10000, optimistic via
|
||||||
`lib/localMutes.ts`) feed `useAdminModeration`'s new `mutedPubkeys`, so the
|
`lib/localMutes.ts`) feed `useAdminModeration`'s new `mutedPubkeys`, so the
|
||||||
shared `filterAdminHidden` drops muted authors across every list; the flagged
|
shared `filterAdminHidden` drops muted authors across every list; the flagged
|
||||||
notice has a Mute action. **Alerts landed:** `useNetworkAlerts` + `/alerts`
|
notice has a Mute action. **Alerts landed:** `useNetworkAlerts` + `/alerts`
|
||||||
(`pages/NetworkAlertsPage.tsx`) list accounts you follow that trusted people
|
(`pages/NetworkAlertsPage.tsx`) list accounts you follow that trusted people
|
||||||
reported/muted, with Ignore/Unfollow/Mute/Report; Settings → Trust links to it
|
reported/muted, with Ignore/Unfollow/Mute/Report; Settings → Trust links to it
|
||||||
with a count (ignored set in `skatespots:trust:alerts-ignored`). Still open: the
|
with a count (ignored set in `skatespots:trust:alerts-ignored`). Still open:
|
||||||
confidence-gated scoring model. Check
|
running our own GrapeRank-style scorer in `cvm-server` — the full confidence
|
||||||
|
model is provider-side today. Check
|
||||||
`TRUST_SCORE.md` before touching `useTrustScore` / `useRelatrScore` /
|
`TRUST_SCORE.md` before touching `useTrustScore` / `useRelatrScore` /
|
||||||
`trustFilter`.
|
`trustFilter`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,16 @@ flagged notice's reporter breakdown, and Settings → Trust.
|
||||||
`useMuteUser`, `ReportDialog`) — inform, never auto-act.
|
`useMuteUser`, `ReportDialog`) — inform, never auto-act.
|
||||||
- Settings → Trust links to it with a live count.
|
- Settings → Trust links to it with a live count.
|
||||||
|
|
||||||
Still open: the confidence-gated scoring model.
|
## Sixth pass — local confidence gate (landed)
|
||||||
|
|
||||||
|
- The fallback local ladder now gates a friend-of-a-friend behind a confidence
|
||||||
|
threshold: `useViewerGraph` counts distinct follows per friend-of-a-friend, and
|
||||||
|
`deriveLocalScore` returns `verified` only at `FOF_MIN_RATERS` (2) or more. A
|
||||||
|
lone endorsement no longer reads as trust.
|
||||||
|
- Provider assertions are unaffected: the full GrapeRank confidence model lives
|
||||||
|
in the provider and we consume it via NIP-85. Running our own scorer (planned
|
||||||
|
for `cvm-server`, publishing kind-30382) remains the open item — it is the only
|
||||||
|
way to get the real confidence gate and a server-published score.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,14 @@ const HOUSE_GRAPH_PUBKEY = '20e7c953439e66f3882ab96fe0faa8ba246d6fb12c03b0cb4e2d
|
||||||
|
|
||||||
const MAX_FOLLOWS = 100;
|
const MAX_FOLLOWS = 100;
|
||||||
|
|
||||||
|
/** Distinct follows that must agree before a friend-of-a-friend counts as a vouch. */
|
||||||
|
const FOF_MIN_RATERS = 2;
|
||||||
|
|
||||||
export interface ViewerGraph {
|
export interface ViewerGraph {
|
||||||
/** Pubkeys the graph owner follows. */
|
/** Pubkeys the graph owner follows. */
|
||||||
following: Set<string>;
|
following: Set<string>;
|
||||||
/** Friends-of-friends: pubkeys followed by someone the owner follows. */
|
/** Friends-of-friends, keyed by how many of the owner's follows follow them. */
|
||||||
fof: Set<string>;
|
fofCounts: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -44,28 +47,33 @@ export function useViewerGraph() {
|
||||||
if (tag[0] === 'p' && tag[1]) following.add(tag[1]);
|
if (tag[0] === 'p' && tag[1]) following.add(tag[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fof = new Set<string>();
|
const fofCounts = new Map<string, number>();
|
||||||
if (following.size > 0) {
|
if (following.size > 0) {
|
||||||
const fofEvents = onlyVerified(await nostr.query(
|
const fofEvents = onlyVerified(await nostr.query(
|
||||||
[{ kinds: [3], authors: [...following].slice(0, MAX_FOLLOWS), limit: MAX_FOLLOWS }],
|
[{ kinds: [3], authors: [...following].slice(0, MAX_FOLLOWS), limit: MAX_FOLLOWS }],
|
||||||
{ signal },
|
{ signal },
|
||||||
));
|
));
|
||||||
for (const event of fofEvents) {
|
for (const event of fofEvents) {
|
||||||
|
// Count DISTINCT raters: guard against a malformed list repeating a p tag.
|
||||||
|
const seen = new Set<string>();
|
||||||
for (const [name, value] of event.tags) {
|
for (const [name, value] of event.tags) {
|
||||||
if (name === 'p' && value) fof.add(value);
|
if (name !== 'p' || !value || seen.has(value)) continue;
|
||||||
|
seen.add(value);
|
||||||
|
fofCounts.set(value, (fofCounts.get(value) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { following, fof };
|
return { following, fofCounts };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The same 100 / 90 / 60 / 0 ladder the trust badges used, minus the NIP-05 tier
|
* The 100 / 90 / 60 / 0 ladder the trust badges used, minus the NIP-05 tier
|
||||||
* (30) which classified as `unknown` anyway. `self` is the real viewer only —
|
* (30) which classified as `unknown` anyway, and with a confidence gate: a
|
||||||
* never the house graph owner.
|
* friend-of-a-friend counts only when at least `FOF_MIN_RATERS` distinct follows
|
||||||
|
* agree. `self` is the real viewer only — never the house graph owner.
|
||||||
*/
|
*/
|
||||||
export function deriveLocalScore(
|
export function deriveLocalScore(
|
||||||
graph: ViewerGraph | undefined,
|
graph: ViewerGraph | undefined,
|
||||||
|
|
@ -76,6 +84,8 @@ export function deriveLocalScore(
|
||||||
if (viewer && pubkey === viewer) return 100;
|
if (viewer && pubkey === viewer) return 100;
|
||||||
if (!graph) return undefined;
|
if (!graph) return undefined;
|
||||||
if (graph.following.has(pubkey)) return 90;
|
if (graph.following.has(pubkey)) return 90;
|
||||||
if (graph.fof.has(pubkey)) return 60;
|
// Confidence gate: a single friend-of-a-friend is not a network vouch. Require
|
||||||
|
// at least FOF_MIN_RATERS distinct follows to agree before calling it verified.
|
||||||
|
if ((graph.fofCounts.get(pubkey) ?? 0) >= FOF_MIN_RATERS) return 60;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue