Canvas Viewer
Feature Overview [edit]
memo canvas (alias memo serve-canvas) starts an embedded, zero-dependency HTTP server and SVG graph visualizer for inspecting vault memory. Default bind is http://127.0.0.1:3125 (config ports.canvas), loopback only unless an auth token is supplied. The page is a dark-mode single document with a sidebar (project selector, search filter, relation/legend controls) and an SVG viewport that lays out records as a force/hierarchical graph with zoom and pan.
Journeys:
- An operator lists projects, picks one, and sees its nodes (traps, decisions, specs, plans, reviews) colored by kind, with edges for
supersedesandrelatedrelations. - Typing in the search box filters/highlights nodes live.
- Clicking a node opens the details drawer, which fetches the full markdown body and frontmatter.
GET /api/searchreturns ranked SQLite FTS matches for a query scoped to a project.
The Canvas server is read-only for vault assets and is separate from the Status Monitor (which owns ops backup, not the graph).
Business Rules & Logic [edit]
- Bind refusal: a non-loopback
--hostwithout an auth token throws beforelisten(--auth-token/SPEC_MEMO_AUTH_TOKEN). - When a token is configured, only
/api/*paths requireAuthorization: Bearer <token>; the HTML page is served without auth so the browser can load it, and the embedded JS forwards?token=from the page URL as a bearer header on API calls. Unauthorized API calls return401 { error: "Unauthorized" }. - Project ids are resolved inside
projects/withisPathInside; a traversal-like id returns an empty graph rather than reading outside the vault. - Record listing excludes conflict sidecars (
*.conflict.*.md) and malformed records (silently skipped). - Log records and records carrying the tags
activity,monitor, ormcp-activityare hidden from the graph by default (they are status-monitor traffic, not topology); pass?includeLogs=1to include them. - Graph edges are derived from
supersedes(resolved against existing node ids) andrelatedSlug(resolved via the slug map, self-links dropped). Edges forlinkedPaths,tags, andkindare part of the spec's intended relation model but the shippedgenerateProjectGraphemitssupersedesandrelatedonly. - Node titles fall back in order:
titlefrontmatter, first#heading,slug, thenid. - Record and search API responses pass through
sanitizeToolOutputso record bodies and metadata are redaction-checked before leaving the server (/api/projectsand/api/project/{id}/graphdo not).
Technical Architecture [edit]
src/canvas.tsstartCanvasServer(options)accepts{ vaultRoot?, port?, host?, project?, authToken? }and returns{ server, port, host, url, close }.close()resolves whenserver.close()completes.- REST contract:
- GET / / GET /index.html -> generateCanvasHtml() self-contained HTML. - GET /api/projects -> getVaultProjectList(vaultRoot) array (enriched with displayName, aliasOf, recordCount). - GET /api/project/{projectId}/graph -> ProjectGraph ({ projectId, nodes, edges }); optional ?includeLogs=1|true. - GET /api/record/{projectId}/{kind}/{id} -> sanitizeToolOutput({ record }) with full body and frontmatter. - GET /api/search?q=...&project=... -> searchIndex({ query, projectId, vaultRoot }) ranked matches. - Unknown paths fall through to 404 { error: "Not found" }.
GraphNodefields:id,kind,slug?,title,status,severity?,updated,project,pathPatterns?,tags?,hits(fromhitCountOf),occurrences?(fromoccurrenceOf).GraphEdgeis{ source, target, relation: supersedes | related | links | shares-tag }.listProjectRecordsInternalwalksRECORD_SUBDIRSand parses each.mdviaparseRecord;generateProjectGraphbuilds nodes then edges.- CLI (
src/cli.ts):memo canvas [--port <p>] [--host <h>] [--project <id>] [--auth-token] [--json] [--vaultRoot]boots the server and prints the URL;--jsonemits{ url, port, host }.memo canvas/serve-canvasare refused in remote mode (REMOTE_MODE_RESTRICTION). - Provenance:
0042-canvas-viewer.spec.md(spec default:4100; current shipped default3125). The viewer deliberately does not consume the status activity bus and does not write vault records.