Graph computing
Graph computing
Grid graphs are resident, immutable, directed property multigraphs. They are named handles rather than cell values: topology is built once per source revision and reused by every graph query.
Symbol relationships and the model graph
The model itself also has a graph. Every model symbol/cell is a node; ordinary
formula references contribute directed :depends edges. You can add a
separate, authored relationship layer without wrapping the model in a graph
block:
predicate shipment(qty: number)
factory = 1200 relates to warehouse via :shipment(12)
warehouse = 800 connects to retailer via :peer
frame downstream = MODEL.GRAPH.BFS(:factory)A relationship clause following an assignment inherits the assignment target
as its source. The first declaration above is equivalent to writing factory = 1200 and factory relates to warehouse via :shipment(12) as separate
statements. The assigned value is not a graph node and the relation is not an
operator on that value. Standalone relationship statements remain available
for symbols declared elsewhere or symbols with no value declaration.
One declaration may attach multiple relationships inline or on directly following lines:
sensor = 0
connects to upstream_sensor via :proximity
relates to controller via :telemetryTargets and relation uses may also be comma-delimited. Equal-length lists pair positionally; a singleton on either side broadcasts across the other list:
A1 connects to B1, C1, D1 via :type1, :type2, :type3
A2 connects to B2, C2, D2 via :proximity
A3 relates to B3 via :primary, :fallbackThese declarations lower to independent relationships. Lists with unequal lengths are rejected unless one side is a singleton. Repeating the same source, target, direction, and kind is valid: each declaration is a distinct parallel logical relationship.
Relationships are static model metadata. Attaching one to a lazy (~=),
conditional (?=), scheduled, DEFAULT, or STATE assignment does not make
the edge conditional on that assignment's current value or execution state.
The relationship exists for the compiled model revision; only a symbol-backed
payload property refreshes as its source cell changes.
relates to is directed. connects to is bidirectional and denotes one
logical relationship; the resident directed representation keeps its two
traversal arcs under a shared relation_id. Relation kinds are symbols and
become the edge kind property, so ordinary Graph views and predicates can
select :depends, :shipment, or :peer without a separate query system.
Logical relation_id values are stable under insertion of unrelated
declarations. Parallel duplicates receive distinct duplicate ordinals, while
the two physical arcs of one connects to declaration share one ID. Treat the
ID as opaque; equality and source stability are the public contract, not its
encoded spelling.
predicate declares the payload independently of its carrier. The same
declaration may type a structured unary predicate, such as
order is :shipment(qty) = total (the readability article is optional), and a
relationship payload. An undeclared fieldless predicate such as :peer
remains valid; any predicate carrying fields requires a matching declaration.
The older tag and relation declaration keywords remain source-compatible
aliases, but predicate is canonical. Predicate declarations may come from
imported .gs libraries. A trailing one (the default) or
many by (field, ...) controls how many predicate instances may attach to one
symbol and how those instances are identified. It does not collapse or key
graph edges: relationship identity remains the compiler-owned relation_id.
MODEL.GRAPH is a distinguished receiver, not a constructor. It uses the
same Graph methods, path declarations, views, and filtering surface as an
explicit graph handle, and is materialized only when a model relationship is
authored or the receiver is queried. Declared relation payloads are typed and
may be scalar literals or model symbols. Symbol-backed payloads refresh the
edge property snapshot when their source cell changes without rebuilding the
graph's topology. Both literal and symbol-backed payload values must inhabit
the shared predicate field contract; mismatches fail closed rather than
becoming unchecked loose-mode overlays. Payload parameter names kind,
relation_id, tag, target, membership_id, and present are reserved for
carrier-owned relationship, membership, or state metadata.
Unary-predicate membership forms the bipartite portion of
MODEL.PREDICATES. MODEL.TAGS remains a stable unary-only compatibility
view; both are separate from MODEL.GRAPH:
reviewed = MODEL.PREDICATES.NEIGHBORS(:reviewed)
predicates = MODEL.PREDICATES.IN_NEIGHBORS("A1")
instances = MODEL.PREDICATES.OUT_DEGREE(:reviewed)
memberships_and_facts = MODEL.PREDICATES.EDGES()The predicate side uses colon-symbol node identity (:reviewed); the
model-symbol side uses canonical text identity ("A1", "order"). This makes
the leading colon a true namespace separator even when a model symbol and
predicate share the same spelling. Each :<predicate> -> "symbol" edge has
kind :applies. Structured field values and field.__type metadata are edge
properties. A single predicate has at most one such edge per symbol. A
many by (...) predicate has one parallel edge per identity, so adjacency
returns a distinct set of symbols while degree and edge operations retain every
instance. The membership edge ID derives from the predicate, target, and
complete typed declared identity and is stable across
payload upserts. It is an opaque, versioned ID; current IDs use
tag-membership:v2:<sha256>. Consumers may compare or retain the full ID but
should not parse the digest or assume an unversioned format.
Hidden unary-predicate state cells are live Graph inputs. Adds, keyed removes,
and bare remove-all rule actions revise the view and invalidate cached Graph
results; unchanged state reuses the exact-input cache without rebuilding adjacency.
The view is materialized lazily only when queried. Changing only the lookup
target reuses its resident adjacency, while a same-identity structured payload
update shares the topology and replaces only the affected property data. Only
a real membership add or remove rebuilds adjacency. Keeping the view distinct
means unary-predicate membership cannot change MODEL.GRAPH dependency paths,
components, or centrality.
MODEL.PREDICATES is the single extensional view of predicates. It combines
those live unary-membership edges with authored binary facts:
next = MODEL.PREDICATES.NEIGHBORS("design")
places = MODEL.PREDICATES.NEIGHBORS("parcel")
facts = MODEL.PREDICATES.EDGES()Binary edges carry kind: :predicate, the predicate family, authored relation
alternatives, source order, and any condition metadata. Unary edges carry
kind: :applies and their structured payload. The view does not materialize
inferred closure as edges; query inference belongs to the Predicate dialect and
is available through plain queries, MUST, MAY, CANNOT, RELATION, and
WHY. This keeps extensional knowledge, inferred knowledge, and model
dependency topology distinct.
Temporal, topological, and conceptual-set facts coexist in this view.
Family-local inference remains separate even when the same symbols participate
in several families; select temporal, topology, or sets only when an
inspection would otherwise be ambiguous. See the predicate guide.
Declaring a graph
Use an exact graph named binding. A graph cannot be assigned to a cell,
range, qualified sheet target, input, output, default, lazy assignment, or
scheduled assignment.
frame road_rows = READ_PARQUET("roads.parquet")
graph roads = GRAPH(
road_rows AS edges,
"road_id" AS edge_id,
"origin" AS from,
"destination" AS to,
["minutes", "capacity", "build_cost"] AS edge_properties,
TRUE AS directed
)edges, edge_id, from, and to are required. edges may be a Frame, a
finite range, or a literal row array. Column bindings may be names or indexes
appropriate to the source. Stable edge IDs are mandatory. Optional nodes
and node_id bindings add isolated nodes. node_properties and
edge_properties select retained property columns; when omitted, lowering
retains every non-binding column. Parallel edges and self-loops are supported.
Node identity is exact and type-sensitive. Strings and checked integers are valid node keys; blanks, floating-point identity, and implicit text coercion are rejected. Construction is deterministic and rejects missing endpoints, duplicate edge IDs, and schema mismatches.
Frame-backed graphs are source-revision-backed. Literal row arrays and ranges
whose required cells are all literals carry portable records in LIR. A live or
computed Workbook range lowers through a Frame source with explicit cell
dependencies. A compiler must not mark a graph SOURCE_BACKED unless it
emitted both source handles and the complete node/edge ID and endpoint column
contract.
Projecting a graph into Predicate knowledge
An arbitrary named graph can be an extensional fact source for the general Predicate program:
graph knowledge = GRAPH(
relationship_rows AS edges,
"id" AS edge_id,
"subject" AS from,
"object" AS to,
["relation"] AS edge_properties,
TRUE AS directed
)
KNOWLEDGE GRAPH knowledge VIA edge.relation WITH PROPERTIES
predicate ancestor is transitive
answer = MUST("ada" ancestor "charles")VIA edge.<property> selects the property whose nonempty string or symbol
value names each open binary predicate. It defaults to edge.kind, making
KNOWLEDGE GRAPH knowledge sufficient for conventional typed property
graphs. A missing, blank, or non-textual relationship property fails the
Predicate region closed and identifies the offending stable edge ID.
WITH PROPERTIES additionally exposes the resident property graph through
four ordinary, joinable logical predicates:
graph_node("knowledge", node)
graph_edge("knowledge", edge_id, from, to, relation)
graph_node_property("knowledge", node, property, value)
graph_edge_property("knowledge", edge_id, property, value)The graph binding, edge IDs, property names, string values, and node strings
use quoted string terms; symbols, exact integers, finite binary64 numbers,
booleans, and BLANK retain distinct logical constants. These facts are read
from the current snapshot at dispatch and need not be copied into authored
Predicate facts. Omitting WITH PROPERTIES projects only the direct binary
relationships and avoids the additional fact volume.
The projection reads the current resident graph revision. Frame-backed source changes and accepted atomic graph patches therefore recompute the fact base before dependent Predicate publications resolve. A directed edge contributes one fact; an undirected edge contributes both directions. Parallel graph edges remain distinct graph relationships while their projected logical truth is extensional.
Graph identity remains exact across the boundary. A symbol :1, string
"1", and integer 1 are distinct Predicate constants, and string or integer
node identities may be written directly in logical atoms. Projected facts may
participate in ordinary laws, rules, stratified negation, graph quantifiers,
virtual paths, evidence queries, and WHY. Proof output retains the
contributing graph handle, exact resident revision, stable edge ID, typed
endpoints, and whether an undirected reverse fact was synthesized.
This is an explicit cross-dialect projection, not a topology merge.
MODEL.GRAPH, MODEL.TAGS, MODEL.PREDICATES, the source GraphHandle, and
inferred Predicate closure retain their separate identities and lifecycle.
Projection size counts against the Predicate region's symbol, assertion, work,
metadata, and derivation bounds.
Graph-backed reachability publishes the graph's authored string and integer
node values; integers use Grid's exact bigint representation, so even large
identities remain lossless. The reserved typed-identity encoding used inside
Predicate never appears in cell results or provenance. A complete executable model is available at
examples/canonical/17-predicate-knowledge.grid.
Typed property schemas
Use a closed property schema when graph algorithms should be checked before GIR execution:
graph schema Roads {
node kind City
edge kind Road: City -> City
node.kind: symbol
node.population: number?
edge.kind: symbol
edge.minutes: number
edge.capacity: number
edge.open: boolean
}
graph roads: Roads = GRAPH(
city_rows AS nodes,
"city_id" AS node_id,
["kind", "population"] AS node_properties,
road_rows AS edges,
"road_id" AS edge_id,
"origin" AS from,
"destination" AS to,
["kind", "minutes", "capacity", "open"] AS edge_properties
)Each entry is node.<path>: <type> or edge.<path>: <type>; a trailing ?
marks an optional value. Nested paths such as node.location.latitude are
valid. Types use normal Grid type tags, including semantic and dimensional
tags such as currency:usd and unit:m/s. A tag must refine a native Graph
property representation: number/integer, boolean, string, or symbol. Date,
object, array, and other representation roots are rejected until Graph storage
can preserve them losslessly.
Schemas may also declare named node kinds and endpoint-constrained edge kinds.
node kind City introduces a node kind; edge kind Road: City -> City
introduces an edge kind and its valid endpoint kinds. A schema with named kinds
must declare the corresponding total node.kind and edge.kind property as a
string or symbol tag. Unknown kinds, missing kind values, and invalid edge
endpoints are rejected.
The node_properties and edge_properties arrays must exactly match the
declared paths. Selectors in paths, community partitions, patterns,
comprehensions, rewrites, and fixpoints are checked for scope and existence.
Numeric objectives also require
a non-optional property whose type refines number. Reverse, induced-subgraph,
and minimum-spanning-tree bindings inherit their source schema; an explicit
different schema is rejected. Bare graph bindings remain available when a
closed schema is not wanted.
The closed schema is serialized into Graph GIR metadata and checked again when
the resident snapshot is loaded and whenever an atomic property patch is
staged. Required properties must be populated on every row, undeclared
properties are rejected, and storage representations must inhabit their type
tags. Equality predicates (= and !=) are also checked at compile time when
one operand is a literal, so for example edge.minutes = "fast" cannot reach
runtime when edge.minutes is numeric.
graph <name>: <Schema> is the preferred spelling and the formatter's output.
The generic prefix tag graph:<Schema> <name> = ... remains accepted for
compatibility.
id is intrinsic node/edge identity rather than a property: a.id and e.id
are valid pattern projections without schema entries, while pseudo-properties
such as a.identity must be declared.
Querying
Method form is preferred. It desugars to the corresponding namespaced
GRAPH_* intrinsic with the receiver as argument zero.
A1 = roads.NODE_COUNT()
A2 = roads.OUT_DEGREE("Denver")
A3 = roads.HAS_PATH("Denver", "Boulder")
frame path = roads.SHORTEST_PATH(
"Denver",
"Boulder",
"minutes" AS weight
)
frame ranks = roads.PAGE_RANK(
"capacity" AS weight,
0.85 AS damping,
1e-8 AS tolerance,
100 AS max_iterations
)Path queries and partitions are resident typed handles. Their data is exposed through bounded projections rather than by spilling opaque handles into cells:
path fastest = path in roads from "Denver" to "Boulder"
minimize sum(edge.minutes)
limit 32 AS hops
A4 = fastest.cost
A5 = fastest.hops
frame fastest_nodes = fastest.nodes
frame fastest_edges = fastest.edges
partition districts = partition nodes of roads by community(
leiden AS method,
0 AS seed
)
frame district_membership = districts.members
frame district_summary = districts.summarypath.nodes has columns (ordinal, node). path.edges has
(ordinal, edge, from, to) followed by the source graph's edge-property
columns. A missing path produces blank scalar projections and empty Frames.
Partition membership has (node, partition) and summary has
(partition, size, first_node, label). All rows use canonical deterministic
order and count against Graph output-row and scratch-memory budgets.
Scalar operations are NODE_COUNT, EDGE_COUNT, DENSITY, DEGREE,
IN_DEGREE, OUT_DEGREE, HAS_PATH, and HAS_EULERIAN_PATH. Frame results
include NODES, EDGES, NEIGHBORS,
IN_NEIGHBORS, BFS, DFS, SHORTEST_PATH, BELLMAN_FORD,
WEAK_COMPONENTS, STRONG_COMPONENTS, TOPOLOGICAL_ORDER, PAGE_RANK,
bounded DIAMETER, ARTICULATION_POINTS, BRIDGES, CLOSENESS_CENTRALITY, and
BETWEENNESS_CENTRALITY. K_CORE returns per-node core numbers,
TRIANGLE_COUNT returns a scalar, CLUSTERING_COEFFICIENTS returns per-node
coefficients, and MAXIMUM_BIPARTITE_MATCHING(left_partition) returns the
selected edges.
Weighted algorithms name an edge property for each call. SHORTEST_PATH
rejects negative weights; use BELLMAN_FORD when negative weights are part of
the model. Iterative and all-sources algorithms expose convergence or source
bounds so work remains explicit.
Declarative paths
Use a first-class path binding when the intent is an objective rather than a
specific algorithm. Source never names BFS, Dijkstra, or a heap:
path fastest = path in roads from "Denver" to "Boulder"
minimize sum(edge.minutes)
limit 40 AS hops
path resilient = path in roads from "Denver" to "Boulder"
maximize min(edge.capacity)
path fewest_transfers = path in roads from origin to destination
minimize hopsThe planner maps fewest hops to breadth-first search, an unbounded non-negative
sum to Dijkstra, a hop-bounded objective to expanded-state dynamic programming,
and a max-min objective to widest path. This choice is visible through
explainGraph and the resolved PathHandle report, but is not part of source
semantics. Paths are non-spillable resident values bound to the exact source
graph revision. getPathReport returns the ordered node and edge IDs, cost,
objective, chosen plan and reason, deterministic flag, and implementation
version.
Partitions
Connectivity, core decomposition, and community detection all produce the same first-class partition value:
partition weak = partition nodes of roads by connectivity(weak AS direction)
partition cores = partition nodes of roads by core_number
partition core4 = partition nodes of roads by k_core(4 AS k)
partition regions = partition nodes of roads by community(
leiden AS method,
modularity(1 AS resolution) AS objective,
edge.capacity AS weight,
symmetrize(sum) AS direction,
0 AS seed,
all AS hierarchy,
50 AS max_passes,
16 AS max_levels,
0.000000001 AS tolerance
)Leiden is the recommended community method. louvain is available as an
explicit compatibility choice. Directedness is never guessed: select
directed, undirected, or an explicit symmetrize(sum|max|mean) policy.
Community IDs are canonicalized by member order and therefore do not expose
backend-local labels. getPartitionReport returns provenance, convergence and
work metadata, typed membership rows, hierarchy, and per-community summaries.
Bounded pattern matching
Patterns are typed Graph syntax and always require an output bound:
frame alternatives = match roads limit 1000:
(a)-[first where first.open = TRUE]->(b)-[second where second.open = TRUE]->(c)
return a as origin, b as via, c as destination,
first.minutes as first_minutes, second.minutes as second_minutesNode and edge bindings are statically scoped. Predicates use typed properties;
projections must have unique aliases. An exact start identity uses an ID-seek
plan, while an unconstrained start uses a canonical node scan. Expansion and
output order remain deterministic. Results materialize through a declared
FrameHandle; they never spill implicitly into Workbook cells.
Repeating a binding with the same entity role is a bounded equality join:
frame mutual = match roads limit 1000:
(a)-[out]->(b)-[back]->(a)
return a.id as origin, b.id as peerA binding cannot change roles between node and edge positions.
DISTINCT and GROUP are not clauses of Graph pattern syntax. match first
produces a typed FrameHandle; deduplication, grouping, aggregation, sorting,
and windowing are downstream Frame-dialect operations over that Frame. Graph
owns bounded traversal and identity semantics, while Frame owns relational row
analytics and its Polars/Lance physical plans.
Graph comprehensions and immutable rewrites
A comprehension creates a property-preserving logical graph view:
graph active: Roads = roads {
node city where city.population > 0
edge road where road.open = TRUE
}Graph MIR retains the node/edge predicates, source identity, consumer count, and cardinality, work, and retained-memory evidence. A single selective cardinality consumer can fuse the scan/filter and allocate no derived snapshot. Shared or topology-consuming views materialize one admitted resident snapshot. Both plans preserve canonical identity, ordering, and errors.
Rewrites are bounded immutable transactions. V2 accepts deletion of exactly one edge binding from a one-edge match:
graph normalized: Roads = rewrite roads limit 100000:
match (a)-[road where road.open = FALSE]->(b)
delete roadlimit N bounds distinct stable edge IDs. The native scan detects one unique
edit beyond the limit—even for undirected duplicate traversal—and rejects the
whole operation before patch staging. Every match observes one source revision
and readers never see partial edits.
Bounded graph fixed points
A fixpoint frame is the native propagation form. It has an explicit semiring,
direction, initial state, seeds, convergence condition, and iteration bound:
frame minutes_from_origin = fixpoint in roads:
using min_plus(edge.minutes AS weight)
along AS direction
1000000000000 AS default
0 AS seed origin
until max_delta < 0.000000001
within 100 iterationsAvailable operators are sum_product, min_plus, max_min, and
boolean_or. They specialize respectively to deterministic sparse gather,
shortest-path relaxation, widest-path relaxation, and reachability
propagation. along gathers through incoming edges and against through
outgoing edges. Every program needs at least one seed and a positive literal
iteration bound. Sum-product, min-plus, and boolean-or initial values are
finite. Max-min additionally accepts negative_infinity as its default and
positive_infinity for seeds, matching its widest-path identities.
EDGES() returns canonical edge-ID order. Other order-sensitive graph results
use canonical type-sensitive node order, with edge ID and source order as
deterministic tie-breakers. DENSITY() measures the graph's simple loop-free
projection: parallel edges count once and self-loops do not count, against the
directed (n*(n-1)) or undirected (n*(n-1)/2) denominator. Density is
therefore always between 0 and 1; graphs with fewer than two nodes report
0.
HAS_EULERIAN_PATH() works for directed and undirected graphs. Directed
graphs require all non-isolated nodes to be weakly connected and either every
node balanced or exactly one out_degree - in_degree = 1 start and one -1
end. Undirected graphs require connected non-isolated nodes and zero or two
odd-degree nodes. A graph with no edges has an Eulerian path.
When max_sources is supplied to DIAMETER, CLOSENESS_CENTRALITY, or
BETWEENNESS_CENTRALITY, Grid deterministically spreads
min(node_count, max_sources) samples across canonical node order, including
both endpoints when at least two sources are selected. This avoids the
adversarial bias of a canonical-prefix sample. A bound smaller than the node
count still approximates the full all-sources query: bounded closeness returns
only sampled source rows, while bounded betweenness returns every node's score
accumulated from those sources. The native core result reports the sampled
source IDs, total source count, exact/approximate status, and strategy. Current
Frame schemas do not add those fields, so retain the authored max_sources
argument when that provenance is needed downstream.
Transformations
Graph-producing operations must be rebound as graph handles:
graph reverse_roads = roads.REVERSE()
graph backbone = roads.MINIMUM_SPANNING_TREE("build_cost" AS weight)
graph regional = roads.INDUCED_SUBGRAPH(region_node_ids)
graph quotient = contract roads by districtsThe result is another immutable graph snapshot. Existing CS_* edge-list
functions remain compatibility surfaces; the compiler may promote stable,
repeated edge-list operands to the resident graph core without changing their
observable results.
Partition contraction produces a deterministic quotient GraphHandle. Each
partition becomes an integer node with size, first_node, and optional
label properties. Internal edges are removed; every cross-partition edge
keeps its stable ID and properties. Parallel edges remain distinct, so
directedness and multigraph semantics are preserved without an implicit
aggregation policy. The partition must belong to the exact source revision.
Because the quotient has a generated property shape rather than the source
schema, source-schema annotation on the contraction binding is rejected.
Compatibility promotion is deliberately conservative. A MIR promotion pass
may share a resident graph only when every promoted call has the same
direction, exact node-key representation, edge order, weight/property source,
blank handling, error contract, and deterministic tie-breaking as the legacy
CS_* call. Dynamic or ambiguous operands stay on the legacy intrinsic. The
source function name and source span remain attached to diagnostics, and the
pass never rewrites authored source or requires migration to GRAPH.
Row-shaped results may be bound directly as resident Frames. Such bindings carry the produced FrameHandle into downstream Frame/Geo regions without a Workbook spill:
frame road_edges = roads.EDGES()
R1C1 = FRAME_GEO_FILTER_BBOX(road_edges, "geometry", -105, 39, -104, 40)Result and work bounds
Graph handles never spill into workbook cells. Scalars may be assigned to
cells; tabular results are Frames and require explicit collection to spill.
Runtime limits cover nodes, edges, resident bytes, result rows, work units,
iterations, per-operation scratch, deadlines, and cancellation. The declared
max_scratch_bytes is intersected with host policy, including
GRID_MAX_GRAPH_SCRATCH_BYTES. Kernels with scratch plans preflight
conservative major phase peaks. GRID_GRAPH_SCRATCH_BUDGET means preflight
rejected the operation; GRID_GRAPH_SCRATCH_ALLOCATION is reserved for the
covered fallible vector reservations that fail after preflight. Grid does not
claim to intercept every allocation made by Rust or its dependencies.
Tie-breaking is stable by node key, edge ID, then source order and never depends on hash-map iteration order.
Resident host API
The Rust runtime host advertises listGraphHandles, listPathHandles,
listPartitionHandles, getPathReport, getPartitionReport, explainGraph,
resolveGraph, patchGraph, appendGraphPatchEvent,
replayGraphPatchEvents, getGraphPatchJournal, projectGraphToGeo,
solveGraphMinCostFlow, and runGraphDes through its HTTP RPC capability
list. listGraphHandles(modelId)
returns the stable wire IDs currently resident for the model;
resolveGraph(modelId) executes its Graph regions and returns bounded typed
reports; and patchGraph(modelId, graphHandle, patches) atomically applies a
typed patch batch. Derived handles are read-only: patch their primary source
handle instead. A rejected batch leaves both the prior snapshot and active
readers unchanged. Every accepted patch is also appended to the model-local
GraphPatchJournalSnapshot. Durable storage persists that stream beside the
compiled model as graph-patches.jsonl. Restart replay checks journal order,
each graph revision transition, and the runtime's exact before/after patch
evidence. A torn final record is discarded; malformed complete records or a
different base graph fail closed. The journal is bounded to 512 MiB by default;
GRID_GRAPH_PATCH_MAX_JOURNAL_BYTES may lower or raise that ceiling. Authored
source or source Frames remain the durable base snapshot. If a patch commits
in memory but its journal append or fsync fails, the host immediately unloads
the resident model. The next access must rehydrate from the last complete
durable prefix, so no later request can observe or extend an unjournaled
revision. Batch replay is prefix-preserving: if a later event is rejected,
every earlier accepted atomic event is fsynced before the error is returned,
and the unapplied suffix remains available for correction or retry.
GraphPatchEventRequest.clientEventId is an optional, bounded idempotency key.
A retry with the same key, authority, source URI, graph handle, and patches
returns the original event even when its optimistic revision coordinates are
now stale; reusing the key for a different mutation fails closed. HTTP ingress
overwrites authority from the trusted principal and stamps observedAt.
sourceUri remains caller-provided trace context and is validated and retained
with the durable event.
The three domain bridge calls consume the current
resident revision directly, preserve canonical node identity, and return a
pinned { source, value } envelope without an external intermediate file.
source contains graphHandle, the graph's local revision, optional
sourceRevision, and its content fingerprint; value is the Geo, Network
Flow, or Sim domain result. Derived views/rewrites may reuse a stable handle and
start their own local revision at zero after reconstruction, so downstream
capabilities must retain the complete pin rather than comparing only the local
revision. A primary-source patch invalidates derived handles before an adapter
can observe stale topology.
Portable GraphHandles with node/edge counts fixed in LIR accept property
updates and count-preserving endpoint rewires. They reject node/edge additions
and removals before commit with GRID_GRAPH_PATCH_SHAPE_IMMUTABLE, because the
immutable handle header cannot represent a different topology shape. Dynamic
source-backed handles whose counts are left open may accept shape changes under
their runtime budgets.
The Node web API exposes the core lifecycle and diagnostics as thin, model-scoped facades; Graph semantics and validation remain in Rust. The typed frontend transport exports the matching calls:
| Method | Model-scoped endpoint | Frontend transport |
|---|---|---|
GET |
/api/models/:id/graph/handles |
listGraphHandles |
POST |
/api/models/:id/graph/resolve |
resolveGraph |
POST |
/api/models/:id/graph/handles/:handle/patch |
patchGraph |
POST |
/api/models/:id/graph/journal/events |
appendGraphPatchEvent |
POST |
/api/models/:id/graph/journal/replay |
replayGraphPatchEvents |
GET |
/api/models/:id/graph/journal |
getGraphPatchJournal |
GET |
/api/models/:id/graph/explain |
explainGraphModel |
GET |
/api/models/:id/graph/paths |
listGraphPathHandles |
GET |
/api/models/:id/graph/paths/:handle |
getGraphPathReport |
GET |
/api/models/:id/graph/partitions |
listGraphPartitionHandles |
GET |
/api/models/:id/graph/partitions/:handle |
getGraphPartitionReport |
All routes accept the standard model partition query/header. Patch
bodies are { patches: GraphPatch[] }; graph mutation is atomic and the
response includes revision and patch evidence. Rust u64 revisions and
fingerprints cross this JSON wire as exact decimal strings; legacy numeric
requests remain accepted. Work, count, budget, and size fields remain numeric
quantities.
Durable graph patch tails checkpoint after 8 MiB by default (configurable with
GRID_GRAPH_PATCH_CHECKPOINT_TAIL_BYTES). A checkpoint retains the complete
event prefix for audit and idempotency, plus exact resident images for every
patched primary GraphHandle. Recovery validates those images against the
authored graph's base revision and fingerprint, installs the whole checkpoint
atomically, and replays only the post-checkpoint tail. Legacy event-only
checkpoints remain readable and use full replay. Checkpoints publish before the
tail is truncated, and recovery verifies/deduplicates that crash window. New
journals are bound to the model id and source hash; source edits therefore fail
closed instead of rebasing patches onto potentially different graph identities.
explainGraph(modelId) is read-only and does not execute the model. It returns
one report per Graph region containing:
- the primary handle, current revision, node/edge counts, and resident bytes;
- hard node, edge, work, output-row, iteration, and scratch budgets;
- every semantic operation contract and source/result slot;
- selected physical kernel and the reason for that choice;
- result materialization (
GraphHandle,PathHandle,PartitionHandle, orFrameHandle) and current resident state.
After resolveGraph, call getPathReport(modelId, pathHandle) or
getPartitionReport(modelId, partitionHandle) for data-dependent execution
details. Patching a primary graph invalidates all derived paths, partitions,
and derived graph descendants before the next read.
Prometheus exposure includes grid_graph_resident_artifacts{kind="path"} and
{kind="partition"}, per-operation call/work/budget/latency metrics, stable
Graph error codes, resident bytes, cache outcomes, and atomic patch outcomes.
Scratch telemetry is
grid_graph_scratch_peak_bytes, grid_graph_scratch_rejections_total, and
grid_graph_scratch_allocation_errors_total, labeled by operation. The last
counter covers only the fallible reservation sites described above.
Graph V2 completion contract
Graph V2 is feature-complete for the surface documented here. Completion is pinned by these acceptance gates:
- schemas, named kinds, endpoint contracts, selector types, equality literals, hydration, and atomic property patches fail closed on invalid representation;
- paths, partitions, patterns, views, rewrites, fixed points, projections, and contraction lower through GIR and survive LIR serialization;
- PathHandle, PartitionHandle, FrameHandle, and derived GraphHandle lineage is revision-bound, non-spillable where applicable, and invalidated atomically;
- deterministic sparse/dense monsters exercise work, output, iteration, and scratch fences; view fusion equals materialization and rewrite overflow leaves the source untouched;
- the canonical example reaches the production host, returns typed projection
Frames, materializes the quotient, exposes
EXPLAIN, and emits scratch telemetry.
This completion claim does not move Frame DISTINCT/GROUP into Graph, claim
global allocator coverage, or certify a fixed-runner performance ratio.
The end-to-end authored example for the implemented V2 slice is
examples/canonical/16-graph-v2-native.grid.