From 4c44eb9941a05113f8b7e3543f2b0ae6c605f675 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 11 Mar 2026 20:51:30 -0700 Subject: [PATCH] feat(rnetwork): enhanced trust flow viz + rename authorities to gov/fin/dev-ops - Compute delegatedWeight per node from delegation edges (both directions) - Animated directional particles on delegation edges (count/size ~ weight) - Wider delegation edges (1+weight*8) with 0.15 curvature - "All" authority overlay mode with per-authority colored edges - Rename 5 authorities (voting/moderation/curation/treasury/membership) to 3 verticals: gov-ops, fin-ops, dev-ops - DB migration: update CHECK constraint + migrate existing data - Update all frontend components + backend defaults Co-Authored-By: Claude Opus 4.6 --- .../components/folk-delegation-manager.ts | 12 ++- .../rnetwork/components/folk-graph-viewer.ts | 87 ++++++++++++++++--- .../rnetwork/components/folk-trust-sankey.ts | 6 +- src/encryptid/db.ts | 2 +- src/encryptid/schema.sql | 25 +++++- src/encryptid/server.ts | 4 +- src/encryptid/trust-engine.ts | 2 +- 7 files changed, 113 insertions(+), 25 deletions(-) diff --git a/modules/rnetwork/components/folk-delegation-manager.ts b/modules/rnetwork/components/folk-delegation-manager.ts index c37d41b..1c2d176 100644 --- a/modules/rnetwork/components/folk-delegation-manager.ts +++ b/modules/rnetwork/components/folk-delegation-manager.ts @@ -1,7 +1,7 @@ /** * — per-vertical delegation management UI. * - * Shows bars for each authority vertical (voting, moderation, curation, treasury, membership) + * Shows bars for each authority vertical (gov-ops, fin-ops, dev-ops) * with percentage allocated and delegate avatars. Supports create, edit, revoke. * Weight sum validated client-side before submission. */ @@ -30,13 +30,11 @@ interface SpaceUser { role: string; } -const AUTHORITIES = ["voting", "moderation", "curation", "treasury", "membership"] as const; +const AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const; const AUTHORITY_ICONS: Record = { - voting: "\u{1F5F3}\uFE0F", - moderation: "\u{1F6E1}\uFE0F", - curation: "\u2728", - treasury: "\u{1F4B0}", - membership: "\u{1F465}", + "gov-ops": "\u{1F3DB}\uFE0F", + "fin-ops": "\u{1F4B0}", + "dev-ops": "\u{1F528}", }; class FolkDelegationManager extends HTMLElement { diff --git a/modules/rnetwork/components/folk-graph-viewer.ts b/modules/rnetwork/components/folk-graph-viewer.ts index 9164af4..a1ab87f 100644 --- a/modules/rnetwork/components/folk-graph-viewer.ts +++ b/modules/rnetwork/components/folk-graph-viewer.ts @@ -15,6 +15,7 @@ interface GraphNode { location?: string; description?: string; trustScore?: number; + delegatedWeight?: number; // 0-1 normalized, computed from delegation edges // 3d-force-graph internal properties x?: number; y?: number; @@ -27,10 +28,19 @@ interface GraphEdge { type: string; label?: string; weight?: number; + authority?: string; } -const DELEGATION_AUTHORITIES = ["voting", "moderation", "curation", "treasury", "membership"] as const; +const DELEGATION_AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const; type DelegationAuthority = typeof DELEGATION_AUTHORITIES[number]; +type AuthoritySelection = "all" | DelegationAuthority; + +// Per-authority edge colors for "all" overlay mode (governance, economics, technology) +const AUTHORITY_COLORS: Record = { + "gov-ops": "#a78bfa", // purple — governance decisions + "fin-ops": "#fbbf24", // amber — economic/financial decisions + "dev-ops": "#34d399", // green — technical decisions +}; // Node colors by type const NODE_COLORS: Record = { @@ -63,7 +73,7 @@ class FolkGraphViewer extends HTMLElement { private error = ""; private selectedNode: GraphNode | null = null; private trustMode = false; - private authority: DelegationAuthority = "voting"; + private authority: AuthoritySelection = "gov-ops"; // 3D graph instance private graph: any = null; @@ -102,7 +112,7 @@ class FolkGraphViewer extends HTMLElement { private async loadData() { const base = this.getApiBase(); try { - const trustParam = this.trustMode ? `?trust=true&authority=${this.authority}` : ""; + const trustParam = this.trustMode ? `?trust=true&authority=${encodeURIComponent(this.authority)}` : ""; const [wsRes, infoRes, graphRes] = await Promise.all([ fetch(`${base}/api/workspaces`), fetch(`${base}/api/info`), @@ -121,7 +131,7 @@ class FolkGraphViewer extends HTMLElement { this.updateGraphData(); } - private async reloadWithAuthority(authority: DelegationAuthority) { + private async reloadWithAuthority(authority: AuthoritySelection) { this.authority = authority; this.trustMode = true; await this.loadData(); @@ -169,8 +179,27 @@ class FolkGraphViewer extends HTMLElement { type: edgeTypeMap[e.type] || e.type, label: e.label, weight: e.weight, + authority: e.authority, } as GraphEdge)); + // Compute delegatedWeight for every node from delegation edges + const nodeWeights = new Map(); + for (const e of this.edges) { + if (e.type !== "delegates_to") continue; + const sid = typeof e.source === "string" ? e.source : e.source.id; + const tid = typeof e.target === "string" ? e.target : e.target.id; + const w = e.weight || 0.5; + nodeWeights.set(sid, (nodeWeights.get(sid) || 0) + w); + nodeWeights.set(tid, (nodeWeights.get(tid) || 0) + w); + } + const maxWeight = Math.max(...nodeWeights.values(), 1); + for (const node of this.nodes) { + const w = nodeWeights.get(node.id); + if (w != null) { + node.delegatedWeight = w / maxWeight; + } + } + // Assign company colors const companies = this.nodes.filter(n => n.type === "company"); this.companyColors.clear(); @@ -215,8 +244,14 @@ class FolkGraphViewer extends HTMLElement { private getNodeRadius(node: GraphNode): number { if (node.type === "company") return 22; - if (node.trustScore != null && this.trustMode) { - return 8 + (node.trustScore * 22); + if (this.trustMode) { + // Prefer edge-computed delegatedWeight, fall back to trustScore + if (node.delegatedWeight != null) { + return 6 + node.delegatedWeight * 24; + } + if (node.trustScore != null) { + return 6 + node.trustScore * 24; + } } return 12; } @@ -308,7 +343,7 @@ class FolkGraphViewer extends HTMLElement { .authority-btn:hover { border-color: var(--rs-border-strong); } .authority-btn.active { border-color: #a78bfa; color: #a78bfa; background: rgba(167, 139, 250, 0.1); } - .legend { display: flex; gap: 16px; margin-top: 12px; } + .legend { display: flex; gap: 16px; margin-top: 12px; flex-wrap: wrap; } .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--rs-text-muted); } .legend-dot { width: 10px; height: 10px; border-radius: 50%; } .dot-person { background: #3b82f6; } @@ -387,6 +422,7 @@ class FolkGraphViewer extends HTMLElement {
+ ${DELEGATION_AUTHORITIES.map(a => ``).join("")}
@@ -409,6 +445,11 @@ class FolkGraphViewer extends HTMLElement {
Works at
Point of contact
+
@@ -449,7 +490,7 @@ class FolkGraphViewer extends HTMLElement { // Authority buttons this.shadow.querySelectorAll("[data-authority]").forEach(el => { el.addEventListener("click", () => { - const authority = (el as HTMLElement).dataset.authority as DelegationAuthority; + const authority = (el as HTMLElement).dataset.authority as AuthoritySelection; this.reloadWithAuthority(authority); }); }); @@ -514,21 +555,45 @@ class FolkGraphViewer extends HTMLElement { .linkSource("source") .linkTarget("target") .linkColor((link: GraphEdge) => { + if (link.type === "delegates_to") { + if (this.authority === "all" && link.authority) { + return AUTHORITY_COLORS[link.authority] || EDGE_STYLES.delegates_to.color; + } + return EDGE_STYLES.delegates_to.color; + } const style = EDGE_STYLES[link.type] || EDGE_STYLES.default; return style.color; }) .linkWidth((link: GraphEdge) => { if (link.type === "delegates_to") { - return 1 + (link.weight || 0.5) * 3; + return 1 + (link.weight || 0.5) * 8; } const style = EDGE_STYLES[link.type] || EDGE_STYLES.default; return style.width; }) + .linkCurvature((link: GraphEdge) => + link.type === "delegates_to" ? 0.15 : 0 + ) + .linkCurveRotation("rotation") .linkOpacity(0.6) .linkDirectionalArrowLength((link: GraphEdge) => link.type === "delegates_to" ? 4 : 0 ) .linkDirectionalArrowRelPos(1) + .linkDirectionalParticles((link: GraphEdge) => + link.type === "delegates_to" ? Math.ceil((link.weight || 0.5) * 4) : 0 + ) + .linkDirectionalParticleSpeed(0.004) + .linkDirectionalParticleWidth((link: GraphEdge) => + link.type === "delegates_to" ? 1 + (link.weight || 0.5) * 2 : 0 + ) + .linkDirectionalParticleColor((link: GraphEdge) => { + if (link.type !== "delegates_to") return null; + if (this.authority === "all" && link.authority) { + return AUTHORITY_COLORS[link.authority] || "#c4b5fd"; + } + return "#c4b5fd"; + }) .linkLineDash((link: GraphEdge) => { const style = EDGE_STYLES[link.type] || EDGE_STYLES.default; return style.dashed ? [4, 2] : null; @@ -745,8 +810,10 @@ class FolkGraphViewer extends HTMLElement { // Update legend visibility for trust mode const membersLegend = this.shadow.getElementById("legend-members"); const delegatesLegend = this.shadow.getElementById("legend-delegates"); + const authorityColors = this.shadow.getElementById("legend-authority-colors"); if (membersLegend) membersLegend.style.display = this.trustMode ? "" : "none"; - if (delegatesLegend) delegatesLegend.style.display = this.trustMode ? "" : "none"; + if (delegatesLegend) delegatesLegend.style.display = (this.trustMode && this.authority !== "all") ? "" : "none"; + if (authorityColors) authorityColors.style.display = (this.trustMode && this.authority === "all") ? "" : "none"; // Fit view after data settles setTimeout(() => { diff --git a/modules/rnetwork/components/folk-trust-sankey.ts b/modules/rnetwork/components/folk-trust-sankey.ts index 73f820f..be9ac7c 100644 --- a/modules/rnetwork/components/folk-trust-sankey.ts +++ b/modules/rnetwork/components/folk-trust-sankey.ts @@ -29,13 +29,13 @@ interface TrustEvent { createdAt: number; } -const SANKEY_AUTHORITIES = ["voting", "moderation", "curation", "treasury", "membership"] as const; +const SANKEY_AUTHORITIES = ["gov-ops", "fin-ops", "dev-ops"] as const; const FLOW_COLOR = "#a78bfa"; class FolkTrustSankey extends HTMLElement { private shadow: ShadowRoot; private space = ""; - private authority = "voting"; + private authority = "gov-ops"; private flows: DelegationFlow[] = []; private events: TrustEvent[] = []; private loading = true; @@ -50,7 +50,7 @@ class FolkTrustSankey extends HTMLElement { connectedCallback() { this.space = this.getAttribute("space") || "demo"; - this.authority = this.getAttribute("authority") || "voting"; + this.authority = this.getAttribute("authority") || "gov-ops"; this.render(); this.loadData(); } diff --git a/src/encryptid/db.ts b/src/encryptid/db.ts index 13a8a42..cfc4a7e 100644 --- a/src/encryptid/db.ts +++ b/src/encryptid/db.ts @@ -1727,7 +1727,7 @@ export async function updatePushSubscriptionLastUsed(id: string): Promise // DELEGATIONS (person-to-person liquid democracy) // ============================================================================ -export type DelegationAuthority = 'voting' | 'moderation' | 'curation' | 'treasury' | 'membership' | 'custom'; +export type DelegationAuthority = 'gov-ops' | 'fin-ops' | 'dev-ops' | 'custom'; export type DelegationState = 'active' | 'paused' | 'revoked'; export interface StoredDelegation { diff --git a/src/encryptid/schema.sql b/src/encryptid/schema.sql index 9d58586..a510ca8 100644 --- a/src/encryptid/schema.sql +++ b/src/encryptid/schema.sql @@ -376,7 +376,7 @@ CREATE TABLE IF NOT EXISTS delegations ( id TEXT PRIMARY KEY, delegator_did TEXT NOT NULL, delegate_did TEXT NOT NULL, - authority TEXT NOT NULL CHECK (authority IN ('voting', 'moderation', 'curation', 'treasury', 'membership', 'custom')), + authority TEXT NOT NULL CHECK (authority IN ('gov-ops', 'fin-ops', 'dev-ops', 'custom')), weight REAL NOT NULL CHECK (weight > 0 AND weight <= 1), max_depth INTEGER NOT NULL DEFAULT 3, retain_authority BOOLEAN NOT NULL DEFAULT TRUE, @@ -452,3 +452,26 @@ CREATE TABLE IF NOT EXISTS legacy_identities ( CREATE INDEX IF NOT EXISTS idx_legacy_identities_user ON legacy_identities(user_id); CREATE INDEX IF NOT EXISTS idx_legacy_identities_pubkey ON legacy_identities(legacy_public_key_hash); + +-- ============================================================================ +-- MIGRATION: Rename authority verticals (voting/moderation/curation/treasury/membership → gov-ops/fin-ops/dev-ops) +-- ============================================================================ + +-- Migrate existing delegation data +UPDATE delegations SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation'); +UPDATE delegations SET authority = 'fin-ops' WHERE authority = 'treasury'; +UPDATE delegations SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership'); + +-- Migrate existing trust_scores data +UPDATE trust_scores SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation'); +UPDATE trust_scores SET authority = 'fin-ops' WHERE authority = 'treasury'; +UPDATE trust_scores SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership'); + +-- Migrate existing trust_events data +UPDATE trust_events SET authority = 'gov-ops' WHERE authority IN ('voting', 'moderation'); +UPDATE trust_events SET authority = 'fin-ops' WHERE authority = 'treasury'; +UPDATE trust_events SET authority = 'dev-ops' WHERE authority IN ('curation', 'membership'); + +-- Update CHECK constraint on delegations table +ALTER TABLE delegations DROP CONSTRAINT IF EXISTS delegations_authority_check; +ALTER TABLE delegations ADD CONSTRAINT delegations_authority_check CHECK (authority IN ('gov-ops', 'fin-ops', 'dev-ops', 'custom')); diff --git a/src/encryptid/server.ts b/src/encryptid/server.ts index 853adec..c53ce60 100644 --- a/src/encryptid/server.ts +++ b/src/encryptid/server.ts @@ -7122,7 +7122,7 @@ app.get('/', (c) => { // DELEGATION ROUTES (person-to-person liquid democracy) // ============================================================================ -const VALID_AUTHORITIES = ['voting', 'moderation', 'curation', 'treasury', 'membership', 'custom']; +const VALID_AUTHORITIES = ['gov-ops', 'fin-ops', 'dev-ops', 'custom']; // POST /api/delegations — create a new delegation app.post('/api/delegations', async (c) => { @@ -7359,7 +7359,7 @@ app.get('/api/delegations/space', async (c) => { // GET /api/trust/scores — aggregated trust scores for visualization app.get('/api/trust/scores', async (c) => { - const authority = c.req.query('authority') || 'voting'; + const authority = c.req.query('authority') || 'gov-ops'; const spaceSlug = c.req.query('space'); if (!spaceSlug) return c.json({ error: 'space query parameter required' }, 400); diff --git a/src/encryptid/trust-engine.ts b/src/encryptid/trust-engine.ts index 46aeca8..0d3df4a 100644 --- a/src/encryptid/trust-engine.ts +++ b/src/encryptid/trust-engine.ts @@ -167,7 +167,7 @@ export function computeTrustScores( // ── Background Recomputation ── -const AUTHORITIES: DelegationAuthority[] = ['voting', 'moderation', 'curation', 'treasury', 'membership', 'custom']; +const AUTHORITIES: DelegationAuthority[] = ['gov-ops', 'fin-ops', 'dev-ops', 'custom']; /** * Recompute all trust scores for a single space.