Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

callscape

A Go module's call graph, as districts you fly through.

The crust: each package a district, each symbol a building

Every package is a district — a patch of one sphere, packed by accretion so the crust fills up rather than radiating out from a middle. Every symbol is a building that straddles the ground, sized by how many packages call it and as tall as the function is long. Above is coder: 7,941 symbols across 321 districts, generated code filtered out.

make sample # clone github.com/cli/cli and dump it
make dev # dev server; open the printed URL
make dump TARGET=/path/to/a/go/module # fly something of your own instead
go run ./cmd/callscape-dump --stats /path/to/a/go/module # same data, as text

make sample takes about ten seconds from nothing: a shallow clone of cli/cli into ~/.cache/callscape, then a dump of it. No graph is committed — one is a derived file the size of the module it came from, and the page watches for it, so you can leave make dev running and fly the graph the moment it lands.

Or fly it without installing anything:

Open in GitHub Codespaces

That builds a container, dumps cli/cli into it while it builds, and opens the page on a city already there. Pointer lock wants a real browser tab rather than the editor's preview pane. The remote endpoints stay off in there; .devcontainer/devcontainer.json says why at some length.

Or install the dumper on its own:

go install github.com/dpritchett/callscape/cmd/callscape-dump@latest

Then edit web/public/view.json while the page is open: what's on screen changes within a second, without a reload and without moving the camera. That loop is the point of the project.

Controls

Click to capture the pointer; escape gives it back.

Mouse and keyboard
move the mouselook
hold left / rightfly forward / back
WASDforward, strafe left, back, strafe right
QEdown, up
arrowsthe control column — down climbs, up dives, left/right roll
shiftburn; drops back half a second after you stop
wheeldolly along the view direction
middle click, spaceselect whatever is under the reticle
Xclear the selection; again to put it back
Rreveal neighbours the occupant filter hid
Clook behind you
Ffly to the selection, or to camera.focus if there is none
Lstep the label ribbon
/search for a symbol
tabswap the panel
escaperelease the pointer, close the search, or take the wheel back
Gamepad
left stickfly
right stickthe control column — pull back to climb, push to dive, left/right roll
triggersrudder
bumpersburn
Afly to the selection
Xselect
Bclear
Yreveal
click either sticklook behind you
d-pad left/rightstep the label ribbon
view / menuhand the controls over, and take them back

The arrows and the right stick read the same way on purpose: pull back to climb, and left/right roll rather than yaw, so turning is banking. It is inverted against what a scrollbar would do, deliberately — there is one convention in this cockpit and the pad set it.

A pad takes control by being moved and loses it by going quiet, which leaves no way to say you are finished. View/menu is that sentence: released, the pad flies nothing and the music stops, the same as letting go of a captured pointer.

Fly to a symbol, and read it

A district up close, its symbols named

Names appear as you get near enough for them to mean something, and a symbol's own label sits at whichever end of its building faces you.

A selection, its callers and callees lit

Selecting a symbol lights its neighbourhood: callers in blue, callees in orange, the rest of the city dimmed to the districts it touches. The selection itself breathes and casts light, so you can tell it is behind you without turning round. Wires inside a district arc over the ground rather than cutting under it, and cross-district calls still tunnel straight through the sphere.

Tab swaps the panel between what the graph knows about the selection, the function's own source (coloured by go/scanner, not a guess), and a list of whatever district you are pointing at.

The mark in the sky

Six discs hang on the sky at the cardinal points, each wearing the module's mark with the repo's name floating in front of it, so turning anywhere tells you what you are flying.

A module hosted somewhere with an owner avatar can wear that. Drop a PNG at web/public/badges/<host>/<owner>.pnggithub.com/cli/cli/v2 looks for badges/github.com/cli.png — and it appears. Nothing is fetched, and nothing is shipped: that directory is ignored by git, because those avatars belong to whoever drew them.

With none there, the discs wear callscape's own mark instead. The name is drawn either way — which repo you are flying is worth saying whether or not somebody vendored a picture for it.

What it does

cmd/callscape-dump loads a module with go/packages and emits one node per top-level func or method declared in that module, plus an edge for every statically resolved call between them. web/ lays each package out as a disc on a sphere — deterministic positions, so two runs are comparable — and encodes size, color and height from whichever node fields view.json names.

Requires Go 1.26+ (per go.mod). Node is pinned in .mise.toml.

view.json

The whole control surface. Unknown fields are an error rather than ignored.

{
"occupants": {
"packages": ["*"], // globs over package paths; * spans slashes"minFanIn": 1,
"limit": 0, // top N by encoding.size; 0 means all"generated": "exclude"// include | exclude | only
},
"encoding": {
"size": "fanInPkgs", // any node field"color": "pkg",
"height": "lines",
"scale": "log"// linear | sqrt | log
},
"edges": {
"show": "auto", // auto | all | cross | selected | none"opacity": 0.7
},
"camera": { "focus": "pkg/path.Symbol", "distance": 120 },
"select": [] // symbols to light up on load
}

Node fields: id name pkg file line lines exported generated fanIn fanOut fanInPkgs fanOutPkgs.

fanIn counts call sites; fanInPkgs counts distinct calling packages. They disagree sharply on real code — coder's most-called function has 837 call sites from 2 packages, because a generated file calls it once per wrapper — and fanInPkgs is usually the one you want.

Known limits

Calls through an interface resolve to the interface method, not the implementation. This is the big one. On gitlab-kiosk — 244 nodes, 16 packages, the module this was built against — only 4 of 67 drawn edges cross a package boundary, because its transport chain is interface-based and most of the inter-package structure resolves into interfaces and disappears. The districts render as nearly disconnected islands. That number is a measurement, not a bug to route around, but it does mean the cross-package view is much sparser than the code really is.

Nothing here is verified in a browser. The test suite covers the pure half — place(graph, view), the ranking behind the search and district panels, the movement model, the label sizing, the token spans. No automated browser has ever loaded the page, so rendering, controls and the reload loop are verified by looking at them.

The screenshots above were taken by the page itself, on request from a terminal (make cue to aim, make shot to capture). That only reaches the canvas: the HUD, the panels and the search have never appeared in any image this project can take, which is why their text is unit-tested instead of screenshotted.

Point it only at code you trust.go/packages shells out to the Go toolchain, so dumping a module runs go list against it — which can fetch dependencies, honour a toolchain directive, and preprocess cgo. That is the same exposure as running any Go build on that code, and the same reason not to do it to a repo you just cloned from a stranger.

The dev server binds loopback unless you tell it not to.make dev is local only. make dev-remote listens on every interface so the page can be flown from a phone, and also registers /__cue and /__shot, which let it be steered and photographed from a terminal. That combination is a camera anything on your network can take, and a file writer. Fine on a home network, not something to run on a shared one, and not a decision a fresh clone should make on your behalf. scripts/page lan answers which one you are running.

Behind a port forward, loopback is not a boundary. In a Codespace — or over an SSH tunnel — the thing doing the forwarding sits on the same side of loopback as the server, so make dev binding 127.0.0.1 does not keep anyone out; what keeps them out is the forward's own visibility, which GitHub defaults to private. The devcontainer therefore leaves the remote control off, and the server says so at startup rather than letting the loopback default look like protection it is not providing.

Checks

make check # go vet, go test, golangci-lint, tsc --noEmit, vitest
make hooks # install the lefthook pre-commit hooks, once per clone

lefthook calls make targets, so there is one source of truth per check. There is no CI — ARCHITECTURE.md says why.

The other documents

WORKING.md is the operating manual: the edit loop, the instruments (make logs, make shot, make cue), the working agreements, and the gotchas already paid for. ARCHITECTURE.md has the ground rules, the seam between the two halves, and the known limits. DECISIONS.md lists every call made under the ten-minute rule with what was rejected. HANDOFF.md is the brief this was built from, unedited. Any of the three tells you more than this README about why it looks like this.

MIT licensed.

About

A Go module's call graph, as districts you fly through.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages