on master help sign in
Merge pull request #1053 from ynx0/graph-store-docs
commitb574b9d58981d666935f1ac05d295ac01585c204 authorL <logan@tlon.io> dateMon 12 Apr 2021, 13:38 (6 years ago) parent67b8ea86782bfc
5 files changed+1021
userspace/graph-store/_index.md+23 userspace/graph-store/advanced-info.md+64 userspace/graph-store/data-structure-overview.md+338 userspace/graph-store/overview.md+109 userspace/graph-store/validator-walkthrough.md+487
userspace/graph-store/_index.mdnew file+23
@@ -0,0 +1,23 @@
1++++
2+title = "Graph Store"
3+weight = 50
4+sort_by = "weight"
5+template = "sections/docs/chapters.html"
6+insert_anchor_links = "right"
7++++
8+
9+## [Overview](@/docs/userspace/graph-store/overview.md)
10+
11+Basic concepts and architectural overview of Graph Store
12+
13+## [Data Structure Overview](@/docs/userspace/graph-store/data-structure-overview.md)
14+
15+A walkthrough of the various data structures and types used in Graph Store.
16+
17+## [Validator Walkthrough](@/docs/userspace/graph-store/validator-walkthrough.md)
18+
19+A walkthrough of existing applications built using Graph Store
20+
21+## [Advanced Information](@/docs/userspace/graph-store/advanced-info.md)
22+
23+Advanced information and selected implementation details about Graph Store.
userspace/graph-store/advanced-info.mdnew file+64
@@ -0,0 +1,64 @@
1++++
2+title = "Advanced Info"
3+weight = 50
4+sort_by = "weight"
5+template = "doc.html"
6+insert_anchor_links = "right"
7++++
8+
9+This section is not required, but does shed light on some implicit assumptions that Graph Store makes.
10+
11+
12+### Miscellaneous
13+- Only nodes that successfully typecheck under the validator will be added to the graph
14+- Graphs are validated recursively (see [`app/graph-store.hoon#L598-L616`](https://github.com/urbit/urbit/blob/5cb6af0433a65fb28b4bc957be10cb436781392d/pkg/arvo/app/graph-store.hoon#L598-L616))
15+ - First, validate all top level nodes of a graph.
16+ - Then, recursively validate all children nodes of those nodes.
17+ - Base case: empty children is always valid
18+- Nodes are also validated against the graph's mark when inserted individually (see [`app/graph-store.hoon#L380`](https://github.com/urbit/urbit/blob/e2ad6e3e9219c8bfad62f27f05c7cac94c9effa8/pkg/arvo/app/graph-store.hoon#L380))
19+- Only the root level graphs get validated with marks. There is only one mark / validator per graph. All child graphs get validated with the same mark as the root/top-level one.
20+- The way in which Graph Store works is by mirroring all data from a given social media channel. Thus, anything you see is your copy of it, and anything you do is sent as a request to the hosting ship.
21+- Unmanaged graphs (a graph that isn't associated to a group) actually still have a unmanaged / hidden group. If you want to construct a `reference` to content in an unmanaged graph, it is still possible; the group in that case is then the same resource id as the graph itself
22+
23+
24+
25+### What happens when you add or remove a node?
26+
27+When adding nodes, Graph Store takes in a `(map index node)`. That is, a map of `node`s to add keyed by an `index` representing where in the graph the node should be inserted. However, there are some limitations. For instance, Graph Store does not allow for a `node` which has non-existent ancestors to be added, meaning that missing ancestor nodes are not automatically created. Graph Store requires that every node up until the 2nd to last level of nesting is created, meaning that every ancestor except the parent must be created before being added. The ancestors must either already exist in Graph Store or must be included in the `graph-update`. To ensure consistent behavior, all nodes are added shallowest-index first, which ensures that no child is added before it's parent (if it exists). This is why any non-leaf node must have its parent exist, either already within the graph or in the update.
28+
29+Also, Graph Store merges the flat list of nodes from an `%add-nodes` update into the fully connected graph structure. This means that when sending an update to `graph-store`, children nodes do not have to be nested within the parent's data structure in the update. They simply have to exist somewhere in the update.
30+
31+Another constraint to be aware of is that in a map of indexed nodes: for each node, the index in map entry has to be the same index as the in that `node`'s post index.
32+
33+To see how Graph Store handles add-nodes, take a look at [`app/graph-store.hoon#L370-L417`](https://github.com/urbit/urbit/blob/e2ad6e3e9219c8bfad62f27f05c7cac94c9effa8/pkg/arvo/app/graph-store.hoon#L370-L417).
34+
35+
36+### Permissions internals
37+Internally, the permissions part of a mark (the grow arm) are built and then watched for changes by Graph Store. See [`app/graph-push-hook.hoon#L154`](https://github.com/urbit/urbit/blob/ac096d85ae847fcfe8786b51039c92c69abc006e/pkg/arvo/app/graph-push-hook.hoon#L154) for more info.
38+
39+It is important to note that the push hook and permissions system is bespoke, and doesn't necessarily fit all permissions schemes out there. In addition, the permissions structure is likely to change, so it can make sense to implement your own permissioning structure (if you are not using `graph-push-hook`), especially in the case where you are making your own resource control system.
40+The `graph-push-hook` permissions system is not the definite way to implement permissions, and is simply one pre-existing way to do it.
41+The validator mark doesn't need any permissions to compile; it just needs a grow arm with at least `noun`.
42+The way to implement your own permissioning structure is in the form of your own grow arm definitions in the validator.
43+There's nothing special about the `graph-permission-add` arms; they are just constants, arms which are known to push-hook.
44+As stated before, Graph Store proper (`app/graph-store.hoon`) doesn't know anything about the permissions.
45+
46+
47+
48+
49+### Push Hook Details
50+
51+As a result of the hook architecture, all hooks have the ability to transform incoming data before it is accepted and trusted as a local input.
52+Graph Push Hook is no exception. This is why the `transform-add-nodes` is required for every validator; Graph Store calls this function before adding any nodes to itself.
53+Right now, the only thing that `transform-add-nodes` is used for is to replace untrusted foreign timestamps with trusted local timestamps.
54+
55+Here is a stubbed out example of a `transform-add-nodes` to show its inputs and outputs:
56+
57+```
58+|= [=index =post =atom was-parent-modified=?]
59+^- [^index ^post]
60+!!
61+```
62+
63+The simplest transformer can simply return the exact same indexed post, which would mean that it would trust all foreign `index`es and `post`s by default.
64+The atom is always `now.bowl`, i.e. what the datetime on us, the receiving side is.
userspace/graph-store/data-structure-overview.mdnew file+338
@@ -0,0 +1,338 @@
1++++
2+title = "Data Structure Overview"
3+weight = 2
4+sort_by = "weight"
5+template = "doc.html"
6+insert_anchor_links = "right"
7++++
8+
9+
10+Let's go through the type definitions of some of the most used types when working with Graph Store.
11+
12+## Post
13+Here's `sur/post.hoon`
14+```
15++$ index (list atom)
16++$ uid [=resource =index]
17+::
18+:: +sham (half sha-256) hash of +validated-portion
19++$ hash @ux
20+::
21++$ signature [p=@ux q=ship r=life]
22++$ signatures (set signature)
23++$ post
24+ $: author=ship
25+ =index
26+ time-sent=time
27+ contents=(list content)
28+ hash=(unit hash)
29+ =signatures
30+ ==
31+::
32++$ indexed-post [a=atom p=post]
33+::
34++$ validated-portion
35+ $: parent-hash=(unit hash)
36+ author=ship
37+ time-sent=time
38+ contents=(list content)
39+ ==
40+::
41++$ reference
42+ $% [%graph group=resource =uid]
43+ [%group group=resource]
44+ ==
45+::
46++$ content
47+ $% [%text text=cord]
48+ [%mention =ship]
49+ [%url url=cord]
50+ [%code expression=cord output=(list tank)]
51+ [%reference =reference]
52+ ==
53+--
54+```
55+
56+
57+### Index
58+```
59++$ index (list atom)
60++$ uid [=resource =index]
61+```
62+
63+`index` is a list of atoms (big integers). It represents a path to a specific node on a graph that is nested arbitrarily deep.
64+
65+`uid` is simply a pair of a `resource` and an `index`. These two pieces of information combine to form an unambiguous way to identify and reference a node. It is used in the `content` type to model a reference.
66+
67+An index fragment is not an explicitly defined type, but since an index is `(list atom)`, it follows that the type of an index fragment is `atom`. This is what gives the developer the flexibility to use more than just numbers in an index: any type that is representable as an atom can be used in the index.
68+
69+### Hashing (Part 1)
70+```
71+:: +sham (half sha-256) hash of +validated-portion
72++$ hash @ux
73+::
74++$ signature [p=@ux q=ship r=life]
75++$ signatures (set signature)
76+```
77+
78+These types are used to cryptographically sign a given post, so that the host of some content cannot act as an imposter, and post content impersonating as someone else. `signature` represents a triple of signed message of hash, author, and author's life at time of posting. These can be used to cryptographically attest to a message. The implementation is a form of asymmetric/public-key encryption, where `q` and `r` are data necessary to look up a ship's public key on azimuth, which can be used to verify the validity of the message.
79+
80+### Content Types
81+```
82++$ reference
83+ $% [%graph group=resource =uid]
84+ [%group group=resource]
85+ ==
86+::
87++$ content
88+ $% [%text text=cord]
89+ [%mention =ship]
90+ [%url url=cord]
91+ [%code expression=cord output=(list tank)]
92+ [%reference =reference]
93+ ==
94+::
95+```
96+
97+`reference` is a type that models a reference to a graph object. Currently, the valid types of references are either a reference to a specific node in a graph, or a specific group.
98+
99+`content` enumerates all the possible content types that a post can have. The possible content types can be:
100+
101+- **Text** - representing plain text content
102+- **Url** - specific data type for urls
103+- **Mention** - mentioning another ship
104+- **Code** - a pair of a piece of code that was executed and its result (static data, no execution takes place inside of Graph Store)
105+- **Reference** - a reference to another post, using the `reference` type
106+
107+Currently, these are the only content types supported by Graph Store.
108+
109+### Post
110+```
111++$ post
112+ $: author=ship
113+ =index
114+ time-sent=time
115+ contents=(list content)
116+ hash=(unit hash)
117+ =signatures
118+ ==
119+::
120++$ indexed-post [a=atom p=post]
121+```
122+
123+Post is a fundamental type that represents what we normally think of as a post on social media. Most of the rest of the types are self-explanatory. `hash` is the optional hash of the post, and `signatures` is the (potentially empty) set of `signature`s if the post is cryptographically signed.
124+
125+An `indexed-post` is a post with an associated index fragment that can be used to validate a post's index with an index fragment that is expected at the end of the index list.
126+
127+### Hashing (Part 2)
128+```
129++$ validated-portion
130+ $: parent-hash=(unit hash)
131+ author=ship
132+ time-sent=time
133+ contents=(list content)
134+ ==
135+::
136+```
137+
138+The parts of a `post` that are actually hashed to obtain a value of type the earlier type `hash`.
139+
140+
141+## Graph Store
142+
143+Here's `sur/graph-store.hoon`
144+
145+```
146++$ graph ((mop atom node) gth)
147++$ marked-graph [p=graph q=(unit mark)]
148+::
149++$ node [=post children=internal-graph]
150++$ graphs (map resource marked-graph)
151+::
152++$ tag-queries (jug term resource)
153+::
154++$ update-log ((mop time logged-update) gth)
155++$ update-logs (map resource update-log)
156+::
157+::
158++$ internal-graph
159+ $~ [%empty ~]
160+ $% [%graph p=graph]
161+ [%empty ~]
162+ ==
163+::
164++$ network
165+ $: =graphs
166+ =tag-queries
167+ =update-logs
168+ archive=graphs
169+ validators=(set mark)
170+ ==
171+::
172++$ update [p=time q=action]
173+::
174++$ logged-update [p=time q=logged-action]
175+
176+::
177++$ logged-action
178+ $% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
179+ [%add-nodes =resource nodes=(map index node)]
180+ [%remove-nodes =resource indices=(set index)]
181+ [%add-signatures =uid =signatures]
182+ [%remove-signatures =uid =signatures]
183+ ==
184+::
185++$ action
186+ $% logged-action
187+ [%remove-graph =resource]
188+ ::
189+ [%add-tag =term =resource]
190+ [%remove-tag =term =resource]
191+ ::
192+ [%archive-graph =resource]
193+ [%unarchive-graph =resource]
194+ [%run-updates =resource =update-log]
195+ ::
196+ :: NOTE: cannot be sent as pokes
197+ ::
198+ [%keys =resources]
199+ [%tags tags=(set term)]
200+ [%tag-queries =tag-queries]
201+ ==
202+--
203++$ permissions
204+ [admin=permission-level writer=permission-level reader=permission-level]
205+::
206+:: $permission-level: levels of permissions in increasing order
207+::
208+:: %no: May not add/remove node
209+:: %self: May only nodes beneath nodes that were added by
210+:: the same pilot, may remove nodes that the pilot 'owns'
211+:: %yes: May add a node or remove node
212++$ permission-level
213+ ?(%no %self %yes)
214+::
215+```
216+
217+### Graph, Node, and Related Objects
218+```
219++$ graph ((mop atom node) gth)
220++$ marked-graph [p=graph q=(unit mark)]
221+::
222++$ node [=post children=internal-graph]
223++$ graphs (map resource marked-graph)
224+::
225++$ internal-graph
226+ $~ [%empty ~]
227+ $% [%graph p=graph]
228+ [%empty ~]
229+ ==
230+::
231+
232++$ network
233+ $: =graphs
234+ =tag-queries
235+ =update-logs
236+ archive=graphs
237+ validators=(set mark)
238+ ==
239+::
240+```
241+A graph is a loosely interconnected set of data which can reference each other and be arbitrarily nested and interconnected. This concept is based off of the mathematical concept of a graph: a collection of nodes, and connections between nodes called edges.
242+
243+`graph` is a `mop`, a map which maintains ordering on its keys based on a sorting function also known as an ordered map. A graph's keys are `atom`s representing a node's index fragment and whose values are `node`s, where entries are sorted by largest valued keys first, using `gth` as the sorting function. This is the fundamental data structure used in Graph Store.
244+
245+Here are some helpful Wikipedia pages for more info on what this data type represents:
246+- [Graph (abstract data type)](https://en.wikipedia.org/wiki/Graph_(abstract_data_type))
247+- [Graph database](https://en.wikipedia.org/wiki/Graph_database#Background)
248+- [Graph (discrete mathematics)](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics))
249+
250+`node` represents a pair of a `post` and all of its children, which is an `internal-graph`.
251+
252+`internal-graph` is a tagged union representing the state that children can be in. Either a `node` has children in the form of a `graph`, or does not have any and is labeled as `%empty`.
253+
254+`marked-graph` is the pair of a `graph` and an optionally present `mark`, which is used by Graph Store to validate a graph against the provided validator.
255+
256+`graphs` is a mapping between `resource`s and`marked-graph`s
257+
258+`network` is the highest level data structure used by Graph Store to represent all the information that the agent is aware of.
259+
260+
261+
262+### Tag Queries
263+
264+```
265++$ tag-queries (jug term resource)
266+```
267+
268+`tag-queries` is a mapping where the keys are `term`s and the values are a `set` of `resource`s (this pattern is called a `jug`). It is a simple tagging system that allows for various ad-hoc collections, similar to filesystem tags being used to sort different files/folders. While the type's name is `tag-queries`, there is no complex querying system as of now. Currently, you can add term/resources pairs into the tag queries, get a list of all terms in tag-queries, and get the whole `jug` out of Graph Store.
269+
270+### Update (Part 1)
271+```
272++$ update [p=time q=action]
273+::
274++$ logged-action
275+ $% [%add-graph =resource =graph mark=(unit mark) overwrite=?]
276+ [%add-nodes =resource nodes=(map index node)]
277+ [%remove-nodes =resource indices=(set index)]
278+ [%add-signatures =uid =signatures]
279+ [%remove-signatures =uid =signatures]
280+ ==
281+::
282++$ action
283+ $% logged-action
284+ [%remove-graph =resource]
285+ ::
286+ [%add-tag =term =resource]
287+ [%remove-tag =term =resource]
288+ ::
289+ [%archive-graph =resource]
290+ [%unarchive-graph =resource]
291+ [%run-updates =resource =update-log]
292+ ::
293+ :: NOTE: cannot be sent as pokes
294+ ::
295+ [%keys =resources]
296+ [%tags tags=(set term)]
297+ [%tag-queries =tag-queries]
298+ ==
299+::
300+```
301+
302+The `update` type is what is used to interact with Graph Store. It is used both to update subscribers with data (outgoing data) and to write to Graph Store itself (incoming data). All actions are meant to be sent as pokes to Graph Store in the form of a `action` (encapsulated by a `graph-update`), except for the last three actions ,`%keys`, `%tags`, and `%tag-queries`, which must be sent scry requests. All actions defined here allow you to create/read/update/delete various objects in a running Graph Store gall agent. An `action` encapsulates all `logged-action`; any `logged-action` is an `action`, but not necessarily the other way around.
303+
304+If you want to check out a relevant code listing to see how graph store handles these pokes, check out the `graph-update` arm in the poke handler at [`app/graph-store.hoon`](https://github.com/urbit/urbit/blob/e2ad6e3e9219c8bfad62f27f05c7cac94c9effa8/pkg/arvo/app/graph-store.hoon#L221-L227)
305+
306+### Update (Part 2)
307+```
308++$ update-log ((mop time logged-update) gth)
309++$ update-logs (map resource update-log)
310+::
311+::
312++$ logged-update [p=time q=logged-action]
313+::
314+```
315+
316+`update-log` is an ordered map where the keys are a timestamp (time is an alias for `@da`, an absolute datetime) and the values are `logged-update`s, where entries are sorted with the most recent timestamp first. It represents a history of updates applied to a graph. `update-logs` is a mapping where keys are resources and values are `update-log`s. This is the data structure used by Graph Store to store the history of actions associated with all graphs that it knows about, where each graph has a unique resource that identifies it. A `logged-update` is a data structure that holds any `logged-update-0` along with a time identifying when the update happened. It follows a versioning pattern similar to the versioned state of a gall agent.
317+
318+It is important to note that the source of truth is actually the `update-logs`, and not the `graphs`. Similar to the Urbit event log, Graph Store also stores all updates that it receives, so that it can rebuild its current state on demand. The current state of the database is more of a product of the event log, like a checkpoint, or a materialized db view, and is not the source of truth. As a result, the Graph Store database is immutable in nature, where all data is preserved and deleted data is only inaccessible in the current view or checkpoint, and is still present and recoverable by replaying the log.
319+
320+The reason for having the main CRUD actions being `logged-update`s is so that Graph Store knows which order to process the log entries in when it is rebuilding its current state. The time associated with the `logged-update` is a way of specifying the canonical order to process operations. All other actions that aren't part of logged-update stand on their own and don't need a timestamp in order to properly apply them.
321+
322+### Permissions
323+```
324++$ permissions
325+ [admin=permission-level writer=permission-level reader=permission-level]
326+::
327+:: $permission-level: levels of permissions in increasing order
328+::
329+:: %no: May not add/remove node
330+:: %self: May only nodes beneath nodes that were added by
331+:: the same pilot, may remove nodes that the pilot 'owns'
332+:: %yes: May add a node or remove node
333++$ permission-level
334+ ?(%no %self %yes)
335+```
336+
337+These are the types from the permissioning system explained earlier. `permissions` is just a length-3 cell of `permissions-level`s for Admins, Writers, and Readers respectively.
338+
userspace/graph-store/overview.mdnew file+109
@@ -0,0 +1,109 @@
1++++
2+title = "Graph Store Overview"
3+weight = 1
4+sort_by = "weight"
5+template = "doc.html"
6+insert_anchor_links = "right"
7++++
8+
9+
10+## Intro
11+Graph store is a non-relational database suitable for use in building social media applications. It is primarily designed for storing text-based content and data that is generally threaded and nested. It is not suitable as a general purpose database for highly structured or pure binary data.
12+
13+<p align="center">
14+ <img src="https://media.urbit.org/docs/userspace/graph-store/1_graph_store_overview.png"/>
15+</p>
16+
17+Graph Store is mainly in charge of two things: data storage and retrieval, and schema validation. There are two related tools: Graph Push Hook, which provides permissioning support to Graph Store and acts as a proxy layer to Graph Store for outside ships to access, and Graph Pull Hook, which can be used to request Graph Store data from other ships. Importantly, Graph Store itself does not have a built-in permissions system (or rather, is permissions-agnostic), and acts in a purely trusted manner, and assumes that all inputs are trusted. However, it only allows local, authenticated connections to communicate with it, meaning that by default, no one else on the network may modify the state of your Graph Store agent. This is why Graph Push Hook exists: to mediate untrusted requests from outside ships to your instance of Graph Store, rejecting invalid ones where a ship should not be able to modify the data.
18+
19+## Data Structures
20+### Posts
21+
22+<p align="center">
23+ <img src="https://media.urbit.org/docs/userspace/graph-store/2_post_diagram.png"/>
24+</p>
25+
26+Above, we can see a representation of a post on the left, along with its table form on the right.
27+A post is the most basic building block of a graph.
28+Every post is made up of an:
29+- **Author** - who created the post
30+- **Index** - the unique path/identifier of the post on the graph (more on this later)
31+- **Time Sent** - when the post was created
32+- **Contents** - data that the user intended to post
33+
34+Currently, Graph Store supports 5 data types for content that is to be stored within a graph:
35+- **Text** - plain text data
36+- **Url** - specific data type for urls
37+- **Mention** - mentioning another ship
38+- **Code** - a pair of a piece of code that was executed and its result (this is static data, and is not used to execute code on other's ships)
39+- **Reference** - a reference to another post
40+
41+
42+### Graphs and Nodes
43+
44+<p align="center">
45+ <img src="https://media.urbit.org/docs/userspace/graph-store/3_graph_node_diagram_a.png"/>
46+</p>
47+
48+<p align="center">
49+ <img src="https://media.urbit.org/docs/userspace/graph-store/4_graph_node_diagram_b.png"/>
50+</p>
51+
52+A graph is a flat, ordered map of nodes, where each node can have a child graph, which is itself a flat ordered map of nodes. Nodes contain a post and a child graph. In the above diagram, we can see an example of a basic graph on the top, along with the underlying structure of the data in table form underneath.
53+
54+A few vocab terms:
55+- **Root graph** refers to the outermost graph, G in this case
56+- **Top level** is a loose term usually used to refer to a node that exists in the root graph. **A** and **D** would be considered nodes at the top level, but **B** and all its descendants would not qualify.
57+- **Sibling nodes** are two nodes that reside next to each other, meaning that they reside in the same `graph`. **A** and **D** are an example of sibling nodes.
58+- We say a node is a **child** of another node if it directly resides within the child graph of that node
59+- A node is a **parent** to another node if it directly contains the node within its children. An example is the relationship between **B** and **C**: **C** is a child of **B**, while **B** is the parent of **C**
60+- Another way of saying a node is a child is by saying that node **B** is nested within node **A** Strictly speaking, **C** is not a child of **A**, but we do say that **A** is an **ancestor** of **C**, while **C** is a **descendant** of **A**.
61+- **Leaf nodes** refer to nodes that do not have children. **C** and **D** are both examples of leaf nodes
62+
63+### Index
64+
65+<p align="center">
66+ <img src="https://media.urbit.org/docs/userspace/graph-store/5_index_diagram.png"/>
67+</p>
68+
69+Indexes are a way of uniquely identifying a node within a graph. You can think of `index`es as similar to file paths, although they aren't exactly the same. Roughly, a file path is a unique reference to a file or folder located in the filesystem. Similarly, an index is a unique reference to a node nested within a graph. The written syntax for a full index is very similar to file paths. It consists of every index fragment in order separated by a slash. A node's **level of nesting** refers to how deeply it is nested within the context of the root graph. The level of nesting directly corresponds to the number of items in the index. An index fragment is the atom by which a node is uniquely identified within it's graph, and roughly corresponds to a specific name of a directory along a path. In the diagrams that follow, we'll use the index fragment instead of the index to avoid repeating redundant information, but please note that internally graph-store still store the full index in every node's post.
70+
71+In the above diagram, we would say that nodes **A** and **D** are nested 1 level deep, while **B** would be at the 2nd level of nesting, and **C** would be nested 3 levels deep.
72+
73+Indexes are usually numbers and are stored as such in the database. Most commonly, they can represent:
74+- Date or time of posting
75+- A sequence of numbers starting from 1 increasing
76+- Structural/constant value: values which are associated with a specific meaning in the context of the schema of an application.
77+
78+However, there is no strict requirement for them to be numbers; they can be cords as well as other data types. As we'll see in the later sections, it is up to the app developer to decide this when creating their application.
79+
80+
81+### Structural Nodes vs. Content-Centric Nodes
82+
83+<p align="center">
84+ <img src="https://media.urbit.org/docs/userspace/graph-store/6_structural_vs_content_centric_diagram.png"/>
85+</p>
86+
87+When using Graph Store, there is a notion of **structural nodes** vs. **content-centric nodes**. In the example diagram, we've color coded the different nodes based on what type of node they are. **Content-centric nodes** represent data created or consumed directly by the user. **Structural nodes**, on the other hand do not directly represent user data, and instead represent a higher level relationship between different user data. Structural nodes are used to implement the structure of the schema that is being implemented. In other words, they exist away from the eyes of the user, solely for organizational purposes.
88+
89+Note that this differentiation between content-centric vs. structural nodes is purely developer-facing, and not encoded within the actual system. Although these patterns aren't hard or fast rules, we'll see how they can be used in practice in the validator walkthrough section.
90+
91+## Validator Overview - Schema and Permissions
92+
93+Every social application has a minimum amount of information it needs to function along with the structure that the information must follow. We'll call this the application's schema. This is enforced by a **validator**. Given a schema, a validator's primary function is to encode its constraints and validate arbitrary data against it. Graph Store uses clay marks to represent validators. Validators are a special case of a mark, so the terms will be used interchangeably.
94+
95+In addition, validators can also encode structural permissions. **Structural permissions** govern who is allowed to add or remove a given node (and by extension modify its children) based on the node's characteristics.
96+
97+There are 3 different classes of users:
98+- **Admin** - An owner of a resource or someone who's been delegated similar privileges
99+- **Writer** - Someone who can create and modify their own content but cannot modify others'
100+- **Reader** - Someone who is only given permission to access but not create or modify, except in special cases
101+
102+There are two different types of privileges:
103+- **Add privileges** - permission to create a given node and create any child nodes
104+- **Remove privileges** - permission to remove a given node and remove any of its children
105+
106+There are also three different levels of access a given permission level can have:
107+- **%no** - user does not have add or remove privileges for this node
108+- **%yes** - user has add or remove privileges for this node, whether or not they authored the parent node
109+- **%self** - user has add or remove privileges for child nodes only if they authored the parent node (determined by `author` of post, i.e., they are the author of the post)
userspace/graph-store/validator-walkthrough.mdnew file+487
@@ -0,0 +1,487 @@
1++++
2+title = "Validator Walkthrough"
3+weight = 3
4+sort_by = "weight"
5+template = "doc.html"
6+insert_anchor_links = "right"
7++++
8+
9+
10+## Schema and Permissioning Implementation
11+In this section, we will analyze various applications and see how they implement their schema and permissions.
12+
13+
14+### Current State of Permissioning
15+Please note that as it stands today, the current permissioning system is likely subject to change. It is by no means the only manner in which to set up permissionings. For many applications, writing a small and bespoke permissioning library may be suitable as an alternative to the `graph-push-hook` permissioning scheme which is what will be covered in the following examples.
16+
17+Also, please be aware that the current set of applications use a special type known as `vip-metadata`, which stands for "variation in permission".
18+It is extra metadata attached to a post that is available to the permissioning arms that is mainly used to specify whether reader comments are enabled or disabled.
19+Here's the source if you want to explore: [`sur/metadata-store.hoon#L20-L30`](https://github.com/urbit/urbit/blob/ac096d85ae847fcfe8786b51039c92c69abc006e/pkg/arvo/sur/metadata-store.hoon#L20-L30)
20+
21+
22+### Chat
23+#### Schema
24+Here's what the schema of chat looks like:
25+
26+<p align="center">
27+ <img src="https://media.urbit.org/docs/userspace/graph-store/7_chat_schema_diagram.png"/>
28+</p>
29+
30+A chat is a flat graph, where all chat messages are nodes appended to the root of the graph.
31+The graph represents a chat channel and contains all chat messages in order. A chat message is a child node of the root graph.
32+
33+
34+Here's the definition of the schema in the chat validator mark:
35+File: [`mar/graph/validator/chat.hoon`](https://github.com/urbit/urbit/blob/fbd85abf4e41d580654606a6defb764f6a97256d/pkg/arvo/mar/graph/validator/chat.hoon#L33-L40)
36+```
37+++ grab
38+ |%
39+ ++ noun
40+ |= p=* :: 1
41+ =/ ip ;;(indexed-post p) :: 2
42+ ?> ?=([@ ~] index.p.ip) :: 3
43+ ip :: 4
44+ --
45+::
46+```
47+
48+Here are the steps:
49+1. Given a noun (we expect an indexed-post)
50+1. Try to coerce p to an indexed-post, crash if it doesn't cast
51+1. Assert that the index of the post of the indexed post is only a single atom, i.e., that it is only nested one level deep
52+1. Return the indexed post
53+
54+Notably, under this set of rules, there is no nesting allowed. Put another way, no node is allowed to have any children. Nodes can only be added to the root graph. Step 3 is what enforces the flat hierarchy. If someone were to manually try to submit a node with children, Graph Store would reject it, preventing them from sending an invalid chat message.
55+
56+Since the schema of the chat application is simple enough, it has no need for structural nodes at all.
57+
58+#### Permissioning
59+
60+<p align="center">
61+ <img src="https://media.urbit.org/docs/userspace/graph-store/8_chat_permissions_diagram.png"/>
62+</p>
63+
64+Let's take a look at the permissions table in the diagram.
65+
66+Chat Message `[@ ~]`
67+- Add Privileges
68+ - Admins and Writers have `%yes` add permissions for all nodes at the top level, meaning that they have the ability to post chat messages, even if they did not create the chat channel
69+ - Readers have `%no` add privileges for any nodes at the root level, so they do not have the ability to post chat messages
70+- Remove Privileges
71+ - Admins and Writers have `%self` remove privileges, meaning that they may only delete chat messages that they posted, not anyone else's
72+ - Readers have `%no` remove privileges for any nodes, meaning they cannot delete any chat messages
73+
74+This follows our general intuition of how permissions for chat messages should be structured.
75+For example, it wouldn't make sense to give readers `%self`, because they do not have the ability to create nodes in the first place, so they will never be in a position to delete any nodes.
76+
77+Let's see how this permissioning system is implemented in the validator code.
78+
79+Here is the `grow` arm of [`mar/validator/chat.hoon`](https://github.com/urbit/urbit/blob/fbd85abf4e41d580654606a6defb764f6a97256d/pkg/arvo/mar/graph/validator/chat.hoon#L7-L20)
80+```
81+|_ i=indexed-post :: A
82+++ grow
83+ |%
84+ ++ graph-permissions-add
85+ |= vip=vip-metadata:met :: 1
86+ ?+ index.p.i !! :: 2
87+ [@ ~] [%yes %yes %no] :: 3
88+ ==
89+ ::
90+ ++ graph-permissions-remove
91+ |= vip=vip-metadata:met :: 4
92+ ?+ index.p.i !! :: 5
93+ [@ ~] [%self %self %no] :: 6
94+ ==
95+ ::
96+ --
97+::
98+```
99+
100+In line A, we accept an `indexed-post` that is used in the rest of the `grow` arm.
101+
102+`graph-permissions-add`
103+1. Accept a noun `vip` of type `vip-metadata`
104+2. Switch on the index of the post found in `i`, crashing if no successful matches occur
105+3. If the index is nested one level deep
106+4. Return a `permissions` noun defined as: [admin: %yes, writer: %yes, reader: %no]
107+
108+
109+ `graph-permissions-remove`
110+
111+5. Accept a noun `vip` of type vip-metadata
112+6. Switch on the index, crashing if no successful matches occur
113+7. If the index is nested one level deep
114+8. Return a `permissions` noun defined as: [admin: %self, writer: %self, reader: %no]
115+
116+In this example, a switch statement is used to determine the `permissions` values based on the index of the post.
117+
118+
119+### Links
120+#### Schema
121+
122+<p align="center">
123+ <img src="https://media.urbit.org/docs/userspace/graph-store/9_link_schema_diagram.png"/>
124+</p>
125+
126+The root graph represents the whole Links collection. Every Links entry is a child node of this graph. Every Links entry is made up of:
127+- The link and its description
128+- A comments section
129+
130+
131+The comments section holds all individual comment nodes, but comments are not simple leaf nodes. An individual comment is actually a structural node that acts as a revision container, storing the comment's full edit history by storing each edit as a child node. The front-end is responsible for properly displaying the latest revision of the comment.
132+
133+
134+Here's the validator, located at [`mar/graph/validator/link.hoon`](https://github.com/urbit/urbit/blob/fbd85abf4e41d580654606a6defb764f6a97256d/pkg/arvo/mar/graph/validator/link.hoon#L49-L73):
135+```
136+++ grab
137+ |%
138+ ++ noun
139+ |= p=* :: 1
140+ =/ ip ;;(indexed-post p) :: 2
141+ ?+ index.p.ip ~|(index+index.p.ip !!) :: 3
142+ :: top-level link post; title and url
143+ ::
144+ [@ ~] :: 4
145+ ?> ?=([[%text @] [%url @] ~] contents.p.ip) :: 4a
146+ ip
147+ ::
148+ :: comment on link post; container structure
149+ ::
150+ [@ @ ~] :: 5
151+ ?> ?=(~ contents.p.ip) :: 5a
152+ ip
153+ ::
154+ :: comment on link post; comment text
155+ ::
156+ [@ @ @ ~] :: 6
157+ ?> ?=(^ contents.p.ip) :: 6a
158+ ip
159+ ==
160+ --
161+```
162+
163+
164+<ol>
165+ <li>Get the post as a noun</li>
166+ <li>Force cast to indexed post</li>
167+ <li>
168+ Switch on index of post, crash if no match occurs
169+ <ol type="a">
170+ <li>If the node is nested one level deep, (if the index is made up of a single atom)</li>
171+ <li>Ensure that it is a cell that has two pieces of data, whose content types are text and url</li>
172+ <li>If the node is nested two levels deep</li>
173+ <li>Ensure that it is empty, this is the structural node for holding comment revisions; it should not contain any content</li>
174+ </ol>
175+ </li>
176+ <li>
177+ If the node is nested three levels deep
178+ <ol type="a">
179+ <li>Ensure that it is a cell, this is a specific revision of a comment under a revision container</li>
180+ </ol>
181+ </li>
182+</ol>
183+
184+
185+It is important to note that you cannot directly edit the url or link afterwards, only the whole link entry itself. This is because you can only add or remove nodes to a graph, not modify them. Comments, however, can be “edited”. This is possible by using a technique called a revision container. Instead of having a single leaf node holding the comment text in its contents, a node with empty contents is created. All revisions of the comment are added as children to this note, and the frontend simply shows the most recent one only.
186+
187+#### Permissioning
188+
189+<p align="center">
190+ <img src="https://media.urbit.org/docs/userspace/graph-store/10_links_permissions_diagram_a.png"/>
191+</p>
192+
193+<p align="center">
194+ <img src="https://media.urbit.org/docs/userspace/graph-store/11_links_permissions_diagram_b.png"/>
195+</p>
196+
197+Let's analyze the permissions structure.
198+
199+**TODO** *stubbed out awaiting potential changes in source*
200+
201+Here's how it is implemented [(source)](https://github.com/urbit/urbit/blob/master/pkg/arvo/mar/graph/validator/link.hoon#L2-L27):
202+
203+```
204+|_ i=indexed-post
205+++ grow
206+ |%
207+ ++ noun i
208+ ::
209+ ++ graph-permissions-add
210+ |= vip=vip-metadata:met :: 1
211+ =/ reader :: 2
212+ ?=(%reader-comments vip)
213+ ?+ index.p.i !! :: 3
214+ [@ ~] [%yes %yes %no] :: 3a
215+ [@ @ ~] [%yes %yes ?:(reader %yes %no)] :: 3b
216+ [@ @ @ ~] [%self %self %self] :: 3c
217+ ==
218+ ::
219+ ++ graph-permissions-remove
220+ |= vip=vip-metadata:met :: 4
221+ =/ reader :: 5
222+ ?=(%reader-comments vip)
223+ ?+ index.p.i !! :: 6
224+ [@ ~] [%yes %self %self] :: 6a
225+ [@ @ ~] [%yes %self %self] ::
226+ [@ @ @ ~] [%yes %self %self] ::
227+ ==
228+ ::
229+```
230+
231+`graph-permissions-add`
232+<ol>
233+ <li>Accept a noun <code>vip</code> of type vip-metadata
234+ <ul>
235+ <li>Declare a variable <code>reader</code>, a flag which is true if reader comments are enabled, false otherwise</li>
236+ </ul>
237+ </li>
238+ <li>Switch on the index of the post found in <code>i</code>, crashing if no successful matches occur</li>
239+ <li>If the index is nested one level deep, return a <code>permissions</code> noun defined as:
240+ <ul>
241+ <li>Admin - <code>%yes</code></li>
242+ <li>Writer - <code>%yes</code></li>
243+ <li>Reader: <code>%no</code></li>
244+ </ul>
245+ </li>
246+ <li>If the index is nested two levels deep, return a <code>permissions</code> noun defined as:
247+ <ul>
248+ <li>Admin - <code>%yes</code></li>
249+ <li>Writer - <code>%yes</code></li>
250+ <li>Reader: <code>%yes</code> if reader comments are enabled, else <code>%no</code></li>
251+ </ul>
252+ </li>
253+ <li>If the index is nested three levels deep, return a <code>permissions</code> noun defined as:
254+ <ul>
255+ <li>Admin - <code>%yes</code></li>
256+ <li>Writer - <code>%yes</code></li>
257+ <li>Reader: <code>%yes</code> if reader comments are enabled, else <code>%no</code></li>
258+ </ul>
259+ </li>
260+</ol>
261+
262+
263+`graph-permissions-remove`
264+<ol>
265+ <li>Accept a noun <code>vip</code> of type vip-metadata</li>
266+ <li>Declare a variable <code>reader</code>, a flag which is true if reader comments are enabled, false otherwise</li>
267+ <li>Switch on the index of the post found in <code>i</code>, crashing if no successful matches occur</li>
268+ <li>If the index is nested one level deep, two levels deep, or three levels deep, return a <code>permissions</code> noun defined as:
269+ <ul>
270+ <li>Admin - <code>%yes</code></li>
271+ <li>Writer - <code>%self</code></li>
272+ <li>Reader: <code>%self</code></li>
273+ </ul>
274+ </li>
275+</ol>
276+
277+
278+
279+### Publish
280+
281+#### Schema
282+
283+<p align="center">
284+ <img src="https://media.urbit.org/docs/userspace/graph-store/12_publish_schema_diagram.png"/>
285+</p>
286+
287+Here, a notebook, which is a collection of blog posts (called notes), is represented by the root graph. All data associated with the blog post is represented by the top level node, which is the note itself along with the associated comments. One level deeper, we see two container structures. The first one is the post revision container; it holds the edit history of your blog post. Every child node of this corresponds to the actual title and text of your blog post. The second one is the comments container. This represents the comment section of your blog post. Every child node of this is not a comment, but a comment revision container, which, as before, contains the edit history of your comment.
288+
289+Here's its validator, located at [`mar/graph/validator.hoon`](https://github.com/urbit/urbit/blob/master/pkg/arvo/mar/graph/validator/publish.hoon#L56-L97)
290+```
291+ ++ noun
292+ |= p=* :: 1
293+ =/ ip ;;(indexed-post p) :: 2
294+ ?+ index.p.ip !! :: 3
295+ :: top level post must have no content
296+ [@ ~] :: 4
297+ ?> ?=(~ contents.p.ip) :: 4a
298+ ip
299+ :: container for revisions
300+ ::
301+ [@ %1 ~] :: 5
302+ ?> ?=(~ contents.p.ip) :: 5a
303+ ip
304+ :: specific revision
305+ :: first content is the title
306+ :: revisions are numbered by the revision count
307+ :: starting at one
308+ [@ %1 @ ~] :: 6
309+ ?> ?=([* * *] contents.p.ip) :: 6a
310+ ?> ?=(%text -.i.contents.p.ip) :: 6b
311+ ip
312+ :: container for comments
313+ ::
314+ [@ %2 ~] :: 7
315+ ?> ?=(~ contents.p.ip) :: 7a
316+ ip
317+ :: container for comment revisions
318+ ::
319+ [@ %2 @ ~] :: 8
320+ ?> ?=(~ contents.p.ip) :: 8a
321+ ip
322+ :: specific comment revision
323+ ::
324+ [@ %2 @ @ ~] :: 9
325+ ?> ?=(^ contents.p.ip) :: 9a
326+ ip
327+ ==
328+ --
329+```
330+
331+Walkthrough
332+
333+
334+<ol>
335+ <li>Get the post as a noun</li>
336+ <li>Force cast to indexed post</li>
337+ <li>Switch on index of post, crashing (reject) if no matches found</li>
338+ <li>If the node is nested one level deep
339+ <ol type="a"><li>Ensure that its contents are empty. The top level node is a structural node containing all of the post and associated data.</li></ol>
340+ </li>
341+ <li>If it is nested 2 levels deep and contains a 1 as it's last index fragment
342+ <ol type="a"><li>Ensure that its contents are empty. This is a structural node for holding revisions to the blog post.</li></ol>
343+ </li>
344+ <li>If the node is nested three levels deep, and has a 1 as it's second index fragment
345+ <ol type="a">
346+ <li>Ensure that its contents is a list of at least 2 elements</li>
347+ <li>Ensure that the first element of contents has a content type of text</li>
348+ </ol>
349+ </li>
350+ <li>If the node is nested two levels deep, and has a 2 as it's last index fragment
351+ <ol type="a"><li>Ensure that its contents are empty. This is a structural node for holding comments.</li></ol>
352+ </li>
353+ <li>If the node is nested three levels deep, and has a 2 as it's second index fragment
354+ <ol type="a"><li>Ensure that its contents are empty. This is a structural node for holding revisions of a specific comment.</li></ol>
355+ </li>
356+ <li>If the node is nested four levels deep, and has a 2 as it's second index fragment
357+ <ol type="a"><li>Ensure that contents has type `cell`. This is a specific revision of a comment</li></ol>
358+ </li>
359+</ol>
360+
361+Items 1-3 are setting up the validator
362+Items 4-6 are enforcing the schema for the post in general
363+Items 7-9 are enforcing the schema for comments specifically
364+
365+Notably, the revision container for the blog post itself allows the post to be edited, unlike the link entry in the previous example. In addition, in step six, the reason that the validator is made this way is because the first element of the contents is interpreted as the title of the post, and the rest of the elements are interpreted as the body of the post. Otherwise, the structure is unchanged from the Links example.
366+
367+#### Permissioning
368+
369+<p align="center">
370+ <img src="https://media.urbit.org/docs/userspace/graph-store/13_publish_permissions_diagram_a.png"/>
371+</p>
372+
373+<p align="center">
374+ <img src="https://media.urbit.org/docs/userspace/graph-store/14_publish_permissions_diagram_b.png"/>
375+</p>
376+
377+Let's take a look at the permissioning structure for Publish.
378+
379+**TODO** *stubbed out awaiting potential changes in source*
380+
381+[(source)](https://github.com/urbit/urbit/blob/fbd85abf4e41d580654606a6defb764f6a97256d/pkg/arvo/mar/graph/validator/publish.hoon#L6-L24)
382+
383+```
384+|_ i=indexed-post
385+++ grow
386+ |%
387+ ++ noun i
388+ ++ graph-permissions-add
389+ |= vip=vip-metadata:met :: 1
390+ ?+ index.p.i !! :: 2
391+ [@ ~] [%yes %yes %no] :: new note :: 2a
392+ [@ %1 @ ~] [%self %self %no] :: 2b
393+ [@ %2 @ ~] [%yes %yes ?:(?=(%reader-comments vip) %yes %no)] :: 2c
394+ [@ %2 @ @ ~] [%self %self %self] :: 2d
395+ ==
396+ ::
397+ ++ graph-permissions-remove
398+ |= vip=vip-metadata:met :: 3
399+ ?+ index.p.i !! :: 4
400+ [@ ~] [%yes %self %self] :: 4a
401+ [@ %1 @ @ ~] [%yes %self %self] ::
402+ [@ %2 @ ~] [%yes %self %self] ::
403+ [@ %2 @ @ ~] [%yes %self %self] ::
404+ ==
405+::
406+```
407+
408+`graph-permissions-add`
409+
410+ <ol>
411+ <li>Accept a noun <code>vip</code> of type vip-metadata</li>
412+ <li>Switch on the index of the post found in <code>i</code>, crashing if no successful matches occur</li>
413+ <li>If the index is nested one level deep, return a <code>permissions</code> noun defined as:
414+ <ul>
415+ <li>Admin - <code>%yes</code></li>
416+ <li>Writer - <code>%yes</code></li>
417+ <li>Reader: <code>%no</code></li>
418+ </ul>
419+ </li>
420+ <li>If the index is nested three levels deep and has a 1 as its 2nd index fragment, return a <code>permissions</code> noun defined as:
421+ <ul>
422+ <li>Admin - <code>%self</code></li>
423+ <li>Writer - <code>%self</code></li>
424+ <li>Reader: <code>%no</code></li>
425+ </ul>
426+ </li>
427+ <li>If the index is nested three levels deep and has a 2 as its 2nd index fragment, return a <code>permissions</code> noun defined as:
428+ <ul>
429+ <li>Admin - <code>%yes</code></li>
430+ <li>Writer - <code>%yes</code></li>
431+ <li>Reader: <code>%yes</code> if reader comments are enabled, else <code>%no</code></li>
432+ </ul>
433+ </li>
434+ <li>If the index is nested four levels deep and has a 2 as its 2nd index fragment, return a <code>permissions</code> noun defined as:
435+ <ul>
436+ <li>Admin - <code>%self</code></li>
437+ <li>Writer - <code>%self</code></li>
438+ <li>Reader: <code>%self</code></li>
439+ </ul>
440+ </li>
441+</ol>
442+
443+
444+`graph-permissions-remove`
445+<ol>
446+ <li>Accept a noun <code>vip</code> of type <code>vip-metadata</code></li>
447+ <li>Declare a variable <code>reader</code>, a flag which is true if reader comments are enabled, false otherwise</li>
448+ <li>Switch on the index of the post found in <code>i</code>, crashing if no successful matches occur</li>
449+ <li>If the index is nested one level deep, two levels deep, or three levels deep, return a <code>permissions</code> noun defined as:
450+ <ul>
451+ <li>Admin - <code>%yes</code></li>
452+ <li>Writer - <code>%self</code></li>
453+ <li>Reader: <code>%self</code></li>
454+ </ul>
455+ </li>
456+</ol>
457+
458+## General Patterns and Best Practices
459+### General Patterns
460+#### Schemas
461+The general pattern for enforcing a schema in a validator is to:
462+1. Cast the incoming data as an `indexed-post`
463+1. Switch on the index, matching by its depth and structure
464+1. Validate the structure of the contents of the post based on what we expect to see at that level of nesting. This logic can definitely be more involved than simply presence or absence of data.
465+
466+When designing your schema, it may help to decide first what data the user will be directly consuming or producing, then try to think of what extra information and structure is needed to support that. For instance, if you may want a user to be able to enter a comment, then later realize that you need a structural node that contains the whole comments section. One pattern that we can see is that all structural nodes tend to have empty contents. Another pattern we can see is that all leaf nodes have their contents set.
467+
468+In general, validators can be as robust and expressive as desired, because the mark system already sets proper limitations of what you can and cannot do, with the main restriction being no side effects can be produced by a mark.
469+
470+
471+#### Permissions
472+In general, you want to first have your schema finalized, then at every node ask the question: who should be able to modify (add/remove) this node, and its children if present in your schema. You may find that you don't need to set permission for every single type of node afterwards, although being thorough can help to find bugs in permissioning early on. Writing out the permissions in plain words and bullet points, sketching them out in the form of a table, then splitting it up into the code can also make the design process easier.
473+
474+### Earth/Mars Interface Details
475+There are a few general ways to talk to a Graph Store gall agent. You can either interact with the agent in the form of pokes/scries/spiders either directly from the dojo, or through HTTP requests via Eyre.
476+
477+- All graph store applications are going to have a mark, which are applied to data going in or coming out
478+- Eyre lets you choose the mark you scry for by specifying the desired format in the url while making the HTTP request
479+- In the case of data going in, you pass in a `marked-graph` which the Graph Store agent uses to validate the whole `graph` with. In the case of adding a node to a graph, the node will simply be checked for validity using the existing mark since the mark is already set on a per-graph basis
480+- In the case of data coming out, Eyre forcibly tries to convert the hoon noun into json, and silently fails if no json conversion exists. This process is handled by Graph Store under the hood because all graphs have a well-defined en-json/de-json format, so you never have to worry about making your own de/serialization arms.
481+
482+
483+Code References
484+- [`interface/src/logic/api/base.ts#L62`](https://github.com/urbit/urbit/blob/master/pkg/interface/src/logic/api/base.ts#L62)
485+- [`mar/graph/update.hoon`](https://github.com/urbit/urbit/blob/e2ad6e3e9219c8bfad62f27f05c7cac94c9effa8/pkg/arvo/mar/graph/update.hoon)
486+- [`sys/vane/eyre.hoon#L1617-L1625`](https://github.com/urbit/urbit/blob/ac096d85ae847fcfe8786b51039c92c69abc006e/pkg/arvo/sys/vane/eyre.hoon#L1617-L1625) shows how Eyre applies marks
487+