(null)
@@ -85,10 +97,19 @@ function ConfirmModal({
return (
-
+
{ if (!loading) onCancel() }}
+ aria-hidden="true"
+ />
{title}
{body}
+ {error ? (
+
+ {error}
+
+ ) : null}
0
- ? Math.round((epoch.voteCount / feed.subscriberCount) * 100)
- : 0
+ const participation = approvedParticipationPercent(epoch.voteCount, feed.approvedParticipantCount)
const stats = [
{ label: "Total posts scored", value: feed.scoredPosts.toLocaleString() },
{ label: "Votes this round", value: epoch.voteCount.toLocaleString() },
{ label: "Participation", value: `${participation}%` },
- { label: "Subscribers", value: feed.subscriberCount.toLocaleString() },
+ { label: "Approved participants", value: feed.approvedParticipantCount.toLocaleString() },
]
return (
@@ -178,17 +197,29 @@ function PanelCurrentRound({ status }: { status: AdminStatus }) {
const epoch = status.system.currentEpoch
const feed = status.system.feed
const phase = epochPhase(epoch)
- const [confirm, setConfirm] = useState(null)
+ const [confirm, setConfirm] = useState(null)
- const invalidate = () => {
- void queryClient.invalidateQueries({ queryKey: ["admin", "status"] })
- void queryClient.invalidateQueries({ queryKey: ["admin", "epochs"] })
- void queryClient.invalidateQueries({ queryKey: ["admin", "feed-health"] })
+ const invalidate = async (): Promise => {
+ await Promise.all([
+ queryClient.invalidateQueries({ queryKey: ["admin", "status"] }),
+ queryClient.invalidateQueries({ queryKey: ["admin", "epochs"] }),
+ queryClient.invalidateQueries({ queryKey: ["admin", "feed-health"] }),
+ ])
}
- const openMutation = useMutation({ mutationFn: () => adminApi.transitionEpoch(), onSuccess: () => { invalidate(); setConfirm(null) } })
- const closeMutation = useMutation({ mutationFn: () => adminApi.endVoting(false), onSuccess: () => { invalidate(); setConfirm(null) } })
- const applyMutation = useMutation({ mutationFn: () => adminApi.approveResults(), onSuccess: () => { invalidate(); setConfirm(null) } })
+ const refreshThenClose = () => completeAdminLifecycleRefresh(invalidate, () => setConfirm(null))
+ const openMutation = useMutation({ mutationFn: () => adminApi.startVoting(72, true), onSuccess: refreshThenClose })
+ const closeMutation = useMutation({ mutationFn: () => adminApi.endVoting(false), onSuccess: refreshThenClose })
+ const approveMutation = useMutation({ mutationFn: () => adminApi.approveResults(true), onSuccess: refreshThenClose })
+ const rejectMutation = useMutation({ mutationFn: () => adminApi.rejectResults(), onSuccess: refreshThenClose })
+
+ const prepareConfirmation = (action: NonNullable): void => {
+ if (action === "open") openMutation.reset()
+ if (action === "close") closeMutation.reset()
+ if (action === "approve") approveMutation.reset()
+ if (action === "reject") rejectMutation.reset()
+ setConfirm(action)
+ }
if (!epoch) {
return (
@@ -199,13 +230,13 @@ function PanelCurrentRound({ status }: { status: AdminStatus }) {
)
}
- const pct = feed.subscriberCount > 0 ? Math.round((epoch.voteCount / feed.subscriberCount) * 100) : 0
+ const pct = approvedParticipationPercent(epoch.voteCount, feed.approvedParticipantCount)
// One row per lifecycle stage (no duplicate `running` row — only one is active).
- const LIFECYCLE: Array<{ phase: EpochPhase; label: string; action?: "open" | "close" | "apply"; actionLabel?: string; danger?: boolean }> = [
+ const LIFECYCLE: Array<{ phase: EpochPhase; label: string; action?: "open" | "close" | "approve"; actionLabel?: string; danger?: boolean }> = [
{ phase: "running", label: "Running", action: "open", actionLabel: "Open voting" },
{ phase: "voting", label: "Voting", action: "close", actionLabel: "Close voting" },
- { phase: "review", label: "Review", action: "apply", actionLabel: "Apply weights", danger: true },
+ { phase: "review", label: "Results review", action: "approve", actionLabel: "Approve policy", danger: true },
]
return (
@@ -216,12 +247,12 @@ function PanelCurrentRound({ status }: { status: AdminStatus }) {
Participation
-
{epoch.voteCount} / {feed.subscriberCount}
+
{epoch.voteCount} / {feed.approvedParticipantCount}
-
{pct}% of members have voted this round.
+
{pct}% of approved pilot participants have voted this round.
{/* Lifecycle actions */}
@@ -236,18 +267,30 @@ function PanelCurrentRound({ status }: { status: AdminStatus }) {
{lc.label}
- {lc.action && isCurrentPhase && (
-
setConfirm(lc.action!)}
- className={`px-4 py-1.5 rounded-full text-xs font-semibold transition-colors ${
- lc.danger
- ? "bg-status-error/10 text-status-error border border-status-error/30 hover:bg-status-error/20"
- : "bg-primary/10 text-primary border border-primary/20 hover:bg-primary/20"
- }`}
- >
- {lc.actionLabel}
-
- )}
+ {lc.action && isCurrentPhase ? (
+
+ {lc.phase === "review" ? (
+ prepareConfirmation("reject")}
+ className="rounded-full border border-border bg-background px-4 py-1.5 text-xs font-semibold text-foreground/65 transition-colors hover:border-foreground/30 hover:text-foreground"
+ >
+ Reject policy
+
+ ) : null}
+ prepareConfirmation(lc.action!)}
+ className={`rounded-full border px-4 py-1.5 text-xs font-semibold transition-colors ${
+ lc.danger
+ ? "border-primary/25 bg-primary text-primary-foreground hover:bg-primary-dark"
+ : "border-primary/20 bg-primary/10 text-primary hover:bg-primary/20"
+ }`}
+ >
+ {lc.actionLabel}
+
+
+ ) : null}
)
})}
@@ -255,10 +298,11 @@ function PanelCurrentRound({ status }: { status: AdminStatus }) {
{confirm === "open" && (
openMutation.mutate()}
onCancel={() => setConfirm(null)}
/>
@@ -266,21 +310,35 @@ function PanelCurrentRound({ status }: { status: AdminStatus }) {
{confirm === "close" && (
closeMutation.mutate()}
onCancel={() => setConfirm(null)}
/>
)}
- {confirm === "apply" && (
+ {confirm === "approve" && (
+ approveMutation.mutate()}
+ onCancel={() => setConfirm(null)}
+ />
+ )}
+ {confirm === "reject" && (
applyMutation.mutate()}
+ loading={rejectMutation.isPending}
+ error={adminMutationError(rejectMutation.error, "Rejecting the policy failed. Please try again.")}
+ onConfirm={() => rejectMutation.mutate()}
onCancel={() => setConfirm(null)}
/>
)}
@@ -1191,7 +1249,7 @@ function AdminConsole({ status }: { status: AdminStatus }) {
{epoch && (
- {epoch.voteCount} / {status.system.feed.subscriberCount} voted
+ {epoch.voteCount} / {status.system.feed.approvedParticipantCount} voted
)}
diff --git a/web-next/app/dashboard/page.tsx b/web-next/app/dashboard/page.tsx
index e14d270a..851375b8 100644
--- a/web-next/app/dashboard/page.tsx
+++ b/web-next/app/dashboard/page.tsx
@@ -195,7 +195,7 @@ export default function DashboardPage() {
) : (
<>
- Five signals decide the feed order. This is the mix the community’s votes have settled on.
+ Five global signals contribute to the ranking score. This is the active, approved production mix.
@@ -209,7 +209,7 @@ export default function DashboardPage() {
Get involved
- The weights are set entirely by community votes. Cast or change yours before the round closes.
+ During the approved pilot, your ballot contributes to a proposed policy. Closed results are reviewed before any production change is applied.
@@ -261,7 +261,7 @@ export default function DashboardPage() {
-
members voted · each vote counts equally
+
ballots submitted · mean below 10, 10% trim at 10+
)}
{participation != null &&
}
diff --git a/web-next/app/demo/__tests__/demo-copy.test.ts b/web-next/app/demo/__tests__/demo-copy.test.ts
index 8cfa3375..c525a8e6 100644
--- a/web-next/app/demo/__tests__/demo-copy.test.ts
+++ b/web-next/app/demo/__tests__/demo-copy.test.ts
@@ -54,7 +54,7 @@ describe("demo copy — honest boundaries", () => {
it("keeps pre-session copy accurate when the corpus falls back", () => {
expect(HERO.subtitle).not.toMatch(/live public posts/i)
expect(STEP_PANELS.community.body).not.toMatch(/live public posts/i)
- expect(DISCLOSURE.posts).toMatch(/snapshot of posts published in corgi's live community governed feed/i)
+ expect(DISCLOSURE.posts).toMatch(/snapshot of live public posts published in corgi commons/i)
expect(DISCLOSURE.posts).toMatch(/mechanics fixture appears only if snapshot loading degrades/i)
})
diff --git a/web-next/app/demo/__tests__/frontend-release-blockers.test.tsx b/web-next/app/demo/__tests__/frontend-release-blockers.test.tsx
index ec9322c7..b2712466 100644
--- a/web-next/app/demo/__tests__/frontend-release-blockers.test.tsx
+++ b/web-next/app/demo/__tests__/frontend-release-blockers.test.tsx
@@ -120,7 +120,7 @@ describe("demo v4 frontend release blockers", () => {
it("keeps the fixture live-proof panel separate and omits the fixture timestamp", () => {
const presentation = getLiveProofPresentation(snapshotProvenance, true)
- expect(presentation.feedName).toBe("Contract-backed feed name")
+ expect(presentation.feedName).toBe("Corgi Commons")
expect(presentation.sourceTimestamp).toBeNull()
expect(presentation.description).toContain("mechanics fixture")
expect(COMMUNITY_GOV_FEED_URL).toBe("https://bsky.app/profile/corgi-network.bsky.social/feed/community-gov")
@@ -131,7 +131,7 @@ describe("demo v4 frontend release blockers", () => {
const presentation = getLiveProofPresentation(snapshotProvenance, false)
expect(presentation.eyebrow).toBe("Snapshot source live on Bluesky")
- expect(presentation.feedName).toBe("Contract-backed feed name")
+ expect(presentation.feedName).toBe("Corgi Commons")
expect(presentation.description).toMatch(/published-feed comparison corpus/i)
expect(presentation.sourceTimestamp).toMatch(/Jul 11, 2026.*UTC/i)
expect(presentation.description).not.toMatch(/reviewed snapshot/i)
diff --git a/web-next/app/demo/__tests__/http-client.test.ts b/web-next/app/demo/__tests__/http-client.test.ts
index ea8471c9..c9d200eb 100644
--- a/web-next/app/demo/__tests__/http-client.test.ts
+++ b/web-next/app/demo/__tests__/http-client.test.ts
@@ -291,7 +291,7 @@ describe("HTTP shadow demo client", () => {
new AbortController().signal,
)
- expect(response.payload.community.name).toBe("Community Governed Feed")
+ expect(response.payload.community.name).toBe("Corgi Commons")
expect(response.payload.community.publicBlueskyFeedUrl).toContain("community-gov")
expect(response.payload.session.capabilities.canOpenNativeBlueskyFeed).toBe(true)
expect(response.payload.session.capabilities.canAdvanceEpoch).toBe(false)
@@ -310,6 +310,70 @@ describe("HTTP shadow demo client", () => {
expect(fetchMock).toHaveBeenCalledTimes(2)
})
+ it("rejects malformed community, provenance, topic, and session identity fields", () => {
+ const invalidPayloads: unknown[] = []
+ const emptyCommunity = structuredClone(sessionPayload("created", "epoch-1", [epoch("epoch-1", 1, BASE_WEIGHTS, 0)])) as {
+ session: { community: Record
}
+ }
+ emptyCommunity.session.community.name = ""
+ invalidPayloads.push(emptyCommunity)
+
+ const nullCommunity = structuredClone(sessionPayload("created", "epoch-1", [epoch("epoch-1", 1, BASE_WEIGHTS, 0)])) as {
+ session: { community: Record }
+ }
+ nullCommunity.session.community.id = null
+ invalidPayloads.push(nullCommunity)
+
+ const missingProvenance = structuredClone(sessionPayload("created", "epoch-1", [epoch("epoch-1", 1, BASE_WEIGHTS, 0)])) as {
+ session: Record
+ }
+ delete missingProvenance.session.corpusProvenance
+ invalidPayloads.push(missingProvenance)
+
+ const staleShapeProvenance = structuredClone(sessionPayload("created", "epoch-1", [epoch("epoch-1", 1, BASE_WEIGHTS, 0)])) as {
+ session: { corpusProvenance: Record }
+ }
+ staleShapeProvenance.session.corpusProvenance.sourceUpdatedAt = "not-a-timestamp"
+ invalidPayloads.push(staleShapeProvenance)
+
+ const duplicateTopics = structuredClone(sessionPayload("created", "epoch-1", [epoch("epoch-1", 1, BASE_WEIGHTS, 0)])) as {
+ session: { topicCatalog: Array> }
+ }
+ duplicateTopics.session.topicCatalog[1].slug = duplicateTopics.session.topicCatalog[0].slug
+ invalidPayloads.push(duplicateTopics)
+
+ const nullEpoch = structuredClone(sessionPayload("created", "epoch-1", [epoch("epoch-1", 1, BASE_WEIGHTS, 0)])) as {
+ session: Record
+ }
+ nullEpoch.session.currentEpochId = null
+ invalidPayloads.push(nullEpoch)
+
+ for (const payload of invalidPayloads) {
+ expect(apiSessionPayloadSchema.safeParse(payload).success).toBe(false)
+ }
+ })
+
+ it("preserves a structured non-2xx conflict message without retrying the mutation", async () => {
+ const fetchMock = vi.fn(async () => new Response(JSON.stringify({
+ error: "StaleEpoch",
+ message: "The shadow epoch changed before this ballot was accepted.",
+ }), { status: 409, headers: { "content-type": "application/json" } }))
+ vi.stubGlobal("fetch", fetchMock)
+
+ await expect(createHttpShadowDemoClient().castVote(
+ SESSION_ID,
+ {
+ idempotencyKey: "stale-vote",
+ baseEpochId: "epoch-1",
+ voterLabel: "Reviewer",
+ weights: BASE_WEIGHTS,
+ topicIntent: PRODUCTION_TOPIC_INTENT,
+ },
+ new AbortController().signal,
+ )).rejects.toThrow("The shadow epoch changed before this ballot was accepted.")
+ expect(fetchMock).toHaveBeenCalledTimes(1)
+ })
+
it("maps withheld posts with their adopted rule and support", async () => {
const feed = feedPayload("epoch-2", [1, 2]) as { posts: Array> } & Record
const withheldPost = { ...(feed.posts[0].post as Record), uri: "at://did:plc:test/app.bsky.feed.post/withheld" }
@@ -664,7 +728,7 @@ function sessionPayload(
sessionId: SESSION_ID,
community: {
id: "community_gov",
- name: "Community Governed Feed",
+ name: "Corgi Commons",
status: "live_shadow",
description: "Research, data, software, and open-source methods.",
liveFeedReady: true,
@@ -751,7 +815,7 @@ function corpusProvenance(): Record {
topicScoreThreshold: 0.5,
eligiblePostCount: 80,
sourceFeedUri: "at://did:plc:amzyknmm4auxijvykyfgznw2/app.bsky.feed.generator/community-gov",
- sourceFeedName: "Community Governed Feed",
+ sourceFeedName: "Corgi Commons",
sourceSnapshotDigest: "7".repeat(64),
sourceRunId: "run-community-gov",
sourceUpdatedAt: NOW,
diff --git a/web-next/app/demo/layout.tsx b/web-next/app/demo/layout.tsx
index f20992f6..81bd1d16 100644
--- a/web-next/app/demo/layout.tsx
+++ b/web-next/app/demo/layout.tsx
@@ -1,9 +1,9 @@
import type { Metadata } from "next"
export const metadata: Metadata = {
- title: "Live demo — re-rank a Community Governed Feed snapshot | Corgi",
+ title: "Governance demo — re-rank a Corgi Commons snapshot | Corgi",
description:
- "Propose a ranking policy, combine it with 24 scripted ballots, and watch a frozen snapshot of Corgi's live Bluesky feed reorder — every move explained by a receipt.",
+ "Propose a ranking policy, combine it with 24 scripted deterministic voter archetypes, and inspect how a frozen Corgi Commons comparison corpus reorders.",
alternates: { canonical: "/demo/" },
}
diff --git a/web-next/app/demo/page.tsx b/web-next/app/demo/page.tsx
index c994b10b..b9985ccc 100644
--- a/web-next/app/demo/page.tsx
+++ b/web-next/app/demo/page.tsx
@@ -466,7 +466,7 @@ export default function DemoPage() {
) : null}
- {busy ? "Updating the shadow demo." : session === null ? "Start a Community Governed Feed demo session." : `Demo phase: ${phase}.`}
+ {busy ? "Updating the shadow demo." : session === null ? "Start a Corgi Commons demo session." : `Demo phase: ${phase}.`}
{receiptAnnouncement}
@@ -519,7 +519,7 @@ export default function DemoPage() {
-
{community.name}
+
Corgi Commons
{corpusPresentation?.provenanceLine}
@@ -537,7 +537,7 @@ export default function DemoPage() {
) : null}
new Set(catalog.map((topic) => topic.slug)).size === 26,
- { message: "Community Governed Feed topic catalog must contain 26 unique active topics." },
+ { message: "Corgi Commons topic catalog must contain 26 unique active topics." },
),
sourceFeedUri: z.string().startsWith("at://"),
voterProfiles: z.array(voterProfileSchema),
diff --git a/web-next/app/demo/shadow-demo-copy.ts b/web-next/app/demo/shadow-demo-copy.ts
index 88defefc..68d69db1 100644
--- a/web-next/app/demo/shadow-demo-copy.ts
+++ b/web-next/app/demo/shadow-demo-copy.ts
@@ -7,9 +7,9 @@
export const HERO = {
eyebrow: "Interactive demo",
- title: "Re-rank a frozen Community Governed Feed snapshot.",
+ title: "Re-rank a frozen Corgi Commons snapshot.",
subtitle:
- "Start with a reviewer-safe published snapshot, propose a policy, combine it with 24 scripted ballots, and inspect how the same posts move.",
+ "Start with a reviewer-safe snapshot of the public feed, propose a policy, combine it with 24 scripted deterministic voter archetypes, and inspect how the same posts move.",
} as const
export interface FlowStepCopy {
@@ -22,15 +22,15 @@ export const FLOW_STEPS: readonly FlowStepCopy[] = [
{ key: "session", label: "Session", hint: "Freeze a comparison corpus" },
{ key: "vote", label: "Your vote", hint: "Pick a policy" },
{ key: "agents", label: "Community", hint: "24 voters weigh in" },
- { key: "epoch", label: "Advance epoch", hint: "Apply the policy" },
+ { key: "epoch", label: "Advance epoch", hint: "Publish the shadow policy" },
{ key: "reorder", label: "Reordered", hint: "Same posts, new order" },
{ key: "receipt", label: "Receipt", hint: "Why it ranked" },
]
export const STEP_PANELS = {
community: {
- heading: "Start from the published feed",
- body: "Corgi freezes an objectively filtered snapshot of its live Community Governed Feed. Your shadow session can change the ranking policy without changing the public feed.",
+ heading: "Start from Corgi Commons",
+ body: "Corgi Commons serves open-network building, research, software, data, and the conversations connecting them. This demo freezes a reviewer-safe comparison corpus from the public feed so your shadow session can change the ranking policy without changing what anyone sees on Bluesky.",
cta: "Start a demo session",
},
vote: {
@@ -40,14 +40,14 @@ export const STEP_PANELS = {
},
agents: {
heading: "Let the community weigh in",
- body: "Twenty-four scripted deterministic voter archetypes represent five stakeholder blocs. Their preferences carry history, respond within bounds to your proposal, and replay exactly from the same inputs.",
+ body: "Twenty-four scripted deterministic voter archetypes represent five preference blocs. Their preferences carry history, respond within bounds to your proposal, and replay exactly from the same inputs. They demonstrate governance mechanics; they are not validated models of human behavior.",
readyHeading: "Twenty-four community voters are ready",
readyBody: "Five scripted blocs balance stable preferences, prior-policy inertia, and a bounded response to your proposal. The run is deterministic and replayable.",
cta: "Simulate 24 voters",
},
epoch: {
- heading: "Advance the epoch",
- body: "Close voting and apply the community-aggregated policy to the frozen corpus.",
+ heading: "Advance the shadow epoch",
+ body: "Close this demo round and publish the aggregated policy inside the isolated shadow session. Production rounds require results review and operator approval before application.",
cta: "Advance epoch",
},
reorder: {
@@ -57,7 +57,7 @@ export const STEP_PANELS = {
},
receipt: {
heading: "Why it ranked here",
- body: "Every position has a receipt: raw signal scores times the community's weights, plus counterfactuals.",
+ body: "Inspect the selected post's raw signal scores, community weights, publication adjustment, provenance, and counterfactuals.",
cta: "Pick another post",
},
} as const
@@ -67,7 +67,7 @@ export const DISCLOSURE = {
production:
"Demo votes and epochs run in an isolated shadow governance namespace. They never enter production governance, audit logs, research exports, or the public Corgi feed.",
posts:
- "The primary path uses an objectively filtered snapshot of posts published in Corgi's live Community Governed Feed. The comparison corpus is frozen for the session so policy changes remain attributable. A labeled mechanics fixture appears only if snapshot loading degrades.",
+ "The primary path uses an objectively filtered snapshot of live public posts published in Corgi Commons. The comparison corpus is frozen for the session so policy changes remain attributable. A labeled mechanics fixture appears only if snapshot loading degrades.",
// Keeps the Bluesky-vs-Corgi boundary honest without claiming native rendering.
annotations:
"Rank badges, scores, and receipts are Corgi annotations shown on Corgi's site, not native Bluesky UI. Bluesky renders the ordered posts; Corgi shows why.",
@@ -75,7 +75,7 @@ export const DISCLOSURE = {
export const LABELS = {
previewBadge: "Coming soon",
- corpusFrozen: "Published feed snapshot · frozen comparison corpus",
+ corpusFrozen: "Corgi Commons snapshot · frozen comparison corpus",
corpusFallback: "Mechanics fixture · frozen comparison corpus",
reset: "Start over",
reviewerVoter: "You (reviewer)",
diff --git a/web-next/app/demo/shadow-demo-fixtures.ts b/web-next/app/demo/shadow-demo-fixtures.ts
index 15df8f14..08b46b97 100644
--- a/web-next/app/demo/shadow-demo-fixtures.ts
+++ b/web-next/app/demo/shadow-demo-fixtures.ts
@@ -241,7 +241,7 @@ export const DEMO_COMMUNITIES: Readonly
- Corgi is a community-governed custom feed for Bluesky. It ingests candidate posts, scores each one against five
- signals, applies the community’s current weights, and serves the ordered result back to Bluesky. Every
- post keeps a receipt so the order is fully inspectable.
+ Corgi Commons is a Bluesky custom feed with inspectable, community-shaped ranking. Corgi ingests candidate
+ posts, computes five global signals, applies the approved policy, and serves an ordered feed for Bluesky
+ clients to render. Corgi exposes policy metadata and ranking receipts when score provenance is available.
This page explains the ranking model. To see it move, use the{" "}
@@ -36,10 +36,10 @@ const SECTIONS = [
Each post gets a raw score (0–1) on five signals:
Recency — how recently the post appeared.
- Engagement — replies, reposts, and likes relative to the author’s reach.
- Bridging — how well the post connects otherwise-separate subgroups in the community.
- Source diversity — whether the feed is hearing from a wider set of authors and sources.
- Relevance — how well the post matches the community’s topics.
+ Engagement — an observed, log-scaled combination of public likes, reposts, and replies; it is not predicted engagement.
+ Bridging — Jaccard distance over engager follower sets, estimating whether a post connects otherwise-separate audiences.
+ Source diversity — an author-repetition penalty within the ranked batch.
+ Relevance — a weighted match between the post’s sparse topic vector and the approved topic-priority map.
>
),
@@ -51,7 +51,7 @@ const SECTIONS = [
<>
A post’s total score is a weighted sum: total = Σ (raw signal score × community weight) .
- The raw signal scores describe the post; the weights are set by the community and always sum to 100%. Change
+ The raw signal scores describe the post; the five global signal weights sum to 100%. Change
the weights and the same posts reorder — without pretending the posts themselves changed.
@@ -61,16 +61,35 @@ const SECTIONS = [
>
),
},
+ {
+ id: "topics-rules",
+ heading: "Topic preferences and content rules",
+ body: (
+ <>
+
+ Topic preferences are separate from the five global signal weights. Posts receive a sparse vector from a
+ curated topic catalog during ingestion. The topic-priority map affects only the relevance
+ signal; it does not add another top-level scoring term.
+
+
+ Content rules determine eligibility. Adopted include keywords act as an allowlist, adopted excludes take
+ precedence, and a keyword needs at least 30% support among ballots that submit content rules. Publication-time
+ adjustments such as duplicate-link handling can also change final order after component scoring.
+
+ >
+ ),
+ },
{
id: "epochs",
heading: "Epochs and voting",
body: (
<>
- An epoch is one saved feed policy — a full set of weights from a governance round. During an
- open round, members vote on the weights. When the round closes, votes are combined with a{" "}
- trimmed mean (a fixed share of outliers on each end is dropped) and the result becomes the
- next epoch. Every epoch is retained, so you can always see how policy changed over time.
+ An epoch is one saved feed policy. A round can be scheduled or opened manually, and its voting
+ window is configurable. Fewer than 10 ballots use an arithmetic mean; 10 or more use a 10% trimmed mean.
+ Closing the window does not apply policy automatically: results are reviewed, an operator approves or rejects
+ the complete proposal, and approval applies signal weights, topic priorities, and adopted content rules before
+ rescoring. Approved policy versions and governance actions are retained so changes remain inspectable.
>
),
@@ -80,15 +99,15 @@ const SECTIONS = [
heading: "Transparency receipts",
body: (
<>
- Every Corgi-scored post can show a receipt with:
+ When score provenance is available, a Corgi-ranked post can show a receipt with:
- The per-signal breakdown (raw score × weight = contribution) and the total.
+ The per-signal breakdown (raw score × weight = contribution), component total, publication adjustment, and final ranking score.
A counterfactual: where the post would rank under a pure-engagement policy versus the community policy.
The epoch the score was computed under, and when.
- Operator actions and weight changes are also written to an append-only audit
- log . Individual votes are never exposed — only aggregated outcomes.
+ Governance actions and policy changes also appear in the history .
+ Individual ballots are not exposed on public transparency surfaces; only aggregate outcomes are shown.
>
),
diff --git a/web-next/app/history/page.tsx b/web-next/app/history/page.tsx
index 85f9a529..982833d7 100644
--- a/web-next/app/history/page.tsx
+++ b/web-next/app/history/page.tsx
@@ -548,7 +548,7 @@ export default function HistoryPage() {
Governance ledger
- Every weight change, keyword rule, and governance event — in full, forever. Select a round to inspect it.
+ Inspect recorded signal, topic, content-rule, and operator changes by policy round.
Corgi is in its pilot phase: the early rounds below were run with a small group of voters to
diff --git a/web-next/app/how-it-works/page.tsx b/web-next/app/how-it-works/page.tsx
index 5417e13d..a37480d3 100644
--- a/web-next/app/how-it-works/page.tsx
+++ b/web-next/app/how-it-works/page.tsx
@@ -2,7 +2,6 @@ import type { Metadata } from "next"
import { Header } from "@/components/header"
import { FooterSection } from "@/components/footer-section"
import { HowItWorksReplay } from "@/components/how-it-works-replay"
-import { ModalityPreview } from "@/components/modality-preview"
import { DemoCTA, WaitlistCTA } from "@/components/landing-ctas"
import { Container, Section } from "@/components/ui/layout"
import { PageHero, HeroGlow, HERO_TOP } from "@/components/ui/page-hero"
@@ -32,6 +31,21 @@ const surfaceBoundaries = [
},
] as const
+const rankingJourney = [
+ { label: "Candidate posts", body: "Corgi ingests public Bluesky posts and computes reusable post signals." },
+ { label: "Ballot", body: "Approved participants can vote on signals, topic priorities, and content rules." },
+ { label: "Aggregation", body: "Corgi aggregates complete ballots after the configurable voting window closes." },
+ { label: "Review and approval", body: "Results are reviewed before an operator approves or rejects the proposed policy." },
+ { label: "Render in Bluesky", body: "After approval and rescoring, Corgi serves the ordered feed for Bluesky clients to render." },
+ { label: "Inspect in Corgi", body: "Corgi shows the policy, score components, publication adjustments, and receipts." },
+] as const
+
+const communityExamples = [
+ "Neighborhood mutual aid can prioritize urgent, well-sourced local updates.",
+ "Open-source maintainers can lift patches, docs, and release notes over generic project chatter.",
+ "Tabletop creators can favor indie releases, safety tools, and useful play resources.",
+] as const
+
export default function HowItWorksPage() {
return (
@@ -49,12 +63,27 @@ export default function HowItWorksPage() {
align="left"
eyebrow="Replay a policy change"
title="Watch the same posts become a different feed."
- subtitle="Corgi scores candidate posts once, then applies the active community policy. Change the epoch and the feed order changes with it."
+ subtitle="Follow a post from candidate set to community ballot, approved policy, Bluesky feed, and inspectable Corgi receipt."
/>
+
+
+ {rankingJourney.map((step, index) => (
+
+ {String(index + 1).padStart(2, "0")}
+ {step.label}
+ {step.body}
+
+ ))}
+
+
+
@@ -69,12 +98,6 @@ export default function HowItWorksPage() {
This separation is intentional: people get a normal Bluesky feed, and the explanation layer stays inspectable on Corgi.
-
-
-
-
-
-
{surfaceBoundaries.map((boundary) => (
+
+
+
+
Any community
+
+ The mechanism is general. The policy is local.
+
+
+
+ {communityExamples.map((example) => (
+ {example}
+ ))}
+
+
+
+
@@ -101,7 +140,7 @@ export default function HowItWorksPage() {
Inspect the live receipt path.
- The walkthrough above is illustrative. The public demo is where live ranking claims use Corgi receipts and snapshot data.
+ The walkthrough above is illustrative. The public demo starts from a frozen comparison corpus sourced from Corgi Commons and keeps every shadow vote isolated from production.
@@ -110,7 +149,7 @@ export default function HowItWorksPage() {
- Demo posts are illustrative; live ranking claims use Corgi receipts and snapshot data.
+ Demo posts on this page are illustrative. The reviewer demo identifies its frozen live-public snapshot and links back to source posts.
diff --git a/web-next/app/not-found.tsx b/web-next/app/not-found.tsx
index 07a40084..4d815421 100644
--- a/web-next/app/not-found.tsx
+++ b/web-next/app/not-found.tsx
@@ -27,7 +27,7 @@ export default function NotFound() {
href="/demo"
className="rounded-full px-5 py-3 text-base font-medium text-foreground/70 hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
- Explore the live demo →
+ Try the governance demo →
diff --git a/web-next/app/page.tsx b/web-next/app/page.tsx
index 7d85632b..ef6061ee 100644
--- a/web-next/app/page.tsx
+++ b/web-next/app/page.tsx
@@ -1,6 +1,5 @@
"use client"
-import Link from "next/link"
import { Header } from "@/components/header"
import { HeroSection } from "@/components/hero-section"
import { ReplayTeaser } from "@/components/replay-teaser"
@@ -28,20 +27,18 @@ export default function LandingPage() {
-
- Interactive preview
+
+ Corgi Commons preview
- See a community reshape its own feed.
+ One feed. A policy the community can inspect.
- Meet Birders Who Code , a sample community of
- developers who bird. Choose how they rank posts and watch this illustrative preview reorder here, with
- the Corgi score behind every move. Then use the{" "}
-
- live shadow demo
- {" "}
- to verify the full flow.
+ Corgi Commons brings together open-network building, research, software, data, and the conversations
+ connecting them. Change the illustrative policy below to see how the same posts reorder.
+
+
+ Bluesky shows the ordered posts. Corgi shows the policy and receipts.
diff --git a/web-next/app/post/page.tsx b/web-next/app/post/page.tsx
index a1a79444..5d20d72f 100644
--- a/web-next/app/post/page.tsx
+++ b/web-next/app/post/page.tsx
@@ -586,7 +586,7 @@ function PostExplanationInner() {
))}
- Topic scores reflect how strongly this post matches each community-weighted topic.
+ Topic scores show how the post’s sparse topic vector combines with the approved topic-priority map inside the relevance signal.
) : (
@@ -604,7 +604,7 @@ function PostExplanationInner() {
- These are the community-voted weights applied at the time this post was scored.
+ These are the approved signal weights applied at the time this post was scored.
diff --git a/web-next/app/proposals/page.tsx b/web-next/app/proposals/page.tsx
index 243029f6..4e9a6a06 100644
--- a/web-next/app/proposals/page.tsx
+++ b/web-next/app/proposals/page.tsx
@@ -8,7 +8,7 @@ import { PolicyBar, PolicyLegend } from "@/components/ui/policy-bar"
import { ErrorCard, Skeleton } from "@/components/ui/state-kit"
import { SIGNAL_KEYS, SIGNAL_LABELS } from "@/lib/signals"
import { weightsApi, transparencyApi, type EpochResponse } from "@/lib/api/client"
-import { votingState, votingClosedDate } from "@/lib/governance-status"
+import { policyApplied, votingState, votingClosedDate } from "@/lib/governance-status"
function pct(value: number): string {
return `${Math.round(value * 100)}%`
@@ -27,8 +27,8 @@ function phaseLabel(epoch: EpochResponse): string {
return policyApplied(epoch) ? "Active policy" : "Closed"
}
-function policyApplied(epoch: EpochResponse): boolean {
- return epoch.status === "active" || epoch.closed_at == null
+function wasEnacted(epoch: EpochResponse): boolean {
+ return epoch.results_approved_at != null
}
/** The signal carrying the most weight in a policy — the round's headline. */
@@ -50,7 +50,7 @@ export default function ProposalsPage() {
})
const current = currentQuery.data
- const past = (historyQuery.data?.epochs ?? []).filter((epoch) => epoch.closed_at != null && epoch.id !== current?.id)
+ const past = (historyQuery.data?.epochs ?? []).filter((epoch) => wasEnacted(epoch) && epoch.id !== current?.id)
return (
@@ -59,9 +59,9 @@ export default function ProposalsPage() {
Governance
Proposals
- Every governance round is a proposal to reweight the feed. Members vote on the five ranking signals; when the
- round closes, the aggregated weights become the next epoch and the feed reranks. Each enacted proposal is
- recorded below and in the{" "}
+ Every governance round proposes a complete policy: five global signal weights, topic priorities, and content
+ rules. When the voting window closes, ballots are aggregated for results review. An operator approves or
+ rejects the proposal before the active policy is changed and rescored. Enacted proposals are recorded below and in the{" "}
) : past.length === 0 ? (
- No past rounds yet. When a round closes, the enacted policy will appear here.
+ No enacted proposals yet. An approved policy will appear here after results review.
) : (
@@ -161,7 +161,7 @@ export default function ProposalsPage() {
Round #{epoch.id}
- Enacted {formatDate(epoch.closed_at ?? epoch.created_at)} · {epoch.vote_count} votes
+ Enacted {formatDate(epoch.results_approved_at)} · {epoch.vote_count} votes
diff --git a/web-next/app/start/page.tsx b/web-next/app/start/page.tsx
index b07dfa78..473b709c 100644
--- a/web-next/app/start/page.tsx
+++ b/web-next/app/start/page.tsx
@@ -7,9 +7,9 @@ import { Container } from "@/components/ui/layout"
import { PageHero, HeroGlow, HERO_TOP } from "@/components/ui/page-hero"
export const metadata: Metadata = {
- title: "Get started — add the Corgi feed in Bluesky",
+ title: "Get started — Corgi Commons and the governance demo",
description:
- "Add the community-governed Corgi feed to your Bluesky app, explore it read-only, and connect an app password when you're ready to vote.",
+ "View Corgi Commons on Bluesky, try the anonymous governance demo, or request access to the limited voting pilot.",
alternates: { canonical: "/start/" },
}
@@ -18,28 +18,22 @@ const CORGI_FEED_URL = "https://bsky.app/profile/corgi-network.bsky.social/feed/
const steps = [
{
n: "01",
- title: "Open the Corgi feed on Bluesky",
+ title: "View and subscribe to Corgi Commons",
body: "Open the feed in your Bluesky app or on the web, then use Bluesky's feed controls to save it to your Home. No Corgi account is required to browse it.",
cta: { label: "Open the feed on Bluesky", href: CORGI_FEED_URL, external: true },
},
{
n: "02",
- title: "Explore it read-only",
- body: "Browse the ranked feed and open any post's receipt to see exactly why it ranked where it did. You can inspect everything without signing in.",
- cta: { label: "Open the live demo", href: "/demo", external: false },
+ title: "Try isolated shadow governance",
+ body: "Use the anonymous demo to propose a policy, combine it with 24 scripted deterministic voter archetypes, rerank a frozen comparison corpus, and inspect what moved. The session never changes the public feed.",
+ cta: { label: "Try the governance demo", href: "/demo", external: false },
},
{
n: "03",
- title: "Join the waitlist to vote",
- body: "Voting is in a limited pilot. Request access with your Bluesky handle; once you're approved, Corgi signs you in with a Bluesky app password — a scoped credential you can revoke anytime. Your real password never touches Corgi.",
+ title: "Request access to the voting pilot",
+ body: "Governance participation is currently limited to approved waitlist participants. If approved, Corgi signs you in with a Bluesky app password — a scoped credential you can revoke anytime. Your real password never touches Corgi.",
cta: { label: "Join the waitlist", href: "/sign-in", external: false },
},
- {
- n: "04",
- title: "Shape the feed with your community",
- body: "During an open round, set how much each ranking signal should matter. When the round closes, the community's aggregated weights become the feed's next policy.",
- cta: { label: "See the ballot", href: "/vote", external: false },
- },
] as const
export default function StartPage() {
@@ -54,8 +48,8 @@ export default function StartPage() {
size="md"
align="center"
eyebrow="Get started"
- title="Add the Corgi feed in Bluesky."
- subtitle="Corgi is a normal Bluesky custom feed — with one difference: your community governs how it ranks. Add it in a minute, explore it read-only, and vote when you’re ready."
+ title="Open Corgi Commons, try the demo, or join the pilot."
+ subtitle="Anyone can view the live Bluesky feed and use the anonymous shadow demo. Governance participation is currently available through an approved waitlist pilot."
actions={
<>
- Explore the demo first →
+ Try the governance demo →
>
}
diff --git a/web-next/app/vote/page.tsx b/web-next/app/vote/page.tsx
index 22248d90..84e21554 100644
--- a/web-next/app/vote/page.tsx
+++ b/web-next/app/vote/page.tsx
@@ -151,7 +151,7 @@ function RoundCard({ epoch }: { epoch: EpochResponse }) {
Community ballot
- Your votes shape how the feed ranks posts.
+ Your ballot contributes to the next proposed feed policy.
@@ -181,12 +181,11 @@ function RoundCard({ epoch }: { epoch: EpochResponse }) {
{state === "review" ? (
- Voting is closed for this round. Results are being tallied.
+ Voting is closed for this round. Aggregated results are awaiting review and operator approval.
) : state === "closed" ? (
- Voting for this round closed{closedOn ? ` on ${closedOn}` : ""}. The aggregated weights are the
- feed’s active policy until the next round opens.
+ Voting for this round closed{closedOn ? ` on ${closedOn}` : ""}. The approved policy is active until another proposal is approved.
) : epoch.voting_ends_at ? (
@@ -426,7 +425,7 @@ function VoteWorkbench({ epoch, myVote, topics, contentRules, isAuthenticated, o
Signal weights
Drag to allocate importance across the five signals. Adjusting one redistributes the rest — they
- always sum to 100%. The thin marker on each track is the community’s current average.
+ always sum to 100%. The thin marker on each track is the active approved policy.
@@ -471,7 +470,7 @@ function VoteWorkbench({ epoch, myVote, topics, contentRules, isAuthenticated, o
aggregation; the API `rules.threshold` is an absolute vote
count for that round, not a percentage, so it is not rendered here. */}
- Vote on which keywords to boost or suppress. Keywords take effect when backed by at least 30% of members voting on content rules.
+ Include keywords form an allowlist; exclude keywords take precedence. A keyword is proposed for adoption when at least 30% of ballots that submit content rules support it, and it is applied only after results review and approval.
@@ -532,7 +531,7 @@ function VoteWorkbench({ epoch, myVote, topics, contentRules, isAuthenticated, o
03 — Topic preferences
Topics
- Slide right to boost a topic, left to reduce it. The vertical marker shows the community average.
+ Slide right to boost a topic, left to reduce it. Topic preferences shape only the relevance signal. The vertical marker shows the active approved topic policy.
{touchedTopicCount > 0 && (
@@ -665,7 +664,7 @@ function VoteWorkbench({ epoch, myVote, topics, contentRules, isAuthenticated, o
{ballotCountText(epoch.vote_count)}
)}
- Each vote counts equally.
+ Fewer than 10 ballots use an arithmetic mean; 10 or more use a 10% trimmed mean.
diff --git a/web-next/components/bento-section.tsx b/web-next/components/bento-section.tsx
index 013fad4d..a8e22b8d 100644
--- a/web-next/components/bento-section.tsx
+++ b/web-next/components/bento-section.tsx
@@ -12,8 +12,8 @@ function SignalBlendUI() {
// arbitrary classes can't interpolate the constant — keep the hexes in sync.
const examples = [
{
- label: "Field note with context",
- detail: "A sighting post with location, correction, and source detail.",
+ label: "Research note with context",
+ detail: "A methods post with data, corrections, and source detail.",
outcome: "Ranks up",
signals: [
{ label: "Relevance", widthClass: "w-[92%]", colorClass: "bg-primary" },
@@ -33,7 +33,7 @@ function SignalBlendUI() {
},
{
label: "Bridge between interests",
- detail: "A post that connects the community topic to adjacent expertise.",
+ detail: "A post that connects open-network work to adjacent research or tooling.",
outcome: "Finds its people",
signals: [
{ label: "Relevance", widthClass: "w-[70%]", colorClass: "bg-primary" },
@@ -96,20 +96,24 @@ function CommunityVoteUI() {
text: "A ranking change is proposed before it affects the feed.",
},
{
- label: "Community vote",
- text: "Members choose which signals should matter more.",
+ label: "Voting window",
+ text: "Approved participants vote on signals, topic priorities, or content rules.",
},
{
- label: "Weights update",
- text: "The next epoch applies a visible set of weights.",
+ label: "Results review",
+ text: "Corgi aggregates the ballots and presents the proposed policy for review.",
},
{
- label: "Feed reorders",
- text: "Bluesky receives the same posts in a new order.",
+ label: "Operator approval",
+ text: "An operator approves or rejects the complete proposed policy.",
},
{
- label: "Receipt stays",
- text: "Corgi keeps the explanation tied to that epoch.",
+ label: "Rescore and publish",
+ text: "After approval, Corgi applies the policy, reranks, and publishes the ordered feed.",
+ },
+ {
+ label: "Inspect",
+ text: "Corgi keeps explanations tied to the policy epoch while Bluesky renders the posts.",
},
]
@@ -121,7 +125,7 @@ function CommunityVoteUI() {
auditable sequence
-
+
{steps.map((step, index) => (
@@ -130,7 +134,7 @@ function CommunityVoteUI() {
{step.label}
{index < steps.length - 1 ? (
-
+
→
) : null}
@@ -148,7 +152,7 @@ function CommunityVoteUI() {
-
After weights update
+
After policy approval
#1 useful source-rich explainer
#2 high-engagement general post
@@ -164,7 +168,7 @@ const features = [
id: "mixed-feed",
headline: "The feed is mixed, not topic-siloed.",
description:
- "A governed recommender has to reconcile recency, engagement, bridging, source diversity, and relevance in one ranking — and the demo shows the live receipts behind it.",
+ "A governed recommender reconciles recency, engagement, bridging, source diversity, and relevance in one ranking. Topic preferences shape relevance; content rules shape which posts are eligible.",
cta: "Explore the demo feed",
href: "/demo",
UI: SignalBlendUI,
@@ -173,7 +177,7 @@ const features = [
id: "epochs",
headline: "Epochs turn proposals into auditable changes.",
description:
- "Corgi's governance loop is proposal, vote, epoch transition, and receipt. The product story stays grounded in that loop instead of promising private communities or made-up vote totals.",
+ "Corgi's production loop is proposal, voting window, results review, operator approval, rescore, publication, and receipt. The shadow demo compresses that sequence without changing the public feed.",
cta: "See epoch history",
href: "/history",
UI: CommunityVoteUI,
diff --git a/web-next/components/community-examples-section.tsx b/web-next/components/community-examples-section.tsx
index 6b10f055..9eed3c98 100644
--- a/web-next/components/community-examples-section.tsx
+++ b/web-next/components/community-examples-section.tsx
@@ -4,13 +4,6 @@ import { SIGNAL_LABELS } from "@/lib/signals"
// Illustrative Bluesky-native community concepts, labeled only with Corgi's
// canonical ranking signals and the kind of feed noise each policy addresses.
const communityExamples = [
- {
- name: "Birders Who Code",
- initials: "BC",
- signal: SIGNAL_LABELS.bridging,
- description: "Field notes, scripts, and bird-call datasets stay balanced against generic tech jokes.",
- accentClass: "bg-[#CFE1D0] text-[#3E643F]",
- },
{
name: "Neighborhood Mutual Aid",
initials: "MA",
@@ -19,26 +12,12 @@ const communityExamples = [
accentClass: "bg-[#C8DDF2] text-[#284C6D]",
},
{
- name: "Open-Source Maintainers",
+ name: "Open-source Maintainers",
initials: "OS",
signal: SIGNAL_LABELS.relevance,
description: "Patches, docs, and release notes outrank memes and project drama.",
accentClass: "bg-[#F0D1A8] text-[#7A4A20]",
},
- {
- name: "Science & Research Feeds",
- initials: "SR",
- signal: SIGNAL_LABELS.source_diversity,
- description: "Papers, replications, and expert context beat hot takes and hype.",
- accentClass: "bg-[#C6E2EA] text-[#245A68]",
- },
- {
- name: "Local Newsroom & Civic Desk",
- initials: "LC",
- signal: `${SIGNAL_LABELS.recency} + ${SIGNAL_LABELS.source_diversity}`,
- description: "Council meetings, transit, housing, and weather stay findable during a news burst.",
- accentClass: "bg-[#D4DBE2] text-[#3A4A5C]",
- },
{
name: "Tabletop Creators",
initials: "TT",
@@ -67,7 +46,7 @@ export function CommunityExamplesSection() {
-
+
{communityExamples.map((community) => (
diff --git a/web-next/components/demo/community-picker.tsx b/web-next/components/demo/community-picker.tsx
index 8c1eb003..c5840065 100644
--- a/web-next/components/demo/community-picker.tsx
+++ b/web-next/components/demo/community-picker.tsx
@@ -27,9 +27,9 @@ export function CommunityPicker({
Public feed · isolated shadow session
-
Community Governed Feed
+
Corgi Commons
- Freeze a reviewer-safe comparison set from Corgi's published Bluesky feed, then run governance without changing what the public sees.
+ Open-network building, research, software, data, and the conversations connecting them. Freeze a reviewer-safe comparison set, then run governance without changing what the public sees.
Current weightAlphabetical
- setChangedOnly(event.target.checked)} aria-label="Show topics different from live policy" />Different from live
+ setChangedOnly(event.target.checked)} aria-label="Show topics different from baseline policy" />Different from baseline
Reset preset
@@ -345,7 +345,7 @@ export function VotePanel({
) : (
- Propose keywords to exclude. A rule is adopted when at least {contentRuleThreshold} of {contentRuleElectorate} ballots back it.
+ Advanced, exclusion-only demo: propose keywords to withhold from this frozen corpus. A rule is adopted when at least {contentRuleThreshold} of {contentRuleElectorate} demo ballots back it. Production also supports include rules, with excludes taking precedence.
{suggestedExcludeKeywords.length > 0 ? (
diff --git a/web-next/components/faq-section.tsx b/web-next/components/faq-section.tsx
index bc05849a..eb243a27 100644
--- a/web-next/components/faq-section.tsx
+++ b/web-next/components/faq-section.tsx
@@ -23,7 +23,7 @@ const faqData = [
{
question: "How does voting on the weights work?",
answer:
- "When a voting round is active, members choose how much each ranking factor should matter: recency, engagement, bridging (posts that connect subgroups), source diversity, and topic relevance. When the round ends, the aggregate weights become the next feed policy and the feed reranks accordingly. The full history stays inspectable. During the pilot, voting accounts are approved from a waitlist — the demo and every transparency page stay open to everyone.",
+ "During the limited pilot, approved participants can vote on five global ranking signals, topic priorities that shape relevance, and include or exclude content rules. When the configurable window closes, Corgi aggregates the ballots for results review. An operator approves or rejects the complete policy before any production rescore. The anonymous shadow demo and public feed remain open to everyone.",
},
{
question: "Is Corgi secure? Does it need my Bluesky password?",
@@ -33,7 +33,7 @@ const faqData = [
{
question: "What is an epoch?",
answer:
- "An epoch is one saved feed policy from a voting round. Your community sets how long rounds last. When a round closes, the new weights go live, and every epoch's weights are saved so you can always see how the ranking policy changed.",
+ "An epoch is one saved feed policy. A round can be scheduled or opened manually, and its voting window is configurable. After the window closes, results are reviewed and approved before the policy is applied and the feed is rescored. Corgi keeps the policy history so changes remain inspectable.",
},
{
question: "Can researchers use Corgi's data?",
diff --git a/web-next/components/feed/corgi-rank-badge.tsx b/web-next/components/feed/corgi-rank-badge.tsx
index e42962a7..1ebfdd15 100644
--- a/web-next/components/feed/corgi-rank-badge.tsx
+++ b/web-next/components/feed/corgi-rank-badge.tsx
@@ -27,7 +27,7 @@ export interface CorgiRankBadgeProps {
readonly fullReceiptHref?: string
/** Hide the movement pill + line when there's no prior epoch to compare against. */
readonly showMovement?: boolean
- /** Hide the "Why" popover when the surface already shows a full receipt panel (the demo). */
+ /** Hide the inspect popover when the surface already shows a full receipt panel (the demo). */
readonly showWhy?: boolean
/** For static/mockup screenshots — start expanded. */
readonly defaultOpen?: boolean
@@ -140,9 +140,10 @@ export function CorgiRankBadge({
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-controls={panelId}
- className={`group mt-2 inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 font-mono text-[10px] font-semibold uppercase tracking-[0.12em] text-foreground/50 transition-colors hover:text-primary ${FOCUS}`}
+ aria-label={`Inspect ranking for post ranked #${rank}`}
+ className={`group mt-2 inline-flex items-center gap-0.5 rounded-full px-1.5 py-0.5 font-mono text-[9px] font-semibold uppercase tracking-[0.08em] text-foreground/55 transition-colors hover:text-primary ${FOCUS}`}
>
- Why
+ Inspect
) : null}
diff --git a/web-next/components/footer-section.tsx b/web-next/components/footer-section.tsx
index 1f9dea22..44578fac 100644
--- a/web-next/components/footer-section.tsx
+++ b/web-next/components/footer-section.tsx
@@ -48,17 +48,17 @@ export function FooterSection() {
Corgi
- Your community runs the feed. Inspectable ranking.
+ Community-shaped feeds. Inspectable ranking.
@@ -294,7 +294,7 @@ export function Header() {
>
{isDemoPage
? "Join the waitlist"
- :
Explore demo}
+ :
Try demo}
{!isDemoPage && (
-
- See how ranking works →
-
+
+ Open Corgi Commons →
+
{/* Trust line */}
- No-login demo first · connect Bluesky when ready · inspectable ranking
+ No login required · shadow votes never change the public feed · governance is in a limited pilot
{/* Scroll cue — invites the visitor down into the interactive demo. */}
diff --git a/web-next/components/how-it-works-replay.tsx b/web-next/components/how-it-works-replay.tsx
index 35a36a55..1b2b8440 100644
--- a/web-next/components/how-it-works-replay.tsx
+++ b/web-next/components/how-it-works-replay.tsx
@@ -3,7 +3,7 @@
import Image from "next/image"
import { useMemo, useState } from "react"
import { motion, useReducedMotion } from "framer-motion"
-import { Bird, Code2, Database } from "lucide-react"
+import { Code2, Database, FlaskConical } from "lucide-react"
import { BlueskyPostCard, RANK_COL_CLASS, RankColumnHeader } from "@/components/feed/bluesky-feed"
import { CorgiRankBadge } from "@/components/feed/corgi-rank-badge"
import { badgeMovementFor, rankSignalsFor } from "@/components/feed/replay-adapter"
@@ -93,7 +93,7 @@ function FeedCard(props: {
likeCount={post.stats.likes}
/>
-
+
+ props.onSelect(post.id)}
+ aria-pressed={props.selected}
+ aria-label={`Inspect ranking for post ranked #${props.rankedPost.rank}`}
+ className="rounded-md px-1 py-1 font-mono text-[9px] font-semibold uppercase tracking-[0.08em] text-primary underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/70"
+ >
+ Inspect
+
)
@@ -173,8 +182,8 @@ function CounterfactualComparison() {
{[
- { label: "Engagement-heavy ranking", rankedPosts: engagementRanking },
- { label: "Community-governed ranking", rankedPosts: communityRanking },
+ { label: "Conversation-heavy ranking", rankedPosts: engagementRanking },
+ { label: "Corgi Commons policy", rankedPosts: communityRanking },
].map((ranking) => (
{ranking.label}
@@ -211,7 +220,7 @@ function ScoringMath() {
Inspect the scoring math.
- The demo keeps raw post scores fixed so you can see the core product idea: community policy weights decide how much each signal counts.
+ This illustrative replay keeps raw post scores fixed so you can isolate the policy change. Production rescoring recomputes candidate scores before publication.
@@ -263,15 +272,27 @@ function ScoringMath() {
For the bridge-building policy, the weights sum to 100%: recency {formatPercent(bridgeEpoch.weights.recency)}, engagement {formatPercent(bridgeEpoch.weights.engagement)}, bridging {formatPercent(bridgeEpoch.weights.bridging)}, source diversity {formatPercent(bridgeEpoch.weights.sourceDiversity)}, relevance {formatPercent(bridgeEpoch.weights.relevance)}.
+
+
Topics shape relevance
+
+ Topic preferences are a separate policy map. They change the relevance signal only; they are not five more global ranking signals.
+
+
+
+
Rules shape eligibility
+
+ Adopted include rules act as an allowlist, exclude rules take precedence, and production adoption requires at least 30% support among ballots that submit content rules.
+
+
What an epoch means
- An epoch is a stored feed policy from a voting round. The post signals stay the same in this demo, but the active epoch changes how much each signal matters.
+ An epoch is a stored feed policy. In production, a closed round is aggregated and reviewed before operator approval applies the complete policy and triggers a rescore.
- Demo posts are illustrative; live ranking claims use Corgi receipts and snapshot data.
+ Component scores explain the weighted sum. Publication-time adjustments, such as duplicate-link handling, can still affect final order and are called out in live receipts.
@@ -345,16 +366,16 @@ export function HowItWorksReplay() {
@@ -374,7 +395,7 @@ export function HowItWorksReplay() {
/>
- {["Discover", "Following", "Birders Who Code", "Tools"].map((tab, index) => (
+ {["Discover", "Following", "Corgi Commons", "Tools"].map((tab, index) => (
{tab}
{index === 2 ? : null}
@@ -407,7 +428,7 @@ export function HowItWorksReplay() {
- The rank numbers and receipt panel are Corgi-site annotations for this product demo. Standard Bluesky clients render the ordered posts, not Corgi score panels.
+ Select a post to inspect its ranking. The rank numbers and receipt panel are Corgi-site annotations; standard Bluesky clients render the ordered posts, not Corgi score panels.
diff --git a/web-next/components/landing-ctas.tsx b/web-next/components/landing-ctas.tsx
index 7b3a0423..9c35e172 100644
--- a/web-next/components/landing-ctas.tsx
+++ b/web-next/components/landing-ctas.tsx
@@ -16,7 +16,7 @@ export function DemoCTA({ className }: LandingCTAProps) {
)}
>
- Explore the live demo
+ Try the governance demo
)
diff --git a/web-next/components/replay-teaser.tsx b/web-next/components/replay-teaser.tsx
index 8e181f7d..2c6dfad0 100644
--- a/web-next/components/replay-teaser.tsx
+++ b/web-next/components/replay-teaser.tsx
@@ -101,10 +101,10 @@ export function ReplayTeaser() {
- {["Discover", "Following", "Birders Who Code", "Tools"].map((tab) => (
+ {["Discover", "Following", "Corgi Commons", "Tools"].map((tab) => (
{tab}
@@ -162,18 +162,17 @@ export function ReplayTeaser() {
- Tap Why on any post for its receipt. Rank badges and receipts are
- Corgi annotations, not native Bluesky chrome.
+ Select Inspect on any post for its score breakdown. Rank badges and receipts are Corgi annotations, not native Bluesky chrome.
- Illustrative demo — live ranking claims use Corgi receipts and snapshot data.
+ Illustrative preview — the governance demo uses a frozen snapshot sourced from Corgi Commons.
- See the full math and every receipt →
+ See the full policy and receipt walkthrough →
diff --git a/web-next/components/social-proof.tsx b/web-next/components/social-proof.tsx
index b8e0b933..e56ffba9 100644
--- a/web-next/components/social-proof.tsx
+++ b/web-next/components/social-proof.tsx
@@ -30,8 +30,8 @@ export function SocialProof() {
label: "recency, engagement, bridging, diversity, relevance",
},
{
- value: "Community-governed",
- label: "member votes set the weights",
+ value: "Three ballot channels",
+ label: "signals, topic priorities, and content rules",
},
{
value: "Open source",
@@ -89,7 +89,7 @@ export function SocialProof() {
})}
- Every ranked post shows its receipt —{" "}
+ Bluesky shows the ordered feed; ranking receipts are inspected on Corgi —{" "}
inspect them in the no-login demo
diff --git a/web-next/e2e/production-hard-refresh.spec.ts b/web-next/e2e/production-hard-refresh.spec.ts
index 2cfbdc9d..05198212 100644
--- a/web-next/e2e/production-hard-refresh.spec.ts
+++ b/web-next/e2e/production-hard-refresh.spec.ts
@@ -18,7 +18,7 @@ interface RouteFailure {
const PUBLIC_ROUTES: readonly PublicRoute[] = [
{ path: "/", heading: /Make Bluesky care about what your community cares about/i },
- { path: "/demo/", heading: /Re-rank a frozen Community Governed Feed snapshot/i },
+ { path: "/demo/", heading: /Re-rank a frozen Corgi Commons snapshot/i },
{ path: "/how-it-works/", heading: /Watch the same posts become a different feed/i },
{ path: "/start/", heading: /Add the Corgi feed in Bluesky/i },
]
diff --git a/web-next/lib/admin-lifecycle.ts b/web-next/lib/admin-lifecycle.ts
new file mode 100644
index 00000000..4afb478c
--- /dev/null
+++ b/web-next/lib/admin-lifecycle.ts
@@ -0,0 +1,15 @@
+export function approvedParticipationPercent(
+ voteCount: number,
+ approvedParticipantCount: number
+): number {
+ if (approvedParticipantCount <= 0) return 0;
+ return Math.round((voteCount / approvedParticipantCount) * 100);
+}
+
+export async function completeAdminLifecycleRefresh(
+ invalidate: () => Promise,
+ closeConfirmation: () => void
+): Promise {
+ await invalidate();
+ closeConfirmation();
+}
diff --git a/web-next/lib/api/admin.ts b/web-next/lib/api/admin.ts
index 5177ef6d..1cde0764 100644
--- a/web-next/lib/api/admin.ts
+++ b/web-next/lib/api/admin.ts
@@ -104,6 +104,7 @@ export interface AdminStatus {
currentEpoch: {
id: number;
status: string;
+ phase?: 'running' | 'voting' | 'results';
votingOpen: boolean;
votingEndsAt: string | null;
autoTransition: boolean;
@@ -119,6 +120,7 @@ export interface AdminStatus {
lastScoringRun: string | null;
lastScoringDuration: number | null;
subscriberCount: number;
+ approvedParticipantCount: number;
};
contentRules: {
includeKeywords: string[];
@@ -349,35 +351,6 @@ export const adminApi = {
return response.data;
},
- async updateEpoch(data: {
- votingOpen?: boolean;
- votingEndsAt?: string | null;
- autoTransition?: boolean;
- }): Promise<{ success: boolean; epoch: Partial }> {
- const response = await api.patch('/api/admin/epochs/current', data);
- return response.data;
- },
-
- async transitionEpoch(options: { force?: boolean; announceResults?: boolean } = {}): Promise<{
- success: boolean;
- previousEpoch: { id: number; totalVotes: number };
- newEpoch: { id: number };
- announcement: { postUrl: string } | null;
- }> {
- const response = await api.post('/api/admin/epochs/transition', options);
- return response.data;
- },
-
- async closeVoting(): Promise<{ success: boolean }> {
- const response = await api.post('/api/admin/epochs/close-voting');
- return response.data;
- },
-
- async openVoting(): Promise<{ success: boolean }> {
- const response = await api.post('/api/admin/epochs/open-voting');
- return response.data;
- },
-
async getAnnouncements(): Promise<{ announcements: Announcement[] }> {
const response = await api.get('/api/admin/announcements');
return response.data;
@@ -482,29 +455,11 @@ export const adminApi = {
return response.data;
},
- async applyResults(): Promise<{
- success: boolean;
- voteCount: number;
- appliedWeights: boolean;
- weights: GovernanceWeights;
- contentRules: ContentRules;
- round: RoundSummary;
- rescoreTriggered: boolean;
- }> {
- const response = await api.post('/api/admin/governance/apply-results');
- return response.data;
- },
-
async getRoundDetails(id: number): Promise {
const response = await api.get(`/api/admin/governance/rounds/${id}`);
return response.data;
},
- async endRound(force = false): Promise<{ success: boolean; newRoundId: number }> {
- const response = await api.post('/api/admin/governance/end-round', { force });
- return response.data;
- },
-
async startVoting(durationHours: number, announce = true): Promise<{ success: boolean; round: RoundSummary }> {
const response = await api.post('/api/admin/governance/start-voting', {
durationHours,
@@ -517,6 +472,7 @@ export const adminApi = {
success: boolean;
voteCount: number;
proposedWeights: GovernanceWeights;
+ proposedTopicWeights: Record;
proposedContentRules: ContentRules;
round: RoundSummary;
}> {
@@ -529,6 +485,7 @@ export const adminApi = {
async approveResults(announce = true): Promise<{
success: boolean;
weights: GovernanceWeights;
+ topicWeights: Record;
contentRules: ContentRules;
rescoreTriggered: boolean;
round: RoundSummary;
diff --git a/web-next/lib/api/client.ts b/web-next/lib/api/client.ts
index d79286d1..71de55a8 100644
--- a/web-next/lib/api/client.ts
+++ b/web-next/lib/api/client.ts
@@ -224,6 +224,7 @@ export interface EpochResponse {
subscriber_count?: number;
created_at: string;
closed_at?: string;
+ results_approved_at?: string | null;
description?: string;
voting_started_at?: string | null;
voting_ends_at?: string | null;
@@ -251,6 +252,7 @@ interface EpochApiResponse {
subscriber_count?: number;
created_at: string;
closed_at?: string;
+ results_approved_at?: string | null;
description?: string;
voting_started_at?: string | null;
voting_ends_at?: string | null;
@@ -277,6 +279,7 @@ function toEpochResponse(epoch: EpochApiResponse): EpochResponse {
subscriber_count: epoch.subscriber_count,
created_at: epoch.created_at,
closed_at: epoch.closed_at,
+ results_approved_at: epoch.results_approved_at,
description: epoch.description,
voting_started_at: epoch.voting_started_at,
voting_ends_at: epoch.voting_ends_at,
diff --git a/web-next/lib/governance-status.ts b/web-next/lib/governance-status.ts
index e02654d8..ce0e2d7d 100644
--- a/web-next/lib/governance-status.ts
+++ b/web-next/lib/governance-status.ts
@@ -46,6 +46,13 @@ export function policyIsActive(epoch: { status: string; closed_at?: string | nul
return epoch.status === "active" || epoch.closed_at == null
}
+/** Whether the current response identifies this epoch as the applied policy.
+ * Older responses may omit `phase`; an explicit non-running phase is not
+ * treated as applied. */
+export function policyApplied(epoch: { status: string; phase?: string }): boolean {
+ return epoch.status === "active" && (epoch.phase === undefined || epoch.phase === "running")
+}
+
/** Below this member count, "N of M voted · X%" reads as a ghost town rather
* than transparency — show plain ballot counts instead of participation math. */
export const MIN_MEANINGFUL_MEMBERS = 10
diff --git a/web-next/lib/replay-model.ts b/web-next/lib/replay-model.ts
index 4c47af81..ef2a059f 100644
--- a/web-next/lib/replay-model.ts
+++ b/web-next/lib/replay-model.ts
@@ -91,7 +91,7 @@ export const signals: readonly Signal[] = [
key: "relevance",
label: "Relevance",
shortLabel: "Match",
- description: "How well the post matches the community's topic.",
+ description: "How well the post matches the community's topic priorities.",
barColor: "#C8612C",
},
] as const
@@ -103,8 +103,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@maya-keene.bsky.social",
time: "14m",
avatarSrc: "/images/avatars/maya-keene.png",
- text: "Built a tiny script to log neighborhood finch sightings from my morning walks.",
- tags: ["birding", "code", "field notes"],
+ text: "Mapped accessibility gaps in open transit data, then shared the notebook and cleaned CSV.",
+ tags: ["open data", "accessibility", "transit"],
scores: { recency: 0.72, engagement: 0.42, bridging: 0.92, sourceDiversity: 0.68, relevance: 0.9 },
stats: { replies: "18", reposts: "42", likes: "164" },
},
@@ -114,8 +114,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@toastwindow.bsky.social",
time: "6m",
avatarSrc: "/images/avatars/claire-rowan.png",
- text: "Rare tanager spotted near the east trailhead this morning.",
- tags: ["sighting", "local", "fresh"],
+ text: "A maintainer note on making privacy-preserving defaults easier to find in project docs.",
+ tags: ["open source", "privacy", "documentation"],
scores: { recency: 0.96, engagement: 0.35, bridging: 0.28, sourceDiversity: 0.55, relevance: 0.82 },
stats: { replies: "9", reposts: "25", likes: "112" },
},
@@ -125,8 +125,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@arjunmehta.dev",
time: "31m",
avatarSrc: "/images/avatars/arjun-mehta.png",
- text: "This CSS bug has haunted me for three days.",
- tags: ["code", "debugging"],
+ text: "Found a timezone bug in our benchmark harness. Patch and before-and-after runs are in the thread.",
+ tags: ["software", "reproducibility", "benchmarking"],
scores: { recency: 0.64, engagement: 0.58, bridging: 0.22, sourceDiversity: 0.42, relevance: 0.48 },
stats: { replies: "21", reposts: "36", likes: "208" },
},
@@ -136,8 +136,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@eli-overthinking.bsky.social",
time: "18m",
avatarSrc: "/images/avatars/eli-moreno.png",
- text: "Programmers will do anything except go outside.",
- tags: ["joke", "viral"],
+ text: "Every open-source project has one file everyone is scared to touch.",
+ tags: ["open source", "joke", "viral"],
scores: { recency: 0.7, engagement: 0.95, bridging: 0.3, sourceDiversity: 0.35, relevance: 0.36 },
stats: { replies: "86", reposts: "511", likes: "4.2K" },
},
@@ -147,8 +147,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@thocknotes.bsky.social",
time: "23m",
avatarSrc: "/images/avatars/theo-kim.png",
- text: "Open-source bird-call classifier dataset just dropped.",
- tags: ["dataset", "birding", "ml"],
+ text: "Published a dataset comparing alt-text quality across image models, plus the scoring rubric.",
+ tags: ["dataset", "accessibility", "machine learning"],
scores: { recency: 0.68, engagement: 0.62, bridging: 0.88, sourceDiversity: 0.84, relevance: 0.94 },
stats: { replies: "32", reposts: "96", likes: "340" },
},
@@ -158,8 +158,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@ninavaldez.bsky.social",
time: "1h",
avatarSrc: "/images/avatars/nina-valdez.png",
- text: "Field notes from a rainy owl survey, plus the messy CSV.",
- tags: ["field notes", "csv", "survey"],
+ text: "Field notes from a community mesh-network test, including the messy CSV and failed runs.",
+ tags: ["community networks", "field notes", "data"],
scores: { recency: 0.46, engagement: 0.28, bridging: 0.74, sourceDiversity: 0.72, relevance: 0.86 },
stats: { replies: "7", reposts: "19", likes: "88" },
},
@@ -169,8 +169,8 @@ export const demoPosts: readonly DemoPost[] = [
handle: "@leilahart.bsky.social",
time: "16m",
avatarSrc: "/images/avatars/leila-hart.png",
- text: "My camera roll is 80% blurry sparrows and 20% screenshots of stack traces.",
- tags: ["birding", "code", "funny"],
+ text: "The best research software is half methods section, half apology to future maintainers.",
+ tags: ["research software", "methods", "culture"],
scores: { recency: 0.75, engagement: 0.66, bridging: 0.7, sourceDiversity: 0.52, relevance: 0.76 },
stats: { replies: "15", reposts: "54", likes: "261" },
},
@@ -180,7 +180,7 @@ export const epochs: readonly Epoch[] = [
{
id: "engagement",
eyebrow: "Epoch 07",
- label: "Engagement-heavy",
+ label: "Conversation-heavy",
headline: "Likes dominate the feed.",
body: "This is the failure mode the community wants to fix: the viral joke wins even though it is a weak match.",
weights: { recency: 0.05, engagement: 0.65, bridging: 0.05, sourceDiversity: 0.05, relevance: 0.2 },
@@ -188,7 +188,7 @@ export const epochs: readonly Epoch[] = [
{
id: "bridge",
eyebrow: "Epoch 08",
- label: "Bridge-building policy",
+ label: "Bridge-building",
headline: "The community boosts posts that connect subgroups.",
body: "The policy rewards posts that carry useful context across both sides of the feed.",
weights: { recency: 0.15, engagement: 0.1, bridging: 0.35, sourceDiversity: 0.15, relevance: 0.25 },
@@ -196,17 +196,17 @@ export const epochs: readonly Epoch[] = [
{
id: "field",
eyebrow: "Epoch 09",
- label: "Field-notes policy",
- headline: "Useful sources get more room.",
- body: "The feed shifts toward relevance and source diversity so datasets, surveys, and field notes do not vanish under jokes.",
+ label: "Research and tooling",
+ headline: "Reusable work gets more room.",
+ body: "The feed shifts toward relevance and source diversity so datasets, methods, and useful tooling do not vanish under jokes.",
weights: { recency: 0.15, engagement: 0.1, bridging: 0.15, sourceDiversity: 0.3, relevance: 0.3 },
},
{
id: "freshness",
eyebrow: "Epoch 10",
label: "Freshness push",
- headline: "Time-sensitive sightings rise.",
- body: "When the community cares about today's field context, fresh sightings move up without making likes the whole policy.",
+ headline: "Time-sensitive work rises.",
+ body: "When the community cares about what is happening now, fresh releases and active conversations move up without making likes the whole policy.",
weights: { recency: 0.55, engagement: 0.05, bridging: 0.05, sourceDiversity: 0.05, relevance: 0.3 },
},
] as const
@@ -214,36 +214,36 @@ export const epochs: readonly Epoch[] = [
export const demoPersonas: readonly DemoPersona[] = [
{
id: "field-notes",
- label: "Field Notes Birder",
- role: "Wants useful sightings and field context to survive the scroll.",
- body: "Boosts source-rich notes and posts that match the Birders Who Code topic.",
+ label: "Research Steward",
+ role: "Wants careful methods and useful context to survive the scroll.",
+ body: "Boosts source-rich work and posts that match the community's research priorities.",
weights: { recency: 0.18, engagement: 0.07, bridging: 0.14, sourceDiversity: 0.31, relevance: 0.3 },
},
{
id: "dataset-maintainer",
label: "Dataset Maintainer",
role: "Cares about reproducible data, open tooling, and reusable context.",
- body: "Raises datasets, messy CSVs, classifiers, and well-labeled observations.",
+ body: "Raises datasets, reproducible runs, useful tools, and well-labeled observations.",
weights: { recency: 0.1, engagement: 0.06, bridging: 0.24, sourceDiversity: 0.28, relevance: 0.32 },
},
{
id: "freshness-watcher",
label: "Freshness Watcher",
- role: "Wants the feed to notice time-sensitive sightings before they go stale.",
+ role: "Wants the feed to notice time-sensitive releases and conversations before they go stale.",
body: "Pushes recency without letting generic virality take over the community.",
weights: { recency: 0.52, engagement: 0.06, bridging: 0.08, sourceDiversity: 0.08, relevance: 0.26 },
},
{
id: "joke-enjoyer",
- label: "Joke Enjoyer",
- role: "Likes the culture posts too, as long as people can rebalance them.",
+ label: "Conversation Follower",
+ role: "Values the culture and conversation posts too, as long as people can rebalance them.",
body: "Gives engagement real weight so funny posts can win when the community chooses that.",
weights: { recency: 0.13, engagement: 0.55, bridging: 0.08, sourceDiversity: 0.04, relevance: 0.2 },
},
{
id: "bridge-builder",
label: "Bridge Builder",
- role: "Wants posts that connect birders, coders, data people, and local observers.",
+ role: "Wants posts that connect researchers, builders, data people, and open-network communities.",
body: "Rewards posts that carry useful context across subgroups inside the feed.",
weights: { recency: 0.12, engagement: 0.08, bridging: 0.42, sourceDiversity: 0.13, relevance: 0.25 },
},