GraphDocumentView.tsxfix(graphs): a deal's org chart is readable by the people on that deal Greptile's remaining open finding on this PR. A conversation chart lives at `conversation/{id}/org-chart` and is stored ORG-OWNED on purpose — `userId` is null so that one `graphs: org-chart: rw` grant covers every deal. That makes ownership the wrong question to ask about it: `findGraph`'s `ownerScope` matches a null owner for any member of the org, so every teammate could read and mutate the buying-committee chart of a deal they were not on, by path, by name or by id. Membership is now asked at RESOLUTION, for the same reason ownership is: all three addressing forms end at that one row, and a check bolted onto a single caller would leave the other two open. `findGraph` runs every resolved row through one gate, which is a no-op for anything that is not a conversation chart. - `GraphScope.canReachConversation` is optional because it is a property of the SURFACE, not of the graph. `graphs.*` supplies it (session user, must be on the deal); the admin router stays deliberately org-wide and the agent tool stays gated by its playbook grant. A scope that omits it is byte-for-byte unchanged. - `graphs.*` answers it with `assertConversationMembership` — the helper `files.*`, `drive.*` and `agent.*` already guard conversation-scoped reads with, so the graph canvas and the file tree beside it agree about who may see a deal. Fails closed on any throw, as `agent.ts` does with it. - `conversationIdFromOrgChartPath` is the inverse of the path builder, and is deliberately exact: a looser match would hand the gate the wrong id for `conversation/{id}/agent-7/notes`, a different document with a different rule. Tested both ways. Also drops a stale duplicated comment above `activeKind` left by an earlier edit — two comments for one constant, the first contradicting the second. Not changed, and worth a look separately: `listGraphs` applies no ownership filter at all, so it still lists other teammates' `user/graphs/*` and every deal's chart. The ids it hands out no longer open anything that check would refuse, so this is an enumeration gap rather than an access one — but it is shared with the agent and admin surfaces, so narrowing it is its own change. Verified: @zero/server and @zero/mail typecheck; 1803 tests pass across services/documents, trpc and the document tools; deps:check clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:50 PMindex.tsfeat(mail): the graph canvas (design: graph-documents phase 5) A graph document renders as an editable node/edge canvas whose nodes open as the documents they point at. ONE tRPC read returns the schema, the nodes, the edges and the layout, because all four live in one Y.Doc and come out of one decode. That is the payoff of keeping nodes inside the document: a board reads its schema from the Y.Doc and its cards from SQL and the two can disagree; a graph cannot. ONE node component, for every graph there will ever be. It renders from the SCHEMA — one row per `displayOnCard` field, typed by the same `TableColumnType` the board and table use, drawn by the same `CardFieldDisplay`. There is no node component per domain, and the day there is, "an org chart is configuration" stops being true. Three things the tests pin that are easy to undo: - The layout runs over ONLY the edges matching `view.layoutEdgeKind`, so which relation is the skeleton is a VIEW property: point it at `influences` and the same people re-rank with nothing written. Adding a non-layout edge moves nobody. - `hydrated ?? derived ?? fields`, in that order. An authored value shows as authored, a bound value shows live once hydration lands, and the cache only fills the gap between open and arrival — so the three can disagree in exactly one direction, toward the document. - Only TWO gestures write: a drag is one `graphLayout` key, a drawn connection is one `connect`. There is deliberately no whole-canvas save, because that is how a derived annotation becomes a persisted field and how one open tab overwrites another's concurrent edit. A computed layout is persisted ONCE, so the next open is stable and any later re-layout is a deliberate act rather than something that happens because someone opened the file. `graph` is registered in all five places a document type has to be — the full-bleed set, FileBrowser, FileArtifactPanel, FilesTab and CompanyExplorer — plus its icon and label. 36 graph tests; 4,588 passing across apps/mail, no regressions. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 14, 2026, 10:04 PM