Skip to main content

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):

FieldSourceDescription
headcompiledNode.headFunctor name used in API calls
aritycompiledNode.arityNumber of arguments
outputcompiledNode.canonicalOutputCanonical pattern string
inputscompiledNode.canonicalInputsCanonical input pattern strings
isDeterministiccompiledNode.source.isDeterministicSame inputs → same output
hasSideEffectscompiledNode.source.hasSideEffectsComputor 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 info
  • graph.debugListMaterializedNodes() — to enumerate instances
  • graph.debugGetFreshness(head, args) — to get freshness of one node
  • graph.debugGetValue(head, args) — to read a cached value without triggering recomputation
  • graph.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:

SituationStatus
Unknown node head404
Node not materialized404
Arity mismatch400
Graph not yet initialized503
Successful pull200
Successful invalidation200

6. Invariants and Constraints

  • GET endpoints are read-only. POST endpoints may materialize or refresh one concrete node key. DELETE endpoints invalidate one concrete node key, marking it and its transitive dependents as potentially-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 value field is always the raw ComputedValue object as stored in LevelDB — no transformation or filtering.
  • The API does not guarantee consistency between calls: a node appearing as up-to-date in one response may be potentially-outdated in the next.