Loading Stave Graph Exports into Graph-Data-Science Libraries
Stave's graph export command produces vendor-neutral graph documents
in two formats. Pick whichever your tool of choice imports natively:
- JSON-LD (
--format jsonld) — universal RDF format with an@contextbound to the Stave ontology (urn:stave:ontology#). Every RDF-aware library can ingest it; OWL reasoners can apply the shipped ontology directly. - GraphML (
--format graphml) — XML graph exchange format with typed<key>attribute declarations. Native to igraph, NetworkX, Gephi, yEd, and Cytoscape; numeric severity weights and partition labels are decoded as numbers, not strings.
Both formats annotate algorithm-shortcut edges (stave:violates,
stave:violatesRequirement, stave:hasEffectiveAccess) with
isAlgorithmShortcut: true. Centrality, community detection, and
shortest-path workflows can ignore the multi-hop reasoning chain
(Resource → Finding → Control) and run directly over the materialized
shortcut subgraph.
Neo4j Graph Data Science (GDS)
Neo4j accepts both formats — GraphML via APOC, JSON-LD via the neosemantics (n10s) plugin.
stave graph export --output assessment.json --format jsonld --out stave.jsonld
// One-time: install n10s and bind the Stave ontology so resolved
// short names work in queries.
CALL n10s.graphconfig.init({handleVocabUris: 'SHORTEN'});
CALL n10s.nsprefixes.add('stave', 'urn:stave:ontology#');
// Import the JSON-LD document.
CALL n10s.rdf.import.fetch("file:///path/to/stave.jsonld", "JSON-LD");
// Run PageRank against the materialized shortcut subgraph.
MATCH (r:stave__Resource)-[v:stave__violates]->(c:stave__Control)
WITH gds.graph.project(
'stave-violations',
collect(distinct r) + collect(distinct c),
collect({source: r, target: c, weight: v.severityWeight})
) AS g
CALL gds.pageRank.stream(g.graphName)
YIELD nodeId, score
RETURN gds.util.asNode(nodeId).id AS resource, score
ORDER BY score DESC LIMIT 10;
GraphML imports work the same way via APOC:
CALL apoc.import.graphml('stave.graphml', {readLabels: true}).
Python — igraph
igraph imports GraphML directly. Severity weights decode as numeric attributes so they're usable as edge weights immediately.
import igraph as ig
g = ig.Graph.Read_GraphML('stave.graphml')
# Filter to the algorithm-shortcut subgraph.
shortcut = g.subgraph_edges([
e.index for e in g.es if e['isAlgorithmShortcut'] is True
])
# Centrality on the shortcut subgraph weighted by severity.
weights = shortcut.es['weight']
print(shortcut.betweenness(weights=weights))
# Community detection partitioned by invariant category.
clusters = shortcut.community_label_propagation(
initial=shortcut.vs['category'],
weights=weights,
)
print(clusters.summary())
For JSON-LD, igraph has no direct importer; round-trip via rdflib:
import rdflib
from rdflib.extras.external_graph_libs import rdflib_to_networkx_multidigraph
import networkx as nx
g_rdf = rdflib.Graph().parse('stave.jsonld', format='json-ld')
g_nx = rdflib_to_networkx_multidigraph(g_rdf)
# convert nx -> igraph if needed:
g_ig = ig.Graph.from_networkx(g_nx)
Python — NetworkX
GraphML loads natively; JSON-LD loads via rdflib's
rdflib_to_networkx_multidigraph helper.
import networkx as nx
g = nx.read_graphml('stave.graphml')
# Severity-weighted shortest path.
path = nx.shortest_path(
g,
source='urn:stave:bucket/123456789012/secret-data',
target='urn:stave:invariant/CTL.S3.PUBLIC.001',
weight='weight',
)
print(path)
# Community detection on the shortcut subgraph.
shortcut = g.edge_subgraph(
(u, v) for u, v, d in g.edges(data=True)
if d.get('isAlgorithmShortcut') == 'true'
)
import networkx.algorithms.community as nxc
communities = nxc.louvain_communities(shortcut.to_undirected())
Apache Spark — GraphX / GraphFrames
Spark accepts CSV exports of the GraphML attribute tables; for direct
GraphML loading use the graphframes
JSON-LD reader on top of spark.read.format("rdf") from the
spark-rdf connector.
from graphframes import GraphFrame
# Read GraphML preprocessed to vertices.csv / edges.csv.
v = spark.read.csv('vertices.csv', header=True, inferSchema=True)
e = spark.read.csv('edges.csv', header=True, inferSchema=True)
g = GraphFrame(v, e)
# Influence propagation: Personalized PageRank seeded at critical
# resources, weighted by severity.
results = g.parallelPersonalizedPageRank(
resetProbability=0.15,
sourceIds=['urn:stave:bucket/123456789012/critical-bucket'],
maxIter=10,
)
Gephi / yEd / Cytoscape (visual exploration)
All three load GraphML directly via File → Import. Stave's GraphML
declares severity_weight as xsd:double and category as
xsd:string, so the visual tools render numeric edge widths and
categorical node colors out of the box.
Verifying the export
The Stave repository ships a deterministic byte-comparison test
(internal/graph/export_test.go) and SPARQL ASK assertions
(docs/graph-experiments.md Experiment 6). Run
make test ./internal/graph/...
and load the resulting JSON-LD into your library of choice via the
recipes above. If the imports surface fewer triples than the export
contains, check that the consumer is reading isAlgorithmShortcut as
a bare key (the JSON-LD @context does not type-coerce it; some
strict parsers expand it as a string-typed literal).