Files
bitgate/examples/feed-ranking.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 1abba23df3 docs: worked example of the ranking dimension as a feed multiplier
The `ranking` dimension is the soft one — "still show this, but lower" — and
integrators reinvented turning it into a feed score. Show it instead.

Add examples/feed-ranking.mjs (runnable): it reads decision.ranking
({ effect: "normal" | "downrank", weight }) and turns it into a multiplier
on the app's own feed score (base ** weight for a downrank). Two authors
that led on base score get downranked by trusted spam reports and drop below
a clean lower-base author -- without being hidden, since visibility is a
separate dimension (capped at "restrict" on a feed). The multiplier lives in
the example, not the engine: core ships no such numbers on purpose.

Pinned by tests/feed-ranking.test.js (downrank never hides on a feed; a
high-base downranked author reorders below a clean one; a clean author is
untouched at x1). README headless section points to it.

Addresses the "BitGate feeding a feed ranking" item of the Tessera feedback
(docs/TODO.md) -- the last open BitGate item.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BXQCvJPA6sVSQ5j8uBd9bB
2026-08-11 20:43:35 -04:00

98 lines
3.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The `ranking` dimension as a feed score multiplier.
*
* Moderation is not only hide/allow. The fourth dimension, `ranking`, is the
* soft one: it says "still show this, but lower." A feed reads
* `decision.ranking` — `{ effect: "normal" | "downrank", weight }`, where
* `weight` grows with each downrank signal — and turns it into a multiplier on
* whatever score the feed already computed. Nothing is hidden; the order just
* reflects the governance signal.
*
* The multiplier is an application policy, so it lives here, not in the engine
* (the core ships no such numbers on purpose). Tune `base` to say how hard a
* downrank bites.
*
* node examples/feed-ranking.mjs
*/
import { SOCIAL_POLICY, createAuthorityState, createSnapshot, evaluateTarget } from "@bitgate/core";
const ROOT = "a1".repeat(32);
const NOW = 1_750_000_000;
const trusted = (i) => i.toString(16).padStart(2, "0").repeat(32);
const TRUSTED = Array.from({ length: 16 }, (_, i) => trusted(i + 1));
// A base feed score the app already computed (recency, affinity, whatever).
// Bob and Carol START ABOVE Alice — the point is that the ranking dimension
// reorders them below her without hiding anything.
// author -> { score, reports: [category, trustedReporterCount] }
const authors = {
alice: { pubkey: "d1".repeat(32), score: 0.70, reports: [] },
bob: { pubkey: "d2".repeat(32), score: 0.95, reports: [["spam", 4]] },
carol: { pubkey: "d3".repeat(32), score: 0.90, reports: [["spam", 8]] },
};
const authority = createAuthorityState({ root: ROOT });
/** Build a snapshot carrying `count` trusted reports of `category` against pubkey. */
function snapshotFor(pubkey, reports) {
return createSnapshot({
authority,
trust: { contacts: new Set(TRUSTED) },
reports: new Map(
reports.map(([category, count]) => [
`user:${pubkey}`,
TRUSTED.slice(0, count).map((reporter) => ({ reporter, category, createdAt: NOW - 100 })),
]),
),
});
}
/**
* Application policy: turn a ranking decision into a score multiplier. A
* `normal` ranking is neutral (×1); a `downrank` bites harder the more signals
* back it (`weight`). Nothing here hides anything — visibility is a separate
* dimension.
*/
function rankingMultiplier(ranking, { base = 0.5 } = {}) {
if (!ranking || ranking.effect !== "downrank") return 1;
return base ** Math.max(1, ranking.weight);
}
const ranked = Object.entries(authors)
.map(([name, author]) => {
const decision = evaluateTarget({ type: "user", pubkey: author.pubkey }, snapshotFor(author.pubkey, author.reports), {
surface: "feed",
policyProfile: "feed",
policy: SOCIAL_POLICY,
now: NOW,
});
const multiplier = rankingMultiplier(decision.ranking);
return {
name,
base: author.score,
ranking: decision.ranking.effect,
weight: decision.ranking.weight,
multiplier,
final: author.score * multiplier,
visible: decision.visibility.effect, // still shown — ranking never hides
};
})
.sort((a, b) => b.final - a.final);
console.log("Feed after applying the ranking dimension as a score multiplier:\n");
console.log(" name base ranking weight ×mult final visible");
for (const r of ranked) {
console.log(
` ${r.name.padEnd(6)} ${r.base.toFixed(2)} ${r.ranking.padEnd(9)} ${String(r.weight).padStart(4)} ${r.multiplier
.toFixed(2)
.padStart(5)} ${r.final.toFixed(2)} ${r.visible}`,
);
}
console.log(`
Bob and Carol led on base score, but their trusted spam reports downranked
them (×0.5) and both dropped below clean Alice — nothing was hidden (visibility
is a separate dimension, capped at "restrict" on a feed). weight is 1 here
because each crossed the downrank threshold once; a target downranked by several
independent signals accrues more weight and bites harder. That reordering is the
ranking dimension doing its job.`);