From 3bea9dc6abdc5a8b44a583392008ffb8acec8256 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Mon, 27 Jul 2026 10:33:13 +0200 Subject: [PATCH 1/3] feat(roadmap): public /roadmap page with a workstream timeline Adds a dedicated Roadmap page rendering the 12-month plan as a workstream timeline, so the roadmap is legible without reading ROADMAP.md. Layout follows the existing public-page pattern (thin page + component under components/pages/homepage, Reveal for staggered entrance, the shared Table and Tooltip primitives). Content lives in data.ts so the plan can be edited without touching layout code. The timeline is grouped by workstream rather than by owner so the continuity from delivered to planned stays visible; owner initials ride on each bar as a corner badge. Status is encoded by fill, icon and label together, never colour alone. Emerald and amber are used for the two chart states because they stay distinguishable under the common forms of colour blindness, where emerald/red would not; red is reserved for the single critical callout and is never a chart colour. "Planned" is a dashed outline rather than a fourth hue, since absence of status reads better as an outline than as grey fill. Three layout details worth noting, each verified in the browser: - Every grid cell is placed explicitly. The "today" rule spans all rows, and a grid item with a definite position is packed before auto-placed siblings, which otherwise shifted the month headers a column right. - The page root needs w-full + min-w-0. It is a flex item, and mx-auto suppresses flex stretch, which left the box sized to the grid's min-content and pushed the whole page wider than the viewport instead of letting the grid scroll on its own. - Bar labels need min-w-0 so a long word wraps inside the bar instead of spilling into the next month. Wires the route into the SEO registry, the sitemap, the public-route allowlist, the no-JS crawler fallback and the footer. Verified at 375px and 1440px, in light and dark: no page-level horizontal overflow, no bar overlaps, no clipped labels. Co-Authored-By: Claude Fable 5 --- .../common/overall-layout/site-footer.tsx | 1 + .../pages/homepage/roadmap/.next/trace | 1 + .../pages/homepage/roadmap/.next/trace-build | 1 + src/components/pages/homepage/roadmap/data.ts | 517 ++++++++++++++++++ .../pages/homepage/roadmap/index.tsx | 417 ++++++++++++++ src/components/ui/seo-fallback.tsx | 1 + src/data/public-routes.ts | 1 + src/lib/seo.ts | 6 + src/pages/roadmap.tsx | 7 + 9 files changed, 952 insertions(+) create mode 100644 src/components/pages/homepage/roadmap/.next/trace create mode 100644 src/components/pages/homepage/roadmap/.next/trace-build create mode 100644 src/components/pages/homepage/roadmap/data.ts create mode 100644 src/components/pages/homepage/roadmap/index.tsx create mode 100644 src/pages/roadmap.tsx diff --git a/src/components/common/overall-layout/site-footer.tsx b/src/components/common/overall-layout/site-footer.tsx index a85f28a5..aa160038 100644 --- a/src/components/common/overall-layout/site-footer.tsx +++ b/src/components/common/overall-layout/site-footer.tsx @@ -6,6 +6,7 @@ const productLinks = [ { label: "Governance", href: "/governance" }, { label: "DRep Explorer", href: "/governance/drep" }, { label: "Import a wallet", href: "/wallets/import-wallet" }, + { label: "Roadmap", href: "/roadmap" }, { label: "Blog", href: "/blog" }, ]; diff --git a/src/components/pages/homepage/roadmap/.next/trace b/src/components/pages/homepage/roadmap/.next/trace new file mode 100644 index 00000000..70b74904 --- /dev/null +++ b/src/components/pages/homepage/roadmap/.next/trace @@ -0,0 +1 @@ +[{"name":"generate-buildid","duration":181,"timestamp":291208474266,"id":4,"parentId":1,"tags":{},"startTime":1785140968470,"traceId":"08b1d61d3f5984a0"},{"name":"load-custom-routes","duration":1051,"timestamp":291208474506,"id":5,"parentId":1,"tags":{},"startTime":1785140968470,"traceId":"08b1d61d3f5984a0"},{"name":"create-dist-dir","duration":5435,"timestamp":291208475573,"id":6,"parentId":1,"tags":{},"startTime":1785140968471,"traceId":"08b1d61d3f5984a0"},{"name":"clean","duration":504,"timestamp":291208481602,"id":7,"parentId":1,"tags":{},"startTime":1785140968477,"traceId":"08b1d61d3f5984a0"},{"name":"next-build","duration":33296,"timestamp":291208448914,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"webpack","failed":true},"startTime":1785140968444,"traceId":"08b1d61d3f5984a0"}] diff --git a/src/components/pages/homepage/roadmap/.next/trace-build b/src/components/pages/homepage/roadmap/.next/trace-build new file mode 100644 index 00000000..deb3a832 --- /dev/null +++ b/src/components/pages/homepage/roadmap/.next/trace-build @@ -0,0 +1 @@ +[{"name":"next-build","duration":33296,"timestamp":291208448914,"id":1,"tags":{"buildMode":"default","version":"16.2.6","bundler":"webpack","failed":true},"startTime":1785140968444,"traceId":"08b1d61d3f5984a0"}] diff --git a/src/components/pages/homepage/roadmap/data.ts b/src/components/pages/homepage/roadmap/data.ts new file mode 100644 index 00000000..c2a975c9 --- /dev/null +++ b/src/components/pages/homepage/roadmap/data.ts @@ -0,0 +1,517 @@ +/** + * Roadmap content, kept apart from the view so the page stays declarative and + * the plan can be edited without touching layout code. + * + * The source of record is ROADMAP.md at the repo root — when a month lands or + * slips, update both. `status` here reflects the `preprod` branch, which is not + * the same as what is live in production (see RELEASE_GAP). + */ + +/** Twelve months of the plan, left to right. `index` is the grid column offset. */ +export const MONTHS = [ + { short: "May", year: "2026" }, + { short: "Jun", year: "2026" }, + { short: "Jul", year: "2026" }, + { short: "Aug", year: "2026" }, + { short: "Sep", year: "2026" }, + { short: "Oct", year: "2026" }, + { short: "Nov", year: "2026" }, + { short: "Dec", year: "2026" }, + { short: "Jan", year: "2027" }, + { short: "Feb", year: "2027" }, + { short: "Mar", year: "2027" }, + { short: "Apr", year: "2027" }, +] as const; + +/** 1-based month number the plan has reached; the "today" rule sits at its right edge. */ +export const CURRENT_MONTH = 3; // July 2026 + +export type Status = "done" | "risk" | "planned"; + +export type RoadmapItem = { + label: string; + /** 1-based month the bar starts in. */ + start: number; + /** Number of months the bar covers. */ + span: number; + status: Status; + /** "Q", "A", or "Q·A". Omitted where the month carries no single owner. */ + owner?: string; + detail: string; +}; + +export type Track = { + name: string; + sub: string; + items: RoadmapItem[]; +}; + +export const TRACKS: Track[] = [ + { + name: "Release & production health", + sub: "Blocking", + items: [ + { + label: "Gap opens", + start: 3, + span: 1, + status: "risk", + detail: + "Production stuck at the 2026-05-10 migration. The 17 June deploy run failed, and the path-filtered workflow never re-fired.", + }, + { + label: "Release", + start: 4, + span: 1, + status: "risk", + owner: "Q", + detail: + "August, first task: merge preprod into main, dispatch Deploy Database Migrations, confirm all four apply. Closes the RLS exposure and un-breaks governance tallies.", + }, + { + label: "Drift gate", + start: 5, + span: 1, + status: "planned", + owner: "Q", + detail: + "A post-deploy prisma migrate status gate with drift alerting, so merged and applied cannot silently diverge again.", + }, + ], + }, + { + name: "Document Sign-Off", + sub: "Flagship", + items: [ + { + label: "MVP — build & ship", + start: 4, + span: 2, + status: "planned", + owner: "Q·A", + detail: + "Finalize PRD-001 (still Draft), then a five-model schema, tRPC routes, CIP-8 enforcement, version-hash binding, Documents UI, six-state lifecycle and proof export. Ready = a pilot team runs all six user stories unaided.", + }, + { + label: "v1 history", + start: 6, + span: 1, + status: "planned", + owner: "Q", + detail: + "v1: revision history first-class, diff and rollback, richer audit export. Still off-chain.", + }, + { + label: "v2 anchor", + start: 9, + span: 1, + status: "planned", + owner: "Q", + detail: + "v2 Checkpoints: opt-in on-chain anchoring of a version hash plus its parent in Cardano transaction metadata.", + }, + { + label: "v3 research", + start: 11, + span: 1, + status: "planned", + owner: "A", + detail: + "v3 Collaboration and standards: CRDT co-authoring, a CIP candidate, an eIDAS/EUDI QES bridge. Scoped as research.", + }, + ], + }, + { + name: "Governance", + sub: "DReps · voting", + items: [ + { + label: "Voting", + start: 2, + span: 1, + status: "done", + owner: "Q", + detail: + "In-app voting for multisig DReps: Ekklesia/Hydra budget voting, DRep-registration detection, ballot UX, DB-cached tallies. Closed #122 five months early.", + }, + { + label: "DRep explorer", + start: 3, + span: 1, + status: "done", + owner: "Q", + detail: + "Public DRep vote-history explorer, no wallet required. CIP-100/136 rationales resolved from IPFS, nine-column CSV export, served through a Koios proxy.", + }, + { + label: "Proxy", + start: 8, + span: 1, + status: "planned", + owner: "Q", + detail: + "Proxy voting polish and documentation, plus the collateral service for proxy usage (#221) — the last backlog item without a slot.", + }, + ], + }, + { + name: "Bot & agent platform", + sub: "Early · M7 freed", + items: [ + { + label: "Bot platform", + start: 3, + span: 1, + status: "done", + owner: "Q", + detail: + "Arrived about four months early. Human-in-the-loop onboarding, five scopes plus per-wallet grants, 27 bot-accessible endpoints, rate limiting and ballot drafting. A bot still cannot move funds alone.", + }, + { + label: "Webhooks", + start: 7, + span: 1, + status: "planned", + owner: "A", + detail: + "All that remains of Bot platform v2 is webhooks — no webhook code exists yet. Plus a multisig MCP server, so an agent can act as observer or ballot drafter.", + }, + ], + }, + { + name: "Wallets & discovery", + sub: "Wallet V2 · devices", + items: [ + { + label: "Legacy", + start: 1, + span: 1, + status: "done", + owner: "Q", + detail: + "Legacy wallet compatibility fixed — DRep retirement and deregistration (#210, #225); issue #223 closed.", + }, + { + label: "Wallet V2", + start: 3, + span: 1, + status: "done", + owner: "A", + detail: + "On-chain wallet registration and discovery shipped in #340, on schedule.", + }, + { + label: "Discover", + start: 5, + span: 1, + status: "planned", + owner: "A", + detail: + "The Discover page moved up from M10 to ride the delivered Wallet V2 discovery work — plus lookup by signer and policy.", + }, + { + label: "Hardware", + start: 6, + span: 1, + status: "planned", + owner: "A", + detail: + "Ledger and Trezor. Their CIP-8 signData support is limited and Sign-Off approvals depend on it — scope that constraint during the Aug–Sep build, not after.", + }, + { + label: "Invites", + start: 10, + span: 1, + status: "planned", + owner: "Q", + detail: "Invite flow (PR #67).", + }, + ], + }, + { + name: "Reliability & CI", + sub: "Tests · pipeline", + items: [ + { + label: "Smoke CI & preprod", + start: 1, + span: 2, + status: "done", + owner: "A", + detail: + "Preprod environment, real-chain smoke system (#213 closed), deploy-migrations on Node 22, pg pool cap, dependency hardening.", + }, + { + label: "E2E suite", + start: 3, + span: 1, + status: "done", + owner: "A", + detail: + "Playwright E2E: 11 spec files, about 54 tests — wallet creation, real preprod ring transfers, staking, proxy, DRep and ballot UI, bot management, access control. Runs in Docker.", + }, + { + label: "Unblock CI", + start: 4, + span: 1, + status: "risk", + owner: "A", + detail: + "pr-multisig-v1-smoke.yml hard-fails when secrets are absent, and dependabot runs never receive them — so all seven open dependency PRs are red for systemic reasons. ci-smoke-preprod.yml already has the guard to copy.", + }, + { + label: "Test depth", + start: 5, + span: 1, + status: "planned", + owner: "Q", + detail: + "Extend Playwright to the Sign-Off flows; transaction-builder and tRPC integration tests (#255).", + }, + ], + }, + { + name: "Notifications", + sub: "Email · outbox", + items: [ + { + label: "Notification center", + start: 2, + span: 2, + status: "done", + owner: "A", + detail: + "Resend-backed email with a real outbox: idempotency keys, retry backoff, nine delivery statuses, per-wallet and per-signer settings, hashed-token verification.", + }, + { + label: "Drain", + start: 4, + span: 1, + status: "planned", + owner: "A", + detail: + "#327 follow-ups, Playwright coverage, and a scheduled drain — no cron currently runs drainNotificationOutbox.", + }, + { + label: "Digests", + start: 5, + span: 1, + status: "planned", + owner: "A", + detail: + "Ballot-deadline and threshold-reached reminders on the existing outbox — product work, since the infrastructure already exists.", + }, + ], + }, + { + name: "Platform & UX", + sub: "Mesh 2.0 · interface", + items: [ + { + label: "UX base", + start: 2, + span: 1, + status: "done", + owner: "Q", + detail: + "Signing and auth reliability, mobile foundations, skeleton and empty states, error toasts, pagination, landing, SEO and the glass theme.", + }, + { + label: "Mesh 2.0 — awaiting upstream 2.x", + start: 3, + span: 3, + status: "risk", + owner: "Q", + detail: + "Blocked upstream — npm latest for @meshsdk/core is still 1.9.1; no 2.x exists. Our side is ready. Demoted from a monthly task to a standing watch item.", + }, + { + label: "Papercuts", + start: 6, + span: 1, + status: "planned", + owner: "A", + detail: + "UX papercut batch: full-address verification (#196), transaction pagination (#30), a better 404 page (#22).", + }, + { + label: "dApp link", + start: 7, + span: 1, + status: "planned", + owner: "Q", + detail: + "dApp connector — external dApps request multi-sig transactions — paired with improved authentication (#135), since they are the same problem surface.", + }, + { + label: "Audit", + start: 11, + span: 1, + status: "planned", + owner: "Q", + detail: "Performance and UX audit, plus the final summary report.", + }, + ], + }, + { + name: "Research", + sub: "FROST · PQC", + items: [ + { + label: "FROST & PQC — kickoff to go/no-go", + start: 4, + span: 3, + status: "planned", + owner: "Q", + detail: + "FROST threshold Schnorr for Cardano (#220), plus Lemour post-quantum multi-sig. Kickoff slipped from July — starting in August keeps runway for the October go/no-go. Deliverables: a trade-off note, a PoC if libraries allow, and a recommendation.", + }, + ], + }, + { + name: "Growth & accounts", + sub: "Profiles · vesting", + items: [ + { + label: "Vesting", + start: 9, + span: 1, + status: "planned", + owner: "Q·A", + detail: + "Vesting — time-locked multi-sig contracts (#81). Alongside user profiles and contacts, which build on the existing Contact model rather than starting fresh.", + }, + { + label: "Buffer", + start: 12, + span: 1, + status: "planned", + detail: + "Buffer month — absorbs slippage, finalizes reporting, plans the next cycle. No fixed feature commitments.", + }, + ], + }, +]; + +/** Headline numbers for the summary strip. */ +export const STATS = [ + { + k: "Shipped", + v: "6", + n: "workstreams delivered in May–July", + tone: "good" as const, + }, + { + k: "Ahead of plan", + v: "4", + n: "capabilities landed early, freeing Nov–Dec", + tone: "good" as const, + }, + { + k: "Unreleased", + v: "75", + n: "commits on preprod, not on main", + tone: "bad" as const, + }, + { + k: "Migrations due", + v: "4", + n: "pending against production", + tone: "bad" as const, + }, + { + k: "Flagship", + v: "Aug", + n: "Document Sign-Off build begins", + tone: "neutral" as const, + }, +]; + +/** Capability that arrived before its planned month, and what that freed up. */ +export const AHEAD_OF_SCHEDULE = [ + { + capability: "Governance metadata fix (#122)", + planned: "Nov 26", + delivered: "Jun 26", + effect: "Closed five months early.", + }, + { + capability: "Bot platform — scoped auth, reference client, ballot API", + planned: "Nov 26", + delivered: "Jul 26", + effect: + "November reduces to webhooks only — the one piece with no code yet.", + }, + { + capability: "API documentation & developer portal", + planned: "Dec 26", + delivered: "Jun–Jul 26", + effect: + "Done. Swagger UI, a 1841-line OpenAPI spec, /llms.txt and a downloadable agent skill. December slot freed.", + }, + { + capability: "Pending transactions on homepage (#125)", + planned: "Nov 26", + delivered: "Shipped", + effect: + "Surfaced on the wallets dashboard. Issue still open — verify and close.", + }, + { + capability: "Playwright E2E suite", + planned: "Unscheduled", + delivered: "Jun–Jul 26", + effect: "Becomes the safety net Document Sign-Off ships against.", + }, +]; + +/** What the product can do today, verified against the codebase on 2026-07-26. */ +export const DELIVERED = [ + { + title: "Governance", + points: [ + "In-app voting for multisig DReps — budget voting, registration detection, ballot UX, DB-cached tallies.", + "Public DRep explorer — vote history with search and filters, rationales resolved from IPFS, CSV export. No wallet required.", + "Rationale drafting, IPFS reliability, ballot CSV import and export.", + ], + }, + { + title: "Bot platform", + points: [ + "Human-in-the-loop onboarding — a bot registers, its owner approves a 30-minute claim code, the secret is retrieved once.", + "Double opt-in authorization — five scopes on the key and a per-wallet grant.", + "27 endpoints reachable by bots, including signing with auto-submit at threshold. A bot can never move funds alone.", + "Rate limiting at 60/min by default, 5/min on secret rotation.", + ], + }, + { + title: "Developer & agent surface", + points: [ + "/api-docs — Swagger UI with a wallet-signature token generator.", + "/api/swagger — a 1841-line OpenAPI 3.0 spec.", + "/llms.txt and /api/skill — agent orientation and a downloadable skill.", + ], + }, + { + title: "Notifications", + points: [ + "A real outbox — idempotency keys, retry backoff, nine delivery statuses including four distinct skip reasons.", + "Per-wallet, per-signer settings and hashed-token email verification.", + "Gap: nothing drains it on a schedule yet.", + ], + }, + { + title: "Testing & CI", + points: [ + "11 Playwright specs, ~54 tests — wallet creation, real preprod ring transfers, staking, proxy, governance, bot management, access control.", + "Real-chain smoke system closed; migrations running on Node 22.", + ], + }, + { + title: "Platform", + points: [ + "Mesh 2.0 groundwork — Prisma 7.8, Next 16, wallet operations behind a single bridge.", + "Signing and auth reliability; byte-preserving witness merge, regression-tested.", + "Mobile foundations, empty states, landing, SEO, glass theme.", + ], + }, +]; diff --git a/src/components/pages/homepage/roadmap/index.tsx b/src/components/pages/homepage/roadmap/index.tsx new file mode 100644 index 00000000..c97c2cad --- /dev/null +++ b/src/components/pages/homepage/roadmap/index.tsx @@ -0,0 +1,417 @@ +import React from "react"; +import { AlertTriangle, Check, Clock } from "lucide-react"; + +import { Reveal } from "@/components/ui/reveal"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +import { + AHEAD_OF_SCHEDULE, + CURRENT_MONTH, + DELIVERED, + MONTHS, + STATS, + TRACKS, + type RoadmapItem, + type Status, +} from "./data"; + +/** + * Status is encoded by fill *and* by icon and label, never by colour alone — + * emerald/amber separate under the common forms of colour blindness in a way + * emerald/red does not, so red stays reserved for the single critical callout + * rather than being reused as a chart colour. "Planned" is deliberately not a + * fourth hue: absence of status reads better as an outline than as grey fill. + */ +const STATUS_BAR: Record = { + done: "border-emerald-600/50 bg-emerald-500/10 text-emerald-700 dark:border-emerald-500/50 dark:text-emerald-300", + risk: "border-amber-600/50 bg-amber-500/10 text-amber-700 dark:border-amber-500/50 dark:text-amber-300", + planned: + "border-dashed border-muted-foreground/40 bg-muted/40 text-muted-foreground hover:border-foreground/40", +}; + +const STATUS_LABEL: Record = { + done: "Delivered", + risk: "Blocked / at risk", + planned: "Planned", +}; + +function StatusIcon({ status }: { status: Status }) { + if (status === "done") return ; + if (status === "risk") + return ; + return null; +} + +/** One bar in the timeline: a month range on a track, with its detail on hover. */ +function Bar({ item, row }: { item: RoadmapItem; row: number }) { + return ( + + +
+ + {/* min-w-0 lets the flex child shrink so a long single word wraps + instead of spilling past the bar into the next month. */} + {item.label} + {item.owner && ( + + {item.owner} + + )} +
+
+ +

{item.label}

+

+ {item.detail} +

+
+
+ ); +} + +function Legend() { + return ( +
+ {(["done", "risk", "planned"] as Status[]).map((s) => ( + + + {STATUS_LABEL[s]} + + ))} + + Q Quirin + A Andre + +
+ ); +} + +/** + * The month grid. Every cell is placed explicitly — the "today" rule spans all + * rows, and a grid item with a definite position is packed before auto-placed + * siblings, which would otherwise shove the month headers a column to the right. + */ +function Timeline() { + const columns = `minmax(168px, 184px) repeat(${MONTHS.length}, minmax(84px, 1fr))`; + + return ( +
+
+ {/* header row */} +
+ Workstream +
+ {MONTHS.map((m, i) => { + const n = i + 1; + const isNow = n === CURRENT_MONTH; + const isPast = n < CURRENT_MONTH; + return ( +
+ {m.short} + {m.year} + {/* the marker lives inside the cell, so it cannot collide with a neighbouring header */} + {isNow && ( + + today + + )} +
+ ); + })} + + {/* today rule, on the right edge of the current month */} +
+ + {TRACKS.map((track, t) => { + const row = t + 2; + return ( + +
+ + {track.name} + + + {track.sub} + +
+ + {MONTHS.map((m, i) => ( +
+ ))} + + {track.items.map((item) => ( + + ))} + + ); + })} +
+
+ ); +} + +export function PageRoadmap() { + return ( + + {/* w-full + min-w-0 keep this flex item at the width of its container: + `mx-auto` suppresses flex stretch, which would otherwise leave the box + sized to the timeline grid's 1180px min-content and push the whole page + wider than the viewport instead of letting the grid scroll on its own. */} +
+
+

+ Roadmap +

+

+ Twelve months of Mesh Multisig, May 2026 to April 2027 — what has + shipped, what is blocked, and what comes next. +

+

+ Quirin + Andre · ~25 h/wk · revised 2026-07-26 +

+
+ +
+ {/* release gap — the one genuinely critical item, so red is reserved for it */} + +
+ +
+

+ Production is three months behind the work +

+

+ The production database has applied no migration since{" "} + 2026-05-10 + . Four are outstanding, and{" "} + + preprod + {" "} + is{" "} + + 75 commits ahead of{" "} + + main + + {" "} + — so June and July are built but unreleased.{" "} + + deploy-migrations.yml + {" "} + only fires on pushes to{" "} + + main + {" "} + touching{" "} + + prisma/migrations/** + + , so repairing the workflow never re-triggered the run that + failed on 17 June. +

+

+ Live consequences: governance tallies error, the notification + center has no tables, address-less bot registration cannot + work — and seven tables still have row-level security + disabled, reachable by the anon PostgREST role. The fix is + written and merged; it is only undeployed. +

+
+
+
+ + {/* summary strip */} +
+ {STATS.map((s, i) => ( + +
+ + {s.k} + + + {s.v} + + + {s.n} + +
+
+ ))} +
+ + {/* timeline */} +
+
+

+ Workstream timeline +

+

+ Grouped by workstream rather than by owner, so continuity from + delivered to planned stays visible. Hover or focus any bar for + the detail behind it. +

+
+ + +

+ + Status reflects the preprod{" "} + branch. Delivered does not mean live. +

+
+ + {/* ahead of schedule */} +
+
+

+ Landed ahead of schedule +

+

+ July over-delivered against the plan. Four capabilities arrived + before their slot, which is what freed November and December to + absorb new work. +

+
+
+ + + + Capability + Planned + Delivered + Effect on the plan + + + + {AHEAD_OF_SCHEDULE.map((r) => ( + + + {r.capability} + + + {r.planned} + + + {r.delivered} + + + {r.effect} + + + ))} + +
+
+
+ + {/* delivered inventory */} +
+
+

+ Delivered to date +

+

+ What the product can do today, verified against the codebase on + 2026-07-26 rather than inferred from pull-request titles. +

+
+
+ {DELIVERED.map((group, i) => ( + +
+

+ {group.title} +

+
    + {group.points.map((p) => ( +
  • + + {p} +
  • + ))} +
+
+
+ ))} +
+
+ +

+ Source of record:{" "} + + ROADMAP.md + +

+
+
+
+ ); +} + +export default PageRoadmap; diff --git a/src/components/ui/seo-fallback.tsx b/src/components/ui/seo-fallback.tsx index 3f10d9b4..a054f282 100644 --- a/src/components/ui/seo-fallback.tsx +++ b/src/components/ui/seo-fallback.tsx @@ -25,6 +25,7 @@ import { // the longer per-link blurb is pulled from routeSeo at render time. const FALLBACK_LINKS = [ { path: "/features", label: "Features" }, + { path: "/roadmap", label: "Roadmap" }, { path: "/governance", label: "Cardano Governance" }, { path: "/governance/drep", label: "DRep Explorer" }, { path: "/api-docs", label: "API & Bot Documentation" }, diff --git a/src/data/public-routes.ts b/src/data/public-routes.ts index af5bcd88..ddc84098 100644 --- a/src/data/public-routes.ts +++ b/src/data/public-routes.ts @@ -4,6 +4,7 @@ export const publicRoutes = [ "/governance/drep", "/governance/drep/[id]", "/features", + "/roadmap", "/api-docs", "/dapps", "/blog", diff --git a/src/lib/seo.ts b/src/lib/seo.ts index 473c9d36..30535b80 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -83,6 +83,11 @@ export const routeSeo: Record = { description: "Use Mesh Multisig with your favourite Cardano DApps in multi-signature mode.", }, + "/roadmap": { + title: "Roadmap", + description: + "The twelve-month Mesh Multisig roadmap: what has shipped, what is in progress and what is planned for the Cardano multi-signature wallet, from May 2026 to April 2027.", + }, "/blog": { title: "Blog", description: @@ -152,6 +157,7 @@ export type SitemapRoute = { export const INDEXABLE_ROUTES: SitemapRoute[] = [ { path: "/", changefreq: "weekly", priority: 1.0 }, { path: "/features", changefreq: "monthly", priority: 0.8 }, + { path: "/roadmap", changefreq: "monthly", priority: 0.6 }, { path: "/blog", changefreq: "weekly", priority: 0.7 }, { path: "/governance", changefreq: "daily", priority: 0.8 }, { path: "/governance/drep", changefreq: "daily", priority: 0.7 }, diff --git a/src/pages/roadmap.tsx b/src/pages/roadmap.tsx new file mode 100644 index 00000000..a37d1e58 --- /dev/null +++ b/src/pages/roadmap.tsx @@ -0,0 +1,7 @@ +import { PageRoadmap } from "@/components/pages/homepage/roadmap"; + +export const getServerSideProps = () => ({ props: {} }); + +export default function Page() { + return ; +} From 8ea199de042202764b52fabf35d024c971355d16 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Mon, 27 Jul 2026 10:57:04 +0200 Subject: [PATCH 2/3] fix(roadmap): stop timeline bars painting over the frozen column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scrolling the timeline horizontally slid the bars over the sticky workstream column instead of under it, so labels and bars collided, and the frozen corner was translucent enough for scrolled month headers to show through it. Sticky cells need both a stacking order and an opaque fill. Cells sit at the base, bars and the today rule at z-10, the frozen workstream column at z-20, the corner at z-30 — and the corner moves from bg-muted/50 with a backdrop blur to solid bg-muted. Verified scrolled in light and dark. Co-Authored-By: Claude Fable 5 --- .../pages/homepage/roadmap/index.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/components/pages/homepage/roadmap/index.tsx b/src/components/pages/homepage/roadmap/index.tsx index c97c2cad..5258dc84 100644 --- a/src/components/pages/homepage/roadmap/index.tsx +++ b/src/components/pages/homepage/roadmap/index.tsx @@ -66,7 +66,7 @@ function Bar({ item, row }: { item: RoadmapItem; row: number }) { gridColumn: `${item.start + 1} / span ${item.span}`, gridRow: row, }} - className={`relative z-20 mx-[3px] my-1.5 flex min-h-[34px] items-center gap-1.5 self-center rounded-md border px-2 pb-3 pt-1.5 text-[11px] font-medium leading-tight outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring ${STATUS_BAR[item.status]}`} + className={`relative z-10 mx-[3px] my-1.5 flex min-h-[34px] items-center gap-1.5 self-center rounded-md border px-2 pb-3 pt-1.5 text-[11px] font-medium leading-tight outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring ${STATUS_BAR[item.status]}`} > {/* min-w-0 lets the flex child shrink so a long single word wraps @@ -112,6 +112,12 @@ function Legend() { * The month grid. Every cell is placed explicitly — the "today" rule spans all * rows, and a grid item with a definite position is packed before auto-placed * siblings, which would otherwise shove the month headers a column to the right. + * + * Stacking order matters because the first column is sticky: cells (auto) sit + * under bars and the today rule (z-10), which slide under the frozen workstream + * column (z-20) as the grid scrolls, which in turn sits under the frozen corner + * (z-30). Every sticky cell needs an opaque background, or scrolled bars show + * through it. */ function Timeline() { const columns = `minmax(168px, 184px) repeat(${MONTHS.length}, minmax(84px, 1fr))`; @@ -125,7 +131,7 @@ function Timeline() { {/* header row */}
Workstream
@@ -157,10 +163,12 @@ function Timeline() { ); })} - {/* today rule, on the right edge of the current month */} + {/* today rule, on the right edge of the current month. Kept at the bar + layer (z-10) so the frozen first column covers it once the grid is + scrolled horizontally, same as the bars. */}
{TRACKS.map((track, t) => { @@ -169,7 +177,7 @@ function Timeline() {
{track.name} From 319f0e013d72ae87107a851be166ccd4c8a6cc17 Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Mon, 27 Jul 2026 11:40:34 +0200 Subject: [PATCH 3/3] feat(roadmap): feature vault + interactive knowledge graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `/vault` folder at the repo root describing every feature and the state it is in, and an interactive graph at /roadmap/graph driven by it. The vault is an Obsidian-compatible folder of plain markdown with YAML frontmatter and [[wikilinks]] — 52 features across 10 areas and 4 states, openable directly in Obsidian with no plugins. Each feature's `area` and `state` become edges to those notes, and every wikilink in a body becomes an edge between features, so there is no separate index to keep in sync: changing one `state:` line is what moves a node. src/lib/vault.ts parses it at build time via getStaticProps — the only page in the app not using a no-op getServerSideProps. The vault only changes when the repo does, so this costs nothing at runtime, and a malformed note fails the build rather than a visitor's request. A feature naming an area or state with no note throws, so typos surface at build. The frontmatter parser covers a documented YAML subset (scalars, inline arrays, block lists) rather than adding a dependency — a deliberate trade-off in a wallet codebase. Types live in vault-types.ts because importing any *value* from the loader would pull `fs` into the client bundle. The graph is a small force simulation rather than a graph library: same reasoning on dependencies, and at 66 nodes the O(n²) pass is cheap. It settles before paint instead of animating, so there is no motion to sit through. Layout is seeded from a deterministic hash so server and client agree. Interaction: click to read a note, hover or focus to isolate a node and its neighbours, drag to rearrange, filter by state, search across titles, summaries, areas and owners, and toggle the state hubs off to untangle the layout into pure area structure. Details worth keeping: - Pointer-down deliberately changes no state. Seeding the drag there re-ran the layout and moved the node out from under the cursor, so the click never completed. Dragging now starts after a 5px threshold, and only the dropped node is pinned while the rest relax around it. - Hub labels get a placement pass that tries below, above, then further out, skipping slots that collide or fall outside the canvas. Without it two area labels sat on top of each other. - Feature labels appear on demand; 52 of them at once is an unreadable mat of text. Wires the route into the SEO registry, sitemap, public-route allowlist, no-JS crawler fallback and footer, and links it from the roadmap page. /roadmap moves to /roadmap/index so the graph is a sibling route. Verified at 375px and 1440px, light and dark: no page overflow, no overlapping or clipped labels, selection, filters, search and drag all working. 514 unit tests pass, 16 of them covering the parser. Co-Authored-By: Claude Fable 5 --- src/__tests__/vault.test.ts | 163 ++++ .../common/overall-layout/site-footer.tsx | 1 + .../pages/homepage/roadmap/graph.tsx | 738 ++++++++++++++++++ .../pages/homepage/roadmap/index.tsx | 22 +- src/components/ui/seo-fallback.tsx | 1 + src/data/public-routes.ts | 1 + src/lib/seo.ts | 6 + src/lib/vault-types.ts | 52 ++ src/lib/vault.ts | 282 +++++++ src/pages/roadmap/graph.tsx | 35 + src/pages/{roadmap.tsx => roadmap/index.tsx} | 0 vault/README.md | 47 ++ vault/areas/Bot & Agent Platform.md | 15 + vault/areas/Document Sign-Off.md | 15 + vault/areas/Governance.md | 14 + vault/areas/Growth & Accounts.md | 13 + vault/areas/Notifications.md | 14 + vault/areas/Platform & UX.md | 14 + vault/areas/Release & Production Health.md | 15 + vault/areas/Reliability & CI.md | 14 + vault/areas/Research.md | 13 + vault/areas/Wallets & Discovery.md | 14 + vault/features/API Documentation Portal.md | 20 + vault/features/Agent & Crawler Legibility.md | 20 + vault/features/Ballot Rationale & IPFS.md | 19 + vault/features/Better 404 Page.md | 17 + vault/features/Bot Ballot Drafting.md | 19 + vault/features/Bot Management UI.md | 19 + vault/features/Bot Rate Limiting.md | 17 + .../features/Bot Registration & Claim Flow.md | 21 + vault/features/Bot Scoped Authorization.md | 21 + vault/features/Bot Webhooks.md | 18 + vault/features/Collaboration & Standards.md | 17 + .../features/Collateral Service for Proxy.md | 18 + vault/features/DRep Vote History Explorer.md | 21 + vault/features/Dependabot CI Unblock.md | 20 + vault/features/Discover Page.md | 19 + vault/features/Document Sign-Off MVP.md | 22 + vault/features/Email Notification Center.md | 19 + vault/features/Email Verification.md | 17 + vault/features/FROST Threshold Signatures.md | 24 + vault/features/Feature Knowledge Graph.md | 20 + vault/features/Full Address Verification.md | 18 + vault/features/Hardware Wallet Support.md | 19 + vault/features/Improved Authentication.md | 18 + vault/features/In-App Governance Voting.md | 21 + vault/features/Invite Flow.md | 17 + vault/features/Landing, SEO & Theme.md | 20 + vault/features/Legacy Wallet Compatibility.md | 20 + vault/features/Mesh 2.0 Migration.md | 21 + vault/features/Migration Deploy Pipeline.md | 19 + .../Mobile & Responsive Foundations.md | 18 + vault/features/Multi-Signature Wallet Core.md | 18 + vault/features/Multisig MCP Server.md | 18 + .../Notification Digests & Reminders.md | 17 + .../features/Notification Outbox & Worker.md | 19 + vault/features/On-Chain Checkpoints.md | 17 + vault/features/Playwright E2E Suite.md | 22 + vault/features/Post-Quantum Multi-Sig.md | 19 + vault/features/Production Release Gap.md | 24 + vault/features/Proxy Voting.md | 17 + vault/features/Real-Chain Smoke Tests.md | 19 + vault/features/Revision Provenance.md | 17 + vault/features/Roadmap Page.md | 20 + .../features/Row-Level Security Hardening.md | 20 + vault/features/Scheduled Outbox Drain.md | 19 + vault/features/Signing & Auth Reliability.md | 21 + .../Transaction Builder & tRPC Tests.md | 18 + vault/features/Transaction Pagination.md | 17 + vault/features/User Profiles & Contacts.md | 18 + vault/features/Vesting Contracts.md | 18 + vault/features/Wallet Import.md | 19 + .../Wallet V2 Registration & Discovery.md | 20 + vault/features/dApp Connector.md | 17 + vault/states/Blocked.md | 12 + vault/states/Delivered.md | 12 + vault/states/In Progress.md | 10 + vault/states/Planned.md | 11 + 78 files changed, 2521 insertions(+), 6 deletions(-) create mode 100644 src/__tests__/vault.test.ts create mode 100644 src/components/pages/homepage/roadmap/graph.tsx create mode 100644 src/lib/vault-types.ts create mode 100644 src/lib/vault.ts create mode 100644 src/pages/roadmap/graph.tsx rename src/pages/{roadmap.tsx => roadmap/index.tsx} (100%) create mode 100644 vault/README.md create mode 100644 vault/areas/Bot & Agent Platform.md create mode 100644 vault/areas/Document Sign-Off.md create mode 100644 vault/areas/Governance.md create mode 100644 vault/areas/Growth & Accounts.md create mode 100644 vault/areas/Notifications.md create mode 100644 vault/areas/Platform & UX.md create mode 100644 vault/areas/Release & Production Health.md create mode 100644 vault/areas/Reliability & CI.md create mode 100644 vault/areas/Research.md create mode 100644 vault/areas/Wallets & Discovery.md create mode 100644 vault/features/API Documentation Portal.md create mode 100644 vault/features/Agent & Crawler Legibility.md create mode 100644 vault/features/Ballot Rationale & IPFS.md create mode 100644 vault/features/Better 404 Page.md create mode 100644 vault/features/Bot Ballot Drafting.md create mode 100644 vault/features/Bot Management UI.md create mode 100644 vault/features/Bot Rate Limiting.md create mode 100644 vault/features/Bot Registration & Claim Flow.md create mode 100644 vault/features/Bot Scoped Authorization.md create mode 100644 vault/features/Bot Webhooks.md create mode 100644 vault/features/Collaboration & Standards.md create mode 100644 vault/features/Collateral Service for Proxy.md create mode 100644 vault/features/DRep Vote History Explorer.md create mode 100644 vault/features/Dependabot CI Unblock.md create mode 100644 vault/features/Discover Page.md create mode 100644 vault/features/Document Sign-Off MVP.md create mode 100644 vault/features/Email Notification Center.md create mode 100644 vault/features/Email Verification.md create mode 100644 vault/features/FROST Threshold Signatures.md create mode 100644 vault/features/Feature Knowledge Graph.md create mode 100644 vault/features/Full Address Verification.md create mode 100644 vault/features/Hardware Wallet Support.md create mode 100644 vault/features/Improved Authentication.md create mode 100644 vault/features/In-App Governance Voting.md create mode 100644 vault/features/Invite Flow.md create mode 100644 vault/features/Landing, SEO & Theme.md create mode 100644 vault/features/Legacy Wallet Compatibility.md create mode 100644 vault/features/Mesh 2.0 Migration.md create mode 100644 vault/features/Migration Deploy Pipeline.md create mode 100644 vault/features/Mobile & Responsive Foundations.md create mode 100644 vault/features/Multi-Signature Wallet Core.md create mode 100644 vault/features/Multisig MCP Server.md create mode 100644 vault/features/Notification Digests & Reminders.md create mode 100644 vault/features/Notification Outbox & Worker.md create mode 100644 vault/features/On-Chain Checkpoints.md create mode 100644 vault/features/Playwright E2E Suite.md create mode 100644 vault/features/Post-Quantum Multi-Sig.md create mode 100644 vault/features/Production Release Gap.md create mode 100644 vault/features/Proxy Voting.md create mode 100644 vault/features/Real-Chain Smoke Tests.md create mode 100644 vault/features/Revision Provenance.md create mode 100644 vault/features/Roadmap Page.md create mode 100644 vault/features/Row-Level Security Hardening.md create mode 100644 vault/features/Scheduled Outbox Drain.md create mode 100644 vault/features/Signing & Auth Reliability.md create mode 100644 vault/features/Transaction Builder & tRPC Tests.md create mode 100644 vault/features/Transaction Pagination.md create mode 100644 vault/features/User Profiles & Contacts.md create mode 100644 vault/features/Vesting Contracts.md create mode 100644 vault/features/Wallet Import.md create mode 100644 vault/features/Wallet V2 Registration & Discovery.md create mode 100644 vault/features/dApp Connector.md create mode 100644 vault/states/Blocked.md create mode 100644 vault/states/Delivered.md create mode 100644 vault/states/In Progress.md create mode 100644 vault/states/Planned.md diff --git a/src/__tests__/vault.test.ts b/src/__tests__/vault.test.ts new file mode 100644 index 00000000..c1e44c49 --- /dev/null +++ b/src/__tests__/vault.test.ts @@ -0,0 +1,163 @@ +import { + extractWikilinks, + loadVaultGraph, + splitFrontmatter, + summarize, + FEATURE_STATES, +} from "@/lib/vault"; + +describe("splitFrontmatter", () => { + it("parses scalars, inline arrays and block lists", () => { + const { frontmatter, body } = splitFrontmatter( + [ + "---", + "type: feature", + "state: delivered", + "issues: [122, 213]", + "code_paths:", + " - src/lib/vault.ts", + " - src/pages/roadmap", + "empty: []", + "---", + "", + "# Title", + "", + "Body text.", + ].join("\n"), + ); + + expect(frontmatter.type).toBe("feature"); + expect(frontmatter.state).toBe("delivered"); + expect(frontmatter.issues).toEqual(["122", "213"]); + expect(frontmatter.code_paths).toEqual([ + "src/lib/vault.ts", + "src/pages/roadmap", + ]); + expect(frontmatter.empty).toEqual([]); + expect(body).toContain("# Title"); + }); + + it("strips matching quotes but leaves inner punctuation alone", () => { + const { frontmatter } = splitFrontmatter( + ['---', 'owner: "Quirin & Andre"', "area: 'Platform & UX'", "---", ""].join( + "\n", + ), + ); + expect(frontmatter.owner).toBe("Quirin & Andre"); + expect(frontmatter.area).toBe("Platform & UX"); + }); + + it("returns an empty result for a note with no frontmatter", () => { + const { frontmatter, body } = splitFrontmatter("# Just a heading\n\nText."); + expect(frontmatter).toEqual({}); + expect(body).toBe("# Just a heading\n\nText."); + }); + + it("does not treat an unterminated fence as frontmatter", () => { + const { frontmatter } = splitFrontmatter("---\ntype: feature\nno end fence"); + expect(frontmatter).toEqual({}); + }); + + it("handles CRLF line endings", () => { + const { frontmatter } = splitFrontmatter( + "---\r\ntype: feature\r\nstate: blocked\r\n---\r\n\r\nBody.", + ); + expect(frontmatter.state).toBe("blocked"); + }); +}); + +describe("extractWikilinks", () => { + it("collects links, de-duplicates them and drops aliases and anchors", () => { + expect( + extractWikilinks( + "See [[Alpha]] and [[Beta|the second]] and [[Alpha]] and [[Gamma#section]].", + ), + ).toEqual(["Alpha", "Beta", "Gamma"]); + }); + + it("returns nothing when there are no links", () => { + expect(extractWikilinks("Plain prose with [a link](https://x.dev).")).toEqual( + [], + ); + }); +}); + +describe("summarize", () => { + it("uses the first paragraph, dropping the heading and link syntax", () => { + expect( + summarize("# Title\n\nFirst para with [[A Link]] and `code`.\n\nSecond."), + ).toBe("First para with A Link and code."); + }); +}); + +describe("loadVaultGraph", () => { + const graph = loadVaultGraph(); + + it("loads features, areas and states from the vault", () => { + const kinds = graph.nodes.map((n) => n.kind); + expect(kinds.filter((k) => k === "feature").length).toBeGreaterThan(0); + expect(kinds.filter((k) => k === "area").length).toBeGreaterThan(0); + expect(kinds.filter((k) => k === "state")).toHaveLength( + FEATURE_STATES.length, + ); + }); + + it("gives every feature exactly one area edge and one state edge", () => { + const features = graph.nodes.filter((n) => n.kind === "feature"); + for (const feature of features) { + const area = graph.edges.filter( + (e) => e.source === feature.id && e.kind === "in-area", + ); + const state = graph.edges.filter( + (e) => e.source === feature.id && e.kind === "has-state", + ); + expect(area).toHaveLength(1); + expect(state).toHaveLength(1); + } + }); + + it("only emits edges between nodes that exist", () => { + const ids = new Set(graph.nodes.map((n) => n.id)); + for (const edge of graph.edges) { + expect(ids.has(edge.source)).toBe(true); + expect(ids.has(edge.target)).toBe(true); + } + }); + + it("de-duplicates mutual references into a single relates-to edge", () => { + const pairs = graph.edges + .filter((e) => e.kind === "relates-to") + .map((e) => [e.source, e.target].sort().join("|")); + expect(new Set(pairs).size).toBe(pairs.length); + }); + + it("never links a feature to itself", () => { + for (const edge of graph.edges) { + expect(edge.source).not.toBe(edge.target); + } + }); + + it("counts features by state consistently with the nodes", () => { + for (const state of FEATURE_STATES) { + const actual = graph.nodes.filter( + (n) => n.kind === "feature" && n.state === state, + ).length; + expect(graph.counts[state]).toBe(actual); + } + }); + + it("gives every feature a non-empty summary", () => { + for (const feature of graph.nodes.filter((n) => n.kind === "feature")) { + expect(feature.summary.length).toBeGreaterThan(0); + } + }); + + it("records degree matching the edges that touch each node", () => { + for (const node of graph.nodes) { + const touching = graph.edges.filter( + (e) => e.source === node.id || e.target === node.id, + ).length; + expect(node.degree).toBe(touching); + } + }); +}); diff --git a/src/components/common/overall-layout/site-footer.tsx b/src/components/common/overall-layout/site-footer.tsx index aa160038..a743588e 100644 --- a/src/components/common/overall-layout/site-footer.tsx +++ b/src/components/common/overall-layout/site-footer.tsx @@ -7,6 +7,7 @@ const productLinks = [ { label: "DRep Explorer", href: "/governance/drep" }, { label: "Import a wallet", href: "/wallets/import-wallet" }, { label: "Roadmap", href: "/roadmap" }, + { label: "Feature graph", href: "/roadmap/graph" }, { label: "Blog", href: "/blog" }, ]; diff --git a/src/components/pages/homepage/roadmap/graph.tsx b/src/components/pages/homepage/roadmap/graph.tsx new file mode 100644 index 00000000..c8b18205 --- /dev/null +++ b/src/components/pages/homepage/roadmap/graph.tsx @@ -0,0 +1,738 @@ +import React from "react"; +import Link from "next/link"; +import { ArrowLeft, Search, X } from "lucide-react"; + +import { Input } from "@/components/ui/input"; +import { + FEATURE_STATES, + type FeatureState, + type VaultEdge, + type VaultGraph, + type VaultNode, +} from "@/lib/vault-types"; + +/** + * Interactive knowledge graph over the feature vault. + * + * The layout is a small force simulation run on mount — repulsion between every + * pair, springs along edges, and a pull toward the centre — rather than a graph + * dependency. At this size (tens of nodes) the O(n²) pass is cheap, and it keeps + * the wallet's dependency surface unchanged. + * + * The simulation is settled *before paint* rather than animated: it runs a fixed + * number of iterations in a `useMemo`, so there is no motion to sit through and + * nothing to reflow on every frame. Dragging a node re-runs a short relaxation + * from the current positions. + */ + +type Props = { graph: VaultGraph }; + +const STATE_LABEL: Record = { + delivered: "Delivered", + "in-progress": "In progress", + planned: "Planned", + blocked: "Blocked", +}; + +/** + * Emerald / blue / amber stay distinguishable under the common forms of colour + * blindness; every use is paired with a text label, and the legend doubles as the + * filter, so colour is never the only channel. + */ +const STATE_DOT: Record = { + delivered: "bg-emerald-500", + "in-progress": "bg-blue-500", + planned: "bg-muted-foreground/40", + blocked: "bg-amber-500", +}; + +const STATE_STROKE: Record = { + delivered: "stroke-emerald-600 dark:stroke-emerald-400", + "in-progress": "stroke-blue-600 dark:stroke-blue-400", + planned: "stroke-muted-foreground/50", + blocked: "stroke-amber-600 dark:stroke-amber-400", +}; + +const STATE_FILL: Record = { + delivered: "fill-emerald-500/25", + "in-progress": "fill-blue-500/25", + planned: "fill-muted", + blocked: "fill-amber-500/30", +}; + +const WIDTH = 1000; +const HEIGHT = 680; + +type Point = { x: number; y: number }; +type Positions = Record; + +/** Deterministic [0,1) hash, so the layout is identical on server and client. */ +function seededUnit(key: string, salt: number): number { + let h = 2166136261 ^ salt; + for (let i = 0; i < key.length; i++) { + h = Math.imul(h ^ key.charCodeAt(i), 16777619); + } + return ((h >>> 0) % 10000) / 10000; +} + +function radiusFor(node: VaultNode): number { + if (node.kind === "state") return 26; + if (node.kind === "area") return 20; + return 7 + Math.min(node.degree, 8) * 1.2; +} + +/** + * Relax the layout. Areas and states carry more mass so they anchor their + * clusters instead of being flung out by their many edges. + */ +function simulate( + nodes: VaultNode[], + edges: VaultEdge[], + start: Positions, + iterations: number, + /** Nodes the user has dragged: they exert force but never move. */ + pinned: ReadonlySet = new Set(), +): Positions { + const pos: Positions = {}; + for (const n of nodes) { + const p = start[n.id]; + pos[n.id] = p + ? { x: p.x, y: p.y } + : { + x: WIDTH / 2 + (seededUnit(n.id, 1) - 0.5) * WIDTH * 0.75, + y: HEIGHT / 2 + (seededUnit(n.id, 2) - 0.5) * HEIGHT * 0.75, + }; + } + + const mass = (n: VaultNode) => + n.kind === "state" ? 5 : n.kind === "area" ? 3 : 1; + const byId = new Map(nodes.map((n) => [n.id, n])); + + for (let step = 0; step < iterations; step++) { + const cool = 1 - step / (iterations + 1); + const force: Positions = {}; + for (const n of nodes) force[n.id] = { x: 0, y: 0 }; + + // Repulsion between every pair. + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const a = nodes[i]!; + const b = nodes[j]!; + const pa = pos[a.id]!; + const pb = pos[b.id]!; + let dx = pa.x - pb.x; + let dy = pa.y - pb.y; + let dist = Math.hypot(dx, dy); + if (dist < 0.01) { + // Identical positions have no direction; nudge deterministically. + dx = seededUnit(a.id + b.id, 3) - 0.5; + dy = seededUnit(b.id + a.id, 4) - 0.5; + dist = 0.01; + } + // Hubs are the only labelled nodes, so push them apart harder than + // features — otherwise two area labels land on top of each other. + const bothHubs = a.kind !== "feature" && b.kind !== "feature"; + const push = + ((bothHubs ? 96000 : 38000) * (mass(a) + mass(b))) / + (dist * dist * 2); + const ux = (dx / dist) * push; + const uy = (dy / dist) * push; + force[a.id]!.x += ux / mass(a); + force[a.id]!.y += uy / mass(a); + force[b.id]!.x -= ux / mass(b); + force[b.id]!.y -= uy / mass(b); + } + } + + // Springs along edges. + for (const e of edges) { + const a = byId.get(e.source); + const b = byId.get(e.target); + if (!a || !b) continue; + const pa = pos[a.id]!; + const pb = pos[b.id]!; + const dx = pb.x - pa.x; + const dy = pb.y - pa.y; + const dist = Math.max(Math.hypot(dx, dy), 0.01); + // Area edges rest longer than state edges, so areas splay into their own + // neighbourhoods rather than piling onto the state hubs. + const rest = e.kind === "in-area" ? 118 : e.kind === "has-state" ? 190 : 96; + const pull = (dist - rest) * 0.045; + const ux = (dx / dist) * pull; + const uy = (dy / dist) * pull; + force[a.id]!.x += ux / mass(a); + force[a.id]!.y += uy / mass(a); + force[b.id]!.x -= ux / mass(b); + force[b.id]!.y -= uy / mass(b); + } + + // Gentle pull to centre so nothing drifts off-canvas. + for (const n of nodes) { + const p = pos[n.id]!; + force[n.id]!.x += (WIDTH / 2 - p.x) * 0.012; + force[n.id]!.y += (HEIGHT / 2 - p.y) * 0.012; + } + + for (const n of nodes) { + if (pinned.has(n.id)) continue; + const p = pos[n.id]!; + const f = force[n.id]!; + const limit = 28 * cool; + p.x += Math.max(-limit, Math.min(limit, f.x * cool)); + p.y += Math.max(-limit, Math.min(limit, f.y * cool)); + // Hubs are always labelled, so keep them far enough from the bottom and top + // edges that the label has somewhere to sit. + const pad = radiusFor(n) + (n.kind === "feature" ? 6 : 30); + p.x = Math.max(pad, Math.min(WIDTH - pad, p.x)); + p.y = Math.max(pad, Math.min(HEIGHT - pad, p.y)); + } + } + + return pos; +} + +export function VaultGraphView({ graph }: Props) { + const [activeStates, setActiveStates] = React.useState>( + () => new Set(FEATURE_STATES), + ); + const [query, setQuery] = React.useState(""); + const [showStates, setShowStates] = React.useState(true); + const [selected, setSelected] = React.useState(null); + const [hovered, setHovered] = React.useState(null); + /** Where the next relaxation starts from, so a drag nudges rather than reshuffles. */ + const [seed, setSeed] = React.useState({}); + /** Only the nodes the user actually dragged stay put. */ + const [pinnedIds, setPinnedIds] = React.useState>( + () => new Set(), + ); + /** The node under an in-flight drag, tracked outside the layout memo. */ + const [live, setLive] = React.useState<(Point & { id: string }) | null>(null); + const svgRef = React.useRef(null); + const gesture = React.useRef<{ + id: string; + x: number; + y: number; + moved: boolean; + } | null>(null); + + const visibleNodes = React.useMemo(() => { + return graph.nodes.filter((n) => { + if (n.kind === "feature") return activeStates.has(n.state!); + // Every feature links to both its area and its state, so two hub systems + // pull on the same nodes. Dropping the state hubs untangles the layout into + // pure area structure, which is the more useful view of the product. + if (n.kind === "state") return showStates; + return true; + }); + }, [graph.nodes, activeStates, showStates]); + + const visibleIds = React.useMemo( + () => new Set(visibleNodes.map((n) => n.id)), + [visibleNodes], + ); + + const visibleEdges = React.useMemo( + () => + graph.edges.filter( + (e) => visibleIds.has(e.source) && visibleIds.has(e.target), + ), + [graph.edges, visibleIds], + ); + + const layout = React.useMemo( + () => simulate(visibleNodes, visibleEdges, seed, 320, pinnedIds), + [visibleNodes, visibleEdges, seed, pinnedIds], + ); + + /** + * Render positions. A drag in flight moves only the dragged node — re-running the + * simulation on every pointer move would be both janky and disorienting, so the + * relaxation happens once, on release. + */ + const positions = React.useMemo( + () => (live ? { ...layout, [live.id]: { x: live.x, y: live.y } } : layout), + [layout, live], + ); + + /** + * Hubs are always labelled, so two that settle near each other collide. Walk them + * in a stable order and flip a label above its node when the slot below is taken. + * Width is approximated from the character count — good enough to separate them, + * and it avoids measuring text during render. + */ + const labelDy = React.useMemo(() => { + const placed: { x1: number; x2: number; y1: number; y2: number }[] = []; + const offsets: Record = {}; + const hubs = visibleNodes + .filter((n) => n.kind !== "feature") + .sort((a, b) => a.id.localeCompare(b.id)); + + for (const hub of hubs) { + const p = layout[hub.id]; + if (!p) continue; + const r = radiusFor(hub); + // Slightly over-estimate the rendered width (semibold 11px) plus padding — + // under-estimating is what lets two labels sit on top of each other. + const halfWidth = (hub.id.length * 6.6 + 10) / 2; + // Below, above, then progressively further out in each direction. + const candidates = [ + r + 12, + -(r + 6), + r + 26, + -(r + 20), + r + 40, + -(r + 34), + ]; + + const boxFor = (dy: number) => ({ + x1: p.x - halfWidth, + x2: p.x + halfWidth, + y1: p.y + dy - 10, + y2: p.y + dy + 3, + }); + const usable = (b: ReturnType) => + b.y1 > 2 && + b.y2 < HEIGHT - 2 && + !placed.some( + (o) => b.x1 < o.x2 && o.x1 < b.x2 && b.y1 < o.y2 && o.y1 < b.y2, + ); + + const chosen = candidates.find((dy) => usable(boxFor(dy))); + const dy = chosen ?? candidates[0]!; + offsets[hub.id] = dy; + placed.push(boxFor(dy)); + } + return offsets; + }, [visibleNodes, layout]); + + /** Neighbours of the focused node, used to dim everything else. */ + const focusId = hovered ?? selected; + const neighbours = React.useMemo(() => { + if (!focusId) return null; + const set = new Set([focusId]); + for (const e of visibleEdges) { + if (e.source === focusId) set.add(e.target); + if (e.target === focusId) set.add(e.source); + } + return set; + }, [focusId, visibleEdges]); + + const matches = React.useMemo(() => { + const q = query.trim().toLowerCase(); + if (!q) return null; + return new Set( + visibleNodes + .filter( + (n) => + n.id.toLowerCase().includes(q) || + n.summary.toLowerCase().includes(q) || + (n.area ?? "").toLowerCase().includes(q) || + (n.owner ?? "").toLowerCase().includes(q), + ) + .map((n) => n.id), + ); + }, [query, visibleNodes]); + + const nodeById = React.useMemo( + () => new Map(graph.nodes.map((n) => [n.id, n])), + [graph.nodes], + ); + const detail = selected ? nodeById.get(selected) : undefined; + + const toggleState = (state: FeatureState) => { + setActiveStates((prev) => { + const next = new Set(prev); + if (next.has(state)) { + // Never let the last filter be switched off — an empty graph reads as broken. + if (next.size > 1) next.delete(state); + } else { + next.add(state); + } + return next; + }); + }; + + /** Convert a pointer event to SVG user units, so drag tracks the cursor at any scale. */ + const toSvgPoint = (event: React.PointerEvent): Point | null => { + const svg = svgRef.current; + if (!svg) return null; + const rect = svg.getBoundingClientRect(); + if (!rect.width || !rect.height) return null; + return { + x: ((event.clientX - rect.left) / rect.width) * WIDTH, + y: ((event.clientY - rect.top) / rect.height) * HEIGHT, + }; + }; + + const onPointerDown = (id: string) => (event: React.PointerEvent) => { + const point = toSvgPoint(event); + if (!point) return; + // Record the gesture but change nothing yet: touching state here would move the + // node out from under the cursor and the click would never complete. + gesture.current = { id, x: point.x, y: point.y, moved: false }; + }; + + const onPointerMove = (event: React.PointerEvent) => { + const g = gesture.current; + if (!g) return; + const point = toSvgPoint(event); + if (!point) return; + if (!g.moved && Math.hypot(point.x - g.x, point.y - g.y) < 5) return; + g.moved = true; + setLive({ id: g.id, x: point.x, y: point.y }); + }; + + const endDrag = () => { + const g = gesture.current; + gesture.current = null; + if (!g?.moved || !live) { + setLive(null); + return; + } + // Commit: everything restarts from where it currently sits, and only the + // dropped node is pinned so the rest can relax around it. + setSeed({ ...layout, [live.id]: { x: live.x, y: live.y } }); + setPinnedIds((prev) => new Set(prev).add(live.id)); + setLive(null); + }; + + // Dimmed rather than hidden, so the surrounding shape stays readable — on a dark + // ground anything below about a quarter opacity disappears entirely. + const opacityFor = (id: string) => { + if (matches && !matches.has(id)) return 0.2; + if (neighbours && !neighbours.has(id)) return 0.25; + return 1; + }; + + return ( +
+ {/* controls */} +
+
+ {FEATURE_STATES.map((state) => { + const on = activeStates.has(state); + return ( + + ); + })} +
+ +
+ +
+ + setQuery(e.target.value)} + placeholder="Search features…" + aria-label="Search features" + className="h-9 pl-9 text-sm" + /> +
+
+
+ +
+ {/* graph */} +
+ + + {visibleEdges.map((e, i) => { + const a = positions[e.source]; + const b = positions[e.target]; + if (!a || !b) return null; + const lit = + !neighbours || + (neighbours.has(e.source) && neighbours.has(e.target)); + return ( + + ); + })} + + + + {visibleNodes.map((node) => { + const p = positions[node.id]; + if (!p) return null; + const r = radiusFor(node); + const isFeature = node.kind === "feature"; + const stroke = isFeature + ? STATE_STROKE[node.state!] + : "stroke-foreground/60"; + const fill = isFeature + ? STATE_FILL[node.state!] + : node.kind === "state" + ? "fill-background" + : "fill-muted"; + const isSelected = selected === node.id; + // Hubs stay labelled; feature labels appear on demand, otherwise + // 52 of them collide into an unreadable mat of text. + const showLabel = + !isFeature || + isSelected || + hovered === node.id || + (matches?.has(node.id) ?? false); + + return ( + setHovered(node.id)} + onPointerLeave={() => setHovered(null)} + onFocus={() => setHovered(node.id)} + onBlur={() => setHovered(null)} + onClick={() => + setSelected((cur) => (cur === node.id ? null : node.id)) + } + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setSelected((cur) => + cur === node.id ? null : node.id, + ); + } + }} + > + {isSelected && ( + + )} + + {showLabel && ( + // Anchor labels inward near the edges, or a long hub name + // runs outside the viewBox and gets clipped. + WIDTH - 130 + ? p.x + r + : p.x + } + y={p.y + (labelDy[node.id] ?? r + 12)} + textAnchor={ + p.x < 130 + ? "start" + : p.x > WIDTH - 130 + ? "end" + : "middle" + } + className={`pointer-events-none select-none ${ + isFeature + ? "fill-foreground/85 text-[10px]" + : "fill-foreground text-[11px] font-semibold" + }`} + > + {node.id} + + )} + + ); + })} + + +
+ + {/* detail panel */} + +
+ +

+ Generated from{" "} + {graph.generatedFrom} at build time — + edit a note there and the graph follows.{" "} + + + Back to the roadmap + +

+
+ ); +} + +export default VaultGraphView; diff --git a/src/components/pages/homepage/roadmap/index.tsx b/src/components/pages/homepage/roadmap/index.tsx index 5258dc84..632e908d 100644 --- a/src/components/pages/homepage/roadmap/index.tsx +++ b/src/components/pages/homepage/roadmap/index.tsx @@ -1,5 +1,6 @@ import React from "react"; -import { AlertTriangle, Check, Clock } from "lucide-react"; +import Link from "next/link"; +import { AlertTriangle, Check, Clock, Network } from "lucide-react"; import { Reveal } from "@/components/ui/reveal"; import { @@ -319,11 +320,20 @@ export function PageRoadmap() {
-

- - Status reflects the preprod{" "} - branch. Delivered does not mean live. -

+
+

+ + Status reflects the preprod{" "} + branch. Delivered does not mean live. +

+ + + Explore the same work as a feature graph + +
{/* ahead of schedule */} diff --git a/src/components/ui/seo-fallback.tsx b/src/components/ui/seo-fallback.tsx index a054f282..e08d6087 100644 --- a/src/components/ui/seo-fallback.tsx +++ b/src/components/ui/seo-fallback.tsx @@ -26,6 +26,7 @@ import { const FALLBACK_LINKS = [ { path: "/features", label: "Features" }, { path: "/roadmap", label: "Roadmap" }, + { path: "/roadmap/graph", label: "Feature Graph" }, { path: "/governance", label: "Cardano Governance" }, { path: "/governance/drep", label: "DRep Explorer" }, { path: "/api-docs", label: "API & Bot Documentation" }, diff --git a/src/data/public-routes.ts b/src/data/public-routes.ts index ddc84098..c3d9b3f0 100644 --- a/src/data/public-routes.ts +++ b/src/data/public-routes.ts @@ -5,6 +5,7 @@ export const publicRoutes = [ "/governance/drep/[id]", "/features", "/roadmap", + "/roadmap/graph", "/api-docs", "/dapps", "/blog", diff --git a/src/lib/seo.ts b/src/lib/seo.ts index 30535b80..ed57fe69 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -88,6 +88,11 @@ export const routeSeo: Record = { description: "The twelve-month Mesh Multisig roadmap: what has shipped, what is in progress and what is planned for the Cardano multi-signature wallet, from May 2026 to April 2027.", }, + "/roadmap/graph": { + title: "Feature Graph", + description: + "An interactive knowledge graph of every Mesh Multisig feature and the state it is in — delivered, planned or blocked — linked to its area and the features it touches.", + }, "/blog": { title: "Blog", description: @@ -158,6 +163,7 @@ export const INDEXABLE_ROUTES: SitemapRoute[] = [ { path: "/", changefreq: "weekly", priority: 1.0 }, { path: "/features", changefreq: "monthly", priority: 0.8 }, { path: "/roadmap", changefreq: "monthly", priority: 0.6 }, + { path: "/roadmap/graph", changefreq: "monthly", priority: 0.5 }, { path: "/blog", changefreq: "weekly", priority: 0.7 }, { path: "/governance", changefreq: "daily", priority: 0.8 }, { path: "/governance/drep", changefreq: "daily", priority: 0.7 }, diff --git a/src/lib/vault-types.ts b/src/lib/vault-types.ts new file mode 100644 index 00000000..a5e4f10c --- /dev/null +++ b/src/lib/vault-types.ts @@ -0,0 +1,52 @@ +/** + * The shape of the feature-vault graph, shared by the build-time loader and the + * browser. + * + * Kept apart from `@/lib/vault` on purpose: that module reads the filesystem, so + * importing any *value* from it — not just a type — would pull `fs` into the client + * bundle and fail the build. Anything the graph UI needs at runtime belongs here. + */ + +export const FEATURE_STATES = [ + "delivered", + "in-progress", + "planned", + "blocked", +] as const; + +export type FeatureState = (typeof FEATURE_STATES)[number]; + +export type NodeKind = "feature" | "area" | "state"; + +export type VaultNode = { + /** Note title, which is also its filename and the target of `[[wikilinks]]`. */ + id: string; + kind: NodeKind; + /** Prose body with the leading `# Heading` stripped. */ + summary: string; + state?: FeatureState; + area?: string; + owner?: string; + /** `YYYY-MM` the work landed or is scheduled for. */ + milestone?: string; + issues: number[]; + prs: number[]; + /** Count of edges touching this node, used to size it in the graph. */ + degree: number; +}; + +export type EdgeKind = "in-area" | "has-state" | "relates-to"; + +export type VaultEdge = { + source: string; + target: string; + kind: EdgeKind; +}; + +export type VaultGraph = { + nodes: VaultNode[]; + edges: VaultEdge[]; + /** Feature counts keyed by state, for the summary strip. */ + counts: Record; + generatedFrom: string; +}; diff --git a/src/lib/vault.ts b/src/lib/vault.ts new file mode 100644 index 00000000..38afb37a --- /dev/null +++ b/src/lib/vault.ts @@ -0,0 +1,282 @@ +/** + * Reads the feature vault at the repository root and turns it into a graph. + * + * The vault (`/vault`) is an Obsidian-compatible folder of markdown notes: one per + * feature, area and state. This module is the only thing that knows the file + * layout — everything downstream consumes {@link VaultGraph}. + * + * **Server-only.** It touches `fs`, so it must be called from `getStaticProps` (or + * another server context), never from a component. Parsing at build time means the + * deployed app never reads the vault at runtime, and a malformed note fails the + * build rather than a request. + * + * The frontmatter parser deliberately supports a small YAML subset rather than + * pulling in a dependency: scalars, inline arrays (`[1, 2]`) and block lists. That + * is all the vault uses, and it is documented in `vault/README.md`. Anything richer + * should add a real YAML parser rather than growing this one. + */ +import fs from "fs"; +import path from "path"; + +import { + FEATURE_STATES, + type FeatureState, + type NodeKind, + type VaultEdge, + type VaultGraph, + type VaultNode, +} from "@/lib/vault-types"; + +// Re-exported so server-side callers need only one import. The browser must import +// from `@/lib/vault-types` directly — anything importing this module gets `fs`. +export { + FEATURE_STATES, + type FeatureState, + type NodeKind, + type EdgeKind, + type VaultEdge, + type VaultGraph, + type VaultNode, +} from "@/lib/vault-types"; + +type Frontmatter = Record; + +const VAULT_DIR = path.join(process.cwd(), "vault"); + +/** Strip one layer of matching quotes. */ +function unquote(value: string): string { + const trimmed = value.trim(); + if (trimmed.length >= 2) { + const first = trimmed[0]; + const last = trimmed[trimmed.length - 1]; + if ((first === '"' || first === "'") && first === last) { + return trimmed.slice(1, -1); + } + } + return trimmed; +} + +/** + * Split the `---` fenced frontmatter from the body. Returns empty frontmatter when + * a note has none, so a stray file never throws. + */ +export function splitFrontmatter(raw: string): { + frontmatter: Frontmatter; + body: string; +} { + const normalized = raw.replace(/\r\n/g, "\n"); + if (!normalized.startsWith("---\n")) { + return { frontmatter: {}, body: normalized }; + } + const end = normalized.indexOf("\n---", 3); + if (end === -1) return { frontmatter: {}, body: normalized }; + + const block = normalized.slice(4, end); + const body = normalized.slice(end + 4).replace(/^\n+/, ""); + + const frontmatter: Frontmatter = {}; + let currentKey: string | null = null; + + for (const line of block.split("\n")) { + if (!line.trim() || line.trim().startsWith("#")) continue; + + // Block-list item belonging to the previous key: " - value" + const listItem = line.match(/^\s+-\s+(.*)$/); + if (listItem && currentKey) { + const existing = frontmatter[currentKey]; + const next = Array.isArray(existing) ? existing : []; + next.push(unquote(listItem[1] ?? "")); + frontmatter[currentKey] = next; + continue; + } + + const pair = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!pair) continue; + + const key = pair[1] ?? ""; + const rawValue = (pair[2] ?? "").trim(); + currentKey = key; + + if (rawValue === "") { + // Either an empty scalar or the head of a block list; the next line decides. + frontmatter[key] = []; + continue; + } + + if (rawValue.startsWith("[") && rawValue.endsWith("]")) { + const inner = rawValue.slice(1, -1).trim(); + frontmatter[key] = inner + ? inner.split(",").map((part) => unquote(part)) + : []; + continue; + } + + frontmatter[key] = unquote(rawValue); + } + + return { frontmatter, body }; +} + +function readScalar(fm: Frontmatter, key: string): string | undefined { + const value = fm[key]; + if (typeof value === "string" && value !== "") return value; + return undefined; +} + +function readNumbers(fm: Frontmatter, key: string): number[] { + const value = fm[key]; + const items = Array.isArray(value) ? value : value ? [value] : []; + return items + .map((item) => Number.parseInt(String(item).replace(/^#/, ""), 10)) + .filter((n) => Number.isFinite(n)); +} + +function isFeatureState(value: string | undefined): value is FeatureState { + return ( + value !== undefined && (FEATURE_STATES as readonly string[]).includes(value) + ); +} + +/** Every `[[wikilink]]` in the body, de-duplicated, ignoring `|` display aliases. */ +export function extractWikilinks(body: string): string[] { + const found = new Set(); + for (const match of body.matchAll(/\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g)) { + const title = (match[1] ?? "").trim(); + if (title) found.add(title); + } + return [...found]; +} + +/** First prose paragraph of a note, with the `# Title` heading removed. */ +export function summarize(body: string): string { + const withoutHeading = body.replace(/^#\s+.*$/m, "").trim(); + const paragraph = withoutHeading.split(/\n\s*\n/)[0] ?? ""; + return paragraph + .replace(/^##\s+.*$/gm, "") + .replace(/`/g, "") + .replace(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g, "$1") + .replace(/\s+/g, " ") + .trim(); +} + +function readNotes(dir: string): { title: string; raw: string }[] { + const full = path.join(VAULT_DIR, dir); + if (!fs.existsSync(full)) return []; + return fs + .readdirSync(full) + .filter((name) => name.endsWith(".md")) + .sort() + .map((name) => ({ + title: name.replace(/\.md$/, ""), + raw: fs.readFileSync(path.join(full, name), "utf8"), + })); +} + +/** + * Build the graph. Throws when a feature names an area or state that has no note, + * so a typo surfaces at build time instead of silently dropping an edge. + */ +export function loadVaultGraph(): VaultGraph { + const nodes: VaultNode[] = []; + const edges: VaultEdge[] = []; + + const base = (title: string, kind: NodeKind, body: string): VaultNode => ({ + id: title, + kind, + summary: summarize(body), + issues: [], + prs: [], + degree: 0, + }); + + for (const { title, raw } of readNotes("states")) { + nodes.push(base(title, "state", splitFrontmatter(raw).body)); + } + for (const { title, raw } of readNotes("areas")) { + nodes.push(base(title, "area", splitFrontmatter(raw).body)); + } + + const areaTitles = new Set( + nodes.filter((n) => n.kind === "area").map((n) => n.id), + ); + // States are titled for display ("In Progress") but referenced by slug + // ("in-progress"), so map one to the other rather than duplicating the note. + const stateBySlug = new Map( + nodes + .filter((n) => n.kind === "state") + .map((n) => [n.id.toLowerCase().replace(/\s+/g, "-"), n.id]), + ); + + const featureLinks: { from: string; to: string[] }[] = []; + + for (const { title, raw } of readNotes("features")) { + const { frontmatter, body } = splitFrontmatter(raw); + const state = readScalar(frontmatter, "state"); + const area = readScalar(frontmatter, "area"); + + if (!isFeatureState(state)) { + throw new Error( + `vault: feature "${title}" has state "${state ?? ""}", expected one of ${FEATURE_STATES.join(", ")}`, + ); + } + if (!area || !areaTitles.has(area)) { + throw new Error( + `vault: feature "${title}" names area "${area ?? ""}", which has no note in vault/areas`, + ); + } + const stateTitle = stateBySlug.get(state); + if (!stateTitle) { + throw new Error(`vault: no note in vault/states for state "${state}"`); + } + + nodes.push({ + ...base(title, "feature", body), + state, + area, + owner: readScalar(frontmatter, "owner"), + milestone: readScalar(frontmatter, "milestone"), + issues: readNumbers(frontmatter, "issues"), + prs: readNumbers(frontmatter, "prs"), + }); + + edges.push({ source: title, target: area, kind: "in-area" }); + edges.push({ source: title, target: stateTitle, kind: "has-state" }); + featureLinks.push({ from: title, to: extractWikilinks(body) }); + } + + const featureTitles = new Set( + nodes.filter((n) => n.kind === "feature").map((n) => n.id), + ); + + // Feature-to-feature links, de-duplicated so a mutual reference is one edge. + const seen = new Set(); + for (const { from, to } of featureLinks) { + for (const target of to) { + if (!featureTitles.has(target) || target === from) continue; + const key = [from, target].sort().join(" "); + if (seen.has(key)) continue; + seen.add(key); + edges.push({ source: from, target, kind: "relates-to" }); + } + } + + const byId = new Map(nodes.map((n) => [n.id, n])); + for (const edge of edges) { + const a = byId.get(edge.source); + const b = byId.get(edge.target); + if (a) a.degree += 1; + if (b) b.degree += 1; + } + + const counts = FEATURE_STATES.reduce( + (acc, state) => { + acc[state] = nodes.filter( + (n) => n.kind === "feature" && n.state === state, + ).length; + return acc; + }, + {} as Record, + ); + + return { nodes, edges, counts, generatedFrom: "vault/" }; +} diff --git a/src/pages/roadmap/graph.tsx b/src/pages/roadmap/graph.tsx new file mode 100644 index 00000000..59a68a10 --- /dev/null +++ b/src/pages/roadmap/graph.tsx @@ -0,0 +1,35 @@ +import type { InferGetStaticPropsType } from "next"; + +import { VaultGraphView } from "@/components/pages/homepage/roadmap/graph"; +import { loadVaultGraph } from "@/lib/vault"; + +/** + * The vault is a folder of files that only changes when the repo does, so it is + * read once at build time rather than on every request. This is the one page in + * the app using `getStaticProps` instead of a no-op `getServerSideProps`, and the + * reason is that: no runtime filesystem access, and a malformed note fails the + * build rather than a visitor's request. + */ +export const getStaticProps = () => ({ props: { graph: loadVaultGraph() } }); + +export default function Page({ + graph, +}: InferGetStaticPropsType) { + return ( +
+
+

+ Feature graph +

+

+ Every feature of Mesh Multisig and the state it is in, linked to the + area it belongs to and the features it touches. +

+
+ +
+ +
+
+ ); +} diff --git a/src/pages/roadmap.tsx b/src/pages/roadmap/index.tsx similarity index 100% rename from src/pages/roadmap.tsx rename to src/pages/roadmap/index.tsx diff --git a/vault/README.md b/vault/README.md new file mode 100644 index 00000000..de46f132 --- /dev/null +++ b/vault/README.md @@ -0,0 +1,47 @@ +# Feature Vault + +An Obsidian-compatible vault describing **every feature of Mesh Multisig and the +state it is in**. It is the source of data for the interactive knowledge graph at +[`/roadmap/graph`](../src/pages/roadmap/graph.tsx), parsed at build time by +[`src/lib/vault.ts`](../src/lib/vault.ts). + +Open this folder directly in Obsidian — it uses plain markdown, YAML frontmatter +and `[[wikilinks]]`, with no plugins required. + +## Layout + +| Folder | Note type | What it is | +|--------|-----------|------------| +| `features/` | `feature` | One note per feature, carrying the state it is in | +| `areas/` | `area` | The workstreams features belong to | +| `states/` | `state` | The four states a feature can be in | + +## Frontmatter + +Every feature note carries: + +```yaml +--- +type: feature +area: Governance # must match an areas/ note title +state: delivered # delivered | in-progress | planned | blocked +owner: Quirin # Quirin | Andre | Quirin & Andre | (omitted) +milestone: 2026-06 # YYYY-MM the work landed or is scheduled for +issues: [122] # GitHub issue numbers +prs: [272, 296] # GitHub PR numbers +updated: 2026-07-27 +--- +``` + +`area` and `state` become edges to the corresponding note, and every `[[wikilink]]` +in the body becomes an edge between features. That is the whole graph model — there +is no separate index to keep in sync. + +## Editing + +Add a feature by copying any note in `features/`, or change a state by editing one +`state:` line. The graph picks it up on the next build. Keep titles stable: a note's +filename is its node id, and wikilinks resolve by title. + +The narrative roadmap lives in [`ROADMAP.md`](../ROADMAP.md); this vault is the +structured view of the same work. When a feature ships, update both. diff --git a/vault/areas/Bot & Agent Platform.md b/vault/areas/Bot & Agent Platform.md new file mode 100644 index 00000000..aacf74d7 --- /dev/null +++ b/vault/areas/Bot & Agent Platform.md @@ -0,0 +1,15 @@ +--- +type: area +order: 4 +updated: 2026-07-27 +--- + +# Bot & Agent Platform + +Programmatic access for bots and AI agents, under an authorization model where a +bot can act but can never move funds alone. Arrived roughly four months ahead of +its planned month. + +## Related + +[[Bot Registration & Claim Flow]] · [[Bot Scoped Authorization]] · [[Bot Ballot Drafting]] · [[API Documentation Portal]] · [[Multisig MCP Server]] diff --git a/vault/areas/Document Sign-Off.md b/vault/areas/Document Sign-Off.md new file mode 100644 index 00000000..83ea5282 --- /dev/null +++ b/vault/areas/Document Sign-Off.md @@ -0,0 +1,15 @@ +--- +type: area +order: 2 +updated: 2026-07-27 +--- + +# Document Sign-Off + +The flagship feature line: a wallet-native, off-chain document approval layer that +binds approval to an exact version hash and inherits the wallet's signer set and +threshold. Specified as PRD-001 in the document-driven development vault. + +## Related + +[[Document Sign-Off MVP]] · [[Revision Provenance]] · [[On-Chain Checkpoints]] · [[Collaboration & Standards]] diff --git a/vault/areas/Governance.md b/vault/areas/Governance.md new file mode 100644 index 00000000..742f8e9d --- /dev/null +++ b/vault/areas/Governance.md @@ -0,0 +1,14 @@ +--- +type: area +order: 3 +updated: 2026-07-27 +--- + +# Governance + +Cardano on-chain governance for teams: voting as a multisig DRep, browsing +proposals, publishing rationales, and exploring how other DReps have voted. + +## Related + +[[In-App Governance Voting]] · [[DRep Vote History Explorer]] · [[Ballot Rationale & IPFS]] · [[Proxy Voting]] diff --git a/vault/areas/Growth & Accounts.md b/vault/areas/Growth & Accounts.md new file mode 100644 index 00000000..52b2d2a8 --- /dev/null +++ b/vault/areas/Growth & Accounts.md @@ -0,0 +1,13 @@ +--- +type: area +order: 10 +updated: 2026-07-27 +--- + +# Growth & Accounts + +Identity, onboarding and the social layer around a wallet. + +## Related + +[[Invite Flow]] · [[User Profiles & Contacts]] · [[Discover Page]] diff --git a/vault/areas/Notifications.md b/vault/areas/Notifications.md new file mode 100644 index 00000000..a2a0b729 --- /dev/null +++ b/vault/areas/Notifications.md @@ -0,0 +1,14 @@ +--- +type: area +order: 7 +updated: 2026-07-27 +--- + +# Notifications + +Telling signers that something needs them — reliably, once, and only if they asked +for it. + +## Related + +[[Email Notification Center]] · [[Notification Outbox & Worker]] · [[Scheduled Outbox Drain]] · [[Notification Digests & Reminders]] diff --git a/vault/areas/Platform & UX.md b/vault/areas/Platform & UX.md new file mode 100644 index 00000000..edfde94e --- /dev/null +++ b/vault/areas/Platform & UX.md @@ -0,0 +1,14 @@ +--- +type: area +order: 8 +updated: 2026-07-27 +--- + +# Platform & UX + +The foundations everything else sits on: the SDK runtime, signing correctness, the +interface, and how the product presents itself to people and to crawlers. + +## Related + +[[Signing & Auth Reliability]] · [[Mesh 2.0 Migration]] · [[Mobile & Responsive Foundations]] · [[dApp Connector]] diff --git a/vault/areas/Release & Production Health.md b/vault/areas/Release & Production Health.md new file mode 100644 index 00000000..f951d9b2 --- /dev/null +++ b/vault/areas/Release & Production Health.md @@ -0,0 +1,15 @@ +--- +type: area +order: 1 +updated: 2026-07-27 +--- + +# Release & Production Health + +Getting merged work actually into production, and keeping it healthy once there. +The gap between "merged on `preprod`" and "running in production" is the single +largest risk on the roadmap today. + +## Related + +[[Production Release Gap]] · [[Row-Level Security Hardening]] · [[Migration Deploy Pipeline]] diff --git a/vault/areas/Reliability & CI.md b/vault/areas/Reliability & CI.md new file mode 100644 index 00000000..ec10c7eb --- /dev/null +++ b/vault/areas/Reliability & CI.md @@ -0,0 +1,14 @@ +--- +type: area +order: 6 +updated: 2026-07-27 +--- + +# Reliability & CI + +The safety net: real-chain smoke tests, browser end-to-end coverage, unit and tRPC +suites, and the pipelines that run them. + +## Related + +[[Playwright E2E Suite]] · [[Real-Chain Smoke Tests]] · [[Dependabot CI Unblock]] · [[Transaction Builder & tRPC Tests]] diff --git a/vault/areas/Research.md b/vault/areas/Research.md new file mode 100644 index 00000000..15840a4b --- /dev/null +++ b/vault/areas/Research.md @@ -0,0 +1,13 @@ +--- +type: area +order: 9 +updated: 2026-07-27 +--- + +# Research + +Forward-looking work with a go/no-go at the end rather than a ship date. + +## Related + +[[FROST Threshold Signatures]] · [[Post-Quantum Multi-Sig]] diff --git a/vault/areas/Wallets & Discovery.md b/vault/areas/Wallets & Discovery.md new file mode 100644 index 00000000..3a6e7e84 --- /dev/null +++ b/vault/areas/Wallets & Discovery.md @@ -0,0 +1,14 @@ +--- +type: area +order: 5 +updated: 2026-07-27 +--- + +# Wallets & Discovery + +Creating, importing, finding and signing with multi-signature wallets, plus the +devices and contracts that hold them. + +## Related + +[[Multi-Signature Wallet Core]] · [[Wallet V2 Registration & Discovery]] · [[Hardware Wallet Support]] · [[Vesting Contracts]] diff --git a/vault/features/API Documentation Portal.md b/vault/features/API Documentation Portal.md new file mode 100644 index 00000000..f6af8573 --- /dev/null +++ b/vault/features/API Documentation Portal.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [328, 346] +updated: 2026-07-27 +--- + +# API Documentation Portal + +Swagger UI at `/api-docs` with a wallet-signature bearer-token generator, an +OpenAPI 3.0 spec at `/api/swagger`, an authoritative endpoint reference in the repo, +and a reference bot client. Delivered five months ahead of its planned month, which +freed the December slot on the roadmap. + +## Related + +[[Agent & Crawler Legibility]] · [[Bot Scoped Authorization]] · [[Multisig MCP Server]] diff --git a/vault/features/Agent & Crawler Legibility.md b/vault/features/Agent & Crawler Legibility.md new file mode 100644 index 00000000..74a9d596 --- /dev/null +++ b/vault/features/Agent & Crawler Legibility.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [346] +updated: 2026-07-27 +--- + +# Agent & Crawler Legibility + +Making the site readable to AI agents and crawlers that do not run JavaScript: +`/llms.txt` with a self-contained bot quickstart, a downloadable agent skill at +`/api/skill`, a no-JS crawler fallback listing every public surface, and the +sitemap entry that ties them together. + +## Related + +[[API Documentation Portal]] · [[Landing, SEO & Theme]] · [[Multisig MCP Server]] diff --git a/vault/features/Ballot Rationale & IPFS.md b/vault/features/Ballot Rationale & IPFS.md new file mode 100644 index 00000000..7233c985 --- /dev/null +++ b/vault/features/Ballot Rationale & IPFS.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Governance +state: delivered +owner: Quirin +milestone: 2026-06 +prs: [300, 315] +updated: 2026-07-27 +--- + +# Ballot Rationale & IPFS + +Rationale drafting and caching, a guarded multi-gateway IPFS resolve proxy, and +ballot CSV import and export. Includes ReDoS hardening of the CID path extractor +after a CodeQL finding. + +## Related + +[[In-App Governance Voting]] · [[DRep Vote History Explorer]] diff --git a/vault/features/Better 404 Page.md b/vault/features/Better 404 Page.md new file mode 100644 index 00000000..5e92e269 --- /dev/null +++ b/vault/features/Better 404 Page.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Platform & UX +state: planned +owner: Andre +milestone: 2026-10 +issues: [22] +updated: 2026-07-27 +--- + +# Better 404 Page + +A useful not-found page rather than the default. Batched with the other papercuts. + +## Related + +[[Transaction Pagination]] · [[Full Address Verification]] · [[Landing, SEO & Theme]] diff --git a/vault/features/Bot Ballot Drafting.md b/vault/features/Bot Ballot Drafting.md new file mode 100644 index 00000000..6aaa0e1e --- /dev/null +++ b/vault/features/Bot Ballot Drafting.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [342, 343] +updated: 2026-07-27 +--- + +# Bot Ballot Drafting + +Bots can write vote decisions and draft rationales through the ballot API, with +observer access being sufficient to draft. Includes ballot lifecycle handling and +proposal-id validation. + +## Related + +[[Bot Scoped Authorization]] · [[In-App Governance Voting]] · [[Ballot Rationale & IPFS]] diff --git a/vault/features/Bot Management UI.md b/vault/features/Bot Management UI.md new file mode 100644 index 00000000..4f01a4ae --- /dev/null +++ b/vault/features/Bot Management UI.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [344, 345] +updated: 2026-07-27 +--- + +# Bot Management UI + +A Bot accounts card on the profile page and the wallets dashboard: claim a bot, +inspect it, edit its scopes, see its per-wallet grants at a glance, and revoke it. +Grants are recorded in an append-only audit log. + +## Related + +[[Bot Registration & Claim Flow]] · [[Bot Scoped Authorization]] diff --git a/vault/features/Bot Rate Limiting.md b/vault/features/Bot Rate Limiting.md new file mode 100644 index 00000000..fa924919 --- /dev/null +++ b/vault/features/Bot Rate Limiting.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +updated: 2026-07-27 +--- + +# Bot Rate Limiting + +Three tiers of request guard plus body-size caps: 60/min by default, 15/min on +register, pickup and auth, 5/min on secret rotation, and 40/min per bot id. + +## Related + +[[Bot Registration & Claim Flow]] · [[Bot Scoped Authorization]] diff --git a/vault/features/Bot Registration & Claim Flow.md b/vault/features/Bot Registration & Claim Flow.md new file mode 100644 index 00000000..5265f675 --- /dev/null +++ b/vault/features/Bot Registration & Claim Flow.md @@ -0,0 +1,21 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [341] +updated: 2026-07-27 +--- + +# Bot Registration & Claim Flow + +Human-in-the-loop onboarding: a bot self-registers with the scopes it wants and +receives a claim code valid for 30 minutes; its human owner approves that code with +their own session; the secret is then retrievable exactly once. Secrets are stored +as HMAC-SHA256 peppered with the app's JWT secret, and a bot is bound to one payment +address at first authentication. Bots can also register without an address. + +## Related + +[[Bot Scoped Authorization]] · [[Bot Management UI]] · [[Bot Rate Limiting]] diff --git a/vault/features/Bot Scoped Authorization.md b/vault/features/Bot Scoped Authorization.md new file mode 100644 index 00000000..16fefb94 --- /dev/null +++ b/vault/features/Bot Scoped Authorization.md @@ -0,0 +1,21 @@ +--- +type: feature +area: Bot & Agent Platform +state: delivered +owner: Quirin +milestone: 2026-07 +updated: 2026-07-27 +--- + +# Bot Scoped Authorization + +Double opt-in: a scope on the key **and** a per-wallet grant. Five scopes cover +wallet creation, reads, signing, governance reads and ballot writes; each wallet +separately grants a bot either cosigner or observer access. 27 endpoints accept bot +tokens, including signing with auto-submit once the threshold is met — but the +wallet's M-of-N threshold still gates submission, so a bot can never move funds +alone. + +## Related + +[[Bot Registration & Claim Flow]] · [[Multi-Signature Wallet Core]] · [[Improved Authentication]] diff --git a/vault/features/Bot Webhooks.md b/vault/features/Bot Webhooks.md new file mode 100644 index 00000000..c9f8f3a1 --- /dev/null +++ b/vault/features/Bot Webhooks.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Bot & Agent Platform +state: planned +owner: Andre +milestone: 2026-11 +updated: 2026-07-27 +--- + +# Bot Webhooks + +Outbound events so a bot learns that a transaction needs its signature without +polling. The only unbuilt piece of what the roadmap called Bot platform v2 — the +rest arrived in July. + +## Related + +[[Bot Scoped Authorization]] · [[Multisig MCP Server]] · [[Notification Outbox & Worker]] diff --git a/vault/features/Collaboration & Standards.md b/vault/features/Collaboration & Standards.md new file mode 100644 index 00000000..a35e47be --- /dev/null +++ b/vault/features/Collaboration & Standards.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Document Sign-Off +state: planned +owner: Andre +milestone: 2027-03 +updated: 2026-07-27 +--- + +# Collaboration & Standards + +Version 3, scoped as research: real-time co-authoring via CRDTs, a metadata standard +put forward as a CIP candidate, and an eIDAS/EUDI qualified-signature bridge. + +## Related + +[[On-Chain Checkpoints]] · [[Document Sign-Off MVP]] diff --git a/vault/features/Collateral Service for Proxy.md b/vault/features/Collateral Service for Proxy.md new file mode 100644 index 00000000..0ed4012b --- /dev/null +++ b/vault/features/Collateral Service for Proxy.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Governance +state: planned +owner: Quirin +milestone: 2026-12 +issues: [221] +updated: 2026-07-27 +--- + +# Collateral Service for Proxy + +A collateral service for proxy usage — the last backlog item that had no roadmap +slot until the December replan. + +## Related + +[[Proxy Voting]] diff --git a/vault/features/DRep Vote History Explorer.md b/vault/features/DRep Vote History Explorer.md new file mode 100644 index 00000000..13b28ce4 --- /dev/null +++ b/vault/features/DRep Vote History Explorer.md @@ -0,0 +1,21 @@ +--- +type: feature +area: Governance +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [337, 338, 339] +updated: 2026-07-27 +--- + +# DRep Vote History Explorer + +A public explorer at `/governance/drep` — no connected wallet required — showing +every governance action a DRep voted on, with search, a vote filter, CIP-100 and +CIP-136 rationales resolved lazily from IPFS, and a nine-column CSV export that +resolves all rationales before writing. Served through a Koios proxy because +Blockfrost omits the proposal and rationale anchor. + +## Related + +[[In-App Governance Voting]] · [[Ballot Rationale & IPFS]] · [[Landing, SEO & Theme]] diff --git a/vault/features/Dependabot CI Unblock.md b/vault/features/Dependabot CI Unblock.md new file mode 100644 index 00000000..6e6d3393 --- /dev/null +++ b/vault/features/Dependabot CI Unblock.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Reliability & CI +state: blocked +owner: Andre +milestone: 2026-08 +updated: 2026-07-27 +--- + +# Dependabot CI Unblock + +The v1 smoke workflow hard-fails when its secrets are absent, and dependabot-triggered +runs never receive repository secrets — so every open dependency PR is red for +systemic reasons rather than because of the bump. Seven are open, the oldest since +2026-06-15. The sibling preprod smoke workflow already has the skip-when-unconfigured +guard to copy. + +## Related + +[[Real-Chain Smoke Tests]] · [[Playwright E2E Suite]] diff --git a/vault/features/Discover Page.md b/vault/features/Discover Page.md new file mode 100644 index 00000000..0ddb358c --- /dev/null +++ b/vault/features/Discover Page.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Growth & Accounts +state: planned +owner: Andre +milestone: 2026-09 +issues: [52] +updated: 2026-07-27 +--- + +# Discover Page + +Browse wallets, DAOs and governance activity, plus lookup by signer and policy. +Moved up from February to ride the delivered Wallet V2 discovery work rather than +being built standalone. + +## Related + +[[Wallet V2 Registration & Discovery]] · [[User Profiles & Contacts]] diff --git a/vault/features/Document Sign-Off MVP.md b/vault/features/Document Sign-Off MVP.md new file mode 100644 index 00000000..9dc14f8e --- /dev/null +++ b/vault/features/Document Sign-Off MVP.md @@ -0,0 +1,22 @@ +--- +type: feature +area: Document Sign-Off +state: planned +owner: Quirin & Andre +milestone: 2026-08 +updated: 2026-07-27 +--- + +# Document Sign-Off MVP + +The four primitives: document creation, hash-bound versioning, signer review against +the wallet's inherited threshold, and an exportable audit proof as JSON and PDF. +Approval belongs to a version, never a mutable container — a new version starts a +fresh round at zero approvals. Specified as PRD-001, which is still in Draft, so +finalizing it is the first sub-task. + +Ready means a pilot team runs all six user stories end to end without developer help. + +## Related + +[[Revision Provenance]] · [[Multi-Signature Wallet Core]] · [[Hardware Wallet Support]] · [[Playwright E2E Suite]] diff --git a/vault/features/Email Notification Center.md b/vault/features/Email Notification Center.md new file mode 100644 index 00000000..6d6c9bdf --- /dev/null +++ b/vault/features/Email Notification Center.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Notifications +state: delivered +owner: Andre +milestone: 2026-06 +prs: [322, 326] +updated: 2026-07-27 +--- + +# Email Notification Center + +Signature-required emails through Resend, with per-wallet and per-signer settings on +the wallet Info page: a master opt-in plus separate switches for transaction +signatures and signable payloads. + +## Related + +[[Notification Outbox & Worker]] · [[Email Verification]] · [[Scheduled Outbox Drain]] diff --git a/vault/features/Email Verification.md b/vault/features/Email Verification.md new file mode 100644 index 00000000..2e0b0de0 --- /dev/null +++ b/vault/features/Email Verification.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Notifications +state: delivered +owner: Andre +milestone: 2026-06 +updated: 2026-07-27 +--- + +# Email Verification + +Hashed-token email verification with expiry and single use, so notifications only +ever reach an address its owner confirmed. + +## Related + +[[Email Notification Center]] · [[Notification Outbox & Worker]] diff --git a/vault/features/FROST Threshold Signatures.md b/vault/features/FROST Threshold Signatures.md new file mode 100644 index 00000000..d7260b4c --- /dev/null +++ b/vault/features/FROST Threshold Signatures.md @@ -0,0 +1,24 @@ +--- +type: feature +area: Research +state: planned +owner: Quirin +milestone: 2026-08 +issues: [220] +updated: 2026-07-27 +--- + +# FROST Threshold Signatures + +Evaluate FROST — flexible round-optimized Schnorr threshold signatures — as a +replacement for or complement to native-script multisig: a smaller on-chain +footprint, better privacy from a single on-chain signature, and flexible thresholds. +Deliverables are a trade-off note against native scripts, a proof of concept if the +libraries allow, and a go/no-go. + +Kickoff slipped from July; starting in August is what keeps runway before the +October decision. + +## Related + +[[Post-Quantum Multi-Sig]] · [[Multi-Signature Wallet Core]] diff --git a/vault/features/Feature Knowledge Graph.md b/vault/features/Feature Knowledge Graph.md new file mode 100644 index 00000000..164d3d5d --- /dev/null +++ b/vault/features/Feature Knowledge Graph.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Platform & UX +state: delivered +owner: Quirin +milestone: 2026-07 +updated: 2026-07-27 +--- + +# Feature Knowledge Graph + +An interactive force-directed graph at `/roadmap/graph`, driven by this vault. Every +feature note becomes a node linked to its area, its state and the other features it +references, so the shape of the product — and where the blocked work clusters — is +visible at a glance. The vault is parsed at build time, so there is no index to keep +in sync: editing a `state:` line is what moves a node. + +## Related + +[[Roadmap Page]] · [[Agent & Crawler Legibility]] diff --git a/vault/features/Full Address Verification.md b/vault/features/Full Address Verification.md new file mode 100644 index 00000000..95bb043c --- /dev/null +++ b/vault/features/Full Address Verification.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Platform & UX +state: planned +owner: Andre +milestone: 2026-10 +issues: [196] +updated: 2026-07-27 +--- + +# Full Address Verification + +Let a signer check a full address rather than a truncated one before approving — +a small change that removes a real class of mistake. + +## Related + +[[Transaction Pagination]] · [[Better 404 Page]] · [[Multi-Signature Wallet Core]] diff --git a/vault/features/Hardware Wallet Support.md b/vault/features/Hardware Wallet Support.md new file mode 100644 index 00000000..d6ef8716 --- /dev/null +++ b/vault/features/Hardware Wallet Support.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Wallets & Discovery +state: planned +owner: Andre +milestone: 2026-10 +issues: [44] +updated: 2026-07-27 +--- + +# Hardware Wallet Support + +Ledger and Trezor. Their CIP-8 `signData` support is limited, and Document Sign-Off +approvals depend on exactly that call — so the constraint needs scoping during the +Sign-Off build rather than discovered afterwards. + +## Related + +[[Document Sign-Off MVP]] · [[Multi-Signature Wallet Core]] · [[Signing & Auth Reliability]] diff --git a/vault/features/Improved Authentication.md b/vault/features/Improved Authentication.md new file mode 100644 index 00000000..72993a8f --- /dev/null +++ b/vault/features/Improved Authentication.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Platform & UX +state: planned +owner: Quirin +milestone: 2026-11 +issues: [135] +updated: 2026-07-27 +--- + +# Improved Authentication + +Paired with the connector, since external dApp access and authentication are the +same problem surface. + +## Related + +[[dApp Connector]] · [[Bot Scoped Authorization]] diff --git a/vault/features/In-App Governance Voting.md b/vault/features/In-App Governance Voting.md new file mode 100644 index 00000000..c9cca5d6 --- /dev/null +++ b/vault/features/In-App Governance Voting.md @@ -0,0 +1,21 @@ +--- +type: feature +area: Governance +state: delivered +owner: Quirin +milestone: 2026-06 +issues: [122] +prs: [272, 279, 296, 297, 302] +updated: 2026-07-27 +--- + +# In-App Governance Voting + +Ekklesia and Hydra budget voting for multisig DReps, with DRep-registration +detection, a segmented ballot UI with type chips, proposal cards and DB-cached +tallies. Closed the long-standing governance metadata hash mismatch five months +ahead of its planned month. + +## Related + +[[Ballot Rationale & IPFS]] · [[DRep Vote History Explorer]] · [[Proxy Voting]] · [[Bot Ballot Drafting]] diff --git a/vault/features/Invite Flow.md b/vault/features/Invite Flow.md new file mode 100644 index 00000000..215e358d --- /dev/null +++ b/vault/features/Invite Flow.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Growth & Accounts +state: planned +owner: Quirin +milestone: 2027-02 +prs: [67] +updated: 2026-07-27 +--- + +# Invite Flow + +A first-class flow for bringing signers into a wallet, replacing link-sharing. + +## Related + +[[User Profiles & Contacts]] · [[Multi-Signature Wallet Core]] · [[Discover Page]] diff --git a/vault/features/Landing, SEO & Theme.md b/vault/features/Landing, SEO & Theme.md new file mode 100644 index 00000000..bd257c4c --- /dev/null +++ b/vault/features/Landing, SEO & Theme.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Platform & UX +state: delivered +owner: Quirin +milestone: 2026-06 +prs: [298, 299, 308, 313, 314, 316, 317, 318] +updated: 2026-07-27 +--- + +# Landing, SEO & Theme + +A landing page driven by live mock-data previews rather than screenshots, a central +route-aware SEO surface with sitemap and robots, a static OG image, and a +glass-morphism theme extended across all cards — with the background animation cost +cut down so scrolling stays smooth. + +## Related + +[[Mobile & Responsive Foundations]] · [[Agent & Crawler Legibility]] · [[Roadmap Page]] diff --git a/vault/features/Legacy Wallet Compatibility.md b/vault/features/Legacy Wallet Compatibility.md new file mode 100644 index 00000000..49c7583e --- /dev/null +++ b/vault/features/Legacy Wallet Compatibility.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Wallets & Discovery +state: delivered +owner: Quirin +milestone: 2026-05 +issues: [223] +prs: [210, 225] +updated: 2026-07-27 +--- + +# Legacy Wallet Compatibility + +Keeping wallets created by earlier versions working, including DRep retirement and +deregistration. Legacy wallets carry raw imported bodies, so their bytes must be +preserved exactly or co-signers end up signing different transactions. + +## Related + +[[Multi-Signature Wallet Core]] · [[Signing & Auth Reliability]] · [[Wallet Import]] diff --git a/vault/features/Mesh 2.0 Migration.md b/vault/features/Mesh 2.0 Migration.md new file mode 100644 index 00000000..e604ebc4 --- /dev/null +++ b/vault/features/Mesh 2.0 Migration.md @@ -0,0 +1,21 @@ +--- +type: feature +area: Platform & UX +state: blocked +owner: Quirin +milestone: 2026-07 +prs: [268, 269, 278] +updated: 2026-07-27 +--- + +# Mesh 2.0 Migration + +Groundwork is done — Prisma 7.8 and Next 16, a hardfork-ready transaction builder, +and every wallet operation funnelled through a single bridge with an ESLint +guardrail. The cutover itself is blocked upstream: npm's latest `@meshsdk/core` is +still 1.9.1 and no 2.x has been published. Demoted from a monthly task to a standing +watch item, since it cannot be scheduled against an unpublished dependency. + +## Related + +[[Signing & Auth Reliability]] · [[Multi-Signature Wallet Core]] diff --git a/vault/features/Migration Deploy Pipeline.md b/vault/features/Migration Deploy Pipeline.md new file mode 100644 index 00000000..c9b67d8c --- /dev/null +++ b/vault/features/Migration Deploy Pipeline.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Release & Production Health +state: planned +owner: Quirin +milestone: 2026-09 +prs: [319] +updated: 2026-07-27 +--- + +# Migration Deploy Pipeline + +Make the release path self-verifying instead of dependent on a path filter: run +`prisma migrate status` as a post-deploy gate and alert on drift, so "merged" and +"applied" cannot silently diverge again. + +## Related + +[[Production Release Gap]] · [[Row-Level Security Hardening]] · [[Real-Chain Smoke Tests]] diff --git a/vault/features/Mobile & Responsive Foundations.md b/vault/features/Mobile & Responsive Foundations.md new file mode 100644 index 00000000..dd0d98b5 --- /dev/null +++ b/vault/features/Mobile & Responsive Foundations.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Platform & UX +state: delivered +owner: Quirin +milestone: 2026-06 +prs: [287, 288, 289, 290, 291, 292, 293, 294, 295] +updated: 2026-07-27 +--- + +# Mobile & Responsive Foundations + +Viewport, touch targets, dialogs and inputs on small screens, plus skeleton and +empty states, error toasts, pagination, labels and asset display. + +## Related + +[[Landing, SEO & Theme]] · [[Wallet Import]] · [[Transaction Pagination]] diff --git a/vault/features/Multi-Signature Wallet Core.md b/vault/features/Multi-Signature Wallet Core.md new file mode 100644 index 00000000..835c8464 --- /dev/null +++ b/vault/features/Multi-Signature Wallet Core.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Wallets & Discovery +state: delivered +owner: Quirin +milestone: 2026-05 +updated: 2026-07-27 +--- + +# Multi-Signature Wallet Core + +M-of-N native-script wallets: create a wallet, invite and cryptographically verify +signers, build transactions, collect witnesses and submit once the threshold is met. +Everything else in the product inherits this signer set and threshold. + +## Related + +[[Signing & Auth Reliability]] · [[Legacy Wallet Compatibility]] · [[Wallet V2 Registration & Discovery]] · [[Document Sign-Off MVP]] diff --git a/vault/features/Multisig MCP Server.md b/vault/features/Multisig MCP Server.md new file mode 100644 index 00000000..20e8c4c2 --- /dev/null +++ b/vault/features/Multisig MCP Server.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Bot & Agent Platform +state: planned +owner: Andre +milestone: 2026-11 +updated: 2026-07-27 +--- + +# Multisig MCP Server + +Expose the existing bot API as an MCP server, so an AI agent can act as a wallet +observer or ballot drafter directly. A short step from the delivered agent surface, +and a genuine differentiator. + +## Related + +[[Agent & Crawler Legibility]] · [[API Documentation Portal]] · [[Bot Scoped Authorization]] diff --git a/vault/features/Notification Digests & Reminders.md b/vault/features/Notification Digests & Reminders.md new file mode 100644 index 00000000..b01347ee --- /dev/null +++ b/vault/features/Notification Digests & Reminders.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Notifications +state: planned +owner: Andre +milestone: 2026-09 +updated: 2026-07-27 +--- + +# Notification Digests & Reminders + +Ballot-deadline and threshold-reached reminders riding on the existing outbox. This +is product work rather than infrastructure — the delivery machinery is already built. + +## Related + +[[Notification Outbox & Worker]] · [[Scheduled Outbox Drain]] · [[In-App Governance Voting]] diff --git a/vault/features/Notification Outbox & Worker.md b/vault/features/Notification Outbox & Worker.md new file mode 100644 index 00000000..3f8d09dd --- /dev/null +++ b/vault/features/Notification Outbox & Worker.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Notifications +state: delivered +owner: Andre +milestone: 2026-06 +updated: 2026-07-27 +--- + +# Notification Outbox & Worker + +A real outbox rather than fire-and-forget sending: every delivery carries an +idempotency key, an attempt counter and a retry backoff, across nine statuses that +include four distinct skip reasons — no email, not verified, opted out, disabled. +Drained by a token-authenticated endpoint. + +## Related + +[[Email Notification Center]] · [[Scheduled Outbox Drain]] · [[Notification Digests & Reminders]] diff --git a/vault/features/On-Chain Checkpoints.md b/vault/features/On-Chain Checkpoints.md new file mode 100644 index 00000000..d204a9c6 --- /dev/null +++ b/vault/features/On-Chain Checkpoints.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Document Sign-Off +state: planned +owner: Quirin +milestone: 2027-01 +updated: 2026-07-27 +--- + +# On-Chain Checkpoints + +Version 2: opt-in anchoring of a version's hash and its parent in Cardano +transaction metadata, giving a chain of custody without putting documents on chain. + +## Related + +[[Revision Provenance]] · [[Document Sign-Off MVP]] diff --git a/vault/features/Playwright E2E Suite.md b/vault/features/Playwright E2E Suite.md new file mode 100644 index 00000000..20afa4b9 --- /dev/null +++ b/vault/features/Playwright E2E Suite.md @@ -0,0 +1,22 @@ +--- +type: feature +area: Reliability & CI +state: delivered +owner: Andre +milestone: 2026-07 +prs: [323, 335, 336] +updated: 2026-07-27 +--- + +# Playwright E2E Suite + +Eleven spec files and roughly 54 browser tests covering wallet creation for legacy +and SDK wallets, real preprod ring transfers, staking, proxy, DRep and ballot UI, +bot management, notification settings, wallet access control, signing rejection and +responsive overflow. Runs in Docker, serialized against the smoke job through a +shared concurrency group. Unscheduled work that became the safety net Document +Sign-Off will ship against. + +## Related + +[[Real-Chain Smoke Tests]] · [[Transaction Builder & tRPC Tests]] · [[Document Sign-Off MVP]] diff --git a/vault/features/Post-Quantum Multi-Sig.md b/vault/features/Post-Quantum Multi-Sig.md new file mode 100644 index 00000000..ef09430e --- /dev/null +++ b/vault/features/Post-Quantum Multi-Sig.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Research +state: planned +owner: Quirin +milestone: 2026-10 +issues: [220] +updated: 2026-07-27 +--- + +# Post-Quantum Multi-Sig + +Assess Lemour lattice-based threshold signatures for long-term quantum resistance, +as a forward-looking alternative or complement to FROST. Maturity and available +libraries are the open questions. + +## Related + +[[FROST Threshold Signatures]] diff --git a/vault/features/Production Release Gap.md b/vault/features/Production Release Gap.md new file mode 100644 index 00000000..75657efe --- /dev/null +++ b/vault/features/Production Release Gap.md @@ -0,0 +1,24 @@ +--- +type: feature +area: Release & Production Health +state: blocked +owner: Quirin +milestone: 2026-08 +prs: [319, 321] +updated: 2026-07-27 +--- + +# Production Release Gap + +Production has applied no migration since 2026-05-10, and `preprod` sits 75 commits +ahead of `main` — so June and July are built but unreleased. The migration workflow +only fires on pushes to `main` touching `prisma/migrations/**`, so fixing it never +re-triggered the run that failed on 17 June. Governance tallies error, the +notification center has no tables, and address-less bot registration cannot work. + +Unblocking is one release plus one manual workflow dispatch, and it is the first +task of August. + +## Related + +[[Migration Deploy Pipeline]] · [[Row-Level Security Hardening]] · [[Email Notification Center]] · [[Bot Registration & Claim Flow]] diff --git a/vault/features/Proxy Voting.md b/vault/features/Proxy Voting.md new file mode 100644 index 00000000..639e5b94 --- /dev/null +++ b/vault/features/Proxy Voting.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Governance +state: planned +owner: Quirin +milestone: 2026-12 +updated: 2026-07-27 +--- + +# Proxy Voting + +Polish and documentation for voting through a proxy, so a wallet can delegate the +mechanics of casting a vote without delegating the decision. + +## Related + +[[Collateral Service for Proxy]] · [[In-App Governance Voting]] diff --git a/vault/features/Real-Chain Smoke Tests.md b/vault/features/Real-Chain Smoke Tests.md new file mode 100644 index 00000000..f0b52ba5 --- /dev/null +++ b/vault/features/Real-Chain Smoke Tests.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Reliability & CI +state: delivered +owner: Andre +milestone: 2026-06 +issues: [213] +prs: [217, 218] +updated: 2026-07-27 +--- + +# Real-Chain Smoke Tests + +A preprod environment plus a smoke system that exercises real route chains against +the actual chain, rather than mocks. + +## Related + +[[Playwright E2E Suite]] · [[Dependabot CI Unblock]] · [[Migration Deploy Pipeline]] diff --git a/vault/features/Revision Provenance.md b/vault/features/Revision Provenance.md new file mode 100644 index 00000000..92f9668f --- /dev/null +++ b/vault/features/Revision Provenance.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Document Sign-Off +state: planned +owner: Quirin +milestone: 2026-10 +updated: 2026-07-27 +--- + +# Revision Provenance + +Version 1 of Sign-Off: revision history as a first-class object, diff and rollback, +and a richer audit export. Still entirely off-chain. + +## Related + +[[Document Sign-Off MVP]] · [[On-Chain Checkpoints]] diff --git a/vault/features/Roadmap Page.md b/vault/features/Roadmap Page.md new file mode 100644 index 00000000..593a929e --- /dev/null +++ b/vault/features/Roadmap Page.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Platform & UX +state: delivered +owner: Quirin +milestone: 2026-07 +prs: [350] +updated: 2026-07-27 +--- + +# Roadmap Page + +A public `/roadmap` page rendering the twelve-month plan as a workstream timeline, +grouped by workstream rather than owner so the continuity from delivered to planned +stays visible. Status is encoded by fill, icon and label together, never colour +alone. + +## Related + +[[Feature Knowledge Graph]] · [[Landing, SEO & Theme]] · [[Production Release Gap]] diff --git a/vault/features/Row-Level Security Hardening.md b/vault/features/Row-Level Security Hardening.md new file mode 100644 index 00000000..acfd34fb --- /dev/null +++ b/vault/features/Row-Level Security Hardening.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Release & Production Health +state: blocked +owner: Quirin +milestone: 2026-08 +prs: [332] +updated: 2026-07-27 +--- + +# Row-Level Security Hardening + +Seven production tables — including the bot key and claim-token tables and the audit +log — still have row-level security disabled and are reachable by the anon PostgREST +role. The migration that enables RLS with deny-all policies is written and merged; +it is purely undeployed, so this unblocks the moment the release lands. + +## Related + +[[Production Release Gap]] · [[Migration Deploy Pipeline]] · [[Bot Registration & Claim Flow]] diff --git a/vault/features/Scheduled Outbox Drain.md b/vault/features/Scheduled Outbox Drain.md new file mode 100644 index 00000000..20e81d23 --- /dev/null +++ b/vault/features/Scheduled Outbox Drain.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Notifications +state: planned +owner: Andre +milestone: 2026-08 +issues: [327] +updated: 2026-07-27 +--- + +# Scheduled Outbox Drain + +The outbox exists and the drain endpoint exists, but nothing calls it on a schedule — +the daily balance snapshot is the only cron in the repository. Until this lands, +queued notifications sit undelivered. + +## Related + +[[Notification Outbox & Worker]] · [[Email Notification Center]] · [[Notification Digests & Reminders]] diff --git a/vault/features/Signing & Auth Reliability.md b/vault/features/Signing & Auth Reliability.md new file mode 100644 index 00000000..15fee328 --- /dev/null +++ b/vault/features/Signing & Auth Reliability.md @@ -0,0 +1,21 @@ +--- +type: feature +area: Platform & UX +state: delivered +owner: Quirin +milestone: 2026-06 +prs: [273, 277, 281, 282, 286, 324, 325] +updated: 2026-07-27 +--- + +# Signing & Auth Reliability + +The correctness work under signing: bech32 normalization, the Mesh 1.9 `signData` +argument order, a byte-preserving core-cst witness and body-hash merge with a +regression test, recovery from a stuck "Loading…" state, and login no longer +hanging on "Authorize". Byte preservation is the load-bearing part — co-signers must +sign identical bytes. + +## Related + +[[Multi-Signature Wallet Core]] · [[Mesh 2.0 Migration]] · [[Legacy Wallet Compatibility]] diff --git a/vault/features/Transaction Builder & tRPC Tests.md b/vault/features/Transaction Builder & tRPC Tests.md new file mode 100644 index 00000000..223fbcb1 --- /dev/null +++ b/vault/features/Transaction Builder & tRPC Tests.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Reliability & CI +state: planned +owner: Quirin +milestone: 2026-09 +issues: [255] +updated: 2026-07-27 +--- + +# Transaction Builder & tRPC Tests + +Integration coverage for the transaction builder and the tRPC routers, plus +extending the browser suite to the Sign-Off flows before they ship. + +## Related + +[[Playwright E2E Suite]] · [[Document Sign-Off MVP]] diff --git a/vault/features/Transaction Pagination.md b/vault/features/Transaction Pagination.md new file mode 100644 index 00000000..50d67735 --- /dev/null +++ b/vault/features/Transaction Pagination.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Platform & UX +state: planned +owner: Andre +milestone: 2026-10 +issues: [30] +updated: 2026-07-27 +--- + +# Transaction Pagination + +Pagination for wallets with long transaction histories. + +## Related + +[[Mobile & Responsive Foundations]] · [[Full Address Verification]] · [[Better 404 Page]] diff --git a/vault/features/User Profiles & Contacts.md b/vault/features/User Profiles & Contacts.md new file mode 100644 index 00000000..fda2612c --- /dev/null +++ b/vault/features/User Profiles & Contacts.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Growth & Accounts +state: planned +owner: Andre +milestone: 2027-01 +updated: 2026-07-27 +--- + +# User Profiles & Contacts + +The social layer: profiles and a contact book. The Contact model and profile-image +storage already exist, so this is the interface on top rather than a build from +scratch. + +## Related + +[[Invite Flow]] · [[Discover Page]] diff --git a/vault/features/Vesting Contracts.md b/vault/features/Vesting Contracts.md new file mode 100644 index 00000000..eeb0fe56 --- /dev/null +++ b/vault/features/Vesting Contracts.md @@ -0,0 +1,18 @@ +--- +type: feature +area: Wallets & Discovery +state: planned +owner: Quirin +milestone: 2027-01 +issues: [81] +updated: 2026-07-27 +--- + +# Vesting Contracts + +Time-locked multi-sig contracts, so a treasury can commit funds to a schedule that +the threshold alone cannot shortcut. + +## Related + +[[Multi-Signature Wallet Core]] · [[On-Chain Checkpoints]] diff --git a/vault/features/Wallet Import.md b/vault/features/Wallet Import.md new file mode 100644 index 00000000..f78c1f5e --- /dev/null +++ b/vault/features/Wallet Import.md @@ -0,0 +1,19 @@ +--- +type: feature +area: Wallets & Discovery +state: delivered +owner: Quirin +milestone: 2026-06 +prs: [274] +updated: 2026-07-27 +--- + +# Wallet Import + +Importing an existing multisig wallet, including across instances and on mobile. +The import wizard renders before a wallet is connected so people can see what is +available; the per-tab actions still gate on a live connection. + +## Related + +[[Legacy Wallet Compatibility]] · [[Multi-Signature Wallet Core]] · [[Mobile & Responsive Foundations]] diff --git a/vault/features/Wallet V2 Registration & Discovery.md b/vault/features/Wallet V2 Registration & Discovery.md new file mode 100644 index 00000000..fc737481 --- /dev/null +++ b/vault/features/Wallet V2 Registration & Discovery.md @@ -0,0 +1,20 @@ +--- +type: feature +area: Wallets & Discovery +state: delivered +owner: Andre +milestone: 2026-07 +issues: [33] +prs: [340] +updated: 2026-07-27 +--- + +# Wallet V2 Registration & Discovery + +On-chain registration records and a discovery index, so a multisig wallet can be +found rather than only shared by link. Delivered on schedule, and the foundation the +Discover page now builds on rather than starting fresh. + +## Related + +[[Discover Page]] · [[Multi-Signature Wallet Core]] · [[Wallet Import]] diff --git a/vault/features/dApp Connector.md b/vault/features/dApp Connector.md new file mode 100644 index 00000000..6d4bd7c3 --- /dev/null +++ b/vault/features/dApp Connector.md @@ -0,0 +1,17 @@ +--- +type: feature +area: Platform & UX +state: planned +owner: Quirin +milestone: 2026-11 +updated: 2026-07-27 +--- + +# dApp Connector + +Let external dApps request multi-sig transactions, so a team treasury can be used +wherever a single-signature wallet would be. + +## Related + +[[Improved Authentication]] · [[Multi-Signature Wallet Core]] diff --git a/vault/states/Blocked.md b/vault/states/Blocked.md new file mode 100644 index 00000000..0914ed80 --- /dev/null +++ b/vault/states/Blocked.md @@ -0,0 +1,12 @@ +--- +type: state +order: 4 +tone: bad +updated: 2026-07-27 +--- + +# Blocked + +Cannot proceed, or has regressed, for a reason outside the normal build cycle — an +unpublished upstream dependency, an undeployed migration, or a broken pipeline. +Blocked items are the ones worth looking at first. diff --git a/vault/states/Delivered.md b/vault/states/Delivered.md new file mode 100644 index 00000000..a7f292b6 --- /dev/null +++ b/vault/states/Delivered.md @@ -0,0 +1,12 @@ +--- +type: state +order: 1 +tone: good +updated: 2026-07-27 +--- + +# Delivered + +Built, reviewed and merged to `preprod`. **Delivered is not the same as live** — the +production database and `main` can trail `preprod` by weeks, so a delivered feature +may still be unreachable in production. See [[Production Release Gap]]. diff --git a/vault/states/In Progress.md b/vault/states/In Progress.md new file mode 100644 index 00000000..5c2af7f5 --- /dev/null +++ b/vault/states/In Progress.md @@ -0,0 +1,10 @@ +--- +type: state +order: 2 +tone: active +updated: 2026-07-27 +--- + +# In Progress + +Actively being built right now, with code on a branch but not yet merged to `preprod`. diff --git a/vault/states/Planned.md b/vault/states/Planned.md new file mode 100644 index 00000000..5d6dd52a --- /dev/null +++ b/vault/states/Planned.md @@ -0,0 +1,11 @@ +--- +type: state +order: 3 +tone: neutral +updated: 2026-07-27 +--- + +# Planned + +Scheduled in [`ROADMAP.md`](../../ROADMAP.md) with an owner and a target month, but +not started. A planned feature has no code yet.