Graph Architecture

Verified against commit 942a18727c on 2026-09-26 (UTC). Re-verify: git grep -n "AddHostedService\|AddReachGraphApplication\|SnapshotGraphChangeSource" -- src/Graph and ls src/Graph/__Libraries/StellaOps.Graph.Persistence/Migrations.

Graph owns the tenant-scoped dependency and service graph, the asset registry and the reachability store. src/ is the source of truth; where this document and the code disagree, the code wins.

1. Components

src/Graph/
  StellaOps.Graph.Api/                  # the one host (graph-api): Program.cs, Endpoints/, Hosting/, Security/, Services/
  StellaOps.Graph.Indexer/              # library composed by graph-api: Ingestion/, Analytics/, Incremental/, Assets/, Retention/
  __Libraries/
    StellaOps.Graph.Contracts/          # wire contracts (asset-registry event feed)
    StellaOps.Graph.Indexer.Persistence/# Postgres repositories for the Indexer (no migrations registered by graph-api)
    StellaOps.Graph.Persistence/        # the consolidated migration authority (graph + reachgraph schemas)
    StellaOps.Graph.Operations/         # schema topology and Doctor checks
    StellaOps.ReachGraph.Application/   # reachability controllers and services lifted from reachgraph-web
  __Tests/
src/__Libraries/StellaOps.ReachGraph*/  # shared reachability model, hashing, serialization, cache, persistence

graph-api composes everything in src/Graph/StellaOps.Graph.Api/Program.cs. There is no separate Indexer or ReachGraph process.

2. Request path, identity and scopes

PolicyScopes
ReadOrQuerygraph:read or graph:query
Querygraph:query
Exportgraph:export
AssetRegistryProjectionReadgraph:asset-registry:read-all (the cross-tenant asset-registry feed only)
HealthReadops.health (service Doctor endpoints)
Authenticatedany authenticated principal (reachability controllers)

3. API surface

FamilyRoutesSource
Graph queriesPOST /graph/search, /graph/query, /graph/paths, /graph/diff, /graph/lineage, /graph/export; GET /graph/export/{jobId}Program.cs
Edge metadataPOST /graph/edges/metadata; GET /graph/edges/{edgeId}/metadata, /graph/edges/path/{source}/{target}, /graph/edges/by-reason/{reason}, /graph/edges/by-evidenceProgram.cs
Asset registryGET /graph/assets, POST /graph/assets/query, GET /graph/assets/{assetId}, POST /graph/assets/exportEndpoints/AssetRegistryEndpoints.cs
Asset-registry event feedGET /graph/asset-registry/events (cross-tenant catch-up feed read by Findings)Endpoints/AssetRegistryEventFeedEndpoints.cs, StellaOps.Graph.Contracts/AssetRegistry/AssetRegistryEventFeedContract.cs
Console compatibilityGET /graphs, /graphs/{graphId}, /graphs/{graphId}/tiles, /graphs/{graphId}/export, /search, /paths, /assets/{assetId}/snapshot, /nodes/{nodeId}/adjacency; saved views GET/POST /graphs/{graphId}/saved-views, DELETE /graphs/{graphId}/saved-views/{viewId}Endpoints/CompatibilityEndpoints.cs
Scheduler build and overlay contractPOST/GET /api/graphs/builds[/{jobId}], POST/GET /api/graphs/overlays[/{jobId}]Endpoints/CartographerEndpoints.cs
Reachability storePOST /v1/reachgraphs, GET /v1/reachgraphs/{digest}, GET /v1/reachgraphs/{digest}/slice, POST /v1/reachgraphs/replay, GET /v1/reachgraphs/by-artifact/{artifactDigest}, DELETE /v1/reachgraphs/{digest}StellaOps.ReachGraph.Application/Controllers/ReachGraphController.cs
Unified reachability queryPOST /v1/reachability/static, /runtime, /hybrid, /batchControllers/ReachabilityController.cs
CVE-to-symbol mappingGET /v1/cve-mappings/{cveId}, /by-package, /by-symbol, /stats; POST /v1/cve-mappings, /analyze-patch, /{cveId}/enrichControllers/CveMappingController.cs
OperationsGET /healthz, build info, service Doctor endpointsProgram.cs

Endpoints/GraphConsolidatedRouteAliases.cs publishes every family above a second time under the consolidated root /api/graph/v1 (for example /graph becomes /api/graph/v1/graph, /v1/reachgraphs becomes /api/graph/v1/reachgraphs) as real route endpoints, so the Router discovers them.

Behaviour notes verified in source:

4. Indexer pipeline (in-process)

StageRegistrationNotes
SBOM ingestionAddSbomIngestPipeline; driven by graph builds (AddGraphSbomBuilds, section 3)The file snapshot export is opt-in: it runs only when STELLAOPS_GRAPH_SNAPSHOT_DIR or SbomIngestOptions.SnapshotRootDirectory names a root; there is no default root. Without one, the ingest writes the graph rows, skips the export and logs that it did. With one, an export that fails fails the build, and every ingest overwrites the same snapshot file names under that root, whatever the tenant.
Inspector ingestionAddInspectorIngestPipelineContract: contracts/graph.inspect.v1.md.
AnalyticsAddGraphAnalyticsPipeline registers GraphAnalyticsHostedServiceWrites graph.centrality_scores and graph.cluster_assignments. GraphAnalyticsOptions carry code defaults; no configuration section binds them.
Change stream and backfillAddGraphChangeStreamProcessor registers GraphChangeStreamProcessor and GraphChangeStreamRuntimeGuardHostedServiceSee section 5. GraphChangeStreamOptions carry code defaults.
Asset-source subscriptionsAddGraphIndexerPersistence (via AddAssetSourceSubscriptionBridge) registers AssetSourceSubscriptionHostedServicePolls *.asset-source-batch.json batches from Graph:AssetSourceSubscriptions:SpoolDirectory. See asset-registry.md.
Ephemeral retentionAddGraphEphemeralRetention registers GraphEphemeralRetentionServiceSection 7.
Configuration guardGraphRuntimeConfigurationGuardHostedService (Services/GraphRuntimeFallbackServices.cs)Fails startup when the Graph connection is missing outside Testing, and refuses further unsupported runtime configuration outside Development and Testing.

5. Change source (CIC-2)

The change and backfill source is SnapshotGraphChangeSource, which reads the persisted snapshot queue graph.pending_snapshots in Graph’s own database. Program.cs registers it for both IGraphChangeEventSource and IGraphBackfillSource whenever the Graph connection resolves. The consolidated baseline creates the queue empty, so the same source serves a fresh install. Without the connection the processor falls back to a no-op source, which the runtime guard refuses outside Development and Testing. The Doctor check doctor.graph.indexer.change-source (Services/GraphChangeSourceDoctorCheck.cs) reports the composed source, whether the queue exists and its depth, and is Critical when the no-op source is composed.

6. Persistence

TablePurpose
graph.graph_nodes, graph.graph_edgesGraph documents; tenant carried in document_json (no row-level security in this schema)
graph.pending_snapshotsSnapshot queue feeding the change stream
graph.centrality_scores, graph.cluster_assignmentsAnalytics output
graph.idempotency_tokensIngest deduplication
graph.asset_registry_eventsAppend-only asset lifecycle events
graph.saved_viewsConsole saved views
reachgraph.subgraphsContent-addressed reachability subgraphs; row-level security keyed on app.tenant_id
reachgraph.replay_logReplay verification audit

Migrations (src/Graph/__Libraries/StellaOps.Graph.Persistence/Migrations/):

FileEffect
001_graph_consolidated_baseline.sqlBoth schemas
002_force_reachgraph_tenant_rls.sqlForces tenant row-level security on the reachability tables
003_graph_asset_registry_events_retention_class.sqlRetention class of the asset-registry events
004_graph_idempotency_token_horizon.sqlDedup horizon routine (RFC-15)
005_graph_pending_snapshot_age_cap.sqlSnapshot-queue age cap routine (RFC-15)
006_reachgraph_replay_log_window.sqlReplay-log audit window routine (RFC-15)
007_graph_topology_retention_classes.sqlTopology retention classes
008_reachgraph_retire_slice_cache.sqlRetires the unused reachgraph.slice_cache table (RFC-15, owner ruling Q-39); refuses if the table holds rows

The reachability hot-path cache is Valkey through ConnectionStrings:Redis (ReachGraphValkeyCache, options section ReachGraphCache); outside Development and Testing a missing Redis connection string fails startup.

7. Retention

GraphEphemeralRetentionService (StellaOps.Graph.Indexer/Retention/), options section Graph:EphemeralRetention:

KeyDefaultBounds
IdempotencyHorizonHours24graph.idempotency_tokens
PendingSnapshotAgeCapHours24graph.pending_snapshots rows the drain never reached
ReplayLogRetentionDays365reachgraph.replay_log, keeping the newest row per tenant and subgraph digest
BatchSize, MaxBatchesPerSweep, SweepInterval10000, 10, 1 hoursweep limits

It is registered only when the Graph connection resolves. The Doctor check doctor.graph.retention.ephemeral-backlog reports rows past these bounds.

8. Integrations

9. Doctor checks

CheckSource
doctor.graph.database.own-database-cutoverStellaOps.Graph.Operations/Doctor/GraphDoctorChecks.cs
doctor.graph.reachgraph.cas-tenant-isolationsame
doctor.graph.retention.ephemeral-backlogsame
doctor.graph.indexer.change-sourceStellaOps.Graph.Api/Services/GraphChangeSourceDoctorCheck.cs

The standard service checks come from AddServiceDoctorChecks("graph-api"). The Doctor endpoints require ops.health.

10. Observability

Meter StellaOps.Graph.Api (Services/GraphMetrics.cs): graph_tile_latency_seconds, graph_export_latency_seconds, graph_query_budget_denied_total, graph_overlay_cache_hits_total, graph_overlay_cache_misses_total. The Indexer stages define their own meters (GraphAnalyticsMetrics, GraphBackfillMetrics, SbomIngestMetrics, PolicyOverlayMetrics, AdvisoryLinksetMetrics).

11. Retired surfaces