346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
/**
|
|
* rGov module — Modular governance decision circuits (GovMods).
|
|
*
|
|
* Do-ocratic circuit components for multiplayer collaboration around
|
|
* shared goals. Wire together governance primitives on a shared canvas:
|
|
* signoff gates, resource thresholds, tunable knobs, project aggregators,
|
|
* and amendable circuits.
|
|
*/
|
|
|
|
import { Hono } from "hono";
|
|
import { resolve } from "path";
|
|
import { renderShell } from "../../server/shell";
|
|
import { getModuleInfoList } from "../../shared/module";
|
|
import type { RSpaceModule } from "../../shared/module";
|
|
import { renderLanding } from "./landing";
|
|
import { addShapes, getDocumentData } from "../../server/community-store";
|
|
|
|
const routes = new Hono();
|
|
|
|
// ── Canvas content loader (same approach as rspace module) ──
|
|
|
|
const DIST_DIR = resolve(import.meta.dir, "../../dist");
|
|
let canvasCache: { body: string; styles: string; scripts: string } | null = null;
|
|
|
|
function extractCanvasContent(html: string) {
|
|
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
const styleMatches = [...html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi)];
|
|
const scriptMatches = [...html.matchAll(/<script[^>]*>[\s\S]*?<\/script>/gi)];
|
|
return {
|
|
body: bodyMatch?.[1] || "",
|
|
styles: styleMatches.map(m => m[0]).join("\n"),
|
|
scripts: scriptMatches.map(m => m[0]).join("\n"),
|
|
};
|
|
}
|
|
|
|
async function getCanvasContent() {
|
|
if (canvasCache) return canvasCache;
|
|
|
|
const moduleFile = Bun.file(resolve(DIST_DIR, "canvas-module.html"));
|
|
if (await moduleFile.exists()) {
|
|
canvasCache = {
|
|
body: await moduleFile.text(),
|
|
styles: "",
|
|
scripts: `<script type="module" src="/canvas-module.js"></script>`,
|
|
};
|
|
return canvasCache;
|
|
}
|
|
|
|
const fullFile = Bun.file(resolve(DIST_DIR, "canvas.html"));
|
|
if (await fullFile.exists()) {
|
|
canvasCache = extractCanvasContent(await fullFile.text());
|
|
return canvasCache;
|
|
}
|
|
|
|
return {
|
|
body: `<div style="padding:2rem;text-align:center;color:#64748b;">Canvas loading...</div>`,
|
|
styles: "",
|
|
scripts: "",
|
|
};
|
|
}
|
|
|
|
// ── Module page (within a space) — renders canvas directly ──
|
|
|
|
routes.get("/", async (c) => {
|
|
const space = c.req.param("space") || "demo";
|
|
const canvas = await getCanvasContent();
|
|
|
|
return c.html(renderShell({
|
|
title: `${space} — rGov | rSpace`,
|
|
moduleId: "rgov",
|
|
spaceSlug: space,
|
|
modules: getModuleInfoList(),
|
|
theme: "dark",
|
|
body: canvas.body,
|
|
styles: canvas.styles,
|
|
scripts: canvas.scripts,
|
|
}));
|
|
});
|
|
|
|
// ── API: list gov shapes in a space ──
|
|
|
|
routes.get("/api/shapes", (c) => {
|
|
return c.json({
|
|
info: "Gov shapes are stored in the space's Automerge document. Query the canvas shapes map for types listed below.",
|
|
types: [
|
|
"folk-gov-binary",
|
|
"folk-gov-threshold",
|
|
"folk-gov-knob",
|
|
"folk-gov-project",
|
|
"folk-gov-amendment",
|
|
"folk-gov-quadratic",
|
|
"folk-gov-conviction",
|
|
"folk-gov-multisig",
|
|
"folk-gov-sankey",
|
|
],
|
|
});
|
|
});
|
|
|
|
// ── Seed template: example governance circuits ──
|
|
|
|
function seedTemplateGov(space: string) {
|
|
const docData = getDocumentData(space);
|
|
const govTypes = [
|
|
"folk-gov-binary", "folk-gov-threshold", "folk-gov-knob",
|
|
"folk-gov-project", "folk-gov-amendment",
|
|
"folk-gov-quadratic", "folk-gov-conviction", "folk-gov-multisig", "folk-gov-sankey",
|
|
];
|
|
if (docData?.shapes) {
|
|
const existing = Object.values(docData.shapes as Record<string, any>)
|
|
.filter((s: any) => !s.forgotten && govTypes.includes(s.type));
|
|
if (existing.length > 0) return;
|
|
}
|
|
|
|
const now = Date.now();
|
|
|
|
// ── Circuit 1: "Build a Climbing Wall" ──
|
|
// Labor threshold + Capital threshold + Proprietor signoff → Project aggregator
|
|
const laborId = `gov-labor-${now}`;
|
|
const capitalId = `gov-capital-${now}`;
|
|
const signoffId = `gov-signoff-${now}`;
|
|
const projectId = `gov-project-${now}`;
|
|
|
|
// ── Circuit 2: "Community Event Approval" ──
|
|
// Budget knob → Attendance threshold, plus Venue signoff → Event project
|
|
const knobId = `gov-knob-${now}`;
|
|
const attendId = `gov-attend-${now}`;
|
|
const venueId = `gov-venue-${now}`;
|
|
const eventId = `gov-event-${now}`;
|
|
|
|
const baseY = 2600;
|
|
|
|
const shapes: Record<string, unknown>[] = [
|
|
// ── Circuit 1 shapes ──
|
|
{
|
|
id: laborId, type: "folk-gov-threshold",
|
|
x: 50, y: baseY, width: 240, height: 160, rotation: 0,
|
|
title: "Labor Contributed", target: 50, unit: "hours",
|
|
contributions: [
|
|
{ who: "Alice", amount: 12, timestamp: now - 86400000 },
|
|
{ who: "Bob", amount: 8, timestamp: now - 43200000 },
|
|
],
|
|
},
|
|
{
|
|
id: capitalId, type: "folk-gov-threshold",
|
|
x: 50, y: baseY + 200, width: 240, height: 160, rotation: 0,
|
|
title: "Capital Raised", target: 3000, unit: "$",
|
|
contributions: [
|
|
{ who: "Community Fund", amount: 1500, timestamp: now - 172800000 },
|
|
{ who: "Sponsor A", amount: 500, timestamp: now - 86400000 },
|
|
],
|
|
},
|
|
{
|
|
id: signoffId, type: "folk-gov-binary",
|
|
x: 50, y: baseY + 440, width: 240, height: 120, rotation: 0,
|
|
title: "Proprietor Approval", assignee: "Dana (proprietor)",
|
|
satisfied: false, signedBy: "", timestamp: 0,
|
|
},
|
|
{
|
|
id: projectId, type: "folk-gov-project",
|
|
x: 400, y: baseY + 140, width: 300, height: 240, rotation: 0,
|
|
title: "Build a Climbing Wall",
|
|
description: "Community climbing wall project — requires labor, capital, and proprietor signoff.",
|
|
status: "active",
|
|
},
|
|
// Arrows wiring Circuit 1
|
|
{
|
|
id: `gov-arrow-labor-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: laborId, targetId: projectId, color: "#0891b2",
|
|
},
|
|
{
|
|
id: `gov-arrow-capital-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: capitalId, targetId: projectId, color: "#0891b2",
|
|
},
|
|
{
|
|
id: `gov-arrow-signoff-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: signoffId, targetId: projectId, color: "#7c3aed",
|
|
},
|
|
|
|
// ── Circuit 2 shapes ──
|
|
{
|
|
id: knobId, type: "folk-gov-knob",
|
|
x: 800, y: baseY, width: 200, height: 170, rotation: 0,
|
|
title: "Event Budget", min: 100, max: 5000, step: 100,
|
|
value: 1500, unit: "$", cooldown: 0,
|
|
},
|
|
{
|
|
id: attendId, type: "folk-gov-threshold",
|
|
x: 800, y: baseY + 210, width: 240, height: 160, rotation: 0,
|
|
title: "RSVPs Collected", target: 20, unit: "people",
|
|
contributions: [
|
|
{ who: "Mailing list", amount: 14, timestamp: now - 86400000 },
|
|
],
|
|
},
|
|
{
|
|
id: venueId, type: "folk-gov-binary",
|
|
x: 800, y: baseY + 410, width: 240, height: 120, rotation: 0,
|
|
title: "Venue Confirmed", assignee: "Events Team",
|
|
satisfied: true, signedBy: "Carlos", timestamp: now - 43200000,
|
|
},
|
|
{
|
|
id: eventId, type: "folk-gov-project",
|
|
x: 1140, y: baseY + 140, width: 300, height: 240, rotation: 0,
|
|
title: "Community Potluck",
|
|
description: "Monthly community potluck — needs budget, RSVPs, and venue confirmation.",
|
|
status: "active",
|
|
},
|
|
// Arrows wiring Circuit 2
|
|
{
|
|
id: `gov-arrow-knob-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: knobId, targetId: eventId, color: "#b45309",
|
|
},
|
|
{
|
|
id: `gov-arrow-attend-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: attendId, targetId: eventId, color: "#0891b2",
|
|
},
|
|
{
|
|
id: `gov-arrow-venue-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: venueId, targetId: eventId, color: "#7c3aed",
|
|
},
|
|
];
|
|
|
|
// ── Circuit 3: "Delegated Budget Approval" ──
|
|
// Quadratic transform → Conviction gate, plus 3-of-5 Multisig → Project, plus Sankey visualizer
|
|
const quadId = `gov-quad-${now}`;
|
|
const convId = `gov-conv-${now}`;
|
|
const msigId = `gov-msig-${now}`;
|
|
const budgetProjId = `gov-budgetproj-${now}`;
|
|
const sankeyId = `gov-sankey-${now}`;
|
|
|
|
const c3BaseY = baseY + 700;
|
|
|
|
shapes.push(
|
|
// Quadratic weight transform
|
|
{
|
|
id: quadId, type: "folk-gov-quadratic",
|
|
x: 1600, y: c3BaseY, width: 240, height: 160, rotation: 0,
|
|
title: "Vote Weight Dampener", mode: "sqrt",
|
|
entries: [
|
|
{ who: "Whale", raw: 100, effective: 10 },
|
|
{ who: "Alice", raw: 4, effective: 2 },
|
|
{ who: "Bob", raw: 1, effective: 1 },
|
|
],
|
|
},
|
|
// Conviction accumulator
|
|
{
|
|
id: convId, type: "folk-gov-conviction",
|
|
x: 1600, y: c3BaseY + 200, width: 240, height: 200, rotation: 0,
|
|
title: "Community Support", convictionMode: "gate", threshold: 5,
|
|
stakes: [
|
|
{ userId: "u1", userName: "Alice", optionId: "gate", weight: 2, since: now - 7200000 },
|
|
{ userId: "u2", userName: "Bob", optionId: "gate", weight: 1, since: now - 3600000 },
|
|
],
|
|
},
|
|
// Multisig 3-of-5
|
|
{
|
|
id: msigId, type: "folk-gov-multisig",
|
|
x: 1600, y: c3BaseY + 440, width: 260, height: 220, rotation: 0,
|
|
title: "Council Approval", requiredM: 3,
|
|
signers: [
|
|
{ name: "Alice", signed: true, timestamp: now - 86400000 },
|
|
{ name: "Bob", signed: true, timestamp: now - 43200000 },
|
|
{ name: "Carol", signed: false, timestamp: 0 },
|
|
{ name: "Dave", signed: false, timestamp: 0 },
|
|
{ name: "Eve", signed: false, timestamp: 0 },
|
|
],
|
|
},
|
|
// Project aggregator
|
|
{
|
|
id: budgetProjId, type: "folk-gov-project",
|
|
x: 1960, y: c3BaseY + 180, width: 300, height: 240, rotation: 0,
|
|
title: "Delegated Budget Approval",
|
|
description: "Budget approval with quadratic dampening, time-weighted conviction, and council multisig.",
|
|
status: "active",
|
|
},
|
|
// Sankey visualizer
|
|
{
|
|
id: sankeyId, type: "folk-gov-sankey",
|
|
x: 2320, y: c3BaseY + 100, width: 380, height: 300, rotation: 0,
|
|
title: "Governance Flow",
|
|
},
|
|
// Arrows wiring Circuit 3
|
|
{
|
|
id: `gov-arrow-quad-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: quadId, targetId: budgetProjId, color: "#14b8a6",
|
|
},
|
|
{
|
|
id: `gov-arrow-conv-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: convId, targetId: budgetProjId, color: "#d97706",
|
|
},
|
|
{
|
|
id: `gov-arrow-msig-${now}`, type: "folk-arrow",
|
|
x: 0, y: 0, width: 0, height: 0, rotation: 0,
|
|
sourceId: msigId, targetId: budgetProjId, color: "#6366f1",
|
|
},
|
|
);
|
|
|
|
addShapes(space, shapes);
|
|
console.log(`[rGov] Template seeded for "${space}": 3 circuits (13 shapes + 9 arrows)`);
|
|
}
|
|
|
|
// ── Module export ──
|
|
|
|
export const govModule: RSpaceModule = {
|
|
id: "rgov",
|
|
name: "rGov",
|
|
icon: "⚖️",
|
|
description: "Modular governance decision circuits (GovMods)",
|
|
routes,
|
|
scoping: { defaultScope: "space", userConfigurable: false },
|
|
landingPage: renderLanding,
|
|
seedTemplate: seedTemplateGov,
|
|
canvasShapes: [
|
|
"folk-gov-binary",
|
|
"folk-gov-threshold",
|
|
"folk-gov-knob",
|
|
"folk-gov-project",
|
|
"folk-gov-amendment",
|
|
"folk-gov-quadratic",
|
|
"folk-gov-conviction",
|
|
"folk-gov-multisig",
|
|
"folk-gov-sankey",
|
|
],
|
|
canvasToolIds: [
|
|
"create_binary_gate",
|
|
"create_threshold",
|
|
"create_gov_knob",
|
|
"create_gov_project",
|
|
"create_amendment",
|
|
"create_quadratic_transform",
|
|
"create_conviction_gate",
|
|
"create_multisig_gate",
|
|
"create_sankey_visualizer",
|
|
],
|
|
onboardingActions: [
|
|
{ label: "Build a Circuit", icon: "⚖️", description: "Create a governance decision circuit on the canvas", type: 'create', href: '/rgov' },
|
|
],
|
|
};
|