Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Browse roots: a repository as resources

A ledger item about a file is only as useful as the reader’s way to the file. gonk can serve the repositories you work in beside the ledger, as resources under urn:repo:{root}:*: a directory’s listing, a file’s bytes, the working tree’s state, a content hash. They come from ikigai-browse, and gonk adds the one thing a library cannot: it watches each repository, so the reads can be cached and still never be stale.

This page makes a repository of its own, serves it, reads it, and changes it while gonk runs. In a new empty directory, as in every chapter: if the gonk from Over MCP is still running, stop it first, since this one takes the same port.

A repository, and a root

Any git working tree will do. This one is made here, with a commit identity set on the repository alone so that nothing outside it is touched:

$ git init -q -b main notes
$ printf '# Notes\n\nA repository this book creates.\n' > notes/README.md
$ git -C notes config user.name 'A Reader'
$ git -C notes config user.email reader@example.com
$ git -C notes add README.md
$ git -C notes commit -qm 'Start the notes'

A browse root gives a directory a name, and the name is the {root} in every resource gonk serves for it. Roots live in the config home’s config.toml, one line each, as name=path. A path may start with ~, which is the home directory of whoever runs gonk; this page’s root is not in yours, so it writes the path out in full:

$ mkdir -p .config/ikigai
$ printf 'gonk.browse.root = "notes=%s/notes"\n' "$PWD" > .config/ikigai/config.toml
$ cat .config/ikigai/config.toml
gonk.browse.root = "notes=…/notes"

--browse-root notes=PATH on the command line does the same for one run. gonk reads its roots when it starts, and refuses to start if one names a directory that is not there: a missing root is the shape of a typo, not something to serve as empty.

$ ikigai-gonk --config-home "$PWD/.config/ikigai" --data-home "$PWD/.ikigai" --port 1070 --no-quic --no-backup
ikigai-gonk 0.1.0 — holding the store at …/.ikigai/store
  http    http://localhost:1070/ — loopback (127.0.0.1:1070); anonymous read+write: default; 0 passkey(s); anonymous SPARQL budget 1000 ms
  browse  urn:repo:{notes (watched)}:* — annotations and archive in <urn:iki:browse:graph:default>
…

The browse line names each root and whether its watcher started, and says where the browse family keeps what it writes (annotations, archived explanations): one graph of the same store the ledger lives in, so a SPARQL query can join a ledger item to the file it is about.

Reading it

The repository is reached the way the ledger was in Over the socket: another ikigai process mounts gonk’s socket. With the alias at urn:gk:, the root’s listing is urn:gk:repo:notes:tree:

$ ikigai --mount "urn:gk:=$PWD/.ikigai/gonk.sock" -c 'source urn:gk:repo:notes:tree'
README.md	file	41
[uncacheable]
$ ikigai --mount "urn:gk:=$PWD/.ikigai/gonk.sock" -c 'source urn:gk:repo:notes:file:README.md'
# Notes

A repository this book creates.
[uncacheable]

A listing line is name, kind and size, tab-separated. The repository’s .git directory is not in it, and a name under it answers NotFound whatever the grant: since ikigai-browse 0.18.2 (in gonk since gonk PR 123) a root never serves its own git internals, whose config can carry a remote’s credentials. Before that the listing began with a .git line, and an ordinary read grant could read .git/config. The file resource answers the file’s own bytes, under the media type its extension maps to. Two more resources answer the questions a reader of a repository keeps asking, is this what I think it is and has anything changed:

$ ikigai --mount "urn:gk:=$PWD/.ikigai/gonk.sock" -c 'source urn:gk:repo:notes:hash:README.md'
sha256:6140d9c09fbe92774e6cf987758c63e9b4cba8638ee23b6c6c66999c6274de72
[uncacheable]
$ shasum -a 256 notes/README.md
6140d9c09fbe92774e6cf987758c63e9b4cba8638ee23b6c6c66999c6274de72  notes/README.md
$ ikigai --mount "urn:gk:=$PWD/.ikigai/gonk.sock" -c 'source urn:gk:repo:notes:state'
… clean
[uncacheable]

hash is the sha256 of the file’s bytes, so it is the same number shasum gives; on a directory it is a Merkle hash over the entries, so one edit changes exactly the hashes on the path from that file to the root. state is the commit HEAD names and whether the working tree is clean or dirty:{n}. Each of these is a name you can put in a ledger item’s about, which is how browse’s file page and a ledger item find each other.

Every listing has a graph face too, with stable IRIs and no blank nodes, so it can be queried or joined:

$ ikigai --mount "urn:gk:=$PWD/.ikigai/gonk.sock" -c 'source urn:gk:repo:notes:tree as=text/turtle'
@prefix ik: <https://ikigai-rs.dev/ns#> .

<urn:repo:notes:tree> a ik:Directory ;
    ik:repo "notes" ;
    ik:entry <urn:repo:notes:file:README.md> .

<urn:repo:notes:file:README.md> a ik:File ;
    ik:fileName "README.md" ;
    ik:path "README.md" ;
    ik:byteSize 41 .

[uncacheable]

Changing it while gonk runs

Commit a change, without touching gonk:

$ printf 'Read through gonk, without a restart.\n' >> notes/README.md
$ git -C notes commit -qam 'Say how it is read'
$ sleep 1
$ ikigai --mount "urn:gk:=$PWD/.ikigai/gonk.sock" -c 'source urn:gk:repo:notes:file:README.md' -c 'source urn:gk:repo:notes:hash:README.md'
# Notes

A repository this book creates.
Read through gonk, without a restart.
[uncacheable]
sha256:…
[uncacheable]
— batch: 2 commands · 2 uncacheable

The new content, at once, from a server that was never restarted. Here is what made that both fast and right:

  • ikigai-browse declares its reads live and uncacheable. That is the only honest thing a library can say: caching is a promise that something will notice when the file changes, and a library cannot know whether its host is watching.
  • gonk is watching. It watches every root (FSEvents on macOS, inotify on Linux) and, when a file changes, cuts the golden thread the root’s reads hang from. So for a watched root, and only for one, it declares the plain reads cacheable: a read of an unchanged file is a cache hit, and the first read after a change recomputes. That is what (watched) on the banner promised. A root whose watcher cannot start is named as unwatched there, and its reads are served live, never cached and unwatched.
  • The cache is gonk’s, behind the socket. The [uncacheable] this client prints is about its own cache: golden threads live in one kernel and do not cross a wire, so an answer that left gonk cacheable would arrive with nothing that could ever cut it, and a long-lived client (an agent’s ikigai mcp, say) would go on serving the old file. So gonk sends every answer across the socket and the QUIC door uncacheable, and keeps the cache entry, and the thread it hangs from, on its own side: a repeated read is one round trip to gonk’s cache, never a recompute.
  • The sleep 1 is honest, not decoration. The notification arrives with the platform’s own latency, well under a second, so a read issued in the same instant as the write can still see the previous version once.

The watch ignores what git ignores: a change inside .git/ (apart from the files git status reads) or inside a target/ directory cuts nothing, so a build does not invalidate a root a thousand times a minute. The Turtle face above said [uncacheable] for a related reason: only the plain read with no arguments is declared cacheable, because the other faces read state the watch does not track.

In a browser, and from your own config

The browser’s way in is /browse, the roots the caller’s grant may read. An anonymous caller may read none, because the anonymous grant is ledger tokens only, so what it gets is the sign-in notice:

$ curl -s http://localhost:1070/browse | grep -o 'Sign in to browse the repositories.'
Sign in to browse the repositories.

A passkey invited with --browse read, as in the browser chapter, reads every root; --browse read --root notes reads only this one. The pages are browse’s own HTML faces (crumbs, listings, a highlighted file view with #L{n} anchors and the annotation margin) inside gonk’s header.

For an ikigai host that should see the roots under their own names, as the ledger was in Over the socket, the mount line is the same shape with a different prefix (~ is the home of whoever runs ikigai, as there):

mount = "prefer urn:repo:=~/.ikigai/gonk.sock"

⚠ urn:repo: is one prefix over two families: the browse roots (urn:repo:{root}:…) and ikigai-repo’s git and GitHub facades (urn:repo:status, urn:repo:log, urn:repo:pr:list), which then run in gonk’s process. A prefix mount cannot say “the roots remote and the facades local”.

What is next for the browser

A root is also the place an explanation is about: browse can derive one per file or directory from a model gonk mounts, archived once per content version, so the hash above is the key that says when one is stale. That needs a model, and is the next chapter, Explain, with a mounted model.

In resource terms: the atoms are the files on disk, which git and you change and gonk does not; tree, file, hash and state are views of them; and the watcher is what turns a change to an atom into a cut thread, so every view derived from it recomputes on its next read and no sooner.