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
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-08-11 20:43:35 -04:00
1 parent 088e3cdabe
commit 1abba23df3
4 files changed
+173 -2

No files matched your search

+6
View File
@@ -161,6 +161,12 @@ No bundler? `npm run build:standalone` produces `packages/standalone/dist/bitgat
ESM file. Self-host it and `import { evaluateTarget, decodeLabels } from "./bitgate.js"`;
everything in this section works from that single file.
The soft dimension, `ranking`, feeds an ordering rather than a gate:
`decision.ranking` is `{ effect: "normal" | "downrank", weight }`, which an app
turns into a multiplier on its own feed score — a reported author drops in the
feed without being hidden. [`examples/feed-ranking.mjs`](examples/feed-ranking.mjs)
is a runnable walkthrough.
## Extending to a new application
Write an adapter saying what your object is in governance terms, and a policy
+9 -2
View File
@@ -282,5 +282,12 @@ adoption the obvious choice.
whole headless path. Verified dependency-free (no bare imports in the output)
and end-to-end (a smoke test composes a decision from the entry). Documented
in the README Packages table and the headless section; wired into `npm run build`.
- A worked example of BitGate feeding a feed ranking (the `ranking` dimension →
a score multiplier) would have saved us reinventing it in the feed recipe.
- **[DONE 2026-08-11]** A worked example of BitGate feeding a feed ranking.
**Shipped** `examples/feed-ranking.mjs` (runnable): it reads `decision.ranking`
(`{ effect: "normal" | "downrank", weight }`) and turns it into a score
multiplier (`base ** weight` for a downrank), so trusted spam reports push two
high-base authors below a clean lower-base one **without hiding them** —
visibility stays a separate dimension, capped at `restrict` on a feed. The
multiplier lives in the example, not the engine (core ships no such numbers).
Pinned by `tests/feed-ranking.test.js`; the README headless section points to
it.
+97
View File
@@ -0,0 +1,97 @@
/**
* 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.`);
+61
View File
@@ -0,0 +1,61 @@
// Pins examples/feed-ranking.mjs: the `ranking` dimension, applied as a score
// multiplier, reorders a downranked author below a clean one without hiding it.
import { describe, expect, it } from "vitest";
import { SOCIAL_POLICY, createAuthorityState, createSnapshot, evaluateTarget } from "@bitgate/core";
const ROOT = "a1".repeat(32);
const NOW = 1_750_000_000;
const TRUSTED = Array.from({ length: 16 }, (_, i) => (i + 1).toString(16).padStart(2, "0").repeat(32));
const authority = createAuthorityState({ root: ROOT });
const snapshotFor = (pubkey, category, count) =>
createSnapshot({
authority,
trust: { contacts: new Set(TRUSTED) },
reports: count
? new Map([
[
`user:${pubkey}`,
TRUSTED.slice(0, count).map((reporter) => ({ reporter, category, createdAt: NOW - 100 })),
],
])
: new Map(),
});
const rankingOf = (pubkey, category, count) =>
evaluateTarget({ type: "user", pubkey }, snapshotFor(pubkey, category, count), {
surface: "feed",
policyProfile: "feed",
policy: SOCIAL_POLICY,
now: NOW,
});
// The same multiplier the example documents.
const multiplier = (ranking, base = 0.5) =>
ranking?.effect === "downrank" ? base ** Math.max(1, ranking.weight) : 1;
describe("ranking dimension as a feed multiplier", () => {
it("downranks a reported author but never hides them on a feed", () => {
const decision = rankingOf("d2".repeat(32), "spam", 4);
expect(decision.ranking.effect).toBe("downrank");
expect(decision.ranking.weight).toBeGreaterThanOrEqual(1);
// Ranking is the soft dimension: visibility stays out of hide/deny on a feed.
expect(["allow", "warn", "restrict"]).toContain(decision.visibility.effect);
});
it("reorders a high-base downranked author below a clean lower-base one", () => {
const clean = { base: 0.7, ranking: rankingOf("d1".repeat(32), "spam", 0).ranking };
const reported = { base: 0.95, ranking: rankingOf("d2".repeat(32), "spam", 4).ranking };
const cleanFinal = clean.base * multiplier(clean.ranking);
const reportedFinal = reported.base * multiplier(reported.ranking);
expect(reported.base).toBeGreaterThan(clean.base); // led on base score
expect(reportedFinal).toBeLessThan(cleanFinal); // ...but sinks after ranking
});
it("leaves a clean author's score untouched (×1)", () => {
expect(multiplier(rankingOf("d1".repeat(32), "spam", 0).ranking)).toBe(1);
});
});