At each level, walk up AND fork into the capability child. Common config on the main hierarchy, capability-specific handlers at the fork.
A capability request needs two kinds of data from the type hierarchy: the common config (bridge transport, host, port) that lives on the type itself, and the capability-specific handlers (what actions to run for peek vs poke) that need to be scoped to the capability name.
The original design concatenated type_addr:capability into a single address and walked that. This worked but created a fake address — the walk started from a position that only existed if you happened to put a record there. It also meant only the type walk could find capabilities. Sessions and targets couldn't contribute capability-specific behaviour.
The forking walk adds an optional capability parameter. At each level of the walk, after collecting crossings at the current address, the walk also queries at {current}:{capability}. Both sets of results go into the same stack.
Walk :bridges:json-rpc:remark:streams:mdast (capability: peek)
:bridges:json-rpc:remark:streams:mdast ← collect (mdast config)
:bridges:json-rpc:remark:streams:mdast:peek ← fork (peek handlers for mdast)
:bridges:json-rpc:remark:streams ← collect (stream defaults)
:bridges:json-rpc:remark:streams:peek ← fork (peek for streams)
:bridges:json-rpc:remark ← collect (remark bridge config)
:bridges:json-rpc:remark:peek ← fork (peek for remark)
:bridges:json-rpc ← collect (json-rpc transport)
:bridges:json-rpc:peek ← fork
:bridges ← collect (bridge defaults)
:bridges:peek ← fork
: ← collect (root)
:peek ← fork (universal peek)
Fork addresses that don't exist return empty — no cost beyond the query. The fork is one extra store query per level.
The capability fork applies uniformly to all three walks in the compiler:
| Walk | Starting Address | What the Fork Finds |
|---|---|---|
| TO | Target address (e.g. :streams:my-doc) |
Capability-specific overrides on the target or its ancestors. A :streams:peek could add default peek behaviour for all streams. |
| FROM | Session address (e.g. :sessions:users:gfawcett) |
Capability-specific session behaviour. A :sessions:admin:peek could add audit logging for admin peeks. A :sessions:peek could add rate limiting for all sessions. |
| TYPE | Type address (from target's type_addr) | The primary source of capability handlers. Bridge config on the main hierarchy, handlers at the fork. :bridges:json-rpc:remark:streams:mdast:peek has the peek handlers. |
All three contribute, collapse merges them with the standard precedence (type lowest, from highest). This means:
A capability defined at root (:peek, :repl, :introspect) is found by every walk because every walk reaches :. No special registration needed. Put a record at :repl with type_addr: ":capability" and every type in the system can now repl.
Type-specific capabilities override or extend the root default via normal collapse precedence. :bridges:json-rpc:remark:streams:mdast:peek overrides :peek because it's more specific (closer to the leaf).
# query/walk.rb
def initialize(address, types: nil, capability: nil, store: nil)
@capability = capability
end
def perform
while current && !visited.include?(current)
collect_at(current, layers)
if @capability
fork_addr = current == ':' ? ":#{@capability}" : "#{current}:#{@capability}"
collect_at(fork_addr, layers) unless visited.include?(fork_addr)
visited << fork_addr
end
current = parent_of(current)
end
end
# engine/compiler.rb — all three walks get the capability fork
def resolve_ast
to_stack = Query::Walk.new(@address, capability: @capability).perform
from_stack = Query::Walk.new(@session_id, capability: @capability).perform
type_stack = Query::Walk.new(@type_addr, capability: @capability).perform
collapsed = Query::Collapse.new(to_stack, from_stack, type_stack).perform
end
Anti-crossings work naturally with forked addresses. An :anti:poke at :bridges:json-rpc:remark:streams:mdast:peek cancels a :poke at that same fork address. The anti-crossing filtering happens in the store query, before the walk sees the results. The fork just generates one more address to query — all existing store-level mechanisms apply.