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