Spec: Incremental Graph Inspection API
An HTTP API for inspecting the incremental graph — its schema definition and the set of currently materialized node instances — plus targeted pull operations for individual node keys.
1. Motivation
The incremental graph is a live, LevelDB-backed computation cache. Diagnosing staleness bugs, auditing what has been computed, and understanding dependency structure all currently require attaching a debugger or reading raw LevelDB files. This spec defines a small REST API that exposes that information through the existing Express server.
2. URL Prefix
All endpoints are mounted under the server's base path:
{BASE_PATH_PREFIX}/api/graph/
In the default (root-path) deployment this is /api/graph/.
3. Endpoints
3.1 GET /api/graph/schemas
Returns the complete schema — every node family defined in the graph.
Response 200 OK:
[
{
"head": "all_events",
"arity": 0,
"output": "all_events",
"inputs": [],
"isDeterministic": false,
"hasSideEffects": false
},
{
"head": "meta_events",
"arity": 0,
"output": "meta_events",
"inputs": ["all_events"],
"isDeterministic": true,
"hasSideEffects": false
},
{
"head": "event_context",
"arity": 0,
"output": "event_context",
"inputs": ["meta_events"],
"isDeterministic": true,
"hasSideEffects": false
},
{
"head": "event",
"arity": 1,
"output": "event(x)",
"inputs": ["all_events"],
"isDeterministic": true,
"hasSideEffects": false
},
{
"head": "calories",
"arity": 1,
"output": "calories(x)",
"inputs": ["event(x)"],
"isDeterministic": true,
"hasSideEffects": true
}
]
Fields are sourced from CompiledNode (from headIndex on the graph instance):
| Field | Source | Description |
|---|---|---|
head | compiledNode.head | Functor name used in API calls |
arity | compiledNode.arity | Number of arguments |
output | compiledNode.canonicalOutput | Canonical pattern string |
inputs | compiledNode.canonicalInputs | Canonical input pattern strings |
isDeterministic | compiledNode.source.isDeterministic | Same inputs → same output |
hasSideEffects | compiledNode.source.hasSideEffects | Computor has side effects |
3.2 GET /api/graph/schemas/:head
Returns the schema entry for a single node family.
Response 200 OK: Single object with the same shape as one element from §3.1.
Response 404 Not Found:
{ "error": "Unknown node: \"unknown_head\"" }
3.3 GET /api/graph/nodes
Lists all currently materialized node instances with their freshness status and timestamps. Does not trigger recomputation.
Response 200 OK:
[
{ "head": "all_events", "args": [], "freshness": "up-to-date", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-02T00:00:00.000Z" },
{ "head": "meta_events", "args": [], "freshness": "up-to-date", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" },
{ "head": "event_context", "args": [], "freshness": "potentially-outdated", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" },
{ "head": "event", "args": ["evt-abc123"], "freshness": "up-to-date", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" },
{ "head": "event", "args": ["evt-def456"], "freshness": "up-to-date", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" },
{ "head": "calories","args": ["evt-abc123"], "freshness": "up-to-date", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" },
{ "head": "calories","args": ["evt-def456"], "freshness": "potentially-outdated", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" }
]
freshness is one of "up-to-date" | "potentially-outdated".
createdAt and modifiedAt are ISO 8601 timestamp strings recording when the
node instance was first computed and when its value last changed, respectively.
Both fields are null for nodes that were materialized before timestamp
recording was introduced.
Values are not included in this listing. Node values can be large (e.g.
all_events contains the full event log) and fetching them in bulk would be
expensive.
3.4 GET /api/graph/nodes/:head
Arity-0 nodes (singletons like all_events, meta_events): returns the
single materialized instance including its cached value.
Does not trigger recomputation.
Response 200 OK:
{
"head": "all_events",
"args": [],
"freshness": "up-to-date",
"value": { "type": "all_events", "events": [ { "..." } ] },
"createdAt": "2024-01-01T00:00:00.000Z",
"modifiedAt": "2024-01-02T00:00:00.000Z"
}
Arity-N nodes (parameterized families like event, calories): returns the
list of all materialized instances for that head, identical in shape to the
filtered result of §3.3 (no values).
Response 200 OK:
[
{ "head": "calories", "args": ["evt-abc123"], "freshness": "up-to-date", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" },
{ "head": "calories", "args": ["evt-def456"], "freshness": "potentially-outdated", "createdAt": "2024-01-01T00:00:00.000Z", "modifiedAt": "2024-01-01T00:00:00.000Z" }
]
Response 404 Not Found (arity-0 node that has not been materialized yet):
{ "error": "Node not materialized: \"all_events\"" }
Response 404 Not Found (unknown head):
{ "error": "Unknown node: \"unknown_head\"" }
3.5 GET /api/graph/nodes/:head/:arg0[/:arg1[/:arg2…]]
Returns a single materialized instance for a parameterized node, with its cached
value. Does not trigger recomputation. Path segments beyond :head become
the ordered args array; clients must percent-encode any / characters within
an argument value.
Response 200 OK:
{
"head": "calories",
"args": ["evt-abc123"],
"freshness": "up-to-date",
"value": { "type": "calories", "calories": 412 },
"createdAt": "2024-01-01T00:00:00.000Z",
"modifiedAt": "2024-01-02T00:00:00.000Z"
}
Response 404 Not Found (node not yet materialized):
{ "error": "Node not materialized: \"calories(evt-abc123)\"" }
Response 400 Bad Request (wrong number of args for head's arity):
{ "error": "Arity mismatch: \"calories\" expects 1 argument, got 2" }
3.6 POST /api/graph/nodes/:head
Pulls a single arity-0 node key and returns the resulting value, freshness, and timestamps. This endpoint may trigger recomputation.
Response 200 OK: Same shape as the arity-0 response from §3.4.
Response 400 Bad Request (head exists but requires arguments):
{ "error": "Arity mismatch: \"event\" expects 1 argument, got 0" }
3.7 POST /api/graph/nodes/:head/:arg0[/:arg1[/:arg2…]]
Pulls a single parameterized node key and returns the resulting value,
freshness, and timestamps. Path segments beyond :head become the ordered
args array, exactly like §3.5. This endpoint may trigger recomputation.
Response 200 OK: Same shape as the response from §3.5.
Response 400 Bad Request (wrong number of args for head's arity):
{ "error": "Arity mismatch: \"calories\" expects 1 argument, got 2" }
3.8 DELETE /api/graph/nodes/:head
Invalidates a single arity-0 node key, marking it and all its transitive
dependents as potentially-outdated. Does not trigger recomputation — the
next pull (or POST) call will recompute the value.
Response 200 OK:
{ "success": true }
Response 400 Bad Request (head exists but requires arguments):
{ "error": "Arity mismatch: \"event\" expects 1 argument, got 0" }
3.9 DELETE /api/graph/nodes/:head/:arg0[/:arg1[/:arg2…]]
Invalidates a single parameterized node key, marking it and all its transitive
dependents as potentially-outdated. Path segments beyond :head become the
ordered args array, exactly like §3.5. Does not trigger recomputation.
Response 200 OK:
{ "success": true }
Response 400 Bad Request (wrong number of args for head's arity):
{ "error": "Arity mismatch: \"calories\" expects 1 argument, got 2" }
4. Non-Triggering Guarantee
All GET endpoints in this API must never call pull(),
pullWithStatus(), or any method that may trigger recomputation. They may only
call:
graph.headIndex— to resolve schema infograph.debugListMaterializedNodes()— to enumerate instancesgraph.debugGetFreshness(head, args)— to get freshness of one nodegraph.debugGetValue(head, args)— to read a cached value without triggering recomputationgraph.debugGetTimestamps(head, args)— to read timestamps without triggering recomputation
This protects against accidentally triggering expensive computors (e.g. OpenAI
API calls for calories) merely by browsing the inspection endpoints. The
POST endpoints in §3.6 and §3.7 are the explicit opt-in escape hatch for
triggering recomputation of one concrete node key.
5. Error Response Shape
All error responses use the same envelope:
{ "error": "<human-readable message>" }
HTTP status codes used:
| Situation | Status |
|---|---|
| Unknown node head | 404 |
| Node not materialized | 404 |
| Arity mismatch | 400 |
| Graph not yet initialized | 503 |
| Successful pull | 200 |
| Successful invalidation | 200 |
6. Invariants and Constraints
GETendpoints are read-only.POSTendpoints may materialize or refresh one concrete node key.DELETEendpoints invalidate one concrete node key, marking it and its transitive dependents aspotentially-outdated.- The API is unauthenticated at the same level as the rest of the API — authentication is handled at the infrastructure level, not per-route.
- Response bodies are always
application/json. - The
valuefield is always the rawComputedValueobject as stored in LevelDB — no transformation or filtering. - The API does not guarantee consistency between calls: a node appearing as
up-to-datein one response may bepotentially-outdatedin the next.