Graph Architecture
Derived from Epic 5 – SBOM Graph Explorer.
The Graph module is the generic SBOM dependency/service graph store for the Stella Ops release control plane. It ingests SBOMs, advisories, VEX statements, and asset observations into a content-addressed node/edge model, then serves query, path, diff, lineage, overlay, and export surfaces to the Console, CLI, and SDK. This document captures the core model, ingestion pipelines, API surface, storage, and observability as implemented.
Scope note. This module (
src/Graph/) owns the generic dependency/service graph store — nodes, edges, overlays, lineage, and the asset registry — and, since SPRINT_20260722_023 GRA-10 (2026-09-08), the reachability subgraph CAS as well.src/ReachGraph/was a separate deployable (reachgraph-web) that consumed Graph data; GRA-4 lifted its HTTP surface intosrc/Graph/__Libraries/StellaOps.ReachGraph.Application, GRA-9 composed it ontograph-apiand moved the family onto one database, and GRA-10 deleted the old host, its compose service block and its publish key. One deployable, one database (stellaops_graph, schemasgraph+reachgraph), and the directreachgraph.stella-ops.localalias now resolves tograph-api. The module dossier for the reachability model itself isdocs/modules/reach-graph/. Thegraph:simulatescope and AI-assisted recommendations referenced below are roadmap items not yet enforced by this service.
0) Components
Shipped as a single container (stellaops-graph-api, slot 20). Cartographer (formerly slot 21) has been merged in; cartographer.stella-ops.local is now a network alias on this container. The module is composed of:
StellaOps.Graph.Api— the ASP.NET host. Minimal-API endpoints (search/query/paths/diff/lineage/export, edge metadata, asset registry, Angular-explorer compatibility facade, Cartographer build/overlay surface), auth/tenant resolution, rate limiting, audit logging, overlay/export services, saved-view store. Entry pointProgram.cs.StellaOps.Graph.Indexer— ingestion + analytics + incremental processing pipelines (SBOM ingest, Inspector/advisory/policy/VEX transformers, Louvain-style clustering + degree/betweenness analytics, change-stream processor, asset indexing). Hosted in-process by the API.StellaOps.Graph.Indexer.Persistence— PostgreSQL persistence: EF Core context, document/snapshot/analytics/idempotency/asset-registry repositories, and the embedded startup migrations underMigrations/.- Frozen historical prototype:
StellaOps.Graph.Corenever entered a production host, never applied a migration, and lost its last repository implementation at GRA-4. GRA-10 moved its remaining model and self-only tests tosrc/__Obsoleted/Graph/on 2026-08-24 and removed them from every live solution/test lane. It has no supported successor; revival starts with a new owner decision, not with reconnecting the frozen source.
1) Core model
- Nodes (the kind string the indexer emits is shown in
code):artifact(application/image digest) with metadata (tenant, environment, labels).sbom(sbom digest, format, version/sequence, chain id).component(package/version, purl, ecosystem).advisoryandvex_statementnodes linking to Concelier/Excititor records via digests.policy_versionnodes representing signed policy packs.assetnodes for NIS2/DORA/TLPT inventory. Asset v1 types are exactlyhost,container,image,service,integration, andplugin; credentials and people are opaque refs only.filenodes (per-component source/binary files) are emitted today bySbomIngestTransformer.CreateFileNode(kindfile; carriesnormalized_path,content_sha256,language_hint,size_bytes,scope), linked to their declaringcomponentviaDECLARED_INedges. There is no separatePathnode kind — the file path is thenormalized_pathattribute on thefilenode.licensenodes (kindlicense; SPDX id, name, classification, notice URI) are emitted today bySbomIngestTransformer.CreateLicenseNode, deduplicated per(license_spdx, source_digest).- The historical
CveObservationNodeprototype is frozen undersrc/__Obsoleted/Graph/and is not part of the node model. The live node store isgraph.graph_nodes/graph_edgesonly.
- Edges: directed, timestamped relationships. Emitted today:
DEPENDS_ON,CONTAINS,BUILT_FROM,DECLARED_IN,AFFECTED_BY,VEX_EXEMPTS,GOVERNS_WITH,OBSERVED_RUNTIME,PROVIDES,SBOM_VERSION_OF, andSBOM_LINEAGE_*(e.g.,SBOM_LINEAGE_PARENT). Each edge carries provenance (SRM hash, SBOM digest, policy run ID). Asset lifecycle edges useASSET_VERSIONand are backed by append-onlygraph.asset_registry_eventsrows for replay. Edge kind is normalised to upper-case in the canonical edge id; node kind to lower-case. - Overlays: projections over reachability, blast radius, policy verdict, VEX status, and AOC status. The materialised overlay index tables (e.g.,
graph_overlay/vuln/{tenant}/{advisoryKey}) are a roadmap target — no overlay table is shipped today. When aTestinghost serves overlays, three kinds are emitted inline per node:policy(policy.overlay.v1),vex(openvex.v1), andaoc(aoc.overlay.v1), each with a deterministic overlay ID (sha256(tenant|nodeId|overlayKind)) and a sampledexplainTraceon the policy overlay. OutsideTestingthe overlay path returns501 GRAPH_FEATURE_UNAVAILABLEuntil a durable overlay backend exists (see §3.1).
2) Pipelines
- Ingestion: SBOM Service emits SBOM snapshots (
sbom_snapshotevents) captured by the Graph Indexer (now hosted inside graph-api; Cartographer merged). Ledger lineage references becomeSBOM_VERSION_OF+SBOM_LINEAGE_*edges. Advisories/VEX from Concelier/Excititor generate edge updates, policy runs attach overlay metadata. Asset source observations from SBOM scans, integration profiles, runtime services, and plugin catalogs normalize through the Graph-owned asset indexer pipeline intoassetnodes, append-only asset events, andASSET_VERSIONlineage edges. Production asset source subscriptions poll normalized*.asset-source-batch.jsonfiles fromGraph:AssetSourceSubscriptions:SpoolDirectoryon a default and capped 30-minute interval, then process them throughAssetIndexProcessorso source services do not write Graph tables directly. - ETL: Normalises nodes/edges into canonical IDs (
GraphIdentity.ComputeNodeId/ComputeEdgeId— tenant-scoped, kind-prefixed, Base32-Crockford SHA-256 of the sorted identity tuple;gn:/ge:prefixes), deduplicates, enforces tenant partitions, and writes document-JSON adjacency rows to PostgreSQL (graph.graph_nodes/graph.graph_edges). A graph-DB backend is a roadmap option, not shipped (see §4). - Overlay computation: Roadmap. The intended design has batch workers build materialised views for frequently used queries (impact lists, saved queries, policy overlays) and store them as immutable blobs for Offline Kit exports. Today only inline, per-request overlays exist (Testing only — see §1 and §3.1); there is no durable overlay materialisation worker yet.
- Diffing:
graph_diffjobs compare two snapshots (e.g., pre/post deploy) and generate signed diff manifests for UI/CLI consumption. - Analytics (Runtime & Signals 140.A): background workers run Louvain-style clustering + degree/betweenness approximations on ingested graphs, emitting overlays per tenant/snapshot and writing cluster ids back to nodes when enabled.
3) APIs
POST /graph/search— NDJSON node tiles with cursor paging, tenant + scope guards.POST /graph/query— NDJSON nodes/edges/stats/cursor with budgets (tiles/nodes/edges) and optional inline overlays (includeOverlays=true) emittingpolicy.overlay.v1andopenvex.v1payloads; overlay IDs aresha256(tenant|nodeId|overlayKind); policy overlay may include a sampledexplainTrace.POST /graph/paths— bounded BFS (depth ≤6) returning path nodes/edges/stats; honours budgets and overlays.POST /graph/diff— comparessnapshotAvssnapshotB, streaming node/edge added/removed/changed tiles plus stats; budget enforcement mirrors/graph/query.POST /graph/export— returns a deterministic manifest (jobId,sha256, size, format,downloadUrl) for one ofndjson/csv/graphml/png/svg; download viaGET /graph/export/{jobId}(setsX-Content-SHA256).ndjson/csv/graphmlare real serialisers;png/svgare placeholders today. Only available inTesting; production returns501 GRAPH_FEATURE_UNAVAILABLE(see §3.1).POST /graph/lineage— returns SBOM lineage nodes/edges anchored byartifactDigestorsbomDigest, with optional relationship filters and depth limits.GET /graph/assets,POST /graph/assets/query,GET /graph/assets/{assetId}, andPOST /graph/assets/exportexpose the EU Asset Registry v1 runtime surface. Responses use deterministic ordering, preserve credential/person references as opaque refs, and never expand secret material or human PII.- Runtime repository selection is config-driven at service resolution time: when
Postgres:Graphis configured, the live/graph/query,/graph/diff,/graph/assets*, and shipped/graphs*compatibility surfaces materialize persisted rows fromgraph.graph_nodes,graph.graph_edges, andgraph.pending_snapshots; when it is not configured, only theTestingenvironment may use the empty in-memory repository harness. - Naming caveat: the search/query/paths/diff/lineage service classes are named
InMemoryGraph*Servicebut are the production implementations — they read throughIGraphRuntimeRepository, which resolves toPostgresGraphRuntimeRepositoryin production. Only overlays, export, and edge metadata have dedicatedUnsupported*(501) production variants; search/query/paths/diff/lineage serve real Postgres data. - The production Graph API PostgreSQL repository is fail-fast: DI construction must throw on invalid/missing PostgreSQL options instead of resolving
nulland silently falling back to in-memory behavior. - Compatibility facade for the shipped Angular explorer:
GET /graphs,GET /graphs/{graphId},GET /graphs/{graphId}/tilesGET /search,GET /pathsGET /graphs/{graphId}/export,GET /assets/{assetId}/snapshot,GET /nodes/{nodeId}/adjacencyGET/POST/DELETE /graphs/{graphId}/saved-views- The compatibility tile surface emits only
policy,vex, andaocoverlays on the shipped web path. - Saved views are persisted in PostgreSQL table
graph.saved_viewswhenPostgres:Graphis configured; the API falls back to an in-memory store only in theTestingenvironment.
- Canonical consolidated surface (GRA-9 source-ready; live cutover still pending): graph-api publishes real ASP.NET
RouteEndpointaliases beneath/api/graph/v1/{graph,graphs,assets,nodes,search,paths,reachgraphs,reachability,cve-mappings}. Each alias retains the carried endpoint’s handler, HTTP methods, authorization, rate-limit and response metadata, so Router’s endpoint-discovery provider advertises the canonical paths in the service HELLO. This is endpoint composition, not middleware rewriting: Router dispatches directly to discovered endpoints and bypasses middleware. The predecessor direct paths remain available for rollback until GRA-10; the gateway and Console prefix changes occur only in the GRA-9 window. - Cartographer-compatible build/overlay surface (consumed by the Scheduler Worker’s
HttpCartographerBuildClient/HttpCartographerOverlayClient; seeEndpoints/CartographerEndpoints.cs):POST /api/graphs/builds,GET /api/graphs/builds/{jobId}— graph build jobs keyed by a deterministic job id derived from theIdempotency-Keyheader (or, absent it, the canonical request body) so retries collapse. Audited asgraph_build.POST /api/graphs/overlays,GET /api/graphs/overlays/{jobId}— overlay jobs with the same deterministic-id contract. Audited asgraph_overlay.- These are direct service/SDK endpoints, not Router publications. Before endpoint discovery, Graph adds both prefixes to its bridge’s exclusion list and rejects
IncludeExcludedPathsInRouter=true; the HTTP handlers and SDK addresses remain unchanged. This Graph-owned boundary does not change exclusions for other services. - These endpoints currently track jobs in an in-memory dictionary (synchronous
completedresponses); persistent job storage lands when the full pipeline is wired. They are not tenant-resolved throughGraphRequestContextResolver(tenant arrives in the request body). - A Graph image or Compose replay does not preserve that process-local job dictionary. Source authorization/publication tests do not establish safe job-state disposition for a recreation, and this prerequisite does not activate the GRA-9 database move.
- Edge Metadata API (added 2025-01):
POST /graph/edges/metadata— batch query for edge explanations; request containsEdgeIds[], response includesEdgeTileWithMetadata[]with full provenance.GET /graph/edges/{edgeId}/metadata— single edge metadata with explanation, via, provenance, and evidence references.GET /graph/edges/path/{sourceNodeId}/{targetNodeId}— returns all edges on the shortest path between two nodes, each with metadata.
GET /graph/edges/by-reason/{reason}— query edges byEdgeReasonenum (e.g.,SbomDependency,AdvisoryAffects,VexStatement,RuntimeTrace).GET /graph/edges/by-evidence?evidenceType=&evidenceRef=— query edges by evidence reference.- Legacy:
GET /graph/nodes/{id},POST /graph/query/saved,GET /graph/impact/{advisoryKey},POST /graph/overlay/policyremain in spec but should align to the NDJSON surfaces above as they are brought forward.
3.1) Current runtime posture (Sprint 20260416_003)
- Non-testing Graph hosts require
Postgres:Graph; startup fails fast when the canonical Graph PostgreSQL connection string is absent. Testingis the only supported environment for the empty in-memory Graph runtime harness.- Graph Indexer change-stream no-op sources are limited to
DevelopmentandTesting; production hosts must register realIGraphChangeEventSourceandIGraphBackfillSourceimplementations or startup fails closed. - Production change-stream writes use the
IAtomicGraphChangeWriterpath:PostgresGraphDocumentWriterclaims thegraph.idempotency_tokens.sequence_tokenand upsertsgraph_nodes/graph_edgesinside the same PostgreSQL transaction, so concurrent/replayed processors cannot apply the same sequence token twice. - Non-testing
POST /graph/queryandPOST /graph/pathsno longer fabricate overlays. Requests that ask for overlays return truthful501 GRAPH_FEATURE_UNAVAILABLEresponses until a durable overlay backend exists. - Non-testing
POST /graph/exportandGET /graph/export/{jobId}now return truthful501 GRAPH_FEATURE_UNAVAILABLEresponses instead of synthesizing in-memory export jobs. - Non-testing edge-metadata surfaces (
/graph/edges/metadata,/graph/edges/{edgeId}/metadata,/graph/edges/path/...,/graph/edges/by-reason/...,/graph/edges/by-evidence) now return truthful501 GRAPH_FEATURE_UNAVAILABLEresponses until a durable backend exists. - The shipped
/graphs/{graphId}/tilescompatibility surface no longer emits fabricated overlays outsideTesting. - Live audit logging is log-only; request history is no longer retained in-process as pretend durable state.
3.2) Tenant and auth resolution contract (Sprint 20260222.058)
- Graph uses a single tenant resolver path (
GraphRequestContextResolver) across search/query/paths/diff/lineage/export and edge-metadata endpoints. - The tenant comes from the validated claim only — there is no header fallback (CANON-1 Phase 2;
Security/GraphRequestContextResolver.cs:36-68). Corrected 2026-07-12: this section previously read as a claim-then-header precedence list, which invited an operator to believeX-StellaOps-TenantIdselects the tenant. It does not, and never should.- Source (the only one): the claim
stellaops:tenant(with bounded aliasestid,tenant_id). Absent ⇒tenant_missing; a raw header is never promoted to the tenant (it is forgeable on a bypass network). - Headers are a defence-in-depth conflict check, not a source. If any of the three recognised tenant headers —
X-StellaOps-TenantId(canonical),X-Stella-Tenant,X-Tenant-Id— is present and disagrees with the claim, the request is rejectedtenant_conflictrather than silently ignored, so cross-tenant probing is observable.
- Source (the only one): the claim
- Deterministic failures:
- missing tenant:
400 GRAPH_VALIDATION_FAILED - conflicting tenant claim/header values:
400 GRAPH_VALIDATION_FAILED - missing auth:
401 GRAPH_UNAUTHORIZED - missing scope:
403 GRAPH_FORBIDDEN
- missing tenant:
- Scope checks are policy-driven (
Graph.ReadOrQuery,Graph.Query,Graph.Export). Asset list/query/get/export additionally enforce authentication, their declared scope policy, then Graph tenant validation insideGraphAssetAuthorizationFilter, before repository reads. This filter remains in the actualRequestDelegateinvoked by Router and in the canonical endpoint aliases; it does not depend on HTTP authorization middleware running first. A raw-header-onlyStellaRouteridentity is rejected, while the validated signed-envelope path is retained. - Rate limiting and audit logging use the resolved tenant context; authenticated flows no longer collapse to ambiguous
"unknown"tenant keys.
3.3) Edge Metadata Contracts
The edge metadata system provides explainability for graph relationships:
- EdgeReason enum:
Unknown,SbomDependency,StaticSymbol,RuntimeTrace,PackageManifest,Lockfile,BuildArtifact,ImageLayer,AdvisoryAffects,VexStatement,PolicyOverlay,AttestationRef,OperatorAnnotation,TransitiveInference,Provenance. - EdgeVia record: Describes how the edge was discovered (method, version, timestamp, confidence in basis points, evidence reference).
- EdgeExplanationPayload record: Full explanation including reason, via, human-readable summary, evidence list, provenance reference, and tags.
- EdgeProvenanceRef record: Source system, collection timestamp, SBOM digest, scan digest, attestation ID, event offset.
- EdgeTileWithMetadata record: Extends
EdgeTilewithExplanationproperty containing the full metadata.
3.4) Localization runtime contract (Sprint 20260224_002)
- Graph API now initializes localization via
AddStellaOpsLocalization(...),AddTranslationBundle(...),AddRemoteTranslationBundles(),UseStellaOpsLocalization(), andLoadTranslationsAsync(). - Locale resolution order for API messages is deterministic:
X-Localeheader ->Accept-Languageheader -> default locale (en-US). - Translation layering is deterministic: shared embedded
commonbundle -> Graph embedded bundle (Translations/*.graph.json) -> Platform runtime override bundle. - Remote Platform override fetches are bounded and loaded concurrently per provider locale so scratch bootstrap cannot hold the Graph API offline while optional translation overrides load.
- This rollout localizes selected error paths (for example, edge/export not found, invalid reason, and tenant/auth validation text) for
en-USandde-DE.
3.5) Authorization scopes
The authorization policies use the scope sets in Security/GraphPolicies.cs and the typed GraphScopeRequirement; a request is admitted if it is authenticated and carries any scope in the set. RequiredScopes plus RequireAllScopes=false give Router discovery the same OR requirement rather than an opaque assertion that cannot be published:
| Policy | Accepted scopes | Used by |
|---|---|---|
Graph.ReadOrQuery | graph:read or graph:query | search, lineage, edge metadata (single/batch/by-reason/by-evidence), asset list/query/get, compatibility GETs |
Graph.Query | graph:query | query, paths, diff, edges-on-path, compatibility /paths, saved-view create/delete |
Graph.Export | graph:export | export (start + download), asset export, compatibility /graphs/{id}/export |
Graph.AssetRegistryProjectionRead | graph:asset-registry:read-all | cross-tenant asset-registry projection feed; ordinary Graph scopes do not admit this feed |
Graph.HealthRead | ops.health | Doctor operational-health checks |
- Scope constants
graph:readandgraph:exportcome fromStellaOps.Auth.Abstractions/StellaOpsScopes.cs(GraphRead,GraphExport).graph:queryis a Graph-local literal (GraphPolicies.GraphQueryScope) and is not (yet) a named constant in the shared scope catalog. - The shared scope catalog also defines
graph:writeandgraph:simulate; neither is an admission guard for the Cartographer build surface. The carried ReachGraph controller separately enforcesgraph:adminfor destructive operations throughReachGraphDestructiveActionAuthorizer; that guard is not a general Graph read/query policy.
The RAR-7 source prerequisite is verified against 2942a54b194b177a5c2762db84b9b89736410ce2 by GraphRouterAuthorizationTests in src/Graph/__Tests/StellaOps.Graph.Api.Tests: it invokes the production Router dispatcher, proves denial before repository access for anonymous, insufficient-scope, missing-tenant and conflicting-tenant requests, verifies both accepted read scopes independently, and checks discovery plus direct-only Cartographer exclusions. Re-run pwsh ./tools/scripts/test-targeted-xunit.ps1 -Project src/Graph/__Tests/StellaOps.Graph.Api.Tests/StellaOps.Graph.Api.Tests.csproj -Class '*GraphRouterAuthorizationTests' -BuildProjectReferences -Restore. The landing and runtime acceptance belong to the RAR-7 sprint receipt; local conformance does not retire the gateway rewrite or establish live acceptance.
4) Storage considerations
- Shipped backend: PostgreSQL only. There is no graph-DB (Neo4j/Cosmos Gremlin) implementation — the relational store is canonical. The
IGraphRuntimeRepositoryabstraction (Services/IGraphRuntimeRepository.cs) exists so a graph-DB backend could be added later, but only the Postgres and (Testing-only) in-memory implementations exist today. - Persistence is split across two migration sets, both applied via
AddStartupMigrationson startup (embedded SQL resources):StellaOps.Graph.Indexer.Persistence/Migrations/(schemagraph):graph.graph_nodes,graph.graph_edges(document-JSON adjacency rows written byPostgresGraphDocumentWriter),graph.pending_snapshots(the canonical change feed read byPostgresGraphSnapshotProvider),graph.cluster_assignmentsandgraph.centrality_scores(analytics),graph.idempotency_tokens(sequence-token claim table used in the same transaction as document writes), plus the legacy un-qualifiedgraph_nodes/graph_edges/graph_snapshots/graph_analytics/graph_idempotencytables from migration001. There is nograph_overlaystable — overlays are computed/served inline, not persisted in a dedicated overlay table.StellaOps.Graph.Api/Migrations/003_saved_views.sql(thesaved_viewstable) and thegraph.asset_registry_eventstable (see below).
- GRA-9 target (source-ready, not live):
StellaOps.Graph.Persistencereplaces those legacy authorities only whenSTELLAOPS_POSTGRES_GRAPH_CONNECTIONis armed. Its startup set is001_graph_consolidated_baseline.sqlplus the forward002_force_reachgraph_tenant_rls.sql. The latter forces the three ReachGraph CAS policies to apply even when the runtime role owns the tables it migrated;NOSUPERUSER NOBYPASSRLSalone does not constrain a PostgreSQL table owner. - Tenant partitioning is enforced by a
tenant_id/tenantcolumn on every shipped Graph table. The staged ReachGraph CAS is the deliberate RLS exception: its three tables enable and force RLS viaapp.tenant_id, with the constrained runtime role measured by Doctor. The frozenCveObservationNodeprototype created no table and is not part of the shipped tenancy story. Deterministic ordering and streaming NDJSON exports back the Offline Kit story. - The shipped compatibility saved-view surface now owns a small relational persistence slice via startup migration
003_saved_views.sql, which auto-createsgraph.saved_viewsand keeps saved views durable across host restarts. - Asset Registry v1 owns
graph.asset_registry_eventsvia Graph Indexer Persistence startup migrations. Events are append-only, deletions are tombstones, and each event carries payload, Merkle leaf, and event hashes for downstream Findings Ledger projection. Findings Ledger mirrors those rows read-only intofindings.asset_registry_events; Graph remains the event source of truth. The consolidated live Findings host uses the authenticated Graph owner-feed seam; its predecessor direct SQL reader is retired with the predecessor host. The Graph role also cannot connect to the Findings or Platform databases. A local source build is not live feed acceptance, but the old cross-database-read NO-GO no longer describes the deployed consumer.
4.1) Canonical local replay boundary (RAR-7)
The opt-in local Graph chain is docker-compose.stella-services.yml → docker-compose.graph.yml → docker-compose.local-graph.yml → existing networks. It keeps the Graph-owned database variables, preserves the external network aliases and pins the exact rollback image. A docker-compose.local-graph-reset.yml used to sit second, directly after the base file, to !reset the inherited generic connection — the position mattered because Compose ignores a !reset placed after an intermediate environment merge. It is gone: GRA-9 deleted the key it reset, and GRA-10 retired the file on 2026-09-08 once graph-api had been recreated on a chain that no longer names it. Guarding the ABSENCE of a generic connection replaced resetting it (GraphConsolidationConformanceTests.ComposeOverlay_IsWiredByDesign_AndAnOverlayLessBringUpFailsClosed). The manifest local-graph-runtime.json remains fail-closed until two later facts exist: a clean exact-main candidate image, and a stopped-container capture of the current DataProtection key ring at its existing /var/lib/stella/.aspnet/DataProtection-Keys destination. No filename, payload or inventory is invented in source preparation.
Process-local Cartographer job loss is owner-accepted for this replacement. The acceptance does not include DataProtection keys, PostgreSQL rows or mounted inputs. The replacement is also the host’s first move onto its owner database (stellaops_graph, owner ruling 2026-09-02): the manifest’s ownerDatabaseAdoption record plus a live zero-row proof over every graph.* table in the shared stellaops_platform database are the only way the controller admits that connection change. The preservation/replay helper therefore refuses candidate and recovery phases until both immutable image generations and the key-ring inventory are bound. /api/graphs/builds and /api/graphs/overlays stay direct-only SDK contracts: the host fails startup if configuration attempts to republish excluded paths, and the canonical profile does not weaken that guard.
The execution controller fails closed beyond aggregate counts: the committed capture must name the sorted, fixed-shape DataProtection XML entries and their individual hashes, and every pre-stop, stopped-copy, ACL, restart and post-recreate observation must match exactly. Candidate admission binds immutable image ID and RepoDigest, linux/amd64, uid10001, clean buildinfo whose source is HEAD or an ancestor of HEAD with unchanged candidate build inputs, the rendered model, protected .env, exact owner DSN/Redis continuity and Doctor-disabled posture. Process/security/health/ports/capabilities, target aliases, physical networks and every peer network attachment are compared before and after. The only Docker authority is the explicit local npipe; ambient context/host/TLS configuration is rejected. Mutation receipts are reserved and fsynced before execution as separate immutable intent/executing/helper-ready/final records; a failed final write cannot erase executing intent. DataProtection capture rechecks source/estate/target/inventory after that intent and immediately before stop. Candidate uid10001 access is not part of normal preflight: a dedicated reviewed access phase verifies and removes an exact networkless candidate helper and bounded marker, then normal preflight consumes its hash-bound completion without launching anything. Rollback accepts a successful forward or a failed forward that positively installed the exact candidate still present with unchanged immutable identity/configuration. Lifecycle drift is ignored; a now-missing but positively recorded candidate needs an additional explicit recovery flag. Baseline/no-replacement receipts never authorize rollback.
Ambiguous helper creation is not absence: timeout/nonzero/no-output paths resolve the unique helper name and returned ID, validate its complete security/mount/image identity and remove only that exact object. Marker cleanup requires one regular, single-link byte equal to literal x by SHA-256. Forward receipts embed the access-proof hash, so delayed rollback remeasures current host custody/image without requiring the old baseline-only access receipt to remain fresh. Crash recovery from a surviving .executing.json is a separate explicit mode and only proceeds while the exact candidate is installed and engine, peers, model/environment, both DP inventories, the baseline-bound runtime settings and physical network IDs, and the manifest-derived candidate Compose chain still match; it cannot stand in for the missing-container guard.
For the later ready pin, build identity and deployment-manifest identity are separate only when a machine-recomputed digest proves the complete explicit Graph image build-input closure byte-identical between the clean candidate SHA and descendant manifest HEAD. The intervening diff is restricted to the exact canonical Graph pin/evidence/docs set; no Dockerfile, project, product/shared source or build helper may differ. Both SHAs and the closure digest are receipt-bound. Pending state continues to require literal sourceSha == HEAD and does not claim this exception yet.
5) Offline & export
- Each snapshot packages
nodes.jsonl,edges.jsonl,overlays/plus manifest with hash, counts, and provenance. Export Center consumes these artefacts for graph-specific bundles. - Saved queries and overlays include deterministic IDs so Offline Kit consumers can import and replay results.
- Runtime hosts register the SBOM ingest pipeline via
services.AddSbomIngestPipeline(...). Snapshot exports default to./artifacts/graph-snapshotsbut can be redirected withSTELLAOPS_GRAPH_SNAPSHOT_DIRor theSbomIngestOptions.SnapshotRootDirectorycallback. - Runtime hosts register the production asset indexing bridge through
services.AddGraphIndexerPersistence(...), which bindsGraph:AssetSourceSubscriptions:*, registers the file-spool source subscription bridge, and exposes the canonicalIAssetSourceBatchPublisherfor Scanner, Integrations, Signals, and plugin host emitters. Tests and custom hosts can still register the Graph-only pipeline directly viaservices.AddAssetIndexerPipeline(...). - Analytics overlays are exported as NDJSON (
overlays/clusters.ndjson,overlays/centrality.ndjson) ordered by node id;overlays/manifest.jsonmirrors snapshot id and counts for offline parity.
6) Observability
- Graph API metrics (meter
StellaOps.Graph.Api, seeServices/GraphMetrics.cs):graph_query_budget_denied_total(budget enforcement),graph_tile_latency_seconds(query/tile latency histogram),graph_overlay_cache_hits_total/graph_overlay_cache_misses_total(overlay cache), andgraph_export_latency_seconds(export latency histogram). (An earlier draft listedgraph_ingest_lag_seconds, per-saved-query latency, and overlay-generation duration; those names are not emitted — they remain roadmap targets.) - Analytics metrics (meter
StellaOps.Graph.Indexer, seeAnalytics/GraphAnalyticsMetrics.cs):graph_analytics_runs_total,graph_analytics_failures_total,graph_analytics_duration_seconds,graph_analytics_clusters_total,graph_analytics_centrality_total. - Change-stream/backfill metrics (same
StellaOps.Graph.Indexermeter, seeIncremental/GraphBackfillMetrics.cs):graph_changes_total,graph_backfill_total,graph_change_failures_total, and thegraph_change_lag_secondshistogram. All carrytenant,backfill, andsuccesstags. - Logs: structured per-request audit events (
LoggerAuditLogger: tenant, route, method, actor, status, duration, scopes) plus ETL/ingestion stage logs. Audit logging is log-only — request history is not retained in-process (see §3.1). - Traces: ETL pipeline spans, query engine spans.
7) Rollout notes
- Phase 1: ingest SBOM + advisories, deliver impact queries.
- Phase 2: add VEX overlays, policy overlays, diff tooling.
- Phase 3: expose runtime/Zastava edges and AI-assisted recommendations (future).
Local testing note
Set STELLAOPS_TEST_POSTGRES_CONNECTION to a reachable PostgreSQL instance before running tests/Graph/StellaOps.Graph.Indexer.Tests. The test harness falls back to Host=127.0.0.1;Port=5432;Database=stellaops_test, then Testcontainers for PostgreSQL, but the CI workflow requires the environment variable to be present to ensure upsert coverage runs against a managed database. Use STELLAOPS_GRAPH_SNAPSHOT_DIR (or the AddSbomIngestPipeline options callback) to control where graph snapshot artefacts land during local runs.
Refer to the module README and implementation plan for immediate context, and update this document once component boundaries and data flows are finalised.
